feat: per-user reply mutes
This commit is contained in:
parent
e02670bfb0
commit
233685f3fa
38 changed files with 435 additions and 2 deletions
|
@ -144,6 +144,7 @@
|
|||
|
||||
うまく動いていそうだったら本家に push されます
|
||||
|
||||
- 特定のユーザーのリプライをタイムラインから非表示する機能(「ブーストをミュート」のリプライ版)を追加
|
||||
- Docker/Podman の環境で `custom` ディレクトリの内容が反映されない不具合を修正
|
||||
- 画面を下に引いてタイムラインなどを更新する機能を追加(Misskey から取り込み)
|
||||
- 本家にもマージリクエストを出しました ([!10644](https://git.joinfirefish.org/firefish/firefish/-/merge_requests/10644))
|
||||
|
|
|
@ -141,8 +141,10 @@ clickToShowPatterns: "Click to show module patterns"
|
|||
enterFileName: "Enter filename"
|
||||
mute: "Mute"
|
||||
unmute: "Unmute"
|
||||
renoteMute: "Mute boosts"
|
||||
renoteUnmute: "Unmute boosts"
|
||||
renoteMute: "Mute boosts in timelines"
|
||||
renoteUnmute: "Unmute boosts in timelines"
|
||||
replyMute: "Mute replies in timelines"
|
||||
replyUnmute: "Unmute replies in timelines"
|
||||
block: "Block"
|
||||
unblock: "Unblock"
|
||||
suspend: "Suspend"
|
||||
|
|
|
@ -126,6 +126,8 @@ mute: "ミュート"
|
|||
unmute: "ミュート解除"
|
||||
renoteMute: "ブーストをミュート"
|
||||
renoteUnmute: "ブーストのミュートを解除"
|
||||
replyMute: "リプライをミュート"
|
||||
replyUnmute: "リプライのミュートを解除"
|
||||
block: "ブロック"
|
||||
unblock: "ブロック解除"
|
||||
suspend: "凍結"
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
-- reply-muting
|
||||
DROP TABLE "reply_muting";
|
||||
|
||||
-- vervis
|
||||
UPDATE meta SET "repositoryUrl" = 'https://git.joinfirefish.org/firefish/firefish';
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
export class AddReplyMuting1704851359889 {
|
||||
name = "AddReplyMuting1704851359889";
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "reply_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "muteeId" character varying(32) NOT NULL, "muterId" character varying(32) NOT NULL, CONSTRAINT "PK_replyMuting_id" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_reply_muting_createdAt" ON "muting" ("createdAt")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_reply_muting_muteeId" ON "muting" ("muteeId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_reply_muting_muterId" ON "muting" ("muterId")`,
|
||||
);
|
||||
}
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP TABLE "reply_muting"`);
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import { Following } from "@/models/entities/following.js";
|
|||
import { Instance } from "@/models/entities/instance.js";
|
||||
import { Muting } from "@/models/entities/muting.js";
|
||||
import { RenoteMuting } from "@/models/entities/renote-muting.js";
|
||||
import { ReplyMuting } from "@/models/entities/reply-muting.js";
|
||||
import { SwSubscription } from "@/models/entities/sw-subscription.js";
|
||||
import { Blocking } from "@/models/entities/blocking.js";
|
||||
import { UserList } from "@/models/entities/user-list.js";
|
||||
|
@ -137,6 +138,7 @@ export const entities = [
|
|||
FollowRequest,
|
||||
Muting,
|
||||
RenoteMuting,
|
||||
ReplyMuting,
|
||||
Blocking,
|
||||
Note,
|
||||
NoteEdit,
|
||||
|
|
|
@ -17,6 +17,7 @@ import { packedDriveFolderSchema } from "@/models/schema/drive-folder.js";
|
|||
import { packedFollowingSchema } from "@/models/schema/following.js";
|
||||
import { packedMutingSchema } from "@/models/schema/muting.js";
|
||||
import { packedRenoteMutingSchema } from "@/models/schema/renote-muting.js";
|
||||
import { packedReplyMutingSchema } from "@/models/schema/reply-muting.js";
|
||||
import { packedBlockingSchema } from "@/models/schema/blocking.js";
|
||||
import { packedNoteReactionSchema } from "@/models/schema/note-reaction.js";
|
||||
import { packedHashtagSchema } from "@/models/schema/hashtag.js";
|
||||
|
@ -55,6 +56,7 @@ export const refs = {
|
|||
Following: packedFollowingSchema,
|
||||
Muting: packedMutingSchema,
|
||||
RenoteMuting: packedRenoteMutingSchema,
|
||||
ReplyMuting: packedReplyMutingSchema,
|
||||
Blocking: packedBlockingSchema,
|
||||
Hashtag: packedHashtagSchema,
|
||||
Page: packedPageSchema,
|
||||
|
|
49
packages/backend/src/models/entities/reply-muting.ts
Normal file
49
packages/backend/src/models/entities/reply-muting.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import {
|
||||
PrimaryColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
import { id } from "../id.js";
|
||||
import { User } from "./user.js";
|
||||
|
||||
@Entity()
|
||||
@Index(["muterId", "muteeId"], { unique: true })
|
||||
export class ReplyMuting {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column("timestamp with time zone", {
|
||||
comment: "The created date of the Muting.",
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: "The mutee user ID.",
|
||||
})
|
||||
public muteeId: User["id"];
|
||||
|
||||
@ManyToOne((type) => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn()
|
||||
public mutee: User | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: "The muter user ID.",
|
||||
})
|
||||
public muterId: User["id"];
|
||||
|
||||
@ManyToOne((type) => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn()
|
||||
public muter: User | null;
|
||||
}
|
|
@ -26,6 +26,7 @@ import { UserGroupInvitationRepository } from "./repositories/user-group-invitat
|
|||
import { FollowRequestRepository } from "./repositories/follow-request.js";
|
||||
import { MutingRepository } from "./repositories/muting.js";
|
||||
import { RenoteMutingRepository } from "./repositories/renote-muting.js";
|
||||
import { ReplyMutingRepository } from "./repositories/reply-muting.js";
|
||||
import { BlockingRepository } from "./repositories/blocking.js";
|
||||
import { NoteReactionRepository } from "./repositories/note-reaction.js";
|
||||
import { NotificationRepository } from "./repositories/notification.js";
|
||||
|
@ -103,6 +104,7 @@ export const Notifications = NotificationRepository;
|
|||
export const Metas = db.getRepository(Meta);
|
||||
export const Mutings = MutingRepository;
|
||||
export const RenoteMutings = RenoteMutingRepository;
|
||||
export const ReplyMutings = ReplyMutingRepository;
|
||||
export const Blockings = BlockingRepository;
|
||||
export const SwSubscriptions = db.getRepository(SwSubscription);
|
||||
export const Hashtags = HashtagRepository;
|
||||
|
|
29
packages/backend/src/models/repositories/reply-muting.ts
Normal file
29
packages/backend/src/models/repositories/reply-muting.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { db } from "@/db/postgre.js";
|
||||
import { Packed } from "@/misc/schema.js";
|
||||
import { ReplyMuting } from "@/models/entities/reply-muting.js";
|
||||
import { User } from "@/models/entities/user.js";
|
||||
import { awaitAll } from "@/prelude/await-all.js";
|
||||
import { Users } from "../index.js";
|
||||
|
||||
export const ReplyMutingRepository = db.getRepository(ReplyMuting).extend({
|
||||
async pack(
|
||||
src: ReplyMuting["id"] | ReplyMuting,
|
||||
me?: { id: User["id"] } | null | undefined,
|
||||
): Promise<Packed<"ReplyMuting">> {
|
||||
const muting =
|
||||
typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: muting.id,
|
||||
createdAt: muting.createdAt.toISOString(),
|
||||
muteeId: muting.muteeId,
|
||||
mutee: Users.pack(muting.muteeId, me, {
|
||||
detail: true,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
packMany(mutings: any[], me: { id: User["id"] }) {
|
||||
return Promise.all(mutings.map((x) => this.pack(x, me)));
|
||||
},
|
||||
});
|
|
@ -26,6 +26,7 @@ import {
|
|||
MessagingMessages,
|
||||
Mutings,
|
||||
RenoteMutings,
|
||||
ReplyMutings,
|
||||
Notes,
|
||||
NoteUnreads,
|
||||
Notifications,
|
||||
|
@ -179,6 +180,13 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
},
|
||||
take: 1,
|
||||
}).then((n) => n > 0),
|
||||
isReplyMuted: ReplyMutings.count({
|
||||
where: {
|
||||
muterId: me,
|
||||
muteeId: target,
|
||||
},
|
||||
take: 1,
|
||||
}).then((n) => n > 0),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -602,6 +610,7 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
isBlocked: relation.isBlocked,
|
||||
isMuted: relation.isMuted,
|
||||
isRenoteMuted: relation.isRenoteMuted,
|
||||
isReplyMuted: relation.isReplyMuted,
|
||||
}
|
||||
: {}),
|
||||
} as Promiseable<Packed<"User">> as Promiseable<
|
||||
|
|
30
packages/backend/src/models/schema/reply-muting.ts
Normal file
30
packages/backend/src/models/schema/reply-muting.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
export const packedReplyMutingSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id",
|
||||
example: "xxxxxxxxxx",
|
||||
},
|
||||
createdAt: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "date-time",
|
||||
},
|
||||
muteeId: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id",
|
||||
},
|
||||
mutee: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "UserDetailed",
|
||||
},
|
||||
},
|
||||
} as const;
|
|
@ -345,6 +345,11 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
nullable: false,
|
||||
optional: true,
|
||||
},
|
||||
isReplyMuted: {
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
optional: true,
|
||||
},
|
||||
//#endregion
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { Brackets, SelectQueryBuilder } from "typeorm";
|
||||
import { User } from "@/models/entities/user.js";
|
||||
import { ReplyMutings } from "@/models/index.js";
|
||||
|
||||
export function generateMutedUserRepliesQueryForNotes(
|
||||
q: SelectQueryBuilder<any>,
|
||||
me: { id: User["id"] },
|
||||
): void {
|
||||
const mutingQuery = ReplyMutings.createQueryBuilder("reply_muting")
|
||||
.select("reply_muting.muteeId")
|
||||
.where("reply_muting.muterId = :muterId", { muterId: me.id });
|
||||
|
||||
q.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(
|
||||
new Brackets((qb) => {
|
||||
qb.where("note.replyId IS NOT NULL");
|
||||
qb.andWhere(`note.userId NOT IN (${mutingQuery.getQuery()})`);
|
||||
}),
|
||||
).orWhere("note.replyId IS NULL");
|
||||
}),
|
||||
);
|
||||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
}
|
|
@ -241,6 +241,9 @@ import * as ep___mute_list from "./endpoints/mute/list.js";
|
|||
import * as ep___renote_mute_create from "./endpoints/renote-mute/create.js";
|
||||
import * as ep___renote_mute_delete from "./endpoints/renote-mute/delete.js";
|
||||
import * as ep___renote_mute_list from "./endpoints/renote-mute/list.js";
|
||||
import * as ep___reply_mute_create from "./endpoints/reply-mute/create.js";
|
||||
import * as ep___reply_mute_delete from "./endpoints/reply-mute/delete.js";
|
||||
import * as ep___reply_mute_list from "./endpoints/reply-mute/list.js";
|
||||
import * as ep___my_apps from "./endpoints/my/apps.js";
|
||||
import * as ep___notes from "./endpoints/notes.js";
|
||||
import * as ep___notes_children from "./endpoints/notes/children.js";
|
||||
|
@ -651,6 +654,9 @@ const eps = [
|
|||
["renote-mute/create", ep___renote_mute_create],
|
||||
["renote-mute/delete", ep___renote_mute_delete],
|
||||
["renote-mute/list", ep___renote_mute_list],
|
||||
["reply-mute/create", ep___reply_mute_create],
|
||||
["reply-mute/delete", ep___reply_mute_delete],
|
||||
["reply-mute/list", ep___reply_mute_list],
|
||||
["custom-motd", ep___customMOTD],
|
||||
["custom-splash-icons", ep___customSplashIcons],
|
||||
["latest-version", ep___latestVersion],
|
||||
|
|
|
@ -9,6 +9,7 @@ import { generateRepliesQuery } from "@/server/api/common/generate-replies-query
|
|||
import { generateMutedNoteQuery } from "@/server/api/common/generate-muted-note-query.js";
|
||||
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
|
||||
import { generateMutedUserRenotesQueryForNotes } from "@/server/api/common/generated-muted-renote-query.js";
|
||||
import { generateMutedUserRepliesQueryForNotes } from "@/server/api/common/generated-muted-reply-query.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
@ -98,6 +99,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
generateMutedUserRenotesQueryForNotes(query, user);
|
||||
generateMutedUserRepliesQueryForNotes(query, user);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { generateMutedNoteQuery } from "@/server/api/common/generate-muted-note-
|
|||
import { generateChannelQuery } from "@/server/api/common/generate-channel-query.js";
|
||||
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
|
||||
import { generateMutedUserRenotesQueryForNotes } from "@/server/api/common/generated-muted-renote-query.js";
|
||||
import { generateMutedUserRepliesQueryForNotes } from "@/server/api/common/generated-muted-reply-query.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
@ -115,6 +116,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
generateMutedUserRenotesQueryForNotes(query, user);
|
||||
generateMutedUserRepliesQueryForNotes(query, user);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(
|
||||
|
|
|
@ -12,6 +12,7 @@ import { generateMutedNoteQuery } from "@/server/api/common/generate-muted-note-
|
|||
import { generateChannelQuery } from "@/server/api/common/generate-channel-query.js";
|
||||
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
|
||||
import { generateMutedUserRenotesQueryForNotes } from "@/server/api/common/generated-muted-renote-query.js";
|
||||
import { generateMutedUserRepliesQueryForNotes } from "@/server/api/common/generated-muted-reply-query.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
@ -108,6 +109,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
if (user) generateMutedNoteQuery(query, user);
|
||||
if (user) generateBlockedUserQuery(query, user);
|
||||
if (user) generateMutedUserRenotesQueryForNotes(query, user);
|
||||
if (user) generateMutedUserRepliesQueryForNotes(query, user);
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere("note.fileIds != '{}'");
|
||||
|
|
|
@ -12,6 +12,7 @@ import { generateMutedNoteQuery } from "@/server/api/common/generate-muted-note-
|
|||
import { generateChannelQuery } from "@/server/api/common/generate-channel-query.js";
|
||||
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
|
||||
import { generateMutedUserRenotesQueryForNotes } from "@/server/api/common/generated-muted-renote-query.js";
|
||||
import { generateMutedUserRepliesQueryForNotes } from "@/server/api/common/generated-muted-reply-query.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
@ -111,6 +112,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
if (user) generateMutedNoteQuery(query, user);
|
||||
if (user) generateBlockedUserQuery(query, user);
|
||||
if (user) generateMutedUserRenotesQueryForNotes(query, user);
|
||||
if (user) generateMutedUserRepliesQueryForNotes(query, user);
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere("note.fileIds != '{}'");
|
||||
|
|
|
@ -10,6 +10,7 @@ import { generateMutedNoteQuery } from "@/server/api/common/generate-muted-note-
|
|||
import { generateChannelQuery } from "@/server/api/common/generate-channel-query.js";
|
||||
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
|
||||
import { generateMutedUserRenotesQueryForNotes } from "@/server/api/common/generated-muted-renote-query.js";
|
||||
import { generateMutedUserRepliesQueryForNotes } from "@/server/api/common/generated-muted-reply-query.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
|
||||
export const meta = {
|
||||
|
@ -115,6 +116,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
generateMutedUserRenotesQueryForNotes(query, user);
|
||||
generateMutedUserRepliesQueryForNotes(query, user);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { genId } from "@/misc/gen-id.js";
|
||||
import { ReplyMutings } from "@/models/index.js";
|
||||
import { ReplyMuting } from "@/models/entities/reply-muting.js";
|
||||
import define from "@/server/api/define.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
import { getUser } from "@/server/api/common/getters.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["account"],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: "write:mutes",
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: "No such user.",
|
||||
code: "NO_SUCH_USER",
|
||||
id: "6fef56f3-e765-4957-88e5-c6f65329b8a5",
|
||||
},
|
||||
|
||||
alreadyMuting: {
|
||||
message: "You are already muting that user.",
|
||||
code: "ALREADY_MUTING",
|
||||
id: "7e7359cb-160c-4956-b08f-4d1c653cd007",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", format: "misskey:id" },
|
||||
},
|
||||
required: ["userId"],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const muter = user;
|
||||
|
||||
// Get mutee
|
||||
const mutee = await getUser(ps.userId).catch((e) => {
|
||||
if (e.id === "15348ddd-432d-49c2-8a5a-8069753becff")
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
throw e;
|
||||
});
|
||||
|
||||
// Check if already muting
|
||||
const exist = await ReplyMutings.findOneBy({
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
throw new ApiError(meta.errors.alreadyMuting);
|
||||
}
|
||||
|
||||
// Create mute
|
||||
await ReplyMutings.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
} as ReplyMuting);
|
||||
|
||||
// publishUserEvent(user.id, "mute", mutee);
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
import { ReplyMutings } from "@/models/index.js";
|
||||
import define from "@/server/api/define.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
import { getUser } from "@/server/api/common/getters.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["account"],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: "write:mutes",
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: "No such user.",
|
||||
code: "NO_SUCH_USER",
|
||||
id: "b851d00b-8ab1-4a56-8b1b-e24187cb48ef",
|
||||
},
|
||||
|
||||
notMuting: {
|
||||
message: "You are not muting that user.",
|
||||
code: "NOT_MUTING",
|
||||
id: "5467d020-daa9-4553-81e1-135c0c35a96d",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: { type: "string", format: "misskey:id" },
|
||||
},
|
||||
required: ["userId"],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const muter = user;
|
||||
|
||||
// Get mutee
|
||||
const mutee = await getUser(ps.userId).catch((e) => {
|
||||
if (e.id === "15348ddd-432d-49c2-8a5a-8069753becff")
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
throw e;
|
||||
});
|
||||
|
||||
// Check not muting
|
||||
const exist = await ReplyMutings.findOneBy({
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
throw new ApiError(meta.errors.notMuting);
|
||||
}
|
||||
|
||||
// Delete mute
|
||||
await ReplyMutings.delete({
|
||||
id: exist.id,
|
||||
});
|
||||
|
||||
// publishUserEvent(user.id, "unmute", mutee);
|
||||
});
|
46
packages/backend/src/server/api/endpoints/reply-mute/list.ts
Normal file
46
packages/backend/src/server/api/endpoints/reply-mute/list.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { ReplyMutings } from "@/models/index.js";
|
||||
import define from "@/server/api/define.js";
|
||||
import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["account"],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: "read:mutes",
|
||||
|
||||
res: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "ReplyMuting",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: { type: "integer", minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: "string", format: "misskey:id" },
|
||||
untilId: { type: "string", format: "misskey:id" },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const query = makePaginationQuery(
|
||||
ReplyMutings.createQueryBuilder("muting"),
|
||||
ps.sinceId,
|
||||
ps.untilId,
|
||||
).andWhere("muting.muterId = :meId", { meId: me.id });
|
||||
|
||||
const mutings = await query.take(ps.limit).getMany();
|
||||
|
||||
return await ReplyMutings.packMany(mutings, me);
|
||||
});
|
|
@ -62,6 +62,11 @@ export const meta = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
isReplyMuted: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -117,6 +122,11 @@ export const meta = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
isReplyMuted: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -34,6 +34,10 @@ export default abstract class Channel {
|
|||
return this.connection.renoteMuting;
|
||||
}
|
||||
|
||||
protected get replyMuting() {
|
||||
return this.connection.replyMuting;
|
||||
}
|
||||
|
||||
protected get blocking() {
|
||||
return this.connection.blocking;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ export default class extends Channel {
|
|||
|
||||
if (note.renote && !note.text && this.renoteMuting.has(note.userId))
|
||||
return;
|
||||
if (note.replyId != null && this.replyMuting.has(note.userId)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.blocking)) return;
|
||||
|
||||
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
||||
if (note.replyId != null && this.replyMuting.has(note.userId)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.blocking)) return;
|
||||
|
||||
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
||||
if (note.replyId != null && this.replyMuting.has(note.userId)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -39,6 +39,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.blocking)) return;
|
||||
|
||||
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
||||
if (note.replyId != null && this.replyMuting.has(note.userId)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.blocking)) return;
|
||||
|
||||
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
||||
if (note.replyId != null && this.replyMuting.has(note.userId)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -76,6 +76,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.blocking)) return;
|
||||
|
||||
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
||||
if (note.replyId != null && this.replyMuting.has(note.userId)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -54,6 +54,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.blocking)) return;
|
||||
|
||||
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
||||
if (note.replyId != null && this.replyMuting.has(note.userId)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -74,6 +74,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.blocking)) return;
|
||||
|
||||
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
||||
if (note.replyId != null && this.replyMuting.has(note.userId)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -60,6 +60,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.blocking)) return;
|
||||
|
||||
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
||||
if (note.replyId != null && this.replyMuting.has(note.userId)) return;
|
||||
|
||||
this.send("note", note);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
Followings,
|
||||
Mutings,
|
||||
RenoteMutings,
|
||||
ReplyMutings,
|
||||
UserProfiles,
|
||||
ChannelFollowings,
|
||||
Blockings,
|
||||
|
@ -37,6 +38,7 @@ export default class Connection {
|
|||
public following: Set<User["id"]> = new Set();
|
||||
public muting: Set<User["id"]> = new Set();
|
||||
public renoteMuting: Set<User["id"]> = new Set();
|
||||
public replyMuting: Set<User["id"]> = new Set();
|
||||
public blocking: Set<User["id"]> = new Set(); // "被"blocking
|
||||
public followingChannels: Set<ChannelModel["id"]> = new Set();
|
||||
public token?: AccessToken;
|
||||
|
@ -81,6 +83,7 @@ export default class Connection {
|
|||
this.updateFollowing();
|
||||
this.updateMuting();
|
||||
this.updateRenoteMuting();
|
||||
this.updateReplyMuting();
|
||||
this.updateBlocking();
|
||||
this.updateFollowingChannels();
|
||||
this.updateUserProfile();
|
||||
|
@ -115,6 +118,7 @@ export default class Connection {
|
|||
break;
|
||||
|
||||
// TODO: renote mute events
|
||||
// TODO: reply mute events
|
||||
// TODO: block events
|
||||
|
||||
case "followChannel":
|
||||
|
@ -564,6 +568,17 @@ export default class Connection {
|
|||
this.renoteMuting = new Set<string>(renoteMutings.map((x) => x.muteeId));
|
||||
}
|
||||
|
||||
private async updateReplyMuting() {
|
||||
const replyMutings = await ReplyMutings.find({
|
||||
where: {
|
||||
muterId: this.user?.id,
|
||||
},
|
||||
select: ["muteeId"],
|
||||
});
|
||||
|
||||
this.replyMuting = new Set<string>(replyMutings.map((x) => x.muteeId));
|
||||
}
|
||||
|
||||
private async updateBlocking() {
|
||||
// ここでいうBlockingは被Blockingの意
|
||||
const blockings = await Blockings.find({
|
||||
|
|
|
@ -127,6 +127,17 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
|||
});
|
||||
}
|
||||
|
||||
async function toggleReplyMute(): Promise<void> {
|
||||
os.apiWithDialog(
|
||||
user.isReplyMuted ? "reply-mute/delete" : "reply-mute/create",
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
).then(() => {
|
||||
user.isReplyMuted = !user.isReplyMuted;
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleBlock(): Promise<void> {
|
||||
if (
|
||||
!(await getConfirmed(
|
||||
|
@ -315,6 +326,11 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
|||
text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute,
|
||||
action: toggleRenoteMute,
|
||||
},
|
||||
{
|
||||
icon: user.isReplyMuted ? "ph-eye ph-lg" : "ph-eye-slash ph-lg",
|
||||
text: user.isReplyMuted ? i18n.ts.replyUnmute : i18n.ts.replyMute,
|
||||
action: toggleReplyMute,
|
||||
},
|
||||
] as any;
|
||||
|
||||
if (isSignedIn && $i.id !== user.id) {
|
||||
|
|
|
@ -791,6 +791,9 @@ export type Endpoints = {
|
|||
"renote-mute/create": { req: TODO; res: TODO };
|
||||
"renote-mute/delete": { req: { userId: User["id"] }; res: null };
|
||||
"renote-mute/list": { req: TODO; res: TODO };
|
||||
"reply-mute/create": { req: TODO; res: TODO };
|
||||
"reply-mute/delete": { req: { userId: User["id"] }; res: null };
|
||||
"reply-mute/list": { req: TODO; res: TODO };
|
||||
|
||||
// my
|
||||
"my/apps": { req: TODO; res: TODO };
|
||||
|
|
|
@ -58,6 +58,7 @@ export type UserDetailed = UserLite & {
|
|||
isModerator: boolean;
|
||||
isMuted: boolean;
|
||||
isRenoteMuted: boolean;
|
||||
isReplyMuted: boolean;
|
||||
isSilenced: boolean;
|
||||
isSuspended: boolean;
|
||||
lang: string | null;
|
||||
|
|
Loading…
Reference in a new issue