mute-renotes #189
36 changed files with 445 additions and 3 deletions
|
@ -121,6 +121,8 @@ unmarkAsSensitive: "Unmark as NSFW"
|
|||
enterFileName: "Enter filename"
|
||||
mute: "Mute"
|
||||
unmute: "Unmute"
|
||||
renoteMute: "Hide renotes"
|
||||
renoteUnmute: "Show renotes"
|
||||
block: "Block"
|
||||
unblock: "Unblock"
|
||||
suspend: "Suspend"
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
export class addRenoteMuting1665091090561 {
|
||||
constructor() {
|
||||
this.name = 'addRenoteMuting1665091090561';
|
||||
}
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "renote_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_renoteMuting_id" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "IDX_renote_muting_createdAt"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_renote_muting_muteeId"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_renote_muting_muterId"`);
|
||||
await queryRunner.query(`DROP TABLE "renote_muting"`);
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import { Meta } from '@/models/entities/meta.js';
|
|||
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 { SwSubscription } from '@/models/entities/sw-subscription.js';
|
||||
import { Blocking } from '@/models/entities/blocking.js';
|
||||
import { UserList } from '@/models/entities/user-list.js';
|
||||
|
@ -130,6 +131,7 @@ export const entities = [
|
|||
Following,
|
||||
FollowRequest,
|
||||
Muting,
|
||||
RenoteMuting,
|
||||
Blocking,
|
||||
Note,
|
||||
NoteFavorite,
|
||||
|
|
|
@ -16,6 +16,7 @@ import { packedDriveFileSchema } from '@/models/schema/drive-file.js';
|
|||
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 { packedBlockingSchema } from '@/models/schema/blocking.js';
|
||||
import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js';
|
||||
import { packedHashtagSchema } from '@/models/schema/hashtag.js';
|
||||
|
@ -51,6 +52,7 @@ export const refs = {
|
|||
DriveFolder: packedDriveFolderSchema,
|
||||
Following: packedFollowingSchema,
|
||||
Muting: packedMutingSchema,
|
||||
RenoteMuting: packedRenoteMutingSchema,
|
||||
Blocking: packedBlockingSchema,
|
||||
Hashtag: packedHashtagSchema,
|
||||
Page: packedPageSchema,
|
||||
|
|
42
packages/backend/src/models/entities/renote-muting.ts
Normal file
42
packages/backend/src/models/entities/renote-muting.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
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 RenoteMuting {
|
||||
@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;
|
||||
}
|
|
@ -25,6 +25,7 @@ import { UserGroupJoining } from './entities/user-group-joining.js';
|
|||
import { UserGroupInvitationRepository } from './repositories/user-group-invitation.js';
|
||||
import { FollowRequestRepository } from './repositories/follow-request.js';
|
||||
import { MutingRepository } from './repositories/muting.js';
|
||||
import { RenoteMutingRepository } from './repositories/renote-muting.js';
|
||||
import { BlockingRepository } from './repositories/blocking.js';
|
||||
import { NoteReactionRepository } from './repositories/note-reaction.js';
|
||||
import { NotificationRepository } from './repositories/notification.js';
|
||||
|
@ -95,6 +96,7 @@ export const DriveFolders = (DriveFolderRepository);
|
|||
export const Notifications = (NotificationRepository);
|
||||
export const Metas = db.getRepository(Meta);
|
||||
export const Mutings = (MutingRepository);
|
||||
export const RenoteMutings = (RenoteMutingRepository);
|
||||
export const Blockings = (BlockingRepository);
|
||||
export const SwSubscriptions = db.getRepository(SwSubscription);
|
||||
export const Hashtags = (HashtagRepository);
|
||||
|
|
31
packages/backend/src/models/repositories/renote-muting.ts
Normal file
31
packages/backend/src/models/repositories/renote-muting.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { db } from '@/db/postgre.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { RenoteMuting } from '@/models/entities/renote-muting.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
import { Users } from '../index.js';
|
||||
|
||||
export const RenoteMutingRepository = db.getRepository(RenoteMuting).extend({
|
||||
async pack(
|
||||
src: RenoteMuting['id'] | RenoteMuting,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
): Promise<Packed<'RenoteMuting'>> {
|
||||
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)));
|
||||
},
|
||||
});
|
|
@ -10,7 +10,7 @@ import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
|||
import { Cache } from '@/misc/cache.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Instance } from '../entities/instance.js';
|
||||
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
|
||||
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, RenoteMutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
|
||||
|
||||
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
|
||||
|
||||
|
@ -112,6 +112,13 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
isRenoteMuted: RenoteMutings.count({
|
||||
where: {
|
||||
muterId: me,
|
||||
muteeId: target,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -412,6 +419,7 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
isBlocking: relation.isBlocking,
|
||||
isBlocked: relation.isBlocked,
|
||||
isMuted: relation.isMuted,
|
||||
isRenoteMuted: relation.isRenoteMuted,
|
||||
} : {}),
|
||||
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
||||
|
||||
|
|
26
packages/backend/src/models/schema/renote-muting.ts
Normal file
26
packages/backend/src/models/schema/renote-muting.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
export const packedRenoteMutingSchema = {
|
||||
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;
|
|
@ -263,6 +263,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
isRenoteMuted: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
//#endregion
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { RenoteMutings } from '@/models/index.js';
|
||||
|
||||
export function generateMutedRenotesQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void {
|
||||
toast marked this conversation as resolved
Outdated
|
||||
const mutingQuery = RenoteMutings.createQueryBuilder('renote_muting')
|
||||
.select('renote_muting.muteeId')
|
||||
.where('renote_muting.muterId = :muterId', { muterId: me.id });
|
||||
|
||||
q.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where(new Brackets(qb => {
|
||||
qb.where('note.renoteId IS NOT NULL');
|
||||
qb.andWhere('note.text IS NULL');
|
||||
qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
|
||||
}))
|
||||
.orWhere('note.renoteId IS NULL')
|
||||
.orWhere('note.text IS NOT NULL');
|
||||
}));
|
||||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
}
|
|
@ -214,6 +214,9 @@ import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
|
|||
import * as ep___mute_create from './endpoints/mute/create.js';
|
||||
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
||||
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___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';
|
||||
|
@ -521,6 +524,9 @@ const eps = [
|
|||
['mute/create', ep___mute_create],
|
||||
['mute/delete', ep___mute_delete],
|
||||
['mute/list', ep___mute_list],
|
||||
['renote-mute/create', ep___renote_mute_create],
|
||||
['renote-mute/delete', ep___renote_mute_delete],
|
||||
['renote-mute/list', ep___renote_mute_list],
|
||||
['my/apps', ep___my_apps],
|
||||
['notes', ep___notes],
|
||||
['notes/children', ep___notes_children],
|
||||
|
|
|
@ -87,4 +87,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
return await Users.pack(blockee.id, blocker, {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
publishUserEvent(user.id, 'block', blockee);
|
||||
});
|
||||
|
|
|
@ -83,4 +83,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
return await Users.pack(blockee.id, blocker, {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
publishUserEvent(user.id, 'unblock', blockee);
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@ import { generateMutedUserQuery } from '../../common/generate-muted-user-query.j
|
|||
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||
import { generateMutedRenotesQuery } from '../../common/generated-muted-renote-query.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
@ -79,6 +80,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
generateMutedRenotesQuery(query, user);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
|||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||
import { generateMutedRenotesQuery } from '../../common/generated-muted-renote-query.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
@ -93,6 +94,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
generateMutedRenotesQuery(query, user);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
|||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||
import { generateMutedRenotesQuery } from '../../common/generated-muted-renote-query.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
@ -86,6 +87,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
if (user) generateMutedUserQuery(query, user);
|
||||
if (user) generateMutedNoteQuery(query, user);
|
||||
if (user) generateBlockedUserQuery(query, user);
|
||||
if (user) generateMutedRenotesQuery(query, user);
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
|
|
|
@ -9,6 +9,7 @@ import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
|||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||
import { generateMutedRenotesQuery } from '../../common/generated-muted-renote-query.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
@ -85,6 +86,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
generateMutedRenotesQuery(query, user);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import { genId } from '@/misc/gen-id.js';
|
||||
import { RenoteMutings } from '@/models/index.js';
|
||||
import { RenoteMuting } from '@/models/entities/renote-muting.js';
|
||||
import { publishUserEvent } from '@/services/stream.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { getUser } from '../../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',
|
||||
},
|
||||
|
||||
muteeIsYourself: {
|
||||
message: 'Mutee is yourself.',
|
||||
code: 'MUTEE_IS_YOURSELF',
|
||||
id: 'a4619cb2-5f23-484b-9301-94c903074e10',
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
// Check if the mutee is yourself
|
||||
toast marked this conversation as resolved
Outdated
Johann150
commented
Missed a Japanese comment here.
Missed a Japanese comment here.
```diff
- // 自分自身
+ // Check if the mutee is yourself
```
|
||||
if (user.id === ps.userId) {
|
||||
throw new ApiError(meta.errors.muteeIsYourself);
|
||||
}
|
||||
|
||||
// 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 RenoteMutings.findOneBy({
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
throw new ApiError(meta.errors.alreadyMuting);
|
||||
}
|
||||
|
||||
// Create mute
|
||||
await RenoteMutings.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
} as RenoteMuting);
|
||||
|
||||
publishUserEvent(user.id, 'muteRenote', mutee);
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
import { RenoteMutings } from '@/models/index.js';
|
||||
import { publishUserEvent } from '@/services/stream.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { getUser } from '../../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',
|
||||
},
|
||||
|
||||
muteeIsYourself: {
|
||||
message: 'Mutee is yourself.',
|
||||
code: 'MUTEE_IS_YOURSELF',
|
||||
id: 'f428b029-6b39-4d48-a1d2-cc1ae6dd5cf9',
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
// Check if the mutee is yourself
|
||||
if (user.id === ps.userId) {
|
||||
throw new ApiError(meta.errors.muteeIsYourself);
|
||||
}
|
||||
|
||||
// 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 RenoteMutings.findOneBy({
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
throw new ApiError(meta.errors.notMuting);
|
||||
}
|
||||
|
||||
// Delete mute
|
||||
await RenoteMutings.delete({
|
||||
id: exist.id,
|
||||
});
|
||||
|
||||
publishUserEvent(user.id, 'unmuteRenote', mutee);
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
import { RenoteMutings } from '@/models/index.js';
|
||||
import define from '../../define.js';
|
||||
import { makePaginationQuery } from '../../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: 'RenoteMuting',
|
||||
},
|
||||
},
|
||||
} 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(RenoteMutings.createQueryBuilder('muting'), ps.sinceId, ps.untilId)
|
||||
.andWhere('muting.muterId = :meId', { meId: me.id });
|
||||
|
||||
const mutings = await query
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await RenoteMutings.packMany(mutings, me);
|
||||
});
|
|
@ -47,6 +47,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isRenoteMuted: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -88,6 +92,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isRenoteMuted: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -30,6 +30,10 @@ export default abstract class Channel {
|
|||
return this.connection.muting;
|
||||
}
|
||||
|
||||
protected get renoteMuting() {
|
||||
return this.connection.renoteMuting;
|
||||
}
|
||||
|
||||
protected get blocking() {
|
||||
return this.connection.blocking;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -32,6 +32,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -49,6 +49,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -40,6 +40,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -55,6 +55,7 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||
|
||||
this.send('note', note);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as websocket from 'websocket';
|
|||
import readNote from '@/services/note/read.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Channel as ChannelModel } from '@/models/entities/channel.js';
|
||||
import { Followings, Mutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js';
|
||||
import { Followings, Mutings, RenoteMutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js';
|
||||
import { AccessToken } from '@/models/entities/access-token.js';
|
||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||
import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream.js';
|
||||
|
@ -22,6 +22,7 @@ export default class Connection {
|
|||
public userProfile?: UserProfile | null;
|
||||
public following: Set<User['id']> = new Set();
|
||||
public muting: Set<User['id']> = new Set();
|
||||
public renoteMuting: Set<User['id']> = new Set();
|
||||
public blocking: Set<User['id']> = new Set(); // "被"blocking
|
||||
public followingChannels: Set<ChannelModel['id']> = new Set();
|
||||
public token?: AccessToken;
|
||||
|
@ -56,6 +57,7 @@ export default class Connection {
|
|||
if (this.user) {
|
||||
this.updateFollowing();
|
||||
this.updateMuting();
|
||||
this.updateRenoteMuting();
|
||||
this.updateBlocking();
|
||||
this.updateFollowingChannels();
|
||||
this.updateUserProfile();
|
||||
|
@ -82,7 +84,21 @@ export default class Connection {
|
|||
this.muting.delete(data.body.id);
|
||||
break;
|
||||
|
||||
// TODO: block events
|
||||
case 'block':
|
||||
this.blocking.add(data.body.id);
|
||||
break;
|
||||
|
||||
case 'unblock':
|
||||
this.blocking.delete(data.body.id);
|
||||
break;
|
||||
|
||||
case 'muteRenote':
|
||||
this.renoteMuting.add(data.body.id);
|
||||
break;
|
||||
|
||||
case 'unmuteRenote':
|
||||
this.renoteMuting.delete(data.body.id);
|
||||
break;
|
||||
|
||||
case 'followChannel':
|
||||
this.followingChannels.add(data.body.id);
|
||||
|
@ -333,6 +349,17 @@ export default class Connection {
|
|||
this.muting = new Set<string>(mutings.map(x => x.muteeId));
|
||||
}
|
||||
|
||||
private async updateRenoteMuting() {
|
||||
const renoteMutings = await RenoteMutings.find({
|
||||
where: {
|
||||
muterId: this.user!.id,
|
||||
},
|
||||
select: ['muteeId'],
|
||||
});
|
||||
|
||||
this.renoteMuting = new Set<string>(renoteMutings.map(x => x.muteeId));
|
||||
}
|
||||
|
||||
private async updateBlocking() { // ここでいうBlockingは被Blockingの意
|
||||
const blockings = await Blockings.find({
|
||||
where: {
|
||||
|
|
|
@ -44,6 +44,10 @@ export interface UserStreamTypes {
|
|||
updateUserProfile: UserProfile;
|
||||
mute: User;
|
||||
unmute: User;
|
||||
muteRenote: User;
|
||||
unmuteRenote: User;
|
||||
block: User;
|
||||
unblock: User;
|
||||
follow: Packed<'UserDetailedNotMe'>;
|
||||
unfollow: Packed<'User'>;
|
||||
userAdded: Packed<'User'>;
|
||||
|
|
|
@ -98,6 +98,14 @@ export function getUserMenu(user) {
|
|||
}
|
||||
}
|
||||
|
||||
async function toggleRenoteMute(): Promise<void> {
|
||||
os.apiWithDialog(user.isRenoteMuted ? 'renote-mute/delete' : 'renote-mute/create', {
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
user.isRenoteMuted = !user.isRenoteMuted;
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleBlock(): Promise<void> {
|
||||
if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return;
|
||||
|
||||
|
@ -187,6 +195,10 @@ export function getUserMenu(user) {
|
|||
|
||||
if ($i && meId !== user.id) {
|
||||
menu = menu.concat([null, {
|
||||
icon: user.isRenoteMuted ? 'fas fa-eye' : 'fas fa-eye-slash',
|
||||
text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute,
|
||||
action: toggleRenoteMute,
|
||||
}, {
|
||||
icon: user.isMuted ? 'fas fa-eye' : 'fas fa-eye-slash',
|
||||
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
|
||||
action: toggleMute,
|
||||
|
|
|
@ -411,6 +411,9 @@ export type Endpoints = {
|
|||
'mute/create': { req: TODO; res: TODO; };
|
||||
'mute/delete': { req: { userId: User['id'] }; res: null; };
|
||||
'mute/list': { req: TODO; res: TODO; };
|
||||
'renote-mute/create': { req: TODO; res: TODO; };
|
||||
'renote-mute/delete': { req: { userId: User['id'] }; res: null; };
|
||||
'renote-mute/list': { req: TODO; res: TODO; };
|
||||
'my/apps': { req: TODO; res: TODO; };
|
||||
'notes': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; };
|
||||
'notes/children': { req: { noteId: Note['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; };
|
||||
|
|
|
@ -53,6 +53,7 @@ export type UserDetailed = UserLite & {
|
|||
isLocked: boolean;
|
||||
isModerator: boolean;
|
||||
isMuted: boolean;
|
||||
isRenoteMuted: boolean;
|
||||
isSilenced: boolean;
|
||||
isSuspended: boolean;
|
||||
lang: string | null;
|
||||
|
|
Loading…
Reference in a new issue
I'm all for descriptive naming, but maybe this is overdoing it a bit? I think a bit shorter like
generateMutedRenotesQuery
would also be fine since renote implies that its from a user and for notes.