mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-22 01:47:39 -07:00
[backend] [client] Add option to hide user lists from home timeline
This commit is contained in:
parent
fdd8c28aed
commit
89ab890331
25 changed files with 202 additions and 30 deletions
|
@ -2135,3 +2135,4 @@ _cwStyle:
|
|||
classic: "Classic (Misskey/Foundkey-like)"
|
||||
alternative: "Alternative (Firefish-like)"
|
||||
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.",
|
||||
})
|
||||
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,
|
||||
createdAt: userList.createdAt.toISOString(),
|
||||
name: userList.name,
|
||||
hideFromHomeTl: userList.hideFromHomeTl,
|
||||
userIds: users.map((x) => x.userId),
|
||||
};
|
||||
},
|
||||
|
|
|
@ -19,6 +19,11 @@ export const packedUserListSchema = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
hideFromHomeTl: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
userIds: {
|
||||
type: "array",
|
||||
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 { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
||||
import { generateListQuery } from "@/server/api/common/generate-list-query.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
@ -108,6 +109,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
|
||||
.setParameters(followingQuery.getParameters());
|
||||
|
||||
generateListQuery(query, user);
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, ps.withReplies, 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 { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
||||
import { ApiError } from "../../error.js";
|
||||
import { generateListQuery } from "@/server/api/common/generate-list-query.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
@ -104,6 +105,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
|
||||
.setParameters(followingQuery.getParameters());
|
||||
|
||||
generateListQuery(query, user);
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, ps.withReplies, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { publishUserListStream } from "@/services/stream.js";
|
||||
import { UserLists, UserListJoinings, Users } from "@/models/index.js";
|
||||
import { UserLists } from "@/models/index.js";
|
||||
import define from "../../../define.js";
|
||||
import { ApiError } from "../../../error.js";
|
||||
import { getUser } from "../../../common/getters.js";
|
||||
import { pullUserFromUserList } from "@/services/user-list/pull.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["lists", "users"],
|
||||
|
@ -56,7 +56,5 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
});
|
||||
|
||||
// Pull the user
|
||||
await UserListJoinings.delete({ userListId: userList.id, userId: user.id });
|
||||
|
||||
publishUserListStream(userList.id, "userRemoved", await Users.pack(user));
|
||||
await pullUserFromUserList(user, userList);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { UserLists } from "@/models/index.js";
|
||||
import { UserListJoinings, UserLists, Users } from "@/models/index.js";
|
||||
import define from "../../../define.js";
|
||||
import { ApiError } from "../../../error.js";
|
||||
import { publishUserEvent } from "@/services/stream.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["lists"],
|
||||
|
@ -32,8 +33,9 @@ export const paramDef = {
|
|||
properties: {
|
||||
listId: { type: "string", format: "misskey:id" },
|
||||
name: { type: "string", minLength: 1, maxLength: 100 },
|
||||
hideFromHomeTl: { type: "boolean", nullable: true },
|
||||
},
|
||||
required: ["listId", "name"],
|
||||
required: ["listId"],
|
||||
} as const;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
await UserLists.update(userList.id, {
|
||||
name: ps.name,
|
||||
});
|
||||
const partial = {
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -39,7 +39,8 @@ export function setupEndpointsList(router: Router): void {
|
|||
|
||||
const body = ctx.request.body as any;
|
||||
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 } }>(
|
||||
|
|
|
@ -2,5 +2,6 @@ namespace MastodonEntity {
|
|||
export type List = {
|
||||
id: 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 { pushUserToUserList } from "@/services/user-list/push.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 { MastoContext } from "@/server/api/mastodon/index.js";
|
||||
import { pullUserFromUserList } from "@/services/user-list/pull.js";
|
||||
import { publishUserEvent } from "@/services/stream.js";
|
||||
|
||||
export class ListHelpers {
|
||||
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 {
|
||||
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 {
|
||||
id: list.id,
|
||||
title: list.name
|
||||
title: list.name,
|
||||
exclusive: list.hideFromHomeTl
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -110,8 +113,7 @@ export class ListHelpers {
|
|||
});
|
||||
|
||||
if (!exist) continue;
|
||||
await UserListJoinings.delete({ userListId: list.id, userId: user.id });
|
||||
publishUserListStream(list.id, "userRemoved", await Users.pack(user));
|
||||
await pullUserFromUserList(user, list);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,23 +130,35 @@ export class ListHelpers {
|
|||
|
||||
return {
|
||||
id: list.id,
|
||||
title: list.name
|
||||
title: list.name,
|
||||
exclusive: list.hideFromHomeTl
|
||||
};
|
||||
}
|
||||
|
||||
public static async updateList(list: UserList, title: string, ctx: MastoContext) {
|
||||
if (title.length < 1) throw new MastoApiError(400, "Title must not be empty");
|
||||
public static async updateList(list: UserList, title: string, exclusive: boolean | undefined, ctx: MastoContext): Promise<MastodonEntity.List> {
|
||||
if (title.length < 1 && exclusive === undefined) throw new MastoApiError(400, "Either title or exclusive must be set");
|
||||
|
||||
const user = ctx.user as ILocalUser;
|
||||
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)
|
||||
.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 {
|
||||
id: result.id,
|
||||
title: result.name
|
||||
title: result.name,
|
||||
exclusive: result.hideFromHomeTl
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -162,7 +176,8 @@ export class ListHelpers {
|
|||
.then(results => results.map(result => {
|
||||
return {
|
||||
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 { generatePaginationData } from "@/server/api/mastodon/middleware/pagination.js";
|
||||
import { MastoContext } from "@/server/api/mastodon/index.js";
|
||||
import { generateListQuery } from "@/server/api/common/generate-list-query.js";
|
||||
|
||||
export class TimelineHelpers {
|
||||
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");
|
||||
|
||||
generateListQuery(query, user);
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, true, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
|
|
|
@ -31,6 +31,10 @@ export abstract class MastodonStream {
|
|||
return this.connection.blocking;
|
||||
}
|
||||
|
||||
protected get hidden() {
|
||||
return this.connection.hidden;
|
||||
}
|
||||
|
||||
protected get subscriber() {
|
||||
return this.connection.subscriber;
|
||||
}
|
||||
|
|
|
@ -90,12 +90,14 @@ export class MastodonStreamUser extends MastodonStream {
|
|||
|
||||
private async shouldProcessNote(note: Note): Promise<boolean> {
|
||||
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 (this.user!.id !== note.userId && !this.following.has(note.userId)) return false;
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return false;
|
||||
if (isUserRelated(note, this.muting)) 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 (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 { ILocalUser, User } from "@/models/entities/user.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 { StreamEventEmitter, StreamMessages } from "@/server/api/stream/types.js";
|
||||
import { apiLogger } from "@/server/api/logger.js";
|
||||
|
@ -40,6 +40,7 @@ export class MastodonStreamingConnection {
|
|||
public muting: Set<User["id"]> = new Set();
|
||||
public renoteMuting: Set<User["id"]> = new Set();
|
||||
public blocking: Set<User["id"]> = new Set();
|
||||
public hidden: Set<User["id"]> = new Set();
|
||||
public token?: OAuthToken;
|
||||
private wsConnection: websocket.connection;
|
||||
private channels: MastodonStream[] = [];
|
||||
|
@ -69,6 +70,7 @@ export class MastodonStreamingConnection {
|
|||
this.updateMuting();
|
||||
this.updateRenoteMuting();
|
||||
this.updateBlocking();
|
||||
this.updateHidden();
|
||||
this.updateUserProfile();
|
||||
|
||||
this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
|
||||
|
@ -98,6 +100,12 @@ export class MastodonStreamingConnection {
|
|||
case "unmute":
|
||||
this.muting.delete(data.body.id);
|
||||
break;
|
||||
case "userHidden":
|
||||
this.hidden.add(data.body);
|
||||
break;
|
||||
case "userUnhidden":
|
||||
this.hidden.delete(data.body);
|
||||
break;
|
||||
|
||||
// TODO: renote mute events
|
||||
// TODO: block events
|
||||
|
@ -247,6 +255,17 @@ export class MastodonStreamingConnection {
|
|||
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() {
|
||||
this.userProfile = await UserProfiles.findOneBy({
|
||||
userId: this.user!.id,
|
||||
|
|
|
@ -38,6 +38,10 @@ export default abstract class Channel {
|
|||
return this.connection.blocking;
|
||||
}
|
||||
|
||||
protected get hidden() {
|
||||
return this.connection.hidden;
|
||||
}
|
||||
|
||||
protected get followingChannels() {
|
||||
return this.connection.followingChannels;
|
||||
}
|
||||
|
|
|
@ -57,6 +57,8 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
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;
|
||||
|
||||
|
|
|
@ -74,6 +74,8 @@ export default class extends Channel {
|
|||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
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;
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
RenoteMutings,
|
||||
UserProfiles,
|
||||
ChannelFollowings,
|
||||
Blockings,
|
||||
Blockings, UserListJoinings,
|
||||
} from "@/models/index.js";
|
||||
import type { AccessToken } from "@/models/entities/access-token.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 muting: 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 token?: AccessToken;
|
||||
private wsConnection: websocket.connection;
|
||||
|
@ -79,6 +80,7 @@ export default class Connection {
|
|||
this.updateMuting();
|
||||
this.updateRenoteMuting();
|
||||
this.updateBlocking();
|
||||
this.updateHidden();
|
||||
this.updateFollowingChannels();
|
||||
this.updateUserProfile();
|
||||
|
||||
|
@ -122,6 +124,14 @@ export default class Connection {
|
|||
this.followingChannels.delete(data.body.id);
|
||||
break;
|
||||
|
||||
case "userHidden":
|
||||
this.hidden.add(data.body);
|
||||
break;
|
||||
|
||||
case "userUnhidden":
|
||||
this.hidden.delete(data.body);
|
||||
break;
|
||||
|
||||
case "updateUserProfile":
|
||||
this.userProfile = data.body;
|
||||
break;
|
||||
|
@ -432,6 +442,17 @@ export default class Connection {
|
|||
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() {
|
||||
const followings = await ChannelFollowings.find({
|
||||
where: {
|
||||
|
|
|
@ -74,6 +74,8 @@ export interface UserStreamTypes {
|
|||
follow: Packed<"UserDetailedNotMe">;
|
||||
unfollow: Packed<"User">;
|
||||
userAdded: Packed<"User">;
|
||||
userHidden: User["id"];
|
||||
userUnhidden: User["id"];
|
||||
}
|
||||
|
||||
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 { 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 { genId } from "@/misc/gen-id.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
|
||||
export async function pushUserToUserList(target: User, list: UserList) {
|
||||
await UserListJoinings.insert({
|
||||
|
@ -14,5 +13,7 @@ export async function pushUserToUserList(target: User, list: UserList) {
|
|||
userListId: list.id,
|
||||
} 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()">{{
|
||||
i18n.ts.delete
|
||||
}}</MkButton>
|
||||
<FormSection>
|
||||
<FormSwitch v-model="hideFromHomeTl">{{
|
||||
i18n.ts.hideFromHome
|
||||
}}</FormSwitch>
|
||||
</FormSection>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
@ -72,12 +77,15 @@ import * as os from "@/os";
|
|||
import { mainRouter } from "@/router";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import { i18n } from "@/i18n";
|
||||
import FormSwitch from "@/components/form/switch.vue";
|
||||
import FormSection from "@/components/form/section.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
listId: string;
|
||||
}>();
|
||||
|
||||
let list = $ref(null);
|
||||
let hideFromHomeTl = $ref(false);
|
||||
let users = $ref([]);
|
||||
|
||||
function fetchList() {
|
||||
|
@ -85,6 +93,7 @@ function fetchList() {
|
|||
listId: props.listId,
|
||||
}).then((_list) => {
|
||||
list = _list;
|
||||
hideFromHomeTl = _list.hideFromHomeTl;
|
||||
os.api("users/show", {
|
||||
userIds: list.userIds,
|
||||
}).then((_users) => {
|
||||
|
@ -142,7 +151,15 @@ async function deleteList() {
|
|||
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(() => hideFromHomeTl, hideFromHome);
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
|
|
Loading…
Reference in a new issue