summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2025-04-07 19:09:11 +0900
committerGitHub <noreply@github.com>2025-04-07 19:09:11 +0900
commit9d3f3264fdd059f47537da48fd125cdd2f4bad1e (patch)
treededbf1433d0e003465b37e805519c5635b135466 /packages
parentUpdate CHANGELOG.md (diff)
downloadsharkey-9d3f3264fdd059f47537da48fd125cdd2f4bad1e.tar.gz
sharkey-9d3f3264fdd059f47537da48fd125cdd2f4bad1e.tar.bz2
sharkey-9d3f3264fdd059f47537da48fd125cdd2f4bad1e.zip
enhance: チャットの閲覧を無効化できるように (#15765)
* enhance: チャットの閲覧を無効化できるように * fix * fix * fix * readonlyの説明を追加 * enhance: チャットが無効な場合はチャット関連の設定も隠すように * fix * refactor: ChatServiceからApiに関するドメイン知識を排除
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/src/core/ChatService.ts36
-rw-r--r--packages/backend/src/core/RoleService.ts12
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts2
-rw-r--r--packages/backend/src/models/json-schema/role.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/chat/history.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/delete.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/react.ts3
-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/search.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/show.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/unreact.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/create.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/delete.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/join.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/joining.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/leave.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/members.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/mute.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/owned.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/show.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/rooms/update.ts2
-rw-r--r--packages/frontend-shared/js/const.ts2
-rw-r--r--packages/frontend/src/navbar.ts1
-rw-r--r--packages/frontend/src/pages/admin/roles.editor.vue21
-rw-r--r--packages/frontend/src/pages/admin/roles.vue14
-rw-r--r--packages/frontend/src/pages/chat/XMessage.vue10
-rw-r--r--packages/frontend/src/pages/chat/home.home.vue4
-rw-r--r--packages/frontend/src/pages/chat/room.vue84
-rw-r--r--packages/frontend/src/pages/settings/preferences.vue75
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue33
-rw-r--r--packages/frontend/src/router.definition.ts12
-rw-r--r--packages/frontend/src/utility/get-user-menu.ts2
-rw-r--r--packages/misskey-js/src/autogen/types.ts3
40 files changed, 240 insertions, 131 deletions
diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts
index 3984cefc80..b0e8cfb61c 100644
--- a/packages/backend/src/core/ChatService.ts
+++ b/packages/backend/src/core/ChatService.ts
@@ -95,6 +95,40 @@ export class ChatService {
}
@bindThis
+ public async getChatAvailability(userId: MiUser['id']): Promise<{ read: boolean; write: boolean; }> {
+ const policies = await this.roleService.getUserPolicies(userId);
+
+ switch (policies.chatAvailability) {
+ case 'available':
+ return {
+ read: true,
+ write: true,
+ };
+ case 'readonly':
+ return {
+ read: true,
+ write: false,
+ };
+ case 'unavailable':
+ return {
+ read: false,
+ write: false,
+ };
+ default:
+ throw new Error('invalid chat availability (unreachable)');
+ }
+ }
+
+ /** getChatAvailabilityの糖衣。主にAPI呼び出し時に走らせて、権限的に問題ない場合はそのまま続行する */
+ @bindThis
+ public async checkChatAvailability(userId: MiUser['id'], permission: 'read' | 'write') {
+ const policy = await this.getChatAvailability(userId);
+ if (policy[permission] === false) {
+ throw new Error('ROLE_PERMISSION_DENIED');
+ }
+ }
+
+ @bindThis
public async createMessageToUser(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: {
text?: string | null;
file?: MiDriveFile | null;
@@ -140,7 +174,7 @@ export class ChatService {
}
}
- if (!(await this.roleService.getUserPolicies(toUser.id)).canChat) {
+ if (!(await this.getChatAvailability(toUser.id)).write) {
throw new Error('recipient is cannot chat (policy)');
}
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 0a2659ee32..601959cc96 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -63,7 +63,7 @@ export type RolePolicies = {
canImportFollowing: boolean;
canImportMuting: boolean;
canImportUserLists: boolean;
- canChat: boolean;
+ chatAvailability: 'available' | 'readonly' | 'unavailable';
};
export const DEFAULT_POLICIES: RolePolicies = {
@@ -98,7 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canImportFollowing: true,
canImportMuting: true,
canImportUserLists: true,
- canChat: true,
+ chatAvailability: 'available',
};
@Injectable()
@@ -370,6 +370,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
return aggregate(policies.map(policy => policy.useDefault ? basePolicies[name] : policy.value));
}
+ function aggregateChatAvailability(vs: RolePolicies['chatAvailability'][]) {
+ if (vs.some(v => v === 'available')) return 'available';
+ if (vs.some(v => v === 'readonly')) return 'readonly';
+ return 'unavailable';
+ }
+
return {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
@@ -402,7 +408,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
- canChat: calc('canChat', vs => vs.some(v => v === true)),
+ chatAvailability: calc('chatAvailability', aggregateChatAvailability),
};
}
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index ad8052711c..e252ff509e 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -557,7 +557,7 @@ export class UserEntityService implements OnModuleInit {
followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility,
chatScope: user.chatScope,
- canChat: this.roleService.getUserPolicies(user.id).then(r => r.canChat),
+ canChat: this.roleService.getUserPolicies(user.id).then(r => r.chatAvailability === 'available'),
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id,
name: role.name,
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index 6f63dcef2e..1cfcb830e0 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -292,9 +292,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
- canChat: {
- type: 'boolean',
+ chatAvailability: {
+ type: 'string',
optional: false, nullable: false,
+ enum: ['available', 'readonly', 'unavailable'],
},
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/chat/history.ts b/packages/backend/src/server/api/endpoints/chat/history.ts
index 7553a751e0..fdd9055106 100644
--- a/packages/backend/src/server/api/endpoints/chat/history.ts
+++ b/packages/backend/src/server/api/endpoints/chat/history.ts
@@ -46,6 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
const history = ps.room ? await this.chatService.roomHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit);
const packedMessages = await this.chatEntityService.packMessagesDetailed(history, me);
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts
index a988dc60b9..ad2b82e219 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts
@@ -16,7 +16,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
- requiredRolePolicy: 'canChat',
prohibitMoved: true,
@@ -74,6 +73,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
const room = await this.chatService.findRoomById(ps.toRoomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts
index bbaab8a6c3..fa34a7d558 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts
@@ -16,7 +16,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
- requiredRolePolicy: 'canChat',
prohibitMoved: true,
@@ -86,6 +85,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
let file = null;
if (ps.fileId != null) {
file = await this.driveFilesRepository.findOneBy({
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts
index 25fc774d4f..63b75fb6a7 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts
@@ -13,7 +13,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
- requiredRolePolicy: 'canChat',
kind: 'write:chat',
@@ -43,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
const message = await this.chatService.findMyMessageById(me.id, ps.messageId);
if (message == null) {
throw new ApiError(meta.errors.noSuchMessage);
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/react.ts b/packages/backend/src/server/api/endpoints/chat/messages/react.ts
index 0145e380be..5f61e7e992 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/react.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/react.ts
@@ -13,7 +13,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
- requiredRolePolicy: 'canChat',
kind: 'write:chat',
@@ -44,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
await this.chatService.react(ps.messageId, me.id, ps.reaction);
});
}
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 b6d3356196..c0e344b889 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
@@ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
const room = await this.chatService.findRoomById(ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/search.ts b/packages/backend/src/server/api/endpoints/chat/messages/search.ts
index 4c989e5ca9..682597f76d 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/search.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/search.ts
@@ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
if (ps.roomId != null) {
const room = await this.chatService.findRoomById(ps.roomId);
if (room == null) {
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/show.ts b/packages/backend/src/server/api/endpoints/chat/messages/show.ts
index 371f7a7071..9a2bbb8742 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/show.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/show.ts
@@ -50,6 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
const message = await this.chatService.findMessageById(ps.messageId);
if (message == null) {
throw new ApiError(meta.errors.noSuchMessage);
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts
index b97bad8a9c..6784bb6ecf 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts
@@ -13,7 +13,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
- requiredRolePolicy: 'canChat',
kind: 'write:chat',
@@ -44,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
await this.chatService.unreact(ps.messageId, me.id, ps.reaction);
});
}
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts
index a35f121bb1..a057e2e088 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts
@@ -56,6 +56,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
const other = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/create.ts b/packages/backend/src/server/api/endpoints/chat/rooms/create.ts
index fa4cc8ceb4..68a53f0886 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/create.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/create.ts
@@ -15,7 +15,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
- requiredRolePolicy: 'canChat',
prohibitMoved: true,
@@ -52,6 +51,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
const room = await this.chatService.createRoom(me, {
name: ps.name,
description: ps.description ?? '',
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 1d77a06dd8..82a8e1f30d 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts
@@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
const room = await this.chatService.findRoomById(ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts
index 5da4a1a772..b1f049f2b9 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts
@@ -15,7 +15,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
- requiredRolePolicy: 'canChat',
prohibitMoved: true,
@@ -57,6 +56,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts
index 8c017f7d01..b8a228089b 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts
@@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
await this.chatService.ignoreRoomInvitation(me.id, ps.roomId);
});
}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts
index 07337480fc..8a02d1c704 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts
@@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
return this.chatEntityService.packRoomInvitations(invitations, 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
index 12d496e94b..0702ba086c 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts
@@ -55,6 +55,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/join.ts b/packages/backend/src/server/api/endpoints/chat/rooms/join.ts
index dbd4d1ea5a..d561f9e03f 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/join.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/join.ts
@@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
await this.chatService.joinToRoom(me.id, ps.roomId);
});
}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts b/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts
index c4c6253236..ba9242c762 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts
@@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, ps.sinceId, ps.untilId);
return this.chatEntityService.packRoomMemberships(memberships, me, {
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts b/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts
index 724ad61f7e..a3ad0c2d6f 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts
@@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
await this.chatService.leaveRoom(me.id, ps.roomId);
});
}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/members.ts b/packages/backend/src/server/api/endpoints/chat/rooms/members.ts
index 407bfe74f1..f5ffa21d32 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/members.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/members.ts
@@ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
const room = await this.chatService.findRoomById(ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts b/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts
index 5208b8a253..11cbe7b8b9 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts
@@ -43,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
await this.chatService.muteRoom(me.id, ps.roomId, ps.mute);
});
}
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts b/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts
index 6516120bca..accf7e1bee 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts
@@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
return this.chatEntityService.packRooms(rooms, me);
});
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/show.ts b/packages/backend/src/server/api/endpoints/chat/rooms/show.ts
index 547618ee7d..50da210d81 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/show.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/show.ts
@@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'read');
+
const room = await this.chatService.findRoomById(ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);
diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/update.ts b/packages/backend/src/server/api/endpoints/chat/rooms/update.ts
index 6f2a9c10b5..0cd62cb040 100644
--- a/packages/backend/src/server/api/endpoints/chat/rooms/update.ts
+++ b/packages/backend/src/server/api/endpoints/chat/rooms/update.ts
@@ -49,6 +49,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.chatService.checkChatAvailability(me.id, 'write');
+
const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);
diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts
index fa60476b69..de65c3db97 100644
--- a/packages/frontend-shared/js/const.ts
+++ b/packages/frontend-shared/js/const.ts
@@ -108,7 +108,7 @@ export const ROLE_POLICIES = [
'canImportFollowing',
'canImportMuting',
'canImportUserLists',
- 'canChat',
+ 'chatAvailability',
] as const;
export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg';
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index 185d9e81b7..c0fe0f2b85 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -114,6 +114,7 @@ export const navbarItemDef = reactive({
title: i18n.ts.chat,
icon: 'ti ti-messages',
to: '/chat',
+ show: computed(() => $i != null && $i.policies.chatAvailability !== 'unavailable'),
indicated: computed(() => $i != null && $i.hasUnreadChatMessages),
},
achievements: {
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index 73119940c1..930a63f5a9 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -165,21 +165,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
- <MkFolder v-if="matchQuery([i18n.ts._role._options.canChat, 'canChat'])">
- <template #label>{{ i18n.ts._role._options.canChat }}</template>
+ <MkFolder v-if="matchQuery([i18n.ts._role._options.chatAvailability, 'chatAvailability'])">
+ <template #label>{{ i18n.ts._role._options.chatAvailability }}</template>
<template #suffix>
- <span v-if="role.policies.canChat.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
- <span v-else>{{ role.policies.canChat.value ? i18n.ts.yes : i18n.ts.no }}</span>
- <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canChat)"></i></span>
+ <span v-if="role.policies.chatAvailability.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
+ <span v-else>{{ role.policies.chatAvailability.value === 'available' ? i18n.ts.yes : role.policies.chatAvailability.value === 'readonly' ? i18n.ts.readonly : i18n.ts.no }}</span>
+ <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.chatAvailability)"></i></span>
</template>
<div class="_gaps">
- <MkSwitch v-model="role.policies.canChat.useDefault" :readonly="readonly">
+ <MkSwitch v-model="role.policies.chatAvailability.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
- <MkSwitch v-model="role.policies.canChat.value" :disabled="role.policies.canChat.useDefault" :readonly="readonly">
+ <MkSelect v-model="role.policies.chatAvailability.value" :disabled="role.policies.chatAvailability.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
- </MkSwitch>
- <MkRange v-model="role.policies.canChat.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+ <option value="available">{{ i18n.ts.enabled }}</option>
+ <option value="readonly">{{ i18n.ts.readonly }}</option>
+ <option value="unavailable">{{ i18n.ts.disabled }}</option>
+ </MkSelect>
+ <MkRange v-model="role.policies.chatAvailability.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index df4efd1271..7c950957cf 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -51,12 +51,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
- <MkFolder v-if="matchQuery([i18n.ts._role._options.canChat, 'canChat'])">
- <template #label>{{ i18n.ts._role._options.canChat }}</template>
- <template #suffix>{{ policies.canChat ? i18n.ts.yes : i18n.ts.no }}</template>
- <MkSwitch v-model="policies.canChat">
+ <MkFolder v-if="matchQuery([i18n.ts._role._options.chatAvailability, 'chatAvailability'])">
+ <template #label>{{ i18n.ts._role._options.chatAvailability }}</template>
+ <template #suffix>{{ policies.chatAvailability === 'available' ? i18n.ts.yes : policies.chatAvailability === 'readonly' ? i18n.ts.readonly : i18n.ts.no }}</template>
+ <MkSelect v-model="policies.chatAvailability">
<template #label>{{ i18n.ts.enable }}</template>
- </MkSwitch>
+ <option value="available">{{ i18n.ts.enabled }}</option>
+ <option value="readonly">{{ i18n.ts.readonly }}</option>
+ <option value="unavailable">{{ i18n.ts.disabled }}</option>
+ </MkSelect>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
@@ -295,6 +298,7 @@ import MkInput from '@/components/MkInput.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkButton from '@/components/MkButton.vue';
+import MkSelect from '@/components/MkSelect.vue';
import MkRange from '@/components/MkRange.vue';
import MkRolePreview from '@/components/MkRolePreview.vue';
import * as os from '@/os.js';
diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue
index eb8b0d79ee..def6ec7d14 100644
--- a/packages/frontend/src/pages/chat/XMessage.vue
+++ b/packages/frontend/src/pages/chat/XMessage.vue
@@ -85,7 +85,7 @@ const isMe = computed(() => props.message.fromUserId === $i.id);
const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
provide(DI.mfmEmojiReactCallback, (reaction) => {
- if (!$i.policies.canChat) return;
+ if ($i.policies.chatAvailability !== 'available') return;
sound.playMisskeySfx('reaction');
misskeyApi('chat/messages/react', {
@@ -95,7 +95,7 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
});
function react(ev: MouseEvent) {
- if (!$i.policies.canChat) return;
+ if ($i.policies.chatAvailability !== 'available') return;
const targetEl = getHTMLElementOrNull(ev.currentTarget ?? ev.target);
if (!targetEl) return;
@@ -110,7 +110,7 @@ function react(ev: MouseEvent) {
}
function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) {
- if (!$i.policies.canChat) return;
+ if ($i.policies.chatAvailability !== 'available') return;
if (record.user.id === $i.id) {
misskeyApi('chat/messages/unreact', {
@@ -138,7 +138,7 @@ function onContextmenu(ev: MouseEvent) {
function showMenu(ev: MouseEvent, contextmenu = false) {
const menu: MenuItem[] = [];
- if (!isMe.value && $i.policies.canChat) {
+ if (!isMe.value && $i.policies.chatAvailability === 'available') {
menu.push({
text: i18n.ts.reaction,
icon: 'ti ti-mood-plus',
@@ -164,7 +164,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
type: 'divider',
});
- if (isMe.value && $i.policies.canChat) {
+ if (isMe.value && $i.policies.chatAvailability === 'available') {
menu.push({
text: i18n.ts.delete,
icon: 'ti ti-trash',
diff --git a/packages/frontend/src/pages/chat/home.home.vue b/packages/frontend/src/pages/chat/home.home.vue
index 17f0e0fbcd..a8ed891de0 100644
--- a/packages/frontend/src/pages/chat/home.home.vue
+++ b/packages/frontend/src/pages/chat/home.home.vue
@@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
- <MkButton v-if="$i.policies.canChat" primary gradate rounded :class="$style.start" @click="start"><i class="ti ti-plus"></i> {{ i18n.ts.startChat }}</MkButton>
+ <MkButton v-if="$i.policies.chatAvailability === 'available'" primary gradate rounded :class="$style.start" @click="start"><i class="ti ti-plus"></i> {{ i18n.ts.startChat }}</MkButton>
- <MkInfo v-else>{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo>
+ <MkInfo v-else>{{ $i.policies.chatAvailability === 'readonly' ? i18n.ts._chat.chatIsReadOnlyForThisAccountOrServer : i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo>
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue
index 9942dbeee9..8b351c1ec8 100644
--- a/packages/frontend/src/pages/chat/room.vue
+++ b/packages/frontend/src/pages/chat/room.vue
@@ -6,54 +6,56 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader v-model:tab="tab" :reversed="tab === 'chat'" :tabs="headerTabs" :actions="headerActions">
<MkSpacer v-if="tab === 'chat'" :contentMax="700">
- <div v-if="initializing">
- <MkLoading/>
- </div>
+ <div class="_gaps">
+ <div v-if="initializing">
+ <MkLoading/>
+ </div>
- <div v-else-if="messages.length === 0">
- <div class="_gaps" style="text-align: center;">
- <div>{{ i18n.ts._chat.noMessagesYet }}</div>
- <template v-if="user">
- <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-if="user.chatScope === 'none'">{{ i18n.ts._chat.thisUserNotAllowedChatAnyone }}</div>
- </template>
- <template v-else-if="room">
- <div>{{ i18n.ts._chat.inviteUserToChat }}</div>
- </template>
+ <div v-else-if="messages.length === 0">
+ <div class="_gaps" style="text-align: center;">
+ <div>{{ i18n.ts._chat.noMessagesYet }}</div>
+ <template v-if="user">
+ <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-if="user.chatScope === 'none'">{{ i18n.ts._chat.thisUserNotAllowedChatAnyone }}</div>
+ </template>
+ <template v-else-if="room">
+ <div>{{ i18n.ts._chat.inviteUserToChat }}</div>
+ </template>
+ </div>
</div>
- </div>
- <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 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>
+
+ <TransitionGroup
+ :enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''"
+ :leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''"
+ :enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''"
+ :leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''"
+ :moveClass="prefer.s.animation ? $style.transition_x_move : ''"
+ tag="div" class="_gaps"
+ >
+ <template v-for="item in timeline.toReversed()" :key="item.id">
+ <XMessage v-if="item.type === 'item'" :message="item.data"/>
+ <div v-else-if="item.type === 'date'" :class="$style.dateDivider">
+ <span><i class="ti ti-chevron-up"></i> {{ item.nextText }}</span>
+ <span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
+ <span>{{ item.prevText }} <i class="ti ti-chevron-down"></i></span>
+ </div>
+ </template>
+ </TransitionGroup>
</div>
- <TransitionGroup
- :enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''"
- :leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''"
- :enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''"
- :leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''"
- :moveClass="prefer.s.animation ? $style.transition_x_move : ''"
- tag="div" class="_gaps"
- >
- <template v-for="item in timeline.toReversed()" :key="item.id">
- <XMessage v-if="item.type === 'item'" :message="item.data"/>
- <div v-else-if="item.type === 'date'" :class="$style.dateDivider">
- <span><i class="ti ti-chevron-up"></i> {{ item.nextText }}</span>
- <span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
- <span>{{ item.prevText }} <i class="ti ti-chevron-down"></i></span>
- </div>
- </template>
- </TransitionGroup>
- </div>
+ <div v-if="user && (!user.canChat || user.host !== null)">
+ <MkInfo warn>{{ i18n.ts._chat.chatNotAvailableInOtherAccount }}</MkInfo>
+ </div>
- <div v-if="user && (!user.canChat || user.host !== null)">
- <MkInfo warn>{{ i18n.ts._chat.chatNotAvailableInOtherAccount }}</MkInfo>
+ <MkInfo v-if="$i.policies.chatAvailability !== 'available'" warn>{{ $i.policies.chatAvailability === 'readonly' ? i18n.ts._chat.chatIsReadOnlyForThisAccountOrServer : i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo>
</div>
-
- <MkInfo v-if="!$i.policies.canChat" warn>{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo>
</MkSpacer>
<MkSpacer v-else-if="tab === 'search'" :contentMax="700">
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 647fed10e3..42a7b486ed 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -379,44 +379,46 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder>
</SearchMarker>
- <SearchMarker :keywords="['chat', 'messaging']">
- <MkFolder>
- <template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template>
- <template #icon><SearchIcon><i class="ti ti-messages"></i></SearchIcon></template>
+ <template v-if="$i.policies.chatAvailability !== 'unavailable'">
+ <SearchMarker :keywords="['chat', 'messaging']">
+ <MkFolder>
+ <template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template>
+ <template #icon><SearchIcon><i class="ti ti-messages"></i></SearchIcon></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>
+ <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>
+ <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>
- <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>
+ </template>
+ </MkSwitch>
+ </MkPreferenceContainer>
+ </SearchMarker>
+ </div>
+ </MkFolder>
+ </SearchMarker>
+ </template>
<SearchMarker :keywords="['accessibility']">
<MkFolder>
@@ -732,6 +734,9 @@ import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import { globalEvents } from '@/events.js';
import { claimAchievement } from '@/utility/achievements.js';
import { instance } from '@/instance.js';
+import { ensureSignin } from '@/i.js';
+
+const $i = ensureSignin();
const lang = ref(miLocalStorage.getItem('lang'));
const dataSaver = ref(prefer.s.dataSaver);
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index 2f8a697d74..4e6425667e 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -78,19 +78,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</SearchMarker>
- <FormSection>
- <SearchMarker :keywords="['chat']">
- <MkSelect v-model="chatScope" @update:modelValue="save()">
- <template #label><SearchLabel>{{ i18n.ts._chat.chatAllowedUsers }}</SearchLabel></template>
- <option value="everyone">{{ i18n.ts._chat._chatAllowedUsers.everyone }}</option>
- <option value="followers">{{ i18n.ts._chat._chatAllowedUsers.followers }}</option>
- <option value="following">{{ i18n.ts._chat._chatAllowedUsers.following }}</option>
- <option value="mutual">{{ i18n.ts._chat._chatAllowedUsers.mutual }}</option>
- <option value="none">{{ i18n.ts._chat._chatAllowedUsers.none }}</option>
- <template #caption>{{ i18n.ts._chat.chatAllowedUsers_note }}</template>
- </MkSelect>
- </SearchMarker>
- </FormSection>
+ <SearchMarker :keywords="['chat']">
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template>
+
+ <div class="_gaps_m">
+ <MkInfo v-if="$i.policies.chatAvailability === 'unavailable'">{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo>
+ <SearchMarker :keywords="['chat']">
+ <MkSelect v-model="chatScope" @update:modelValue="save()">
+ <template #label><SearchLabel>{{ i18n.ts._chat.chatAllowedUsers }}</SearchLabel></template>
+ <option value="everyone">{{ i18n.ts._chat._chatAllowedUsers.everyone }}</option>
+ <option value="followers">{{ i18n.ts._chat._chatAllowedUsers.followers }}</option>
+ <option value="following">{{ i18n.ts._chat._chatAllowedUsers.following }}</option>
+ <option value="mutual">{{ i18n.ts._chat._chatAllowedUsers.mutual }}</option>
+ <option value="none">{{ i18n.ts._chat._chatAllowedUsers.none }}</option>
+ <template #caption>{{ i18n.ts._chat.chatAllowedUsers_note }}</template>
+ </MkSelect>
+ </SearchMarker>
+ </div>
+ </FormSection>
+ </SearchMarker>
<SearchMarker :keywords="['lockdown']">
<FormSection>
diff --git a/packages/frontend/src/router.definition.ts b/packages/frontend/src/router.definition.ts
index 23118160e2..d59b160b8b 100644
--- a/packages/frontend/src/router.definition.ts
+++ b/packages/frontend/src/router.definition.ts
@@ -16,6 +16,10 @@ export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
errorComponent: MkError,
});
+function chatPage(...args: Parameters<typeof page>) {
+ return $i?.policies.chatAvailability !== 'unavailable' ? page(...args) : page(() => import('@/pages/not-found.vue'));
+}
+
export const ROUTE_DEF = [{
path: '/@:username/pages/:pageName(*)',
component: page(() => import('@/pages/page.vue')),
@@ -42,19 +46,19 @@ export const ROUTE_DEF = [{
component: page(() => import('@/pages/clip.vue')),
}, {
path: '/chat',
- component: page(() => import('@/pages/chat/home.vue')),
+ component: chatPage(() => import('@/pages/chat/home.vue')),
loginRequired: true,
}, {
path: '/chat/user/:userId',
- component: page(() => import('@/pages/chat/room.vue')),
+ component: chatPage(() => import('@/pages/chat/room.vue')),
loginRequired: true,
}, {
path: '/chat/room/:roomId',
- component: page(() => import('@/pages/chat/room.vue')),
+ component: chatPage(() => import('@/pages/chat/room.vue')),
loginRequired: true,
}, {
path: '/chat/messages/:messageId',
- component: page(() => import('@/pages/chat/message.vue')),
+ component: chatPage(() => import('@/pages/chat/message.vue')),
loginRequired: true,
}, {
path: '/instance-info/:host',
diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts
index 8cf9f31d3e..563e45c446 100644
--- a/packages/frontend/src/utility/get-user-menu.ts
+++ b/packages/frontend/src/utility/get-user-menu.ts
@@ -364,7 +364,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
},
});
- if ($i.policies.canChat && user.canChat && user.host == null) {
+ if ($i.policies.chatAvailability === 'available' && user.canChat && user.host == null) {
menuItems.push({
type: 'link',
icon: 'ti ti-messages',
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 6bbf1e3319..6cc55507c0 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5179,7 +5179,8 @@ export type components = {
canImportFollowing: boolean;
canImportMuting: boolean;
canImportUserLists: boolean;
- canChat: boolean;
+ /** @enum {string} */
+ chatAvailability: 'available' | 'readonly' | 'unavailable';
};
ReversiGameLite: {
/** Format: id */