summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-03-25 16:17:34 -0400
committerHazelnoot <acomputerdog@gmail.com>2025-03-25 16:17:34 -0400
commit40975719ec5bb889ab011bbc464dd0fdb09fdb68 (patch)
tree1b8521df694b869b26c6d75c99ef885704b013b0 /packages
parentmerge upstream (diff)
parentMerge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff)
downloadsharkey-40975719ec5bb889ab011bbc464dd0fdb09fdb68.tar.gz
sharkey-40975719ec5bb889ab011bbc464dd0fdb09fdb68.tar.bz2
sharkey-40975719ec5bb889ab011bbc464dd0fdb09fdb68.zip
Merge branch 'misskey-develop' into merge/2025-03-24
# Conflicts: # package.json # packages/backend/src/core/entities/NotificationEntityService.ts # packages/backend/src/types.ts # packages/frontend/src/pages/admin/modlog.ModLog.vue # packages/misskey-js/src/consts.ts # packages/misskey-js/src/entities.ts
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/src/core/ChatService.ts157
-rw-r--r--packages/backend/src/core/GlobalEventService.ts5
-rw-r--r--packages/backend/src/core/entities/NotificationEntityService.ts21
-rw-r--r--packages/backend/src/models/Notification.ts6
-rw-r--r--packages/backend/src/models/json-schema/notification.ts15
-rw-r--r--packages/backend/src/models/json-schema/user.ts1
-rw-r--r--packages/backend/src/server/api/endpoint-list.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-user.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/unreact.ts49
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/delete.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts67
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts1
-rw-r--r--packages/backend/src/types.ts7
-rw-r--r--packages/frontend-shared/js/const.ts1
-rw-r--r--packages/frontend/src/boot/main-boot.ts2
-rw-r--r--packages/frontend/src/components/MkAbuseReport.vue4
-rw-r--r--packages/frontend/src/components/MkNote.vue3
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue4
-rw-r--r--packages/frontend/src/components/MkNotification.vue5
-rw-r--r--packages/frontend/src/components/global/MkCustomEmoji.vue5
-rw-r--r--packages/frontend/src/components/global/MkEmoji.vue5
-rw-r--r--packages/frontend/src/di.ts1
-rw-r--r--packages/frontend/src/navbar.ts2
-rw-r--r--packages/frontend/src/pages/admin/index.vue1
-rw-r--r--packages/frontend/src/pages/admin/modlog.ModLog.vue2
-rw-r--r--packages/frontend/src/pages/chat/XMessage.vue51
-rw-r--r--packages/frontend/src/pages/chat/home.home.vue42
-rw-r--r--packages/frontend/src/pages/chat/home.vue2
-rw-r--r--packages/frontend/src/pages/chat/message.vue2
-rw-r--r--packages/frontend/src/pages/chat/room.form.vue12
-rw-r--r--packages/frontend/src/pages/chat/room.info.vue17
-rw-r--r--packages/frontend/src/pages/chat/room.members.vue32
-rw-r--r--packages/frontend/src/pages/chat/room.vue39
-rw-r--r--packages/frontend/src/pages/settings/navbar.vue87
-rw-r--r--packages/frontend/src/pages/settings/preferences.vue48
-rw-r--r--packages/frontend/src/preferences/def.ts10
-rw-r--r--packages/frontend/src/ui/_common_/navbar.vue21
-rw-r--r--packages/frontend/src/ui/universal.vue22
-rw-r--r--packages/frontend/src/use/use-mutation-observer.ts21
-rw-r--r--packages/frontend/src/utility/autogen/settings-search-index.ts129
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md23
-rw-r--r--packages/misskey-js/package.json2
-rw-r--r--packages/misskey-js/src/autogen/apiClientJSDoc.ts22
-rw-r--r--packages/misskey-js/src/autogen/endpoint.ts6
-rw-r--r--packages/misskey-js/src/autogen/entities.ts4
-rw-r--r--packages/misskey-js/src/autogen/types.ts176
-rw-r--r--packages/misskey-js/src/consts.ts8
-rw-r--r--packages/misskey-js/src/entities.ts3
49 files changed, 975 insertions, 182 deletions
diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts
index 57e33af107..df1c384b54 100644
--- a/packages/backend/src/core/ChatService.ts
+++ b/packages/backend/src/core/ChatService.ts
@@ -26,11 +26,27 @@ import { Packed } from '@/misc/json-schema.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { emojiRegex } from '@/misc/emoji-regex.js';
+import { NotificationService } from '@/core/NotificationService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
const MAX_ROOM_MEMBERS = 30;
const MAX_REACTIONS_PER_MESSAGE = 100;
const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/;
+// TODO: ReactionServiceのやつと共通化
+function normalizeEmojiString(x: string) {
+ const match = emojiRegex.exec(x);
+ if (match) {
+ // 合字を含む1つの絵文字
+ const unicode = match[0];
+
+ // 異体字セレクタ除去
+ return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
+ } else {
+ throw new Error('invalid emoji');
+ }
+}
+
@Injectable()
export class ChatService {
constructor(
@@ -68,11 +84,13 @@ export class ChatService {
private apRendererService: ApRendererService,
private queueService: QueueService,
private pushNotificationService: PushNotificationService,
+ private notificationService: NotificationService,
private userBlockingService: UserBlockingService,
private queryService: QueryService,
private roleService: RoleService,
private userFollowingService: UserFollowingService,
private customEmojiService: CustomEmojiService,
+ private moderationLogService: ModerationLogService,
) {
}
@@ -199,6 +217,8 @@ export class ChatService {
throw new Error('you are not a member of the room');
}
+ const membershipsOtherThanMe = memberships.filter(member => member.userId !== fromUser.id);
+
const message = {
id: this.idService.gen(),
fromUserId: fromUser.id,
@@ -216,7 +236,7 @@ export class ChatService {
this.globalEventService.publishChatRoomStream(toRoom.id, 'message', packedMessage);
const redisPipeline = this.redisClient.pipeline();
- for (const membership of memberships) {
+ for (const membership of membershipsOtherThanMe) {
if (membership.isMuted) continue;
redisPipeline.set(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`, message.id);
@@ -227,7 +247,7 @@ export class ChatService {
// 3秒経っても既読にならなかったらイベント発行
setTimeout(async () => {
const redisPipeline = this.redisClient.pipeline();
- for (const membership of memberships) {
+ for (const membership of membershipsOtherThanMe) {
redisPipeline.get(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`);
}
const markers = await redisPipeline.exec();
@@ -237,12 +257,12 @@ export class ChatService {
const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted);
- for (let i = 0; i < memberships.length; i++) {
+ for (let i = 0; i < membershipsOtherThanMe.length; i++) {
const marker = markers[i][1];
if (marker == null) continue;
- this.globalEventService.publishMainStream(memberships[i].userId, 'newChatMessage', packedMessageForTo);
- //this.pushNotificationService.pushNotification(memberships[i].userId, 'newChatMessage', packedMessageForTo);
+ this.globalEventService.publishMainStream(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo);
+ //this.pushNotificationService.pushNotification(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo);
}
}, 3000);
@@ -282,6 +302,20 @@ export class ChatService {
}
@bindThis
+ public async hasPermissionToViewRoomTimeline(meId: MiUser['id'], room: MiChatRoom) {
+ if (await this.isRoomMember(room, meId)) {
+ return true;
+ } else {
+ const iAmModerator = await this.roleService.isModerator({ id: meId });
+ if (iAmModerator) {
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ @bindThis
public async deleteMessage(message: MiChatMessage) {
await this.chatMessagesRepository.delete(message.id);
@@ -330,7 +364,7 @@ export class ChatService {
@bindThis
public async roomTimeline(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId)
- .where('message.toRoomId = :roomId', { roomId })
+ .andWhere('message.toRoomId = :roomId', { roomId })
.leftJoinAndSelect('message.file', 'file')
.leftJoinAndSelect('message.fromUser', 'fromUser');
@@ -489,8 +523,33 @@ export class ChatService {
}
@bindThis
- public async deleteRoom(room: MiChatRoom) {
+ public async hasPermissionToDeleteRoom(meId: MiUser['id'], room: MiChatRoom) {
+ if (room.ownerId === meId) {
+ return true;
+ }
+
+ const iAmModerator = await this.roleService.isModerator({ id: meId });
+ if (iAmModerator) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @bindThis
+ public async deleteRoom(room: MiChatRoom, deleter?: MiUser) {
await this.chatRoomsRepository.delete(room.id);
+
+ if (deleter) {
+ const deleterIsModerator = await this.roleService.isModerator(deleter);
+
+ if (deleterIsModerator) {
+ this.moderationLogService.log(deleter, 'deleteChatRoom', {
+ roomId: room.id,
+ room: room,
+ });
+ }
+ }
}
@bindThis
@@ -542,13 +601,27 @@ export class ChatService {
const created = await this.chatRoomInvitationsRepository.insertOne(invitation);
+ this.notificationService.createNotification(inviteeId, 'chatRoomInvitationReceived', {
+ invitationId: invitation.id,
+ }, inviterId);
+
return created;
}
@bindThis
+ public async getSentRoomInvitationsWithPagination(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatRoomInvitation['id'] | null, untilId?: MiChatRoomInvitation['id'] | null) {
+ const query = this.queryService.makePaginationQuery(this.chatRoomInvitationsRepository.createQueryBuilder('invitation'), sinceId, untilId)
+ .andWhere('invitation.roomId = :roomId', { roomId });
+
+ const invitations = await query.take(limit).getMany();
+
+ return invitations;
+ }
+
+ @bindThis
public async getOwnedRoomsWithPagination(ownerId: MiUser['id'], limit: number, sinceId?: MiChatRoom['id'] | null, untilId?: MiChatRoom['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatRoomsRepository.createQueryBuilder('room'), sinceId, untilId)
- .where('room.ownerId = :ownerId', { ownerId });
+ .andWhere('room.ownerId = :ownerId', { ownerId });
const rooms = await query.take(limit).getMany();
@@ -558,7 +631,7 @@ export class ChatService {
@bindThis
public async getReceivedRoomInvitationsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomInvitation['id'] | null, untilId?: MiChatRoomInvitation['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatRoomInvitationsRepository.createQueryBuilder('invitation'), sinceId, untilId)
- .where('invitation.userId = :userId', { userId })
+ .andWhere('invitation.userId = :userId', { userId })
.andWhere('invitation.ignored = FALSE');
const invitations = await query.take(limit).getMany();
@@ -622,7 +695,7 @@ export class ChatService {
@bindThis
public async getRoomMembershipsWithPagination(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId)
- .where('membership.roomId = :roomId', { roomId });
+ .andWhere('membership.roomId = :roomId', { roomId });
const memberships = await query.take(limit).getMany();
@@ -692,24 +765,10 @@ export class ChatService {
public async react(messageId: MiChatMessage['id'], userId: MiUser['id'], reaction_: string) {
let reaction;
- // TODO: ReactionServiceのやつと共通化
- function normalize(x: string) {
- const match = emojiRegex.exec(x);
- if (match) {
- // 合字を含む1つの絵文字
- const unicode = match[0];
-
- // 異体字セレクタ除去
- return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
- } else {
- throw new Error('invalid emoji');
- }
- }
-
const custom = reaction_.match(isCustomEmojiRegexp);
if (custom == null) {
- reaction = normalize(reaction_);
+ reaction = normalizeEmojiString(reaction_);
} else {
const name = custom[1];
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
@@ -769,9 +828,55 @@ export class ChatService {
}
@bindThis
+ public async unreact(messageId: MiChatMessage['id'], userId: MiUser['id'], reaction_: string) {
+ let reaction;
+
+ const custom = reaction_.match(isCustomEmojiRegexp);
+
+ if (custom == null) {
+ reaction = normalizeEmojiString(reaction_);
+ } else { // 削除されたカスタム絵文字のリアクションを削除したいかもしれないので絵文字の存在チェックはする必要なし
+ const name = custom[1];
+ reaction = `:${name}:`;
+ }
+
+ // NOTE: 自分のリアクションを(あれば)削除するだけなので諸々の権限チェックは必要なし
+
+ const message = await this.chatMessagesRepository.findOneByOrFail({ id: messageId });
+
+ const room = message.toRoomId ? await this.chatRoomsRepository.findOneByOrFail({ id: message.toRoomId }) : null;
+
+ await this.chatMessagesRepository.createQueryBuilder().update()
+ .set({
+ reactions: () => `array_remove("reactions", '${userId}/${reaction}')`,
+ })
+ .where('id = :id', { id: message.id })
+ .execute();
+
+ // TODO: 実際に削除が行われたときのみイベントを発行する
+
+ if (room) {
+ this.globalEventService.publishChatRoomStream(room.id, 'unreact', {
+ messageId: message.id,
+ user: await this.userEntityService.pack(userId),
+ reaction,
+ });
+ } else {
+ this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId!, 'unreact', {
+ messageId: message.id,
+ reaction,
+ });
+ this.globalEventService.publishChatUserStream(message.toUserId!, message.fromUserId, 'unreact', {
+ messageId: message.id,
+ reaction,
+ });
+ }
+ }
+
+ @bindThis
public async getMyMemberships(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId)
- .where('membership.userId = :userId', { userId });
+ .andWhere('membership.userId = :userId', { userId });
const memberships = await query.take(limit).getMany();
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 94d6311e0d..27d8ee9891 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -172,6 +172,11 @@ export interface ChatEventTypes {
user?: Packed<'UserLite'>;
messageId: MiChatMessage['id'];
};
+ unreact: {
+ reaction: string;
+ user?: Packed<'UserLite'>;
+ messageId: MiChatMessage['id'];
+ };
}
export interface ReversiEventTypes {
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index cea674d96c..77e6a1c7e7 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -16,6 +16,7 @@ import { bindThis } from '@/decorators.js';
import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
import { CacheService } from '@/core/CacheService.js';
import { RoleEntityService } from './RoleEntityService.js';
+import { ChatEntityService } from './ChatEntityService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
@@ -27,6 +28,7 @@ export class NotificationEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService;
private roleEntityService: RoleEntityService;
+ private chatEntityService: ChatEntityService;
constructor(
private moduleRef: ModuleRef,
@@ -41,9 +43,6 @@ export class NotificationEntityService implements OnModuleInit {
private followRequestsRepository: FollowRequestsRepository,
private cacheService: CacheService,
-
- //private userEntityService: UserEntityService,
- //private noteEntityService: NoteEntityService,
) {
}
@@ -51,6 +50,7 @@ export class NotificationEntityService implements OnModuleInit {
this.userEntityService = this.moduleRef.get('UserEntityService');
this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.roleEntityService = this.moduleRef.get('RoleEntityService');
+ this.chatEntityService = this.moduleRef.get('ChatEntityService');
}
/**
@@ -59,7 +59,6 @@ export class NotificationEntityService implements OnModuleInit {
async #packInternal <T extends MiNotification | MiGroupedNotification> (
src: T,
meId: MiUser['id'],
-
options: {
checkValidNotifier?: boolean;
},
@@ -92,7 +91,7 @@ export class NotificationEntityService implements OnModuleInit {
// if the user has been deleted, don't show this notification
if (needsUser && !userIfNeed) return null;
- // #region Grouped notifications
+ //#region Grouped notifications
if (notification.type === 'reaction:grouped') {
const reactions = (await Promise.all(notification.reactions.map(async reaction => {
const user = hint?.packedUsers != null
@@ -137,7 +136,7 @@ export class NotificationEntityService implements OnModuleInit {
users,
});
}
- // #endregion
+ //#endregion
const needsRole = notification.type === 'roleAssigned';
const role = needsRole
@@ -151,6 +150,13 @@ export class NotificationEntityService implements OnModuleInit {
return null;
}
+ const needsChatRoomInvitation = notification.type === 'chatRoomInvitationReceived';
+ const chatRoomInvitation = needsChatRoomInvitation ? await this.chatEntityService.packRoomInvitation(notification.invitationId, { id: meId }).catch(() => null) : undefined;
+ // if the invitation has been deleted, don't show this notification
+ if (needsChatRoomInvitation && !chatRoomInvitation) {
+ return null;
+ }
+
return await awaitAll({
id: notification.id,
createdAt: new Date(notification.createdAt).toISOString(),
@@ -164,6 +170,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
+ ...(notification.type === 'chatRoomInvitationReceived' ? {
+ invitation: chatRoomInvitation,
+ } : {}),
...(notification.type === 'followRequestAccepted' ? {
message: notification.message,
} : {}),
diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts
index 46f88b9f22..cff95cbf6e 100644
--- a/packages/backend/src/models/Notification.ts
+++ b/packages/backend/src/models/Notification.ts
@@ -76,6 +76,12 @@ export type MiNotification = {
createdAt: string;
roleId: MiRole['id'];
} | {
+ type: 'chatRoomInvitationReceived';
+ id: string;
+ createdAt: string;
+ notifierId: MiUser['id'];
+ invitationId: string;
+} | {
type: 'achievementEarned';
id: string;
createdAt: string;
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index bf33dab575..e7f8359556 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -294,6 +294,21 @@ export const packedNotificationSchema = {
type: {
type: 'string',
optional: false, nullable: false,
+ enum: ['chatRoomInvitationReceived'],
+ },
+ invitation: {
+ type: 'object',
+ ref: 'ChatRoomInvitation',
+ optional: false, nullable: false,
+ },
+ },
+ }, {
+ type: 'object',
+ properties: {
+ ...baseSchema.properties,
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
enum: ['achievementEarned'],
},
achievement: {
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index e7160aa40d..e6f85afcce 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -681,6 +681,7 @@ export const packedMeDetailedOnlySchema = {
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },
+ chatRoomInvitationReceived: { optional: true, ...notificationRecieveConfig },
achievementEarned: { optional: true, ...notificationRecieveConfig },
app: { optional: true, ...notificationRecieveConfig },
test: { optional: true, ...notificationRecieveConfig },
diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts
index 562f498cbd..e48b3de8ee 100644
--- a/packages/backend/src/server/api/endpoint-list.ts
+++ b/packages/backend/src/server/api/endpoint-list.ts
@@ -423,6 +423,7 @@ export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/creat
export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js';
export * as 'chat/messages/show' from './endpoints/chat/messages/show.js';
export * as 'chat/messages/react' from './endpoints/chat/messages/react.js';
+export * as 'chat/messages/unreact' from './endpoints/chat/messages/unreact.js';
export * as 'chat/messages/user-timeline' from './endpoints/chat/messages/user-timeline.js';
export * as 'chat/messages/room-timeline' from './endpoints/chat/messages/room-timeline.js';
export * as 'chat/messages/search' from './endpoints/chat/messages/search.js';
@@ -439,5 +440,6 @@ export * as 'chat/rooms/members' from './endpoints/chat/rooms/members.js';
export * as 'chat/rooms/invitations/create' from './endpoints/chat/rooms/invitations/create.js';
export * as 'chat/rooms/invitations/ignore' from './endpoints/chat/rooms/invitations/ignore.js';
export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitations/inbox.js';
+export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.js';
export * as 'chat/history' from './endpoints/chat/history.js';
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 669bffe2dc..6dbfbf9d9a 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -111,6 +111,7 @@ export const meta = {
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },
+ chatRoomInvitationReceived: { optional: true, ...notificationRecieveConfig },
achievementEarned: { optional: true, ...notificationRecieveConfig },
app: { optional: true, ...notificationRecieveConfig },
test: { optional: true, ...notificationRecieveConfig },
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts
index ccc0030403..7aef35db04 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts
@@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchRoom);
}
- if (!(await this.chatService.isRoomMember(room, me.id))) {
+ if (!await this.chatService.hasPermissionToViewRoomTimeline(me.id, room)) {
throw new ApiError(meta.errors.noSuchRoom);
}
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts
new file mode 100644
index 0000000000..4eb25259fb
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'write:chat',
+
+ res: {
+ },
+
+ errors: {
+ noSuchMessage: {
+ message: 'No such message.',
+ code: 'NO_SUCH_MESSAGE',
+ id: 'c39ea42f-e3ca-428a-ad57-390e0a711595',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ messageId: { type: 'string', format: 'misskey:id' },
+ reaction: { type: 'string' },
+ },
+ required: ['messageId', 'reaction'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.chatService.unreact(ps.messageId, me.id, ps.reaction);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts
index 2ef0a778f1..1d77a06dd8 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts
@@ -42,11 +42,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
- const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
+ const room = await this.chatService.findRoomById(ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);
}
- await this.chatService.deleteRoom(room);
+
+ if (!await this.chatService.hasPermissionToDeleteRoom(me.id, room)) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ await this.chatService.deleteRoom(room, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts
new file mode 100644
index 0000000000..12d496e94b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts
@@ -0,0 +1,67 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { ChatService } from '@/core/ChatService.js';
+import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
+
+export const meta = {
+ tags: ['chat'],
+
+ requireCredential: true,
+
+ kind: 'read:chat',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'ChatRoomInvitation',
+ },
+ },
+
+ errors: {
+ noSuchRoom: {
+ message: 'No such room.',
+ code: 'NO_SUCH_ROOM',
+ id: 'a3c6b309-9717-4316-ae94-a69b53437237',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roomId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roomId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private chatService: ChatService,
+ private chatEntityService: ChatEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
+ if (room == null) {
+ throw new ApiError(meta.errors.noSuchRoom);
+ }
+
+ const invitations = await this.chatService.getSentRoomInvitationsWithPagination(ps.roomId, ps.limit, ps.sinceId, ps.untilId);
+ return this.chatEntityService.packRoomInvitations(invitations, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 8abe250f9f..284c986da3 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -237,6 +237,7 @@ export const paramDef = {
receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig,
roleAssigned: notificationRecieveConfig,
+ chatRoomInvitationReceived: notificationRecieveConfig,
achievementEarned: notificationRecieveConfig,
app: notificationRecieveConfig,
test: notificationRecieveConfig,
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 920fed57b1..53eb4e6f3e 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -15,6 +15,7 @@
* receiveFollowRequest - フォローリクエストされた
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
* roleAssigned - ロールが付与された
+ * chatRoomInvitationReceived - チャットルームに招待された
* achievementEarned - 実績を獲得
* exportCompleted - エクスポートが完了
* login - ログイン
@@ -35,6 +36,7 @@ export const notificationTypes = [
'receiveFollowRequest',
'followRequestAccepted',
'roleAssigned',
+ 'chatRoomInvitationReceived',
'achievementEarned',
'exportCompleted',
'login',
@@ -134,6 +136,7 @@ export const moderationLogTypes = [
'deletePage',
'deleteFlash',
'deleteGalleryPost',
+ 'deleteChatRoom',
'acceptQuotesUser',
'rejectQuotesUser',
'acceptQuotesInstance',
@@ -453,6 +456,10 @@ export type ModerationLogPayloads = {
id: string;
host: string;
};
+ deleteChatRoom: {
+ roomId: string;
+ room: any;
+ };
updateProxyAccountDescription: {
before: string | null;
after: string | null;
diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts
index f20152d08e..8341fed41b 100644
--- a/packages/frontend-shared/js/const.ts
+++ b/packages/frontend-shared/js/const.ts
@@ -126,6 +126,7 @@ export const notificationTypes = [
'receiveFollowRequest',
'followRequestAccepted',
'roleAssigned',
+ 'chatRoomInvitationReceived',
'achievementEarned',
'exportCompleted',
'login',
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 38471cd86a..537d61d1a1 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -394,7 +394,7 @@ export async function mainBoot() {
main.on('newChatMessage', () => {
updateCurrentAccountPartial({ hasUnreadChatMessages: true });
- sound.playMisskeySfx('chat');
+ sound.playMisskeySfx('chatMessage');
});
main.on('readAllAnnouncements', () => {
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index 43eb2e5f80..c52fdb898e 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template>
<template #suffix>#{{ report.targetUserId.toUpperCase() }}</template>
- <div style="container-type: inline-size;">
+ <div style="height: 300px; --MI-stickyTop: 0; --MI-stickyBottom: 0;">
<RouterView :router="targetRouter"/>
</div>
</MkFolder>
@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></template>
<template #suffix>#{{ report.reporterId.toUpperCase() }}</template>
- <div style="container-type: inline-size;">
+ <div style="height: 300px; --MI-stickyTop: 0; --MI-stickyBottom: 0;">
<RouterView :router="reporterRouter"/>
</div>
</MkFolder>
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index fed3dafeea..2e2693d319 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -437,7 +437,8 @@ const keymap = {
},
} as const satisfies Keymap;
-provide('react', (reaction: string) => {
+provide(DI.mfmEmojiReactCallback, (reaction) => {
+ sound.playMisskeySfx('reaction');
misskeyApi('notes/reactions/create', {
noteId: appearNote.value.id,
reaction: reaction,
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 531232f86a..d42c46fba0 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -290,6 +290,7 @@ import { isEnabledUrlPreview } from '@/instance.js';
import { getAppearNote } from '@/utility/get-appear-note.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
+import { DI } from '@/di.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@@ -400,7 +401,8 @@ const keymap = {
},
} as const satisfies Keymap;
-provide('react', (reaction: string) => {
+provide(DI.mfmEmojiReactCallback, (reaction) => {
+ sound.playMisskeySfx('reaction');
misskeyApi('notes/reactions/create', {
noteId: appearNote.value.id,
reaction: reaction,
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 8e86d67ab9..ab3947adfb 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -46,6 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
<i v-else-if="notification.type === 'createToken'" class="ti ti-key"></i>
+ <i v-else-if="notification.type === 'chatRoomInvitationReceived'" class="ti ti-messages"></i>
<template v-else-if="notification.type === 'roleAssigned'">
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
<i v-else class="ti ti-badges"></i>
@@ -68,6 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
+ <span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span>
<span v-else-if="notification.type === 'createToken'">{{ i18n.ts._notification.createToken }}</span>
@@ -114,6 +116,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
{{ notification.role.name }}
</div>
+ <div v-else-if="notification.type === 'chatRoomInvitationReceived'" :class="$style.text">
+ {{ notification.invitation.room.name }}
+ </div>
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
</MkA>
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 70af2c7962..bd57e72dcc 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -35,11 +35,11 @@ import { customEmojisMap } from '@/custom-emojis.js';
import * as os from '@/os.js';
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
-import * as sound from '@/utility/sound.js';
import { i18n } from '@/i18n.js';
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
import { $i } from '@/i.js';
import { prefer } from '@/preferences.js';
+import { DI } from '@/di.js';
const props = defineProps<{
name: string;
@@ -53,7 +53,7 @@ const props = defineProps<{
fallbackToImage?: boolean;
}>();
-const react = inject<((name: string) => void) | null>('react', null);
+const react = inject(DI.mfmEmojiReactCallback);
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
@@ -111,7 +111,6 @@ function onClick(ev: MouseEvent) {
icon: 'ph-smiley ph-bold ph-lg',
action: () => {
react(`:${props.name}:`);
- sound.playMisskeySfx('reaction');
},
});
}
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index 432de24478..f5323690d0 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -15,9 +15,9 @@ import { char2fluentEmojiFilePath, char2twemojiFilePath, char2tossfaceFilePath }
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
-import * as sound from '@/utility/sound.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
+import { DI } from '@/di.js';
const props = defineProps<{
emoji: string;
@@ -25,7 +25,7 @@ const props = defineProps<{
menuReaction?: boolean;
}>();
-const react = inject<((name: string) => void) | null>('react', null);
+const react = inject(DI.mfmEmojiReactCallback);
const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : prefer.s.emojiStyle === 'tossface' ? char2tossfaceFilePath : char2fluentEmojiFilePath;
@@ -61,7 +61,6 @@ function onClick(ev: MouseEvent) {
icon: 'ph-smiley ph-bold ph-lg',
action: () => {
react(props.emoji);
- sound.playMisskeySfx('reaction');
},
});
}
diff --git a/packages/frontend/src/di.ts b/packages/frontend/src/di.ts
index f9fc282315..b58c8c9659 100644
--- a/packages/frontend/src/di.ts
+++ b/packages/frontend/src/di.ts
@@ -14,4 +14,5 @@ export const DI = {
viewId: Symbol() as InjectionKey<string>,
currentStickyTop: Symbol() as InjectionKey<Ref<number>>,
currentStickyBottom: Symbol() as InjectionKey<Ref<number>>,
+ mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>,
};
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index 9d4988338a..7c15c9666a 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -132,7 +132,7 @@ export const navbarItemDef = reactive({
},
chat: {
title: i18n.ts.chat,
- icon: 'ti ti-message',
+ icon: 'ti ti-messages',
to: '/chat',
indicated: computed(() => $i != null && $i.hasUnreadChatMessages),
},
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 3142c5a45d..b1bd45baab 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -354,7 +354,6 @@ defineExpose({
&.wide {
display: flex;
margin: 0 auto;
- height: 100%;
> .nav {
position: sticky;
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index 67db9ed6d3..5ea1c7f599 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -58,6 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
'deletePage',
'deleteFlash',
'deleteGalleryPost',
+ 'deleteChatRoom',
'clearUserFiles',
'clearRemoteFiles',
'clearOwnerlessFiles',
@@ -115,6 +116,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span>
<span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span>
<span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span>
+ <span v-else-if="log.type === 'deleteChatRoom'">: @{{ log.info.room.name }}</span>
<span v-else-if="log.type === 'clearUserFiles'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'nsfwUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'unNsfwUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue
index 1e7f8e20ea..cbb817de05 100644
--- a/packages/frontend/src/pages/chat/XMessage.vue
+++ b/packages/frontend/src/pages/chat/XMessage.vue
@@ -7,9 +7,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.root, { [$style.isMe]: isMe }]">
<MkAvatar :class="$style.avatar" :user="message.fromUser" :link="!isMe" :preview="false"/>
<div :class="$style.body">
+ <div v-if="!isMe && prefer.s['chat.showSenderName']" :class="$style.header"><MkUserName :user="message.fromUser"/></div>
<MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe">
<div v-if="!message.isDeleted" :class="$style.content">
- <Mfm v-if="message.text" ref="text" class="_selectable" :text="message.text" :i="$i"/>
+ <Mfm
+ v-if="message.text"
+ ref="text"
+ class="_selectable"
+ :text="message.text"
+ :i="$i"
+ :nyaize="'respect'"
+ :enableEmojiMenu="true"
+ :enableEmojiMenuReaction="true"
+ />
<MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
</div>
<div v-else :class="$style.content">
@@ -31,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass="prefer.s.animation ? $style.transition_reaction_move : ''"
tag="div" :class="$style.reactions"
>
- <div v-for="record in message.reactions" :key="record.reaction + record.user.id" :class="$style.reaction">
+ <div v-for="record in message.reactions" :key="record.reaction + record.user.id" :class="[$style.reaction, record.user.id === $i.id ? $style.reactionMy : null]" @click="onReactionClick(record)">
<MkAvatar :user="record.user" :link="false" :class="$style.reactionAvatar"/>
<MkReactionIcon
:withTooltip="true"
@@ -46,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent } from 'vue';
+import { computed, defineAsyncComponent, provide } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
@@ -64,6 +74,7 @@ import { reactionPicker } from '@/utility/reaction-picker.js';
import * as sound from '@/utility/sound.js';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import { prefer } from '@/preferences.js';
+import { DI } from '@/di.js';
const $i = ensureSignin();
@@ -75,10 +86,17 @@ const props = defineProps<{
const isMe = computed(() => props.message.fromUserId === $i.id);
const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
+provide(DI.mfmEmojiReactCallback, (reaction) => {
+ sound.playMisskeySfx('reaction');
+ misskeyApi('chat/messages/react', {
+ messageId: props.message.id,
+ reaction: reaction,
+ });
+});
+
function react(ev: MouseEvent) {
reactionPicker.show(ev.currentTarget ?? ev.target, null, async (reaction) => {
sound.playMisskeySfx('reaction');
-
misskeyApi('chat/messages/react', {
messageId: props.message.id,
reaction: reaction,
@@ -86,6 +104,23 @@ function react(ev: MouseEvent) {
});
}
+function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) {
+ if (record.user.id === $i.id) {
+ misskeyApi('chat/messages/unreact', {
+ messageId: props.message.id,
+ reaction: record.reaction,
+ });
+ } else {
+ if (!props.message.reactions.some(r => r.user.id === $i.id && r.reaction === record.reaction)) {
+ sound.playMisskeySfx('reaction');
+ misskeyApi('chat/messages/react', {
+ messageId: props.message.id,
+ reaction: record.reaction,
+ });
+ }
+ }
+}
+
function showMenu(ev: MouseEvent) {
const menu: MenuItem[] = [];
@@ -191,6 +226,10 @@ function showMenu(ev: MouseEvent) {
margin: 0 12px;
}
+.header {
+ font-size: 80%;
+}
+
.content {
overflow: clip;
overflow-wrap: break-word;
@@ -230,6 +269,10 @@ function showMenu(ev: MouseEvent) {
border: solid 1px var(--MI_THEME-divider);
border-radius: 999px;
padding: 8px;
+
+ &.reactionMy {
+ border-color: var(--MI_THEME-accent);
+ }
}
.reactionAvatar {
diff --git a/packages/frontend/src/pages/chat/home.home.vue b/packages/frontend/src/pages/chat/home.home.vue
index 1d0605136c..0affef6333 100644
--- a/packages/frontend/src/pages/chat/home.home.vue
+++ b/packages/frontend/src/pages/chat/home.home.vue
@@ -40,10 +40,11 @@ SPDX-License-Identifier: AGPL-3.0-only
class="_panel"
:to="item.message.toRoomId ? `/chat/room/${item.message.toRoomId}` : `/chat/user/${item.other!.id}`"
>
- <MkAvatar v-if="item.other" :class="$style.messageAvatar" :user="item.other" indicator :preview="false"/>
+ <MkAvatar v-if="item.message.toRoomId" :class="$style.messageAvatar" :user="item.message.fromUser" indicator :preview="false"/>
+ <MkAvatar v-else-if="item.other" :class="$style.messageAvatar" :user="item.other" indicator :preview="false"/>
<div :class="$style.messageBody">
<header v-if="item.message.toRoom" :class="$style.messageHeader">
- <span :class="$style.messageHeaderName">{{ item.message.toRoom.name }}</span>
+ <span :class="$style.messageHeaderName"><i class="ti ti-users"></i> {{ item.message.toRoom.name }}</span>
<MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/>
</header>
<header v-else :class="$style.messageHeader">
@@ -55,17 +56,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkA>
</div>
- <div v-if="!fetching && history.length == 0" class="_fullinfo">
+ <div v-if="!initializing && history.length == 0" class="_fullinfo">
<div>{{ i18n.ts._chat.noHistory }}</div>
</div>
- <MkLoading v-if="fetching"/>
+ <MkLoading v-if="initializing"/>
</MkFoldableSection>
</div>
</template>
<script lang="ts" setup>
-import { computed, onMounted, ref } from 'vue';
+import { computed, onActivated, onDeactivated, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
import XMessage from './XMessage.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
@@ -81,7 +83,8 @@ const $i = ensureSignin();
const router = useRouter();
-const fetching = ref(true);
+const initializing = ref(true);
+const fetching = ref(false);
const history = ref<{
id: string;
message: Misskey.entities.ChatMessage;
@@ -142,6 +145,8 @@ async function search() {
}
async function fetchHistory() {
+ if (fetching.value) return;
+
fetching.value = true;
const [userMessages, roomMessages] = await Promise.all([
@@ -159,10 +164,35 @@ async function fetchHistory() {
}));
fetching.value = false;
+ initializing.value = false;
updateCurrentAccountPartial({ hasUnreadChatMessages: false });
}
+let isActivated = true;
+
+onActivated(() => {
+ isActivated = true;
+});
+
+onDeactivated(() => {
+ isActivated = false;
+});
+
+useInterval(() => {
+ // TODO: DOM的にバックグラウンドになっていないかどうかも考慮する
+ if (!window.document.hidden && isActivated) {
+ fetchHistory();
+ }
+}, 1000 * 10, {
+ immediate: false,
+ afterMounted: true,
+});
+
+onActivated(() => {
+ fetchHistory();
+});
+
onMounted(() => {
fetchHistory();
});
diff --git a/packages/frontend/src/pages/chat/home.vue b/packages/frontend/src/pages/chat/home.vue
index c2b272a42d..9bb7235a64 100644
--- a/packages/frontend/src/pages/chat/home.vue
+++ b/packages/frontend/src/pages/chat/home.vue
@@ -52,7 +52,7 @@ const headerTabs = computed(() => [{
definePage(() => ({
title: i18n.ts.chat + ' (beta)',
- icon: 'ti ti-message',
+ icon: 'ti ti-messages',
}));
</script>
diff --git a/packages/frontend/src/pages/chat/message.vue b/packages/frontend/src/pages/chat/message.vue
index be8be7e5d1..975d1a2be9 100644
--- a/packages/frontend/src/pages/chat/message.vue
+++ b/packages/frontend/src/pages/chat/message.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading/>
</div>
<div v-else>
- <XMessage :message="message"/>
+ <XMessage :message="message" :isSearchResult="true"/>
</div>
</MkSpacer>
</PageWithHeader>
diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue
index aba9d6061f..27ddbeb565 100644
--- a/packages/frontend/src/pages/chat/room.form.vue
+++ b/packages/frontend/src/pages/chat/room.form.vue
@@ -151,8 +151,16 @@ function onDrop(ev: DragEvent): void {
}
function onKeydown(ev: KeyboardEvent) {
- if ((ev.key === 'Enter') && (ev.ctrlKey || ev.metaKey)) {
- send();
+ if (ev.key === 'Enter') {
+ if (prefer.s['chat.sendOnEnter']) {
+ if (!(ev.ctrlKey || ev.metaKey || ev.shiftKey)) {
+ send();
+ }
+ } else {
+ if ((ev.ctrlKey || ev.metaKey)) {
+ send();
+ }
+ }
}
}
diff --git a/packages/frontend/src/pages/chat/room.info.vue b/packages/frontend/src/pages/chat/room.info.vue
index 7d38d07b3a..7e10336fd3 100644
--- a/packages/frontend/src/pages/chat/room.info.vue
+++ b/packages/frontend/src/pages/chat/room.info.vue
@@ -17,6 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<hr>
+ <MkButton v-if="isOwner || ($i.isAdmin || $i.isModerator)" danger @click="del">{{ i18n.ts._chat.deleteRoom }}</MkButton>
+
<MkSwitch v-if="!isOwner" v-model="isMuted">
<template #label>{{ i18n.ts._chat.muteThisRoom }}</template>
</MkSwitch>
@@ -34,7 +36,9 @@ import { ensureSignin } from '@/i.js';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
+import { useRouter } from '@/router.js';
+const router = useRouter();
const $i = ensureSignin();
const props = defineProps<{
@@ -56,6 +60,19 @@ function save() {
});
}
+async function del() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.areYouSure,
+ });
+ if (canceled) return;
+
+ misskeyApi('chat/rooms/delete', {
+ roomId: props.room.id,
+ });
+ router.push('/chat');
+}
+
const isMuted = ref(props.room.isMuted);
watch(isMuted, async () => {
diff --git a/packages/frontend/src/pages/chat/room.members.vue b/packages/frontend/src/pages/chat/room.members.vue
index d20216a81c..2b31efab38 100644
--- a/packages/frontend/src/pages/chat/room.members.vue
+++ b/packages/frontend/src/pages/chat/room.members.vue
@@ -18,6 +18,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserCardMini :user="membership.user"/>
</MkA>
</div>
+
+ <template v-if="isOwner">
+ <hr>
+
+ <div>{{ i18n.ts._chat.sentInvitations }}</div>
+
+ <div v-for="invitation in invitations" :key="invitation.id" :class="$style.invitation">
+ <MkA :class="$style.invitationBody" :to="`${userPage(invitation.user)}`">
+ <MkUserCardMini :user="invitation.user"/>
+ </MkA>
+ </div>
+ </template>
</div>
</template>
@@ -47,12 +59,20 @@ const isOwner = computed(() => {
});
const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]);
+const invitations = ref<Misskey.entities.ChatRoomInvitation[]>([]);
onMounted(async () => {
memberships.value = await misskeyApi('chat/rooms/members', {
roomId: props.room.id,
limit: 50,
});
+
+ if (isOwner.value) {
+ invitations.value = await misskeyApi('chat/rooms/invitations/outbox', {
+ roomId: props.room.id,
+ limit: 50,
+ });
+ }
});
</script>
@@ -65,9 +85,15 @@ onMounted(async () => {
flex: 1;
min-width: 0;
margin-right: 8px;
+}
- &:hover {
- text-decoration: none;
- }
+.invitation {
+ display: flex;
+}
+
+.invitationBody {
+ flex: 1;
+ min-width: 0;
+ margin-right: 8px;
}
</style>
diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue
index 15e9f43db2..5938fd2688 100644
--- a/packages/frontend/src/pages/chat/room.vue
+++ b/packages/frontend/src/pages/chat/room.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="user.chatScope === 'followers'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowers }}</div>
<div v-else-if="user.chatScope === 'following'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowing }}</div>
<div v-else-if="user.chatScope === 'mutual'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromMutualFollowing }}</div>
- <div v-else>{{ i18n.ts._chat.thisUserNotAllowedChatAnyone }}</div>
+ <div v-else-if="user.chatScope === 'none'">{{ i18n.ts._chat.thisUserNotAllowedChatAnyone }}</div>
</template>
<template v-else-if="room">
<div>{{ i18n.ts._chat.inviteUserToChat }}</div>
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
- <div v-else class="_gaps">
+ <div v-else ref="timelineEl" class="_gaps">
<div v-if="canFetchMore">
<MkButton :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">{{ i18n.ts.loadMore }}</MkButton>
</div>
@@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
import * as Misskey from 'misskey-js';
-import { isTailVisible } from '@@/js/scroll.js';
+import { getScrollContainer, isTailVisible } from '@@/js/scroll.js';
import XMessage from './XMessage.vue';
import XForm from './room.form.vue';
import XSearch from './room.search.vue';
@@ -92,6 +92,7 @@ import { definePage } from '@/page.js';
import { prefer } from '@/preferences.js';
import MkButton from '@/components/MkButton.vue';
import { useRouter } from '@/router.js';
+import { useMutationObserver } from '@/use/use-mutation-observer.js';
const $i = ensureSignin();
const router = useRouter();
@@ -109,6 +110,26 @@ const user = ref<Misskey.entities.UserDetailed | null>(null);
const room = ref<Misskey.entities.ChatRoom | null>(null);
const connection = ref<Misskey.ChannelConnection<Misskey.Channels['chatUser'] | Misskey.Channels['chatRoom']> | null>(null);
const showIndicator = ref(false);
+const timelineEl = useTemplateRef('timelineEl');
+
+const SCROLL_HEAD_THRESHOLD = 200;
+
+// column-reverseなので本来はスクロール位置の最下部への追従は不要なはずだが、おそらくブラウザのバグにより、最下部にスクロールした状態でも追従されない場合がある(スクロール位置が少数になることがあるのが関わっていそう)
+// そのため補助としてMutationObserverを使って追従を行う
+useMutationObserver(timelineEl, {
+ subtree: true,
+ childList: true,
+ attributes: false,
+}, () => {
+ const scrollContainer = getScrollContainer(timelineEl.value)!;
+ // column-reverseなのでscrollTopは負になる
+ if (-scrollContainer.scrollTop < SCROLL_HEAD_THRESHOLD) {
+ scrollContainer.scrollTo({
+ top: 0,
+ behavior: 'instant',
+ });
+ }
+});
function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage) {
const reactions = [...message.reactions];
@@ -149,6 +170,7 @@ async function initialize() {
connection.value.on('message', onMessage);
connection.value.on('deleted', onDeleted);
connection.value.on('react', onReact);
+ connection.value.on('unreact', onUnreact);
} else {
const [r, m] = await Promise.all([
misskeyApi('chat/rooms/show', { roomId: props.roomId }),
@@ -168,6 +190,7 @@ async function initialize() {
connection.value.on('message', onMessage);
connection.value.on('deleted', onDeleted);
connection.value.on('react', onReact);
+ connection.value.on('unreact', onUnreact);
}
window.document.addEventListener('visibilitychange', onVisibilitychange);
@@ -247,6 +270,16 @@ function onReact(ctx) {
}
}
+function onUnreact(ctx) {
+ const message = messages.value.find(m => m.id === ctx.messageId);
+ if (message) {
+ const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user.id);
+ if (index !== -1) {
+ message.reactions.splice(index, 1);
+ }
+ }
+}
+
function onIndicatorClick() {
showIndicator.value = false;
}
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index 706cb731eb..f944490a66 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -4,45 +4,55 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_gaps_m">
- <FormSlot>
- <template #label>{{ i18n.ts.navbar }}</template>
- <MkContainer :showHeader="false">
- <Sortable
- v-model="items"
- itemKey="id"
- :animation="150"
- :handle="'.' + $style.itemHandle"
- @start="e => e.item.classList.add('active')"
- @end="e => e.item.classList.remove('active')"
- >
- <template #item="{element,index}">
- <div
- v-if="element.type === '-' || navbarItemDef[element.type]"
- :class="$style.item"
- >
- <button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button>
- <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span>
- <button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button>
- </div>
- </template>
- </Sortable>
- </MkContainer>
- </FormSlot>
- <div class="_buttons">
- <MkButton @click="addItem"><i class="ti ti-plus"></i> {{ i18n.ts.addItem }}</MkButton>
- <MkButton danger @click="reset"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
- <MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
- </div>
+<SearchMarker path="/settings/navbar" :label="i18n.ts.navbar" icon="ti ti-list" :keywords="['navbar', 'menu', 'sidebar']">
+ <div class="_gaps_m">
+ <FormSlot>
+ <template #label>{{ i18n.ts.navbar }}</template>
+ <MkContainer :showHeader="false">
+ <Sortable
+ v-model="items"
+ itemKey="id"
+ :animation="150"
+ :handle="'.' + $style.itemHandle"
+ @start="e => e.item.classList.add('active')"
+ @end="e => e.item.classList.remove('active')"
+ >
+ <template #item="{element,index}">
+ <div
+ v-if="element.type === '-' || navbarItemDef[element.type]"
+ :class="$style.item"
+ >
+ <button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button>
+ <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span>
+ <button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button>
+ </div>
+ </template>
+ </Sortable>
+ </MkContainer>
+ </FormSlot>
+ <div class="_buttons">
+ <MkButton @click="addItem"><i class="ti ti-plus"></i> {{ i18n.ts.addItem }}</MkButton>
+ <MkButton danger @click="reset"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
+ <MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+ </div>
- <MkRadios v-model="menuDisplay">
- <template #label>{{ i18n.ts.display }}</template>
- <option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option>
- <option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option>
- <option value="top">{{ i18n.ts._menuDisplay.top }}</option>
+ <MkRadios v-model="menuDisplay">
+ <template #label>{{ i18n.ts.display }}</template>
+ <option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option>
+ <option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option>
+ <option value="top">{{ i18n.ts._menuDisplay.top }}</option>
<!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ i18n.ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
- </MkRadios>
-</div>
+ </MkRadios>
+
+ <SearchMarker :keywords="['navbar', 'sidebar', 'toggle', 'button', 'sub']">
+ <MkPreferenceContainer k="showNavbarSubButtons">
+ <MkSwitch v-model="showNavbarSubButtons">
+ <template #label><SearchLabel>{{ i18n.ts._settings.showNavbarSubButtons }}</SearchLabel></template>
+ </MkSwitch>
+ </MkPreferenceContainer>
+ </SearchMarker>
+ </div>
+</SearchMarker>
</template>
<script lang="ts" setup>
@@ -51,6 +61,8 @@ import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import FormSlot from '@/components/form/slot.vue';
import MkContainer from '@/components/MkContainer.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { store } from '@/store.js';
@@ -68,6 +80,7 @@ const items = ref(prefer.s.menu.map(x => ({
})));
const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
+const showNavbarSubButtons = prefer.model('showNavbarSubButtons');
async function addItem() {
const menu = Object.keys(navbarItemDef).filter(k => !prefer.s.menu.includes(k));
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 9256a565c4..816f8d7435 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['general']">
<MkFolder>
<template #label><SearchLabel>{{ i18n.ts.general }}</SearchLabel></template>
+ <template #icon><i class="ti ti-settings"></i></template>
<div class="_gaps_m">
<SearchMarker :keywords="['language']">
@@ -135,6 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['timeline', 'note']">
<MkFolder>
<template #label><SearchLabel>{{ i18n.ts._settings.timelineAndNote }}</SearchLabel></template>
+ <template #icon><i class="ti ti-notes"></i></template>
<div class="_gaps_m">
<div class="_gaps_s">
@@ -293,6 +295,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['post', 'form']">
<MkFolder>
<template #label><SearchLabel>{{ i18n.ts.postForm }}</SearchLabel></template>
+ <template #icon><i class="ti ti-edit"></i></template>
<div class="_gaps_m">
<div class="_gaps_s">
@@ -354,6 +357,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['notification']">
<MkFolder>
<template #label><SearchLabel>{{ i18n.ts.notifications }}</SearchLabel></template>
+ <template #icon><i class="ti ti-bell"></i></template>
<div class="_gaps_m">
<SearchMarker :keywords="['group']">
@@ -394,6 +398,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['datasaver']">
<MkFolder>
<template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template>
+ <template #icon><i class="ti ti-antenna-bars-3"></i></template>
<div class="_gaps_m">
<MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
@@ -424,9 +429,49 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder>
</SearchMarker>
+ <SearchMarker :keywords="['chat', 'messaging']">
+ <MkFolder>
+ <template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template>
+ <template #icon><i class="ti ti-messages"></i></template>
+
+ <div class="_gaps_s">
+ <SearchMarker :keywords="['show', 'sender', 'name']">
+ <MkPreferenceContainer k="chat.showSenderName">
+ <MkSwitch v-model="chatShowSenderName">
+ <template #label><SearchLabel>{{ i18n.ts._settings._chat.showSenderName }}</SearchLabel></template>
+ </MkSwitch>
+ </MkPreferenceContainer>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['send', 'enter', 'newline']">
+ <MkPreferenceContainer k="chat.sendOnEnter">
+ <MkSwitch v-model="chatSendOnEnter">
+ <template #label><SearchLabel>{{ i18n.ts._settings._chat.sendOnEnter }}</SearchLabel></template>
+ <template #caption>
+ <div class="_gaps_s">
+ <div>
+ <b>{{ i18n.ts._settings.ifOn }}:</b>
+ <div>{{ i18n.ts._chat.send }}: Enter</div>
+ <div>{{ i18n.ts._chat.newline }}: Shift + Enter</div>
+ </div>
+ <div>
+ <b>{{ i18n.ts._settings.ifOff }}:</b>
+ <div>{{ i18n.ts._chat.send }}: Ctrl + Enter</div>
+ <div>{{ i18n.ts._chat.newline }}: Enter</div>
+ </div>
+ </div>
+ </template>
+ </MkSwitch>
+ </MkPreferenceContainer>
+ </SearchMarker>
+ </div>
+ </MkFolder>
+ </SearchMarker>
+
<SearchMarker :keywords="['other']">
<MkFolder>
<template #label><SearchLabel>{{ i18n.ts.other }}</SearchLabel></template>
+ <template #icon><i class="ti ti-settings-cog"></i></template>
<div class="_gaps_m">
<div class="_gaps_s">
@@ -603,6 +648,8 @@ const emojiStyle = prefer.model('emojiStyle');
const useBlurEffectForModal = prefer.model('useBlurEffectForModal');
const useBlurEffect = prefer.model('useBlurEffect');
const defaultFollowWithReplies = prefer.model('defaultFollowWithReplies');
+const chatShowSenderName = prefer.model('chat.showSenderName');
+const chatSendOnEnter = prefer.model('chat.sendOnEnter');
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
@@ -630,6 +677,7 @@ watch([
squareAvatars,
highlightSensitiveMedia,
enableSeasonalScreenEffect,
+ chatShowSenderName,
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts
index 127ebeef0c..b588cc3b5f 100644
--- a/packages/frontend/src/preferences/def.ts
+++ b/packages/frontend/src/preferences/def.ts
@@ -329,6 +329,9 @@ export const PREF_DEF = {
makeEveryTextElementsSelectable: {
default: DEFAULT_DEVICE_KIND === 'desktop',
},
+ showNavbarSubButtons: {
+ default: true,
+ },
plugins: {
default: [] as Plugin[],
},
@@ -371,6 +374,13 @@ export const PREF_DEF = {
default: 'left' as 'left' | 'right' | 'center',
},
+ 'chat.showSenderName': {
+ default: false,
+ },
+ 'chat.sendOnEnter': {
+ default: false,
+ },
+
'game.dropAndFusion': {
default: {
bgmVolume: 0.25,
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index d590455ae5..db5ba75b2a 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -48,6 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</div>
<div :class="$style.bottom">
+ <button v-if="showWidgetButton" class="_button" :class="[$style.widget]" @click="() => emit('widgetButtonClick')">
+ <i class="ti ti-apps ti-fw"></i>
+ </button>
<button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="() => { os.post(); }">
<i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span>
</button>
@@ -65,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</svg>
-->
- <div v-if="!forceIconOnly" :class="$style.subButtons">
+ <div v-if="!forceIconOnly && prefer.r.showNavbarSubButtons.value" :class="$style.subButtons">
<div :class="[$style.subButton, $style.menuEditButton]">
<svg viewBox="0 0 16 64" :class="$style.subButtonShape">
<g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)">
@@ -104,6 +107,14 @@ import { $i } from '@/i.js';
const router = useRouter();
+const props = defineProps<{
+ showWidgetButton?: boolean;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'widgetButtonClick'): void;
+}>();
+
const forceIconOnly = ref(window.innerWidth <= 1279);
const iconOnly = computed(() => {
return forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon');
@@ -567,6 +578,14 @@ function menuEdit() {
backdrop-filter: var(--MI-blur, blur(8px));
}
+ .widget {
+ display: block;
+ position: relative;
+ width: 100%;
+ height: 52px;
+ text-align: center;
+ }
+
.post {
display: block;
position: relative;
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 6724c6f6c9..cc3836c646 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root">
- <XSidebar v-if="!isMobile" :class="$style.sidebar"/>
+ <XSidebar v-if="!isMobile" :class="$style.sidebar" :showWidgetButton="!isDesktop" @widgetButtonClick="widgetsShowing = true"/>
<div :class="$style.contents" @contextmenu.stop="onContextmenu">
<div>
@@ -35,8 +35,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<XWidgets/>
</div>
- <button v-if="!isDesktop && !pageMetadata?.needWideArea && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
-
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
@@ -280,7 +278,7 @@ $widgets-hide-threshold: 1090px;
.transition_widgetsDrawer_enterFrom,
.transition_widgetsDrawer_leaveTo {
opacity: 0;
- transform: translateX(240px);
+ transform: translateX(-240px);
}
.root {
@@ -414,20 +412,6 @@ $widgets-hide-threshold: 1090px;
}
}
-.widgetButton {
- display: block;
- position: fixed;
- z-index: 1000;
- bottom: 32px;
- right: 32px;
- width: 64px;
- height: 64px;
- border-radius: 100%;
- box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
- font-size: 22px;
- background: var(--MI_THEME-panel);
-}
-
.widgetsDrawerBg {
z-index: 1001;
}
@@ -435,7 +419,7 @@ $widgets-hide-threshold: 1090px;
.widgetsDrawer {
position: fixed;
top: 0;
- right: 0;
+ left: 0;
z-index: 1001;
width: 310px;
height: 100dvh;
diff --git a/packages/frontend/src/use/use-mutation-observer.ts b/packages/frontend/src/use/use-mutation-observer.ts
new file mode 100644
index 0000000000..b35dbcd7a8
--- /dev/null
+++ b/packages/frontend/src/use/use-mutation-observer.ts
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { onUnmounted, watch } from 'vue';
+import type { Ref, ShallowRef } from 'vue';
+
+export function useMutationObserver(targetNodeRef: Ref<HTMLElement | undefined>, options: MutationObserverInit, callback: MutationCallback): void {
+ const observer = new MutationObserver(callback);
+
+ watch(targetNodeRef, (targetNode) => {
+ if (targetNode) {
+ observer.observe(targetNode, options);
+ }
+ }, { immediate: true });
+
+ onUnmounted(() => {
+ observer.disconnect();
+ });
+}
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index 64fe328478..c939c93425 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -279,62 +279,62 @@ export const searchIndexes: SearchIndexItem[] = [
id: 'AKvDrxSj5',
children: [
{
- id: 'cAszhShB0',
+ id: 'a5b9RjEvq',
label: i18n.ts.uiLanguage,
keywords: ['language'],
},
{
- id: 'apz9AutPm',
+ id: '9ragaff40',
label: i18n.ts.overridedDeviceKind,
keywords: ['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop'],
},
{
- id: 'nqRVtw1xw',
+ id: 'lfI3yMX9g',
label: i18n.ts.useBlurEffect,
keywords: ['blur'],
},
{
- id: 'EO5WHBeG8',
+ id: '31Y4IcGEf',
label: i18n.ts.useBlurEffectForModal,
keywords: ['blur', 'modal'],
},
{
- id: 'CWpyT9vLK',
+ id: '78q2asrLS',
label: i18n.ts.showAvatarDecorations,
keywords: ['avatar', 'icon', 'decoration', 'show'],
},
{
- id: '1wwACqQz1',
+ id: 'zydOfGYip',
label: i18n.ts.alwaysConfirmFollow,
keywords: ['follow', 'confirm', 'always'],
},
{
- id: '1x3JNXj8N',
+ id: 'wqpOC22Zm',
label: i18n.ts.highlightSensitiveMedia,
keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
},
{
- id: 'CfAg0Qekq',
+ id: 'c98gbF9c6',
label: i18n.ts.confirmWhenRevealingSensitiveMedia,
keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
},
{
- id: 'aefexW9fD',
+ id: '4LxdiOMNh',
label: i18n.ts.enableAdvancedMfm,
keywords: ['mfm', 'enable', 'show', 'advanced'],
},
{
- id: 'lu9v5Spqg',
+ id: '9gTCaLkIf',
label: i18n.ts.enableInfiniteScroll,
keywords: ['auto', 'load', 'auto', 'more', 'scroll'],
},
{
- id: '6kMj4HVOg',
+ id: 'jmJT0twuJ',
label: i18n.ts.emojiStyle,
keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
},
{
- id: 'DftdlLbNu',
+ id: 'igFN7RIUa',
label: i18n.ts.pinnedList,
keywords: ['pinned', 'list'],
},
@@ -343,85 +343,85 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['general'],
},
{
- id: 'CQldliCSi',
+ id: 'ufc2X9voy',
children: [
{
- id: 'kMB2hPyq3',
+ id: 'd2H4E5ys6',
label: i18n.ts.showFixedPostForm,
keywords: ['post', 'form', 'timeline'],
},
{
- id: 'jC7LtTnmc',
+ id: '1LHOhDKGW',
label: i18n.ts.showFixedPostFormInChannel,
keywords: ['post', 'form', 'timeline', 'channel'],
},
{
- id: 'p2wlrnwLo',
+ id: 'DSzwvTp7i',
label: i18n.ts.collapseRenotes,
keywords: ['renote', i18n.ts.collapseRenotesDescription],
},
{
- id: '6SFn3t8VS',
+ id: 'jb3HUeyrx',
label: i18n.ts.showGapBetweenNotesInTimeline,
keywords: ['note', 'timeline', 'gap'],
},
{
- id: 'nygexkaUk',
+ id: '2LNjwv1cr',
label: i18n.ts.disableStreamingTimeline,
keywords: ['disable', 'streaming', 'timeline'],
},
{
- id: '7vnQgR42v',
+ id: '7W6g8Dcqz',
label: i18n.ts.showNoteActionsOnlyHover,
keywords: ['hover', 'show', 'footer', 'action'],
},
{
- id: 'x5q4XZ7Kv',
+ id: 'uAOoH3LFF',
label: i18n.ts.showClipButtonInNoteFooter,
keywords: ['footer', 'action', 'clip', 'show'],
},
{
- id: 'x9irZWjaF',
+ id: 'eCiyZLC8n',
label: i18n.ts.showReactionsCount,
keywords: ['reaction', 'count', 'show'],
},
{
- id: 'dHPv9mrxi',
+ id: '68u9uRmFP',
label: i18n.ts.confirmOnReact,
keywords: ['reaction', 'confirm'],
},
{
- id: 'bj42W4cvN',
+ id: 'rHWm4sXIe',
label: i18n.ts.loadRawImages,
keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'],
},
{
- id: 'fzPca1Gk9',
+ id: '9L2XGJw7e',
label: i18n.ts.useReactionPickerForContextMenu,
keywords: ['reaction', 'picker', 'contextmenu', 'open'],
},
{
- id: 'mNU5IBln7',
+ id: 'uIMCIK7kG',
label: i18n.ts.reactionsDisplaySize,
keywords: ['reaction', 'size', 'scale', 'display'],
},
{
- id: 'kYgorbLUy',
+ id: 'uMckjO9bz',
label: i18n.ts.limitWidthOfReaction,
keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
},
{
- id: 'm75VEWI3S',
+ id: 'yeghU4qiH',
label: i18n.ts.mediaListWithOneImageAppearance,
keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
},
{
- id: 'CA42sC9Mx',
+ id: 'yYSOPoAKE',
label: i18n.ts.instanceTicker,
keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
},
{
- id: 'knEhibyFp',
+ id: 'iOHiIu32L',
label: i18n.ts.displayOfSensitiveMedia,
keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
},
@@ -430,25 +430,25 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['timeline', 'note'],
},
{
- id: 'yIR4YP0yU',
+ id: 'eROFRMtXv',
children: [
{
- id: 'cBkUgQNpH',
+ id: 'BaQfrVO82',
label: i18n.ts.keepCw,
keywords: ['remember', 'keep', 'note', 'cw'],
},
{
- id: 'Bv4YywaKL',
+ id: 'vFerPo2he',
label: i18n.ts.rememberNoteVisibility,
keywords: ['remember', 'keep', 'note', 'visibility'],
},
{
- id: 'F3kpUNvSQ',
+ id: 'dcAC0yJcH',
label: i18n.ts.enableQuickAddMfmFunction,
keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
},
{
- id: 'BBxwy4F6E',
+ id: 'bECeWZVMb',
label: i18n.ts.defaultNoteVisibility,
keywords: ['default', 'note', 'visibility'],
},
@@ -457,20 +457,20 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['post', 'form'],
},
{
- id: 'e5XnQWk68',
+ id: 'tsSP93Cc6',
children: [
{
- id: 'rOttgccaS',
+ id: 'dtw8FepYL',
label: i18n.ts.useGroupedNotifications,
keywords: ['group'],
},
{
- id: 'Ek4Cw3VPq',
+ id: 'eb0yCYJTn',
label: i18n.ts.position,
keywords: ['position'],
},
{
- id: 'pZLzt3i0s',
+ id: '1Spt4Gpr5',
label: i18n.ts.stackAxis,
keywords: ['stack', 'axis', 'direction'],
},
@@ -479,55 +479,72 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['notification'],
},
{
- id: 'c9mbgmHQp',
+ id: 'SYmWxGOF',
label: i18n.ts.dataSaver,
keywords: ['datasaver'],
},
{
- id: '5h8vhCX1S',
+ id: 'vPQPvmntL',
children: [
{
- id: 'bDv03znUy',
+ id: 'zZxyXHk3A',
+ label: i18n.ts._settings._chat.showSenderName,
+ keywords: ['show', 'sender', 'name'],
+ },
+ {
+ id: 'omEy5Q3Ev',
+ label: i18n.ts._settings._chat.sendOnEnter,
+ keywords: ['send', 'enter', 'newline'],
+ },
+ ],
+ label: i18n.ts.chat,
+ keywords: ['chat', 'messaging'],
+ },
+ {
+ id: '5fy7VEy6i',
+ children: [
+ {
+ id: 'EosiWZvak',
label: i18n.ts.squareAvatars,
keywords: ['avatar', 'icon', 'square'],
},
{
- id: 'nkR2LWURW',
+ id: 'qY5xTzl35',
label: i18n.ts.seasonalScreenEffect,
keywords: ['effect', 'show'],
},
{
- id: 'sCscGhMmH',
+ id: '2VSnj81vC',
label: i18n.ts.openImageInNewTab,
keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
},
{
- id: '4yCgcFElF',
+ id: 'hdQa7W2H1',
label: i18n.ts.withRepliesByDefaultForNewlyFollowed,
keywords: ['follow', 'replies'],
},
{
- id: '5iMpm5rES',
+ id: 'nnj4DkjhP',
label: i18n.ts.whenServerDisconnected,
keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
},
{
- id: 'dlQjnWBVU',
+ id: 'Eh7vTluDO',
label: i18n.ts.numberOfPageCache,
keywords: ['cache', 'page'],
},
{
- id: 'qY5xTzl35',
+ id: 'vTRSKf1JA',
label: i18n.ts.forceShowAds,
keywords: ['ad', 'show'],
},
{
- id: '2VSnj81vC',
+ id: 'dwhQfcLGt',
label: i18n.ts.hemisphere,
keywords: [],
},
{
- id: 'vuG3aG3IE',
+ id: 'Ar1lj7f7U',
label: i18n.ts.additionalEmojiDictionary,
keywords: ['emoji', 'dictionary', 'additional', 'extra'],
},
@@ -588,6 +605,20 @@ export const searchIndexes: SearchIndexItem[] = [
icon: 'ti ti-dots',
},
{
+ id: '9bNikHWzQ',
+ children: [
+ {
+ id: 'appYJbpkK',
+ label: i18n.ts._settings.showNavbarSubButtons,
+ keywords: ['navbar', 'sidebar', 'toggle', 'button', 'sub'],
+ },
+ ],
+ label: i18n.ts.navbar,
+ keywords: ['navbar', 'menu', 'sidebar'],
+ path: '/settings/navbar',
+ icon: 'ti ti-list',
+ },
+ {
id: '3icEvyv2D',
children: [
{
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index e79cd794a6..cc397e2270 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1002,6 +1002,12 @@ type ChatMessagesShowRequest = operations['chat___messages___show']['requestBody
type ChatMessagesShowResponse = operations['chat___messages___show']['responses']['200']['content']['application/json'];
// @public (undocumented)
+type ChatMessagesUnreactRequest = operations['chat___messages___unreact']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type ChatMessagesUnreactResponse = operations['chat___messages___unreact']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
type ChatMessagesUserTimelineRequest = operations['chat___messages___user-timeline']['requestBody']['content']['application/json'];
// @public (undocumented)
@@ -1047,6 +1053,12 @@ type ChatRoomsInvitationsInboxRequest = operations['chat___rooms___invitations__
type ChatRoomsInvitationsInboxResponse = operations['chat___rooms___invitations___inbox']['responses']['200']['content']['application/json'];
// @public (undocumented)
+type ChatRoomsInvitationsOutboxRequest = operations['chat___rooms___invitations___outbox']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type ChatRoomsInvitationsOutboxResponse = operations['chat___rooms___invitations___outbox']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
type ChatRoomsJoiningRequest = operations['chat___rooms___joining']['requestBody']['content']['application/json'];
// @public (undocumented)
@@ -1607,6 +1619,8 @@ declare namespace entities {
ChatMessagesSearchResponse,
ChatMessagesShowRequest,
ChatMessagesShowResponse,
+ ChatMessagesUnreactRequest,
+ ChatMessagesUnreactResponse,
ChatMessagesUserTimelineRequest,
ChatMessagesUserTimelineResponse,
ChatRoomsCreateRequest,
@@ -1619,6 +1633,8 @@ declare namespace entities {
ChatRoomsInvitationsIgnoreResponse,
ChatRoomsInvitationsInboxRequest,
ChatRoomsInvitationsInboxResponse,
+ ChatRoomsInvitationsOutboxRequest,
+ ChatRoomsInvitationsOutboxResponse,
ChatRoomsJoinRequest,
ChatRoomsJoinResponse,
ChatRoomsJoiningRequest,
@@ -2824,10 +2840,13 @@ type ModerationLog = {
} | {
type: 'deleteGalleryPost';
info: ModerationLogPayloads['deleteGalleryPost'];
+} | {
+ type: 'deleteChatRoom';
+ info: ModerationLogPayloads['deleteChatRoom'];
});
// @public (undocumented)
-export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
+export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost", "deleteChatRoom"];
// @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
@@ -3032,7 +3051,7 @@ type Notification_2 = components['schemas']['Notification'];
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
// @public (undocumented)
-export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned"];
+export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned"];
// @public (undocumented)
export function nyaize(text: string): string;
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 05b3f11ac2..bfcacaad46 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2025.3.2-beta.10",
+ "version": "2025.3.2-beta.13",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index 53e18b8501..acbe6fa527 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -1724,6 +1724,17 @@ declare module '../api.js' {
/**
* No description provided.
*
+ * **Credential required**: *Yes* / **Permission**: *write:chat*
+ */
+ request<E extends 'chat/messages/unreact', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
* **Credential required**: *Yes* / **Permission**: *read:chat*
*/
request<E extends 'chat/messages/user-timeline', P extends Endpoints[E]['req']>(
@@ -1790,6 +1801,17 @@ declare module '../api.js' {
/**
* No description provided.
*
+ * **Credential required**: *Yes* / **Permission**: *read:chat*
+ */
+ request<E extends 'chat/rooms/invitations/outbox', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
* **Credential required**: *Yes* / **Permission**: *write:chat*
*/
request<E extends 'chat/rooms/join', P extends Endpoints[E]['req']>(
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index ddae16072c..fadf981fc2 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -231,6 +231,8 @@ import type {
ChatMessagesSearchResponse,
ChatMessagesShowRequest,
ChatMessagesShowResponse,
+ ChatMessagesUnreactRequest,
+ ChatMessagesUnreactResponse,
ChatMessagesUserTimelineRequest,
ChatMessagesUserTimelineResponse,
ChatRoomsCreateRequest,
@@ -243,6 +245,8 @@ import type {
ChatRoomsInvitationsIgnoreResponse,
ChatRoomsInvitationsInboxRequest,
ChatRoomsInvitationsInboxResponse,
+ ChatRoomsInvitationsOutboxRequest,
+ ChatRoomsInvitationsOutboxResponse,
ChatRoomsJoinRequest,
ChatRoomsJoinResponse,
ChatRoomsJoiningRequest,
@@ -812,12 +816,14 @@ export type Endpoints = {
'chat/messages/room-timeline': { req: ChatMessagesRoomTimelineRequest; res: ChatMessagesRoomTimelineResponse };
'chat/messages/search': { req: ChatMessagesSearchRequest; res: ChatMessagesSearchResponse };
'chat/messages/show': { req: ChatMessagesShowRequest; res: ChatMessagesShowResponse };
+ 'chat/messages/unreact': { req: ChatMessagesUnreactRequest; res: ChatMessagesUnreactResponse };
'chat/messages/user-timeline': { req: ChatMessagesUserTimelineRequest; res: ChatMessagesUserTimelineResponse };
'chat/rooms/create': { req: ChatRoomsCreateRequest; res: ChatRoomsCreateResponse };
'chat/rooms/delete': { req: ChatRoomsDeleteRequest; res: ChatRoomsDeleteResponse };
'chat/rooms/invitations/create': { req: ChatRoomsInvitationsCreateRequest; res: ChatRoomsInvitationsCreateResponse };
'chat/rooms/invitations/ignore': { req: ChatRoomsInvitationsIgnoreRequest; res: ChatRoomsInvitationsIgnoreResponse };
'chat/rooms/invitations/inbox': { req: ChatRoomsInvitationsInboxRequest; res: ChatRoomsInvitationsInboxResponse };
+ 'chat/rooms/invitations/outbox': { req: ChatRoomsInvitationsOutboxRequest; res: ChatRoomsInvitationsOutboxResponse };
'chat/rooms/join': { req: ChatRoomsJoinRequest; res: ChatRoomsJoinResponse };
'chat/rooms/joining': { req: ChatRoomsJoiningRequest; res: ChatRoomsJoiningResponse };
'chat/rooms/leave': { req: ChatRoomsLeaveRequest; res: ChatRoomsLeaveResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 7340a4424d..b873cb2332 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -234,6 +234,8 @@ export type ChatMessagesSearchRequest = operations['chat___messages___search']['
export type ChatMessagesSearchResponse = operations['chat___messages___search']['responses']['200']['content']['application/json'];
export type ChatMessagesShowRequest = operations['chat___messages___show']['requestBody']['content']['application/json'];
export type ChatMessagesShowResponse = operations['chat___messages___show']['responses']['200']['content']['application/json'];
+export type ChatMessagesUnreactRequest = operations['chat___messages___unreact']['requestBody']['content']['application/json'];
+export type ChatMessagesUnreactResponse = operations['chat___messages___unreact']['responses']['200']['content']['application/json'];
export type ChatMessagesUserTimelineRequest = operations['chat___messages___user-timeline']['requestBody']['content']['application/json'];
export type ChatMessagesUserTimelineResponse = operations['chat___messages___user-timeline']['responses']['200']['content']['application/json'];
export type ChatRoomsCreateRequest = operations['chat___rooms___create']['requestBody']['content']['application/json'];
@@ -246,6 +248,8 @@ export type ChatRoomsInvitationsIgnoreRequest = operations['chat___rooms___invit
export type ChatRoomsInvitationsIgnoreResponse = operations['chat___rooms___invitations___ignore']['responses']['200']['content']['application/json'];
export type ChatRoomsInvitationsInboxRequest = operations['chat___rooms___invitations___inbox']['requestBody']['content']['application/json'];
export type ChatRoomsInvitationsInboxResponse = operations['chat___rooms___invitations___inbox']['responses']['200']['content']['application/json'];
+export type ChatRoomsInvitationsOutboxRequest = operations['chat___rooms___invitations___outbox']['requestBody']['content']['application/json'];
+export type ChatRoomsInvitationsOutboxResponse = operations['chat___rooms___invitations___outbox']['responses']['200']['content']['application/json'];
export type ChatRoomsJoinRequest = operations['chat___rooms___join']['requestBody']['content']['application/json'];
export type ChatRoomsJoinResponse = operations['chat___rooms___join']['responses']['200']['content']['application/json'];
export type ChatRoomsJoiningRequest = operations['chat___rooms___joining']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 31bc34e473..c91fedf2fa 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -1430,6 +1430,15 @@ export type paths = {
*/
post: operations['chat___messages___show'];
};
+ '/chat/messages/unreact': {
+ /**
+ * chat/messages/unreact
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:chat*
+ */
+ post: operations['chat___messages___unreact'];
+ };
'/chat/messages/user-timeline': {
/**
* chat/messages/user-timeline
@@ -1484,6 +1493,15 @@ export type paths = {
*/
post: operations['chat___rooms___invitations___inbox'];
};
+ '/chat/rooms/invitations/outbox': {
+ /**
+ * chat/rooms/invitations/outbox
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:chat*
+ */
+ post: operations['chat___rooms___invitations___outbox'];
+ };
'/chat/rooms/join': {
/**
* chat/rooms/join
@@ -4192,6 +4210,15 @@ export type components = {
/** Format: misskey:id */
userListId: string;
}]>;
+ chatRoomInvitationReceived?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
achievementEarned?: OneOf<[{
/** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
@@ -4529,6 +4556,14 @@ export type components = {
/** @enum {string} */
type: 'roleAssigned';
role: components['schemas']['Role'];
+ } | {
+ /** Format: id */
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ /** @enum {string} */
+ type: 'chatRoomInvitationReceived';
+ invitation: components['schemas']['ChatRoomInvitation'];
} | ({
/** Format: id */
id: string;
@@ -10075,6 +10110,15 @@ export type operations = {
/** Format: misskey:id */
userListId: string;
}]>;
+ chatRoomInvitationReceived?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
achievementEarned?: OneOf<[{
/** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
@@ -14390,6 +14434,61 @@ export type operations = {
};
};
/**
+ * chat/messages/unreact
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:chat*
+ */
+ chat___messages___unreact: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ messageId: string;
+ reaction: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': unknown;
+ };
+ };
+ /** @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/user-timeline
* @description No description provided.
*
@@ -14738,6 +14837,66 @@ export type operations = {
};
};
/**
+ * chat/rooms/invitations/outbox
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:chat*
+ */
+ chat___rooms___invitations___outbox: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ roomId: string;
+ /** @default 30 */
+ limit?: number;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['ChatRoomInvitation'][];
+ };
+ };
+ /** @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/rooms/join
* @description No description provided.
*
@@ -21504,8 +21663,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
- includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
- excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+ includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+ excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
};
};
};
@@ -21572,8 +21731,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
- includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
- excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+ includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+ excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
};
};
};
@@ -22669,6 +22828,15 @@ export type operations = {
/** Format: misskey:id */
userListId: string;
}]>;
+ chatRoomInvitationReceived?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
achievementEarned?: OneOf<[{
/** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index f669da6eb5..d0874aad3a 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -10,9 +10,10 @@ import type {
ReversiGameDetailed,
SystemWebhook,
UserLite,
+ ChatRoom,
} from './autogen/models.js';
-export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited', 'scheduledNoteFailed', 'scheduledNotePosted'] as const;
+export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'chatRoomInvitationReceived', 'achievementEarned', 'edited', 'scheduledNoteFailed', 'scheduledNotePosted'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
@@ -178,6 +179,7 @@ export const moderationLogTypes = [
'deletePage',
'deleteFlash',
'deleteGalleryPost',
+ 'deleteChatRoom',
] as const;
// See: packages/backend/src/core/ReversiService.ts@L410
@@ -569,4 +571,8 @@ export type ModerationLogPayloads = {
removeRelay: {
inbox: string;
};
+ deleteChatRoom: {
+ roomId: string;
+ room: ChatRoom;
+ };
};
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 22efe0c4cd..3ffa1d1a3a 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -229,6 +229,9 @@ export type ModerationLog = {
type: 'deleteGalleryPost';
info: ModerationLogPayloads['deleteGalleryPost'];
} | {
+ type: 'deleteChatRoom';
+ info: ModerationLogPayloads['deleteChatRoom'];
+} | {
type: 'clearUserFiles';
info: ModerationLogPayloads['clearUserFiles'];
} | {