mirror of
https://github.com/misskey-dev/misskey.git
synced 2025-04-29 17:06:57 +02:00
enhance: チャットの閲覧を無効化できるように (#15765)
* enhance: チャットの閲覧を無効化できるように * fix * fix * fix * readonlyの説明を追加 * enhance: チャットが無効な場合はチャット関連の設定も隠すように * fix * refactor: ChatServiceからApiに関するドメイン知識を排除
This commit is contained in:
parent
6c27ab12eb
commit
9d3f3264fd
42 changed files with 255 additions and 136 deletions
locales
packages
backend/src
core
models/json-schema
server/api/endpoints/chat
frontend-shared/js
frontend/src
misskey-js/src/autogen
10
locales/index.d.ts
vendored
10
locales/index.d.ts
vendored
|
@ -5386,6 +5386,10 @@ export interface Locale extends ILocale {
|
|||
* 設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)
|
||||
*/
|
||||
"settingsMigrating": string;
|
||||
/**
|
||||
* 読み取り専用
|
||||
*/
|
||||
"readonly": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* まだメッセージはありません
|
||||
|
@ -5500,6 +5504,10 @@ export interface Locale extends ILocale {
|
|||
* このサーバー、またはこのアカウントでチャットは有効化されていません。
|
||||
*/
|
||||
"chatNotAvailableForThisAccountOrServer": string;
|
||||
/**
|
||||
* このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。
|
||||
*/
|
||||
"chatIsReadOnlyForThisAccountOrServer": string;
|
||||
/**
|
||||
* 相手のアカウントでチャット機能が使えない状態になっています。
|
||||
*/
|
||||
|
@ -7531,7 +7539,7 @@ export interface Locale extends ILocale {
|
|||
/**
|
||||
* チャットを許可
|
||||
*/
|
||||
"canChat": string;
|
||||
"chatAvailability": string;
|
||||
};
|
||||
"_condition": {
|
||||
/**
|
||||
|
|
|
@ -1342,6 +1342,7 @@ bottom: "下"
|
|||
top: "上"
|
||||
embed: "埋め込み"
|
||||
settingsMigrating: "設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)"
|
||||
readonly: "読み取り専用"
|
||||
|
||||
_chat:
|
||||
noMessagesYet: "まだメッセージはありません"
|
||||
|
@ -1372,6 +1373,7 @@ _chat:
|
|||
muteThisRoom: "このルームをミュート"
|
||||
deleteRoom: "ルームを削除"
|
||||
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。"
|
||||
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。"
|
||||
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。"
|
||||
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
|
||||
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
|
||||
|
@ -1950,7 +1952,7 @@ _role:
|
|||
canImportFollowing: "フォローのインポートを許可"
|
||||
canImportMuting: "ミュートのインポートを許可"
|
||||
canImportUserLists: "リストのインポートを許可"
|
||||
canChat: "チャットを許可"
|
||||
chatAvailability: "チャットを許可"
|
||||
_condition:
|
||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||
isLocal: "ローカルユーザー"
|
||||
|
|
|
@ -94,6 +94,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;
|
||||
|
@ -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)');
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ?? '',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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']"/>
|
||||
|
||||
|
|
|
@ -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 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 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 class="_gaps">
|
||||
<div v-if="initializing">
|
||||
<MkLoading/>
|
||||
</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-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 v-if="user && (!user.canChat || user.host !== null)">
|
||||
<MkInfo warn>{{ i18n.ts._chat.chatNotAvailableInOtherAccount }}</MkInfo>
|
||||
</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>
|
||||
|
||||
<MkInfo v-if="!$i.policies.canChat" warn>{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo>
|
||||
<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>
|
||||
|
||||
<MkInfo v-if="$i.policies.chatAvailability !== 'available'" warn>{{ $i.policies.chatAvailability === 'readonly' ? i18n.ts._chat.chatIsReadOnlyForThisAccountOrServer : i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
||||
<MkSpacer v-else-if="tab === 'search'" :contentMax="700">
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -5179,7 +5179,8 @@ export type components = {
|
|||
canImportFollowing: boolean;
|
||||
canImportMuting: boolean;
|
||||
canImportUserLists: boolean;
|
||||
canChat: boolean;
|
||||
/** @enum {string} */
|
||||
chatAvailability: 'available' | 'readonly' | 'unavailable';
|
||||
};
|
||||
ReversiGameLite: {
|
||||
/** Format: id */
|
||||
|
|
Loading…
Add table
Reference in a new issue