mirror of
https://iceshrimp.dev/iceshrimp/iceshrimp
synced 2024-11-30 15:58:05 +09:00
[backend] [client] Add option to hide user lists from home timeline
This commit is contained in:
parent
fdd8c28aed
commit
89ab890331
@ -2135,3 +2135,4 @@ _cwStyle:
|
|||||||
classic: "Classic (Misskey/Foundkey-like)"
|
classic: "Classic (Misskey/Foundkey-like)"
|
||||||
alternative: "Alternative (Firefish-like)"
|
alternative: "Alternative (Firefish-like)"
|
||||||
alwaysExpandCws: "Always expand posts with content warnings"
|
alwaysExpandCws: "Always expand posts with content warnings"
|
||||||
|
hideFromHome: "Hide from home timeline"
|
@ -0,0 +1,15 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class UserListOptions1697733603329 implements MigrationInterface {
|
||||||
|
name = 'UserListOptions1697733603329'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list" ADD "hideFromHomeTl" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user_list"."hideFromHomeTl" IS 'Whether posts from list members should be hidden from the home timeline.'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user_list"."hideFromHomeTl" IS 'Whether posts from list members should be hidden from the home timeline.'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list" DROP COLUMN "hideFromHomeTl"`);
|
||||||
|
}
|
||||||
|
}
|
@ -37,4 +37,10 @@ export class UserList {
|
|||||||
comment: "The name of the UserList.",
|
comment: "The name of the UserList.",
|
||||||
})
|
})
|
||||||
public name: string;
|
public name: string;
|
||||||
|
|
||||||
|
@Column("boolean", {
|
||||||
|
default: false,
|
||||||
|
comment: "Whether posts from list members should be hidden from the home timeline."
|
||||||
|
})
|
||||||
|
public hideFromHomeTl: boolean;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ export const UserListRepository = db.getRepository(UserList).extend({
|
|||||||
id: userList.id,
|
id: userList.id,
|
||||||
createdAt: userList.createdAt.toISOString(),
|
createdAt: userList.createdAt.toISOString(),
|
||||||
name: userList.name,
|
name: userList.name,
|
||||||
|
hideFromHomeTl: userList.hideFromHomeTl,
|
||||||
userIds: users.map((x) => x.userId),
|
userIds: users.map((x) => x.userId),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -19,6 +19,11 @@ export const packedUserListSchema = {
|
|||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
hideFromHomeTl: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
userIds: {
|
userIds: {
|
||||||
type: "array",
|
type: "array",
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import { Brackets, SelectQueryBuilder } from "typeorm";
|
||||||
|
import { User } from "@/models/entities/user.js";
|
||||||
|
import { UserListJoinings, UserLists } from "@/models/index.js";
|
||||||
|
|
||||||
|
export function generateListQuery(
|
||||||
|
q: SelectQueryBuilder<any>,
|
||||||
|
me: { id: User["id"] },
|
||||||
|
): void {
|
||||||
|
const listQuery = UserLists.createQueryBuilder("list")
|
||||||
|
.select("list.id")
|
||||||
|
.where("list.hideFromHomeTl = TRUE")
|
||||||
|
.andWhere("list.userId = :meId");
|
||||||
|
|
||||||
|
const memberQuery = UserListJoinings.createQueryBuilder("member")
|
||||||
|
.select("member.userId")
|
||||||
|
.where(`member.userListId IN (${listQuery.getQuery()})`)
|
||||||
|
|
||||||
|
q.andWhere(new Brackets((qb) => {
|
||||||
|
qb.where(`note.userId = :meId`);
|
||||||
|
qb.orWhere(`note.userId NOT IN (${memberQuery.getQuery()})`);
|
||||||
|
}));
|
||||||
|
|
||||||
|
q.setParameters({ meId: me.id });
|
||||||
|
}
|
@ -12,6 +12,7 @@ import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.j
|
|||||||
import { generateChannelQuery } from "../../common/generate-channel-query.js";
|
import { generateChannelQuery } from "../../common/generate-channel-query.js";
|
||||||
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||||
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
||||||
|
import { generateListQuery } from "@/server/api/common/generate-list-query.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["notes"],
|
tags: ["notes"],
|
||||||
@ -108,6 +109,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||||||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
|
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
|
||||||
.setParameters(followingQuery.getParameters());
|
.setParameters(followingQuery.getParameters());
|
||||||
|
|
||||||
|
generateListQuery(query, user);
|
||||||
generateChannelQuery(query, user);
|
generateChannelQuery(query, user);
|
||||||
generateRepliesQuery(query, ps.withReplies, user);
|
generateRepliesQuery(query, ps.withReplies, user);
|
||||||
generateVisibilityQuery(query, user);
|
generateVisibilityQuery(query, user);
|
||||||
|
@ -11,6 +11,7 @@ import { generateChannelQuery } from "../../common/generate-channel-query.js";
|
|||||||
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||||
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
||||||
import { ApiError } from "../../error.js";
|
import { ApiError } from "../../error.js";
|
||||||
|
import { generateListQuery } from "@/server/api/common/generate-list-query.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["notes"],
|
tags: ["notes"],
|
||||||
@ -104,6 +105,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||||||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
|
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
|
||||||
.setParameters(followingQuery.getParameters());
|
.setParameters(followingQuery.getParameters());
|
||||||
|
|
||||||
|
generateListQuery(query, user);
|
||||||
generateChannelQuery(query, user);
|
generateChannelQuery(query, user);
|
||||||
generateRepliesQuery(query, ps.withReplies, user);
|
generateRepliesQuery(query, ps.withReplies, user);
|
||||||
generateVisibilityQuery(query, user);
|
generateVisibilityQuery(query, user);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { publishUserListStream } from "@/services/stream.js";
|
import { UserLists } from "@/models/index.js";
|
||||||
import { UserLists, UserListJoinings, Users } from "@/models/index.js";
|
|
||||||
import define from "../../../define.js";
|
import define from "../../../define.js";
|
||||||
import { ApiError } from "../../../error.js";
|
import { ApiError } from "../../../error.js";
|
||||||
import { getUser } from "../../../common/getters.js";
|
import { getUser } from "../../../common/getters.js";
|
||||||
|
import { pullUserFromUserList } from "@/services/user-list/pull.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["lists", "users"],
|
tags: ["lists", "users"],
|
||||||
@ -56,7 +56,5 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Pull the user
|
// Pull the user
|
||||||
await UserListJoinings.delete({ userListId: userList.id, userId: user.id });
|
await pullUserFromUserList(user, userList);
|
||||||
|
|
||||||
publishUserListStream(userList.id, "userRemoved", await Users.pack(user));
|
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { UserLists } from "@/models/index.js";
|
import { UserListJoinings, UserLists, Users } from "@/models/index.js";
|
||||||
import define from "../../../define.js";
|
import define from "../../../define.js";
|
||||||
import { ApiError } from "../../../error.js";
|
import { ApiError } from "../../../error.js";
|
||||||
|
import { publishUserEvent } from "@/services/stream.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["lists"],
|
tags: ["lists"],
|
||||||
@ -32,8 +33,9 @@ export const paramDef = {
|
|||||||
properties: {
|
properties: {
|
||||||
listId: { type: "string", format: "misskey:id" },
|
listId: { type: "string", format: "misskey:id" },
|
||||||
name: { type: "string", minLength: 1, maxLength: 100 },
|
name: { type: "string", minLength: 1, maxLength: 100 },
|
||||||
|
hideFromHomeTl: { type: "boolean", nullable: true },
|
||||||
},
|
},
|
||||||
required: ["listId", "name"],
|
required: ["listId"],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
@ -47,9 +49,20 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||||||
throw new ApiError(meta.errors.noSuchList);
|
throw new ApiError(meta.errors.noSuchList);
|
||||||
}
|
}
|
||||||
|
|
||||||
await UserLists.update(userList.id, {
|
const partial = {
|
||||||
name: ps.name,
|
name: ps.name ?? undefined,
|
||||||
});
|
hideFromHomeTl: ps.hideFromHomeTl ?? undefined
|
||||||
|
};
|
||||||
|
if (Object.keys(partial).length > 0) await UserLists.update(userList.id, partial);
|
||||||
|
|
||||||
|
if (ps.hideFromHomeTl != null) {
|
||||||
|
UserListJoinings.findBy({ userListId: ps.listId })
|
||||||
|
.then(members => {
|
||||||
|
for (const member of members) {
|
||||||
|
publishUserEvent(userList.userId, ps.hideFromHomeTl ? "userHidden" : "userUnhidden", member.userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return await UserLists.pack(userList.id);
|
return await UserLists.pack(userList.id);
|
||||||
});
|
});
|
||||||
|
@ -39,7 +39,8 @@ export function setupEndpointsList(router: Router): void {
|
|||||||
|
|
||||||
const body = ctx.request.body as any;
|
const body = ctx.request.body as any;
|
||||||
const title = (body.title ?? '').trim();
|
const title = (body.title ?? '').trim();
|
||||||
ctx.body = await ListHelpers.updateList(list, title, ctx);
|
const exclusive = body.exclusive ?? undefined as boolean | undefined;
|
||||||
|
ctx.body = await ListHelpers.updateList(list, title, exclusive, ctx);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
router.delete<{ Params: { id: string } }>(
|
router.delete<{ Params: { id: string } }>(
|
||||||
|
@ -2,5 +2,6 @@ namespace MastodonEntity {
|
|||||||
export type List = {
|
export type List = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
exclusive: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,10 @@ import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
|||||||
import { UserList } from "@/models/entities/user-list.js";
|
import { UserList } from "@/models/entities/user-list.js";
|
||||||
import { pushUserToUserList } from "@/services/user-list/push.js";
|
import { pushUserToUserList } from "@/services/user-list/push.js";
|
||||||
import { genId } from "@/misc/gen-id.js";
|
import { genId } from "@/misc/gen-id.js";
|
||||||
import { publishUserListStream } from "@/services/stream.js";
|
|
||||||
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
|
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||||
import { MastoContext } from "@/server/api/mastodon/index.js";
|
import { MastoContext } from "@/server/api/mastodon/index.js";
|
||||||
|
import { pullUserFromUserList } from "@/services/user-list/pull.js";
|
||||||
|
import { publishUserEvent } from "@/services/stream.js";
|
||||||
|
|
||||||
export class ListHelpers {
|
export class ListHelpers {
|
||||||
public static async getLists(ctx: MastoContext): Promise<MastodonEntity.List[]> {
|
public static async getLists(ctx: MastoContext): Promise<MastodonEntity.List[]> {
|
||||||
@ -15,7 +16,8 @@ export class ListHelpers {
|
|||||||
return UserLists.findBy({ userId: user.id }).then(p => p.map(list => {
|
return UserLists.findBy({ userId: user.id }).then(p => p.map(list => {
|
||||||
return {
|
return {
|
||||||
id: list.id,
|
id: list.id,
|
||||||
title: list.name
|
title: list.name,
|
||||||
|
exclusive: list.hideFromHomeTl
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -26,7 +28,8 @@ export class ListHelpers {
|
|||||||
return UserLists.findOneByOrFail({ userId: user.id, id: id }).then(list => {
|
return UserLists.findOneByOrFail({ userId: user.id, id: id }).then(list => {
|
||||||
return {
|
return {
|
||||||
id: list.id,
|
id: list.id,
|
||||||
title: list.name
|
title: list.name,
|
||||||
|
exclusive: list.hideFromHomeTl
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -110,8 +113,7 @@ export class ListHelpers {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!exist) continue;
|
if (!exist) continue;
|
||||||
await UserListJoinings.delete({ userListId: list.id, userId: user.id });
|
await pullUserFromUserList(user, list);
|
||||||
publishUserListStream(list.id, "userRemoved", await Users.pack(user));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,23 +130,35 @@ export class ListHelpers {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: list.id,
|
id: list.id,
|
||||||
title: list.name
|
title: list.name,
|
||||||
|
exclusive: list.hideFromHomeTl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async updateList(list: UserList, title: string, ctx: MastoContext) {
|
public static async updateList(list: UserList, title: string, exclusive: boolean | undefined, ctx: MastoContext): Promise<MastodonEntity.List> {
|
||||||
if (title.length < 1) throw new MastoApiError(400, "Title must not be empty");
|
if (title.length < 1 && exclusive === undefined) throw new MastoApiError(400, "Either title or exclusive must be set");
|
||||||
|
|
||||||
const user = ctx.user as ILocalUser;
|
const user = ctx.user as ILocalUser;
|
||||||
if (user.id != list.userId) throw new Error("List is not owned by user");
|
if (user.id != list.userId) throw new Error("List is not owned by user");
|
||||||
|
|
||||||
const partial = { name: title };
|
const name = title.length > 0 ? title : undefined;
|
||||||
|
const partial = { name: name, hideFromHomeTl: exclusive };
|
||||||
const result = await UserLists.update(list.id, partial)
|
const result = await UserLists.update(list.id, partial)
|
||||||
.then(async _ => await UserLists.findOneByOrFail({ id: list.id }));
|
.then(async _ => await UserLists.findOneByOrFail({ id: list.id }));
|
||||||
|
|
||||||
|
if (exclusive !== undefined) {
|
||||||
|
UserListJoinings.findBy({ userListId: list.id })
|
||||||
|
.then(members => {
|
||||||
|
for (const member of members) {
|
||||||
|
publishUserEvent(list.userId, exclusive ? "userHidden" : "userUnhidden", member.userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
title: result.name
|
title: result.name,
|
||||||
|
exclusive: result.hideFromHomeTl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +176,8 @@ export class ListHelpers {
|
|||||||
.then(results => results.map(result => {
|
.then(results => results.map(result => {
|
||||||
return {
|
return {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
title: result.name
|
title: result.name,
|
||||||
|
exclusive: result.hideFromHomeTl
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import { unique } from "@/prelude/array.js";
|
|||||||
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
|
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||||
import { generatePaginationData } from "@/server/api/mastodon/middleware/pagination.js";
|
import { generatePaginationData } from "@/server/api/mastodon/middleware/pagination.js";
|
||||||
import { MastoContext } from "@/server/api/mastodon/index.js";
|
import { MastoContext } from "@/server/api/mastodon/index.js";
|
||||||
|
import { generateListQuery } from "@/server/api/common/generate-list-query.js";
|
||||||
|
|
||||||
export class TimelineHelpers {
|
export class TimelineHelpers {
|
||||||
public static async getHomeTimeline(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, ctx: MastoContext): Promise<Note[]> {
|
public static async getHomeTimeline(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, ctx: MastoContext): Promise<Note[]> {
|
||||||
@ -43,6 +44,7 @@ export class TimelineHelpers {
|
|||||||
)
|
)
|
||||||
.leftJoinAndSelect("note.renote", "renote");
|
.leftJoinAndSelect("note.renote", "renote");
|
||||||
|
|
||||||
|
generateListQuery(query, user);
|
||||||
generateChannelQuery(query, user);
|
generateChannelQuery(query, user);
|
||||||
generateRepliesQuery(query, true, user);
|
generateRepliesQuery(query, true, user);
|
||||||
generateVisibilityQuery(query, user);
|
generateVisibilityQuery(query, user);
|
||||||
|
@ -31,6 +31,10 @@ export abstract class MastodonStream {
|
|||||||
return this.connection.blocking;
|
return this.connection.blocking;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get hidden() {
|
||||||
|
return this.connection.hidden;
|
||||||
|
}
|
||||||
|
|
||||||
protected get subscriber() {
|
protected get subscriber() {
|
||||||
return this.connection.subscriber;
|
return this.connection.subscriber;
|
||||||
}
|
}
|
||||||
|
@ -90,12 +90,14 @@ export class MastodonStreamUser extends MastodonStream {
|
|||||||
|
|
||||||
private async shouldProcessNote(note: Note): Promise<boolean> {
|
private async shouldProcessNote(note: Note): Promise<boolean> {
|
||||||
if (note.visibility === "hidden") return false;
|
if (note.visibility === "hidden") return false;
|
||||||
if (note.visibility === "specified") return note.userId === this.user.id || note.visibleUserIds?.includes(this.user.id);
|
if (note.userId === this.user.id) return true;
|
||||||
|
if (note.visibility === "specified") return note.visibleUserIds?.includes(this.user.id);
|
||||||
if (note.channelId) return false;
|
if (note.channelId) return false;
|
||||||
if (this.user!.id !== note.userId && !this.following.has(note.userId)) return false;
|
if (this.user!.id !== note.userId && !this.following.has(note.userId)) return false;
|
||||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return false;
|
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return false;
|
||||||
if (isUserRelated(note, this.muting)) return false;
|
if (isUserRelated(note, this.muting)) return false;
|
||||||
if (isUserRelated(note, this.blocking)) return false;
|
if (isUserRelated(note, this.blocking)) return false;
|
||||||
|
if (isUserRelated(note, this.hidden)) return false;
|
||||||
if (note.renote && !isQuote(note) && this.renoteMuting.has(note.userId)) return false;
|
if (note.renote && !isQuote(note) && this.renoteMuting.has(note.userId)) return false;
|
||||||
if (this.userProfile && (await getWordHardMute(note, this.user, this.userProfile.mutedWords))) return false;
|
if (this.userProfile && (await getWordHardMute(note, this.user, this.userProfile.mutedWords))) return false;
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import type { EventEmitter } from "events";
|
|||||||
import type * as websocket from "websocket";
|
import type * as websocket from "websocket";
|
||||||
import type { ILocalUser, User } from "@/models/entities/user.js";
|
import type { ILocalUser, User } from "@/models/entities/user.js";
|
||||||
import type { MastodonStream } from "./channel.js";
|
import type { MastodonStream } from "./channel.js";
|
||||||
import { Blockings, Followings, Mutings, RenoteMutings, UserProfiles, } from "@/models/index.js";
|
import { Blockings, Followings, Mutings, RenoteMutings, UserListJoinings, UserProfiles, } from "@/models/index.js";
|
||||||
import type { UserProfile } from "@/models/entities/user-profile.js";
|
import type { UserProfile } from "@/models/entities/user-profile.js";
|
||||||
import { StreamEventEmitter, StreamMessages } from "@/server/api/stream/types.js";
|
import { StreamEventEmitter, StreamMessages } from "@/server/api/stream/types.js";
|
||||||
import { apiLogger } from "@/server/api/logger.js";
|
import { apiLogger } from "@/server/api/logger.js";
|
||||||
@ -40,6 +40,7 @@ export class MastodonStreamingConnection {
|
|||||||
public muting: Set<User["id"]> = new Set();
|
public muting: Set<User["id"]> = new Set();
|
||||||
public renoteMuting: Set<User["id"]> = new Set();
|
public renoteMuting: Set<User["id"]> = new Set();
|
||||||
public blocking: Set<User["id"]> = new Set();
|
public blocking: Set<User["id"]> = new Set();
|
||||||
|
public hidden: Set<User["id"]> = new Set();
|
||||||
public token?: OAuthToken;
|
public token?: OAuthToken;
|
||||||
private wsConnection: websocket.connection;
|
private wsConnection: websocket.connection;
|
||||||
private channels: MastodonStream[] = [];
|
private channels: MastodonStream[] = [];
|
||||||
@ -69,6 +70,7 @@ export class MastodonStreamingConnection {
|
|||||||
this.updateMuting();
|
this.updateMuting();
|
||||||
this.updateRenoteMuting();
|
this.updateRenoteMuting();
|
||||||
this.updateBlocking();
|
this.updateBlocking();
|
||||||
|
this.updateHidden();
|
||||||
this.updateUserProfile();
|
this.updateUserProfile();
|
||||||
|
|
||||||
this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
|
this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
|
||||||
@ -98,6 +100,12 @@ export class MastodonStreamingConnection {
|
|||||||
case "unmute":
|
case "unmute":
|
||||||
this.muting.delete(data.body.id);
|
this.muting.delete(data.body.id);
|
||||||
break;
|
break;
|
||||||
|
case "userHidden":
|
||||||
|
this.hidden.add(data.body);
|
||||||
|
break;
|
||||||
|
case "userUnhidden":
|
||||||
|
this.hidden.delete(data.body);
|
||||||
|
break;
|
||||||
|
|
||||||
// TODO: renote mute events
|
// TODO: renote mute events
|
||||||
// TODO: block events
|
// TODO: block events
|
||||||
@ -247,6 +255,17 @@ export class MastodonStreamingConnection {
|
|||||||
this.blocking = new Set<string>(blockings.map((x) => x.blockerId));
|
this.blocking = new Set<string>(blockings.map((x) => x.blockerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async updateHidden() {
|
||||||
|
const hidden = await UserListJoinings.find({
|
||||||
|
where: {
|
||||||
|
userList: { userId: this.user!.id, hideFromHomeTl: true },
|
||||||
|
},
|
||||||
|
select: ["userId"],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hidden = new Set<string>(hidden.map((x) => x.userId));
|
||||||
|
}
|
||||||
|
|
||||||
private async updateUserProfile() {
|
private async updateUserProfile() {
|
||||||
this.userProfile = await UserProfiles.findOneBy({
|
this.userProfile = await UserProfiles.findOneBy({
|
||||||
userId: this.user!.id,
|
userId: this.user!.id,
|
||||||
|
@ -38,6 +38,10 @@ export default abstract class Channel {
|
|||||||
return this.connection.blocking;
|
return this.connection.blocking;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get hidden() {
|
||||||
|
return this.connection.hidden;
|
||||||
|
}
|
||||||
|
|
||||||
protected get followingChannels() {
|
protected get followingChannels() {
|
||||||
return this.connection.followingChannels;
|
return this.connection.followingChannels;
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,8 @@ export default class extends Channel {
|
|||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
// Members of lists with hideFromHome set
|
||||||
|
if (note.userId !== this.user!.id && isUserRelated(note, this.hidden)) return;
|
||||||
|
|
||||||
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
||||||
|
|
||||||
|
@ -74,6 +74,8 @@ export default class extends Channel {
|
|||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
// Members of lists with hideFromHome set
|
||||||
|
if (note.userId !== this.user!.id && isUserRelated(note, this.hidden)) return;
|
||||||
|
|
||||||
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
RenoteMutings,
|
RenoteMutings,
|
||||||
UserProfiles,
|
UserProfiles,
|
||||||
ChannelFollowings,
|
ChannelFollowings,
|
||||||
Blockings,
|
Blockings, UserListJoinings,
|
||||||
} from "@/models/index.js";
|
} from "@/models/index.js";
|
||||||
import type { AccessToken } from "@/models/entities/access-token.js";
|
import type { AccessToken } from "@/models/entities/access-token.js";
|
||||||
import type { UserProfile } from "@/models/entities/user-profile.js";
|
import type { UserProfile } from "@/models/entities/user-profile.js";
|
||||||
@ -35,7 +35,8 @@ export default class Connection {
|
|||||||
public following: Set<User["id"]> = new Set();
|
public following: Set<User["id"]> = new Set();
|
||||||
public muting: Set<User["id"]> = new Set();
|
public muting: Set<User["id"]> = new Set();
|
||||||
public renoteMuting: Set<User["id"]> = new Set();
|
public renoteMuting: Set<User["id"]> = new Set();
|
||||||
public blocking: Set<User["id"]> = new Set(); // "被"blocking
|
public blocking: Set<User["id"]> = new Set();
|
||||||
|
public hidden: Set<User["id"]> = new Set();
|
||||||
public followingChannels: Set<ChannelModel["id"]> = new Set();
|
public followingChannels: Set<ChannelModel["id"]> = new Set();
|
||||||
public token?: AccessToken;
|
public token?: AccessToken;
|
||||||
private wsConnection: websocket.connection;
|
private wsConnection: websocket.connection;
|
||||||
@ -79,6 +80,7 @@ export default class Connection {
|
|||||||
this.updateMuting();
|
this.updateMuting();
|
||||||
this.updateRenoteMuting();
|
this.updateRenoteMuting();
|
||||||
this.updateBlocking();
|
this.updateBlocking();
|
||||||
|
this.updateHidden();
|
||||||
this.updateFollowingChannels();
|
this.updateFollowingChannels();
|
||||||
this.updateUserProfile();
|
this.updateUserProfile();
|
||||||
|
|
||||||
@ -122,6 +124,14 @@ export default class Connection {
|
|||||||
this.followingChannels.delete(data.body.id);
|
this.followingChannels.delete(data.body.id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "userHidden":
|
||||||
|
this.hidden.add(data.body);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "userUnhidden":
|
||||||
|
this.hidden.delete(data.body);
|
||||||
|
break;
|
||||||
|
|
||||||
case "updateUserProfile":
|
case "updateUserProfile":
|
||||||
this.userProfile = data.body;
|
this.userProfile = data.body;
|
||||||
break;
|
break;
|
||||||
@ -432,6 +442,17 @@ export default class Connection {
|
|||||||
this.blocking = new Set<string>(blockings.map((x) => x.blockerId));
|
this.blocking = new Set<string>(blockings.map((x) => x.blockerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async updateHidden() {
|
||||||
|
const hidden = await UserListJoinings.find({
|
||||||
|
where: {
|
||||||
|
userList: { userId: this.user!.id, hideFromHomeTl: true },
|
||||||
|
},
|
||||||
|
select: ["userId"],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hidden = new Set<string>(hidden.map((x) => x.userId));
|
||||||
|
}
|
||||||
|
|
||||||
private async updateFollowingChannels() {
|
private async updateFollowingChannels() {
|
||||||
const followings = await ChannelFollowings.find({
|
const followings = await ChannelFollowings.find({
|
||||||
where: {
|
where: {
|
||||||
|
@ -74,6 +74,8 @@ export interface UserStreamTypes {
|
|||||||
follow: Packed<"UserDetailedNotMe">;
|
follow: Packed<"UserDetailedNotMe">;
|
||||||
unfollow: Packed<"User">;
|
unfollow: Packed<"User">;
|
||||||
userAdded: Packed<"User">;
|
userAdded: Packed<"User">;
|
||||||
|
userHidden: User["id"];
|
||||||
|
userUnhidden: User["id"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MainStreamTypes {
|
export interface MainStreamTypes {
|
||||||
|
12
packages/backend/src/services/user-list/pull.ts
Normal file
12
packages/backend/src/services/user-list/pull.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { publishUserEvent, publishUserListStream } from "@/services/stream.js";
|
||||||
|
import type { User } from "@/models/entities/user.js";
|
||||||
|
import type { UserList } from "@/models/entities/user-list.js";
|
||||||
|
import { UserListJoinings, Users } from "@/models/index.js";
|
||||||
|
|
||||||
|
export async function pullUserFromUserList(target: User, list: UserList) {
|
||||||
|
await UserListJoinings.delete({ userListId: list.id, userId: target.id });
|
||||||
|
|
||||||
|
const packed = await Users.pack(target);
|
||||||
|
publishUserListStream(list.id, "userRemoved", packed);
|
||||||
|
if (list.hideFromHomeTl) publishUserEvent(list.userId, "userUnhidden", target.id);
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
import { publishUserListStream } from "@/services/stream.js";
|
import { publishUserEvent, publishUserListStream } from "@/services/stream.js";
|
||||||
import type { User } from "@/models/entities/user.js";
|
import type { User } from "@/models/entities/user.js";
|
||||||
import type { UserList } from "@/models/entities/user-list.js";
|
import type { UserList } from "@/models/entities/user-list.js";
|
||||||
import { Followings, UserListJoinings, Users } from "@/models/index.js";
|
import { UserListJoinings, Users } from "@/models/index.js";
|
||||||
import type { UserListJoining } from "@/models/entities/user-list-joining.js";
|
import type { UserListJoining } from "@/models/entities/user-list-joining.js";
|
||||||
import { genId } from "@/misc/gen-id.js";
|
import { genId } from "@/misc/gen-id.js";
|
||||||
import { ApiError } from "@/server/api/error.js";
|
|
||||||
|
|
||||||
export async function pushUserToUserList(target: User, list: UserList) {
|
export async function pushUserToUserList(target: User, list: UserList) {
|
||||||
await UserListJoinings.insert({
|
await UserListJoinings.insert({
|
||||||
@ -14,5 +13,7 @@ export async function pushUserToUserList(target: User, list: UserList) {
|
|||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
} as UserListJoining);
|
} as UserListJoining);
|
||||||
|
|
||||||
publishUserListStream(list.id, "userAdded", await Users.pack(target));
|
const packed = await Users.pack(target);
|
||||||
|
publishUserListStream(list.id, "userAdded", packed);
|
||||||
|
if (list.hideFromHomeTl) publishUserEvent(list.userId, "userHidden", target.id);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,11 @@
|
|||||||
<MkButton inline @click="deleteList()">{{
|
<MkButton inline @click="deleteList()">{{
|
||||||
i18n.ts.delete
|
i18n.ts.delete
|
||||||
}}</MkButton>
|
}}</MkButton>
|
||||||
|
<FormSection>
|
||||||
|
<FormSwitch v-model="hideFromHomeTl">{{
|
||||||
|
i18n.ts.hideFromHome
|
||||||
|
}}</FormSwitch>
|
||||||
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
@ -72,12 +77,15 @@ import * as os from "@/os";
|
|||||||
import { mainRouter } from "@/router";
|
import { mainRouter } from "@/router";
|
||||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
import FormSwitch from "@/components/form/switch.vue";
|
||||||
|
import FormSection from "@/components/form/section.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
listId: string;
|
listId: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let list = $ref(null);
|
let list = $ref(null);
|
||||||
|
let hideFromHomeTl = $ref(false);
|
||||||
let users = $ref([]);
|
let users = $ref([]);
|
||||||
|
|
||||||
function fetchList() {
|
function fetchList() {
|
||||||
@ -85,6 +93,7 @@ function fetchList() {
|
|||||||
listId: props.listId,
|
listId: props.listId,
|
||||||
}).then((_list) => {
|
}).then((_list) => {
|
||||||
list = _list;
|
list = _list;
|
||||||
|
hideFromHomeTl = _list.hideFromHomeTl;
|
||||||
os.api("users/show", {
|
os.api("users/show", {
|
||||||
userIds: list.userIds,
|
userIds: list.userIds,
|
||||||
}).then((_users) => {
|
}).then((_users) => {
|
||||||
@ -142,7 +151,15 @@ async function deleteList() {
|
|||||||
mainRouter.push("/my/lists");
|
mainRouter.push("/my/lists");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hideFromHome() {
|
||||||
|
await os.api("users/lists/update", {
|
||||||
|
listId: list.id,
|
||||||
|
hideFromHomeTl: hideFromHomeTl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
watch(() => props.listId, fetchList, { immediate: true });
|
watch(() => props.listId, fetchList, { immediate: true });
|
||||||
|
watch(() => hideFromHomeTl, hideFromHome);
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user