This commit is contained in:
syuilo
2025-03-22 16:23:16 +09:00
parent 381126bdc7
commit acac58e59c
17 changed files with 253 additions and 61 deletions

82
locales/index.d.ts vendored
View File

@@ -5358,31 +5358,63 @@ export interface Locale extends ILocale {
* チャット * チャット
*/ */
"chat": string; "chat": string;
/** "_chat": {
* 個人チャット /**
*/ * 個人チャット
"individualChat": string; */
/** "individualChat": string;
* 特定ユーザーとの一対一のチャットができます。 /**
*/ * 特定ユーザーとの一対一のチャットができます。
"individualChat_description": string; */
/** "individualChat_description": string;
* ルームチャット /**
*/ * ルームチャット
"roomChat": string; */
/** "roomChat": string;
* 複数人でのチャットができます。 /**
* また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。 * 複数人でのチャットができます。
*/ * また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。
"roomChat_description": string; */
/** "roomChat_description": string;
* このユーザーとのチャットを開始できません /**
*/ * このユーザーとのチャットを開始できません
"cannotChatWithTheUser": string; */
/** "cannotChatWithTheUser": string;
* チャットが使えない状態になっているか、相手がチャットを開放していません。 /**
*/ * チャットが使えない状態になっているか、相手がチャットを開放していません。
"cannotChatWithTheUser_description": string; */
"cannotChatWithTheUser_description": string;
/**
* チャットを許可する相手
*/
"chatAllowedUsers": string;
/**
* 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。
*/
"chatAllowedUsers_note": string;
"_chatAllowedUsers": {
/**
* 誰でも
*/
"everyone": string;
/**
* 自分のフォロワーのみ
*/
"followers": string;
/**
* 自分がフォローしているユーザーのみ
*/
"following": string;
/**
* 相互フォローのユーザーのみ
*/
"mutual": string;
/**
* 誰も許可しない
*/
"none": string;
};
};
"_emojiPalette": { "_emojiPalette": {
/** /**
* パレット * パレット

View File

@@ -1335,12 +1335,23 @@ postForm: "投稿フォーム"
textCount: "文字数" textCount: "文字数"
information: "情報" information: "情報"
chat: "チャット" chat: "チャット"
individualChat: "個人チャット"
individualChat_description: "特定ユーザーとの一対一のチャットができます。" _chat:
roomChat: "ルームチャット" individualChat: "個人チャット"
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。" individualChat_description: "特定ユーザーとの一対一のチャットができます。"
cannotChatWithTheUser: "このユーザーとのチャットを開始できません" roomChat: "ルームチャット"
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。" roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
chatAllowedUsers: "チャットを許可する相手"
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
_chatAllowedUsers:
everyone: "誰でも"
followers: "自分のフォロワーのみ"
following: "自分がフォローしているユーザーのみ"
mutual: "相互フォローのユーザーのみ"
none: "誰も許可しない"
_emojiPalette: _emojiPalette:
palettes: "パレット" palettes: "パレット"

View File

@@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Chat31742617546147 {
name = 'Chat31742617546147'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "chat_approval" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "otherId" character varying(32) NOT NULL, CONSTRAINT "PK_fbbb95d60acf5c85388345b5f5d" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_530257863e1381a7f2f1d3282f" ON "chat_approval" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_b1d46037f23d170da5c05fdf75" ON "chat_approval" ("otherId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_12c4768a2f706fc267f2078903" ON "chat_approval" ("userId", "otherId") `);
await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_530257863e1381a7f2f1d3282fe" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_b1d46037f23d170da5c05fdf755" FOREIGN KEY ("otherId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_b1d46037f23d170da5c05fdf755"`);
await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_530257863e1381a7f2f1d3282fe"`);
await queryRunner.query(`DROP INDEX "public"."IDX_12c4768a2f706fc267f2078903"`);
await queryRunner.query(`DROP INDEX "public"."IDX_b1d46037f23d170da5c05fdf75"`);
await queryRunner.query(`DROP INDEX "public"."IDX_530257863e1381a7f2f1d3282f"`);
await queryRunner.query(`DROP TABLE "chat_approval"`);
}
}

View File

@@ -16,7 +16,7 @@ import { ChatMessageEntityService } from '@/core/entities/ChatMessageEntityServi
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { ChatMessagesRepository, MiChatMessage, MiChatRoom, MiDriveFile, MiUser, MutingsRepository, UsersRepository } from '@/models/_.js'; import type { ChatApprovalsRepository, ChatMessagesRepository, ChatRoomMembershipsRepository, ChatRoomsRepository, MiChatMessage, MiChatRoom, MiDriveFile, MiUser, MutingsRepository, UsersRepository } from '@/models/_.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
@@ -37,6 +37,15 @@ export class ChatService {
@Inject(DI.chatMessagesRepository) @Inject(DI.chatMessagesRepository)
private chatMessagesRepository: ChatMessagesRepository, private chatMessagesRepository: ChatMessagesRepository,
@Inject(DI.chatApprovalsRepository)
private chatApprovalsRepository: ChatApprovalsRepository,
@Inject(DI.chatRoomsRepository)
private chatRoomsRepository: ChatRoomsRepository,
@Inject(DI.chatRoomMembershipsRepository)
private chatRoomMembershipsRepository: ChatRoomMembershipsRepository,
@Inject(DI.mutingsRepository) @Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository, private mutingsRepository: MutingsRepository,
@@ -64,22 +73,39 @@ export class ChatService {
throw new Error('yourself'); throw new Error('yourself');
} }
if (toUser.chatScope === 'none') { const approvals = await this.chatApprovalsRepository.createQueryBuilder('approval')
throw new Error('recipient is cannot chat'); .where(new Brackets(qb => { // 自分が相手を許可しているか
} else if (toUser.chatScope === 'followers') { qb.where('approval.userId = :fromUserId', { fromUserId: fromUser.id })
const isFollower = await this.userFollowingService.isFollowing(fromUser.id, toUser.id); .andWhere('approval.otherId = :toUserId', { toUserId: toUser.id });
if (!isFollower) { }))
throw new Error('recipient is cannot chat'); .orWhere(new Brackets(qb => { // 相手が自分を許可しているか
} qb.where('approval.userId = :toUserId', { toUserId: toUser.id })
} else if (toUser.chatScope === 'following') { .andWhere('approval.otherId = :fromUserId', { fromUserId: fromUser.id });
const isFollowing = await this.userFollowingService.isFollowing(toUser.id, fromUser.id); }))
if (!isFollowing) { .take(2)
throw new Error('recipient is cannot chat'); .getMany();
}
} else if (toUser.chatScope === 'mutual') { const otherApprovedMe = approvals.some(approval => approval.userId === toUser.id);
const isMutual = await this.userFollowingService.isMutual(fromUser.id, toUser.id); const iApprovedOther = approvals.some(approval => approval.userId === fromUser.id);
if (!isMutual) {
if (!otherApprovedMe) {
if (toUser.chatScope === 'none') {
throw new Error('recipient is cannot chat'); throw new Error('recipient is cannot chat');
} else if (toUser.chatScope === 'followers') {
const isFollower = await this.userFollowingService.isFollowing(fromUser.id, toUser.id);
if (!isFollower) {
throw new Error('recipient is cannot chat');
}
} else if (toUser.chatScope === 'following') {
const isFollowing = await this.userFollowingService.isFollowing(toUser.id, fromUser.id);
if (!isFollowing) {
throw new Error('recipient is cannot chat');
}
} else if (toUser.chatScope === 'mutual') {
const isMutual = await this.userFollowingService.isMutual(fromUser.id, toUser.id);
if (!isMutual) {
throw new Error('recipient is cannot chat');
}
} }
} }
@@ -104,6 +130,15 @@ export class ChatService {
const inserted = await this.chatMessagesRepository.insertOne(message); const inserted = await this.chatMessagesRepository.insertOne(message);
// 相手を許可しておく
if (!iApprovedOther) {
this.chatApprovalsRepository.insertOne({
id: this.idService.gen(),
userId: fromUser.id,
otherId: toUser.id,
});
}
const packedMessage = await this.chatMessageEntityService.packLite(inserted); const packedMessage = await this.chatMessageEntityService.packLite(inserted);
if (this.userEntityService.isLocalUser(toUser)) { if (this.userEntityService.isLocalUser(toUser)) {

View File

@@ -558,6 +558,7 @@ export class UserEntityService implements OnModuleInit {
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964 publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
followersVisibility: profile!.followersVisibility, followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility, followingVisibility: profile!.followingVisibility,
chatScope: user.chatScope,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ 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, id: role.id,
name: role.name, name: role.name,

View File

@@ -84,6 +84,7 @@ export const DI = {
flashLikesRepository: Symbol('flashLikesRepository'), flashLikesRepository: Symbol('flashLikesRepository'),
userMemosRepository: Symbol('userMemosRepository'), userMemosRepository: Symbol('userMemosRepository'),
chatMessagesRepository: Symbol('chatMessagesRepository'), chatMessagesRepository: Symbol('chatMessagesRepository'),
chatApprovalsRepository: Symbol('chatApprovalsRepository'),
chatRoomsRepository: Symbol('chatRoomsRepository'), chatRoomsRepository: Symbol('chatRoomsRepository'),
chatRoomMembershipsRepository: Symbol('chatRoomMembershipsRepository'), chatRoomMembershipsRepository: Symbol('chatRoomMembershipsRepository'),
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),

View File

@@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
@Entity('chat_approval')
@Index(['userId', 'otherId'], { unique: true })
export class MiChatApproval {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Index()
@Column({
...id(),
})
public otherId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public other: MiUser | null;
}

View File

@@ -81,6 +81,7 @@ import {
MiChatMessage, MiChatMessage,
MiChatRoom, MiChatRoom,
MiChatRoomMembership, MiChatRoomMembership,
MiChatApproval,
} from './_.js'; } from './_.js';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
import type { DataSource } from 'typeorm'; import type { DataSource } from 'typeorm';
@@ -511,6 +512,12 @@ const $chatRoomMembershipsRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $chatApprovalsRepository: Provider = {
provide: DI.chatApprovalsRepository,
useFactory: (db: DataSource) => db.getRepository(MiChatApproval),
inject: [DI.db],
};
const $bubbleGameRecordsRepository: Provider = { const $bubbleGameRecordsRepository: Provider = {
provide: DI.bubbleGameRecordsRepository, provide: DI.bubbleGameRecordsRepository,
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>), useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>),
@@ -597,6 +604,7 @@ const $reversiGamesRepository: Provider = {
$chatMessagesRepository, $chatMessagesRepository,
$chatRoomsRepository, $chatRoomsRepository,
$chatRoomMembershipsRepository, $chatRoomMembershipsRepository,
$chatApprovalsRepository,
$bubbleGameRecordsRepository, $bubbleGameRecordsRepository,
$reversiGamesRepository, $reversiGamesRepository,
], ],
@@ -672,6 +680,7 @@ const $reversiGamesRepository: Provider = {
$chatMessagesRepository, $chatMessagesRepository,
$chatRoomsRepository, $chatRoomsRepository,
$chatRoomMembershipsRepository, $chatRoomMembershipsRepository,
$chatApprovalsRepository,
$bubbleGameRecordsRepository, $bubbleGameRecordsRepository,
$reversiGamesRepository, $reversiGamesRepository,
], ],

View File

@@ -78,6 +78,7 @@ import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { MiChatMessage } from '@/models/ChatMessage.js'; import { MiChatMessage } from '@/models/ChatMessage.js';
import { MiChatRoom } from '@/models/ChatRoom.js'; import { MiChatRoom } from '@/models/ChatRoom.js';
import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js'; import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
import { MiChatApproval } from '@/models/ChatApproval.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js'; import { MiReversiGame } from '@/models/ReversiGame.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
@@ -197,6 +198,7 @@ export {
MiChatMessage, MiChatMessage,
MiChatRoom, MiChatRoom,
MiChatRoomMembership, MiChatRoomMembership,
MiChatApproval,
MiBubbleGameRecord, MiBubbleGameRecord,
MiReversiGame, MiReversiGame,
}; };
@@ -272,5 +274,6 @@ export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMem
export type ChatMessagesRepository = Repository<MiChatMessage> & MiRepository<MiChatMessage>; export type ChatMessagesRepository = Repository<MiChatMessage> & MiRepository<MiChatMessage>;
export type ChatRoomsRepository = Repository<MiChatRoom> & MiRepository<MiChatRoom>; export type ChatRoomsRepository = Repository<MiChatRoom> & MiRepository<MiChatRoom>;
export type ChatRoomMembershipsRepository = Repository<MiChatRoomMembership> & MiRepository<MiChatRoomMembership>; export type ChatRoomMembershipsRepository = Repository<MiChatRoomMembership> & MiRepository<MiChatRoomMembership>;
export type ChatApprovalsRepository = Repository<MiChatApproval> & MiRepository<MiChatApproval>;
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>; export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>; export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;

View File

@@ -358,6 +358,11 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false, nullable: false, optional: false,
enum: ['public', 'followers', 'private'], enum: ['public', 'followers', 'private'],
}, },
chatScope: {
type: 'string',
nullable: false, optional: false,
enum: ['everyone', 'following', 'followers', 'mutual', 'none'],
},
roles: { roles: {
type: 'array', type: 'array',
nullable: false, optional: false, nullable: false, optional: false,

View File

@@ -8,6 +8,9 @@ import pg from 'pg';
import { DataSource, Logger } from 'typeorm'; import { DataSource, Logger } from 'typeorm';
import * as highlight from 'cli-highlight'; import * as highlight from 'cli-highlight';
import { entities as charts } from '@/core/chart/entities.js'; import { entities as charts } from '@/core/chart/entities.js';
import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
@@ -81,11 +84,8 @@ import { MiChatRoom } from '@/models/ChatRoom.js';
import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js'; import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js'; import { MiReversiGame } from '@/models/ReversiGame.js';
import { MiChatApproval } from '@/models/ChatApproval.js';
import { Config } from '@/config.js'; import { MiSystemAccount } from '@/models/SystemAccount.js';
import MisskeyLogger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { MiSystemAccount } from './models/SystemAccount.js';
pg.types.setTypeParser(20, Number); pg.types.setTypeParser(20, Number);
@@ -242,6 +242,7 @@ export const entities = [
MiChatMessage, MiChatMessage,
MiChatRoom, MiChatRoom,
MiChatRoomMembership, MiChatRoomMembership,
MiChatApproval,
MiBubbleGameRecord, MiBubbleGameRecord,
MiReversiGame, MiReversiGame,
...charts, ...charts,

View File

@@ -190,6 +190,7 @@ export const paramDef = {
autoSensitive: { type: 'boolean' }, autoSensitive: { type: 'boolean' },
followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
chatScope: { type: 'string', enum: ['everyone', 'followers', 'following', 'mutual', 'none'] },
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: muteWords, mutedWords: muteWords,
hardMutedWords: muteWords, hardMutedWords: muteWords,
@@ -288,6 +289,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility; if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility; if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
if (ps.chatScope !== undefined) updates.chatScope = ps.chatScope;
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
// TODO: ちゃんと数える // TODO: ちゃんと数える

View File

@@ -15,7 +15,7 @@ describe('ユーザー', () => {
// エンティティとしてのユーザーを主眼においたテストを記述する // エンティティとしてのユーザーを主眼においたテストを記述する
// (Userを返すエンドポイントとUserエンティティを書き換えるエンドポイントをテストする) // (Userを返すエンドポイントとUserエンティティを書き換えるエンドポイントをテストする)
const stripUndefined = <T extends { [key: string]: any }, >(orig: T): Partial<T> => { const stripUndefined = <T extends { [key: string]: any } >(orig: T): Partial<T> => {
return Object.entries({ ...orig }) return Object.entries({ ...orig })
.filter(([, value]) => value !== undefined) .filter(([, value]) => value !== undefined)
.reduce((obj: Partial<T>, [key, value]) => { .reduce((obj: Partial<T>, [key, value]) => {
@@ -83,6 +83,7 @@ describe('ユーザー', () => {
publicReactions: user.publicReactions, publicReactions: user.publicReactions,
followingVisibility: user.followingVisibility, followingVisibility: user.followingVisibility,
followersVisibility: user.followersVisibility, followersVisibility: user.followersVisibility,
chatScope: user.chatScope,
roles: user.roles, roles: user.roles,
memo: user.memo, memo: user.memo,
}); });
@@ -343,6 +344,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.publicReactions, true); assert.strictEqual(response.publicReactions, true);
assert.strictEqual(response.followingVisibility, 'public'); assert.strictEqual(response.followingVisibility, 'public');
assert.strictEqual(response.followersVisibility, 'public'); assert.strictEqual(response.followersVisibility, 'public');
assert.strictEqual(response.chatScope, 'mutual');
assert.deepStrictEqual(response.roles, []); assert.deepStrictEqual(response.roles, []);
assert.strictEqual(response.memo, null); assert.strictEqual(response.memo, null);

View File

@@ -66,13 +66,13 @@ const history = ref<{
function start(ev: MouseEvent) { function start(ev: MouseEvent) {
os.popupMenu([{ os.popupMenu([{
text: i18n.ts.individualChat, text: i18n.ts._chat.individualChat,
caption: i18n.ts.individualChat_description, caption: i18n.ts._chat.individualChat_description,
icon: 'ti ti-user', icon: 'ti ti-user',
action: () => { startUser(); }, action: () => { startUser(); },
}, { type: 'divider' }, { }, { type: 'divider' }, {
text: i18n.ts.roomChat, text: i18n.ts._chat.roomChat,
caption: i18n.ts.roomChat_description, caption: i18n.ts._chat.roomChat_description,
icon: 'ti ti-users', icon: 'ti ti-users',
action: () => { startRoom(); }, action: () => { startRoom(); },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);

View File

@@ -78,6 +78,20 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch> </MkSwitch>
</SearchMarker> </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="['lockdown']"> <SearchMarker :keywords="['lockdown']">
<FormSection> <FormSection>
<template #label><SearchLabel>{{ i18n.ts.lockdown }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template> <template #label><SearchLabel>{{ i18n.ts.lockdown }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
@@ -208,6 +222,7 @@ const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions); const publicReactions = ref($i.publicReactions);
const followingVisibility = ref($i.followingVisibility); const followingVisibility = ref($i.followingVisibility);
const followersVisibility = ref($i.followersVisibility); const followersVisibility = ref($i.followersVisibility);
const chatScope = ref($i.chatScope);
const makeNotesFollowersOnlyBefore_type = computed(() => { const makeNotesFollowersOnlyBefore_type = computed(() => {
if (makeNotesFollowersOnlyBefore.value == null) { if (makeNotesFollowersOnlyBefore.value == null) {
@@ -260,6 +275,7 @@ function save() {
publicReactions: !!publicReactions.value, publicReactions: !!publicReactions.value,
followingVisibility: followingVisibility.value, followingVisibility: followingVisibility.value,
followersVisibility: followersVisibility.value, followersVisibility: followersVisibility.value,
chatScope: chatScope.value,
}); });
} }

View File

@@ -240,20 +240,25 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['explore', i18n.ts.makeExplorableDescription], keywords: ['explore', i18n.ts.makeExplorableDescription],
}, },
{ {
id: '7vr04wKol', id: 'xEYlOghao',
label: i18n.ts._chat.chatAllowedUsers,
keywords: ['chat'],
},
{
id: 'BnOtlyaAh',
children: [ children: [
{ {
id: 'Av7fAaHv8', id: 'BzMIVBpL0',
label: i18n.ts._accountSettings.requireSigninToViewContents, label: i18n.ts._accountSettings.requireSigninToViewContents,
keywords: ['login', 'signin'], keywords: ['login', 'signin'],
}, },
{ {
id: '5RbESWefG', id: 'jJUqPqBAv',
label: i18n.ts._accountSettings.makeNotesFollowersOnlyBefore, label: i18n.ts._accountSettings.makeNotesFollowersOnlyBefore,
keywords: ['follower', i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription], keywords: ['follower', i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription],
}, },
{ {
id: 'hdzwDs3qd', id: 'ra10txIFV',
label: i18n.ts._accountSettings.makeNotesHiddenBefore, label: i18n.ts._accountSettings.makeNotesHiddenBefore,
keywords: ['hidden', i18n.ts._accountSettings.makeNotesHiddenBeforeDescription], keywords: ['hidden', i18n.ts._accountSettings.makeNotesHiddenBeforeDescription],
}, },

View File

@@ -3875,6 +3875,8 @@ export type components = {
followingVisibility: 'public' | 'followers' | 'private'; followingVisibility: 'public' | 'followers' | 'private';
/** @enum {string} */ /** @enum {string} */
followersVisibility: 'public' | 'followers' | 'private'; followersVisibility: 'public' | 'followers' | 'private';
/** @enum {string} */
chatScope: 'everyone' | 'following' | 'followers' | 'mutual' | 'none';
roles: components['schemas']['RoleLite'][]; roles: components['schemas']['RoleLite'][];
followedMessage?: string | null; followedMessage?: string | null;
memo: string | null; memo: string | null;
@@ -21330,6 +21332,8 @@ export type operations = {
followingVisibility?: 'public' | 'followers' | 'private'; followingVisibility?: 'public' | 'followers' | 'private';
/** @enum {string} */ /** @enum {string} */
followersVisibility?: 'public' | 'followers' | 'private'; followersVisibility?: 'public' | 'followers' | 'private';
/** @enum {string} */
chatScope?: 'everyone' | 'followers' | 'following' | 'mutual' | 'none';
/** Format: misskey:id */ /** Format: misskey:id */
pinnedPageId?: string | null; pinnedPageId?: string | null;
mutedWords?: (string[] | string)[]; mutedWords?: (string[] | string)[];