fix: 🚸 make "show replies in timeline" work as expected

Co-authored-by: Syuilo <syuilotan@yahoo.co.jp>
This commit is contained in:
ThatOneCalculator 2023-06-14 20:17:56 -07:00
parent 47c077a7f3
commit 7f3b9b171c
24 changed files with 145 additions and 86 deletions

View File

@ -0,0 +1,15 @@
export class RemoveShowTimelineReplies1684206886988 {
name = "RemoveShowTimelineReplies1684206886988";
async up(queryRunner) {
await queryRunner.query(
`ALTER TABLE "user" DROP COLUMN "showTimelineReplies"`,
);
}
async down(queryRunner) {
await queryRunner.query(
`ALTER TABLE "user" ADD "showTimelineReplies" boolean NOT NULL DEFAULT false`,
);
}
}

View File

@ -63,8 +63,6 @@ pub struct Model {
pub hide_online_status: bool,
#[sea_orm(column_name = "isDeleted")]
pub is_deleted: bool,
#[sea_orm(column_name = "showTimelineReplies")]
pub show_timeline_replies: bool,
#[sea_orm(column_name = "driveCapacityOverrideMb")]
pub drive_capacity_override_mb: Option<i32>,
#[sea_orm(column_name = "movedToUri")]

View File

@ -249,12 +249,6 @@ export class User {
})
public followersUri: string | null;
@Column("boolean", {
default: false,
comment: "Whether to show users replying to other users in the timeline.",
})
public showTimelineReplies: boolean;
@Index({ unique: true })
@Column("char", {
length: 16,

View File

@ -567,7 +567,6 @@ export const UserRepository = db.getRepository(User).extend({
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: profile!.mutingNotificationTypes,
emailNotificationTypes: profile!.emailNotificationTypes,
showTimelineReplies: user.showTimelineReplies || falsy,
}
: {}),

View File

@ -279,7 +279,6 @@ export async function createPerson(
tags,
isBot,
isCat: (person as any).isCat === true,
showTimelineReplies: false,
}),
)) as IRemoteUser;

View File

@ -4,7 +4,8 @@ import { Brackets } from "typeorm";
export function generateRepliesQuery(
q: SelectQueryBuilder<any>,
me?: Pick<User, "id" | "showTimelineReplies"> | null,
withReplies: boolean,
me?: Pick<User, "id"> | null,
) {
if (me == null) {
q.andWhere(
@ -20,25 +21,21 @@ export function generateRepliesQuery(
);
}),
);
} else if (!me.showTimelineReplies) {
} else if (!withReplies) {
q.andWhere(
new Brackets((qb) => {
qb.where("note.replyId IS NULL") // 返信ではない
.orWhere("note.replyUserId = :meId", { meId: me.id }) // 返信だけど自分のノートへの返信
.orWhere(
new Brackets((qb) => {
qb.where(
// 返信だけど自分の行った返信
"note.replyId IS NOT NULL",
).andWhere("note.userId = :meId", { meId: me.id });
qb.where("note.replyId IS NOT NULL") // 返信だけど自分の行った返信
.andWhere("note.userId = :meId", { meId: me.id });
}),
)
.orWhere(
new Brackets((qb) => {
qb.where(
// 返信だけど投稿者自身への返信
"note.replyId IS NOT NULL",
).andWhere("note.replyUserId = note.userId");
qb.where("note.replyId IS NOT NULL") // 返信だけど投稿者自身への返信
.andWhere("note.replyUserId = note.userId");
}),
);
}),

View File

@ -106,7 +106,6 @@ export const paramDef = {
isBot: { type: "boolean" },
isCat: { type: "boolean" },
speakAsCat: { type: "boolean" },
showTimelineReplies: { type: "boolean" },
injectFeaturedNote: { type: "boolean" },
receiveAnnouncementEmail: { type: "boolean" },
alwaysMarkNsfw: { type: "boolean" },
@ -185,8 +184,6 @@ export default define(meta, paramDef, async (ps, _user, token) => {
if (typeof ps.publicReactions === "boolean")
profileUpdates.publicReactions = ps.publicReactions;
if (typeof ps.isBot === "boolean") updates.isBot = ps.isBot;
if (typeof ps.showTimelineReplies === "boolean")
updates.showTimelineReplies = ps.showTimelineReplies;
if (typeof ps.carefulBot === "boolean")
profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === "boolean")

View File

@ -53,6 +53,11 @@ export const paramDef = {
untilId: { type: "string", format: "misskey:id" },
sinceDate: { type: "integer" },
untilDate: { type: "integer" },
withReplies: {
type: "boolean",
default: false,
description: "Show replies in the timeline",
},
},
required: [],
} as const;
@ -87,7 +92,7 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateRepliesQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user);
if (user) {
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);

View File

@ -60,6 +60,11 @@ export const paramDef = {
default: false,
description: "Only show notes that have attached files.",
},
withReplies: {
type: "boolean",
default: false,
description: "Show replies in the timeline",
},
},
required: [],
} as const;
@ -104,7 +109,7 @@ export default define(meta, paramDef, async (ps, user) => {
.setParameters(followingQuery.getParameters());
generateChannelQuery(query, user);
generateRepliesQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);

View File

@ -63,6 +63,11 @@ export const paramDef = {
untilId: { type: "string", format: "misskey:id" },
sinceDate: { type: "integer" },
untilDate: { type: "integer" },
withReplies: {
type: "boolean",
default: false,
description: "Show replies in the timeline",
},
},
required: [],
} as const;
@ -97,7 +102,7 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateChannelQuery(query, user);
generateRepliesQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user);
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateMutedNoteQuery(query, user);

View File

@ -63,6 +63,11 @@ export const paramDef = {
untilId: { type: "string", format: "misskey:id" },
sinceDate: { type: "integer" },
untilDate: { type: "integer" },
withReplies: {
type: "boolean",
default: false,
description: "Show replies in the timeline",
},
},
required: [],
} as const;
@ -100,7 +105,7 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateChannelQuery(query, user);
generateRepliesQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user);
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateMutedNoteQuery(query, user);

View File

@ -54,6 +54,11 @@ export const paramDef = {
default: false,
description: "Only show notes that have attached files.",
},
withReplies: {
type: "boolean",
default: false,
description: "Show replies in the timeline",
},
},
required: [],
} as const;
@ -100,7 +105,7 @@ export default define(meta, paramDef, async (ps, user) => {
.setParameters(followingQuery.getParameters());
generateChannelQuery(query, user);
generateRepliesQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);

View File

@ -9,6 +9,7 @@ export default class extends Channel {
public readonly chName = "globalTimeline";
public static shouldShare = true;
public static requireCredential = false;
private withReplies: boolean;
constructor(id: string, connection: Channel["connection"]) {
super(id, connection);
@ -22,6 +23,8 @@ export default class extends Channel {
return;
}
this.withReplies = params.withReplies as boolean;
// Subscribe events
this.subscriber.on("notesStream", this.onNote);
}
@ -31,7 +34,7 @@ export default class extends Channel {
if (note.channelId != null) return;
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (

View File

@ -8,6 +8,7 @@ export default class extends Channel {
public readonly chName = "homeTimeline";
public static shouldShare = true;
public static requireCredential = true;
private withReplies: boolean;
constructor(id: string, connection: Channel["connection"]) {
super(id, connection);
@ -15,6 +16,8 @@ export default class extends Channel {
}
public async init(params: any) {
this.withReplies = params.withReplies as boolean;
// Subscribe events
this.subscriber.on("notesStream", this.onNote);
}
@ -39,7 +42,7 @@ export default class extends Channel {
return;
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (

View File

@ -9,6 +9,7 @@ export default class extends Channel {
public readonly chName = "hybridTimeline";
public static shouldShare = true;
public static requireCredential = true;
private withReplies: boolean;
constructor(id: string, connection: Channel["connection"]) {
super(id, connection);
@ -24,6 +25,8 @@ export default class extends Channel {
)
return;
this.withReplies = params.withReplies as boolean;
// Subscribe events
this.subscriber.on("notesStream", this.onNote);
}
@ -56,7 +59,7 @@ export default class extends Channel {
return;
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (

View File

@ -8,6 +8,7 @@ export default class extends Channel {
public readonly chName = "localTimeline";
public static shouldShare = true;
public static requireCredential = false;
private withReplies: boolean;
constructor(id: string, connection: Channel["connection"]) {
super(id, connection);
@ -21,6 +22,8 @@ export default class extends Channel {
return;
}
this.withReplies = params.withReplies as boolean;
// Subscribe events
this.subscriber.on("notesStream", this.onNote);
}
@ -32,7 +35,7 @@ export default class extends Channel {
return;
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (

View File

@ -9,6 +9,7 @@ export default class extends Channel {
public readonly chName = "recommendedTimeline";
public static shouldShare = true;
public static requireCredential = true;
private withReplies: boolean;
constructor(id: string, connection: Channel["connection"]) {
super(id, connection);
@ -24,6 +25,8 @@ export default class extends Channel {
)
return;
this.withReplies = params.withReplies as boolean;
// Subscribe events
this.subscriber.on("notesStream", this.onNote);
}
@ -54,7 +57,7 @@ export default class extends Channel {
return;
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (

View File

@ -44,12 +44,7 @@ describe("ユーザー", () => {
};
type MeDetailed = UserDetailedNotMe &
misskey.entities.MeDetailed & {
showTimelineReplies: boolean;
achievements: object[];
loggedInDays: number;
policies: object;
};
misskey.entities.MeDetailed
type User = MeDetailed & { token: string };
@ -172,9 +167,6 @@ describe("ユーザー", () => {
mutedInstances: user.mutedInstances,
mutingNotificationTypes: user.mutingNotificationTypes,
emailNotificationTypes: user.emailNotificationTypes,
showTimelineReplies: user.showTimelineReplies,
achievements: user.achievements,
loggedInDays: user.loggedInDays,
policies: user.policies,
...(security
? {
@ -479,13 +471,6 @@ describe("ユーザー", () => {
"follow",
"receiveFollowRequest",
]);
assert.strictEqual(response.showTimelineReplies, false);
assert.deepStrictEqual(response.achievements, []);
assert.deepStrictEqual(response.loggedInDays, 0);
assert.deepStrictEqual(response.policies, DEFAULT_POLICIES);
assert.notStrictEqual(response.email, undefined);
assert.strictEqual(response.emailVerified, false);
assert.deepStrictEqual(response.securityKeysList, []);
});
//#endregion
@ -551,8 +536,6 @@ describe("ユーザー", () => {
{ parameters: (): object => ({ isBot: false }) },
{ parameters: (): object => ({ isCat: true }) },
{ parameters: (): object => ({ isCat: false }) },
{ parameters: (): object => ({ showTimelineReplies: true }) },
{ parameters: (): object => ({ showTimelineReplies: false }) },
{ parameters: (): object => ({ injectFeaturedNote: true }) },
{ parameters: (): object => ({ injectFeaturedNote: false }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: true }) },

View File

@ -1,5 +1,10 @@
<template>
<button v-if="!hideMenu" class="menu _button" @click.stop="menu" v-tooltip="i18n.ts.menu">
<button
v-if="!hideMenu"
class="menu _button"
@click.stop="menu"
v-tooltip="i18n.ts.menu"
>
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
</button>
<button

View File

@ -91,7 +91,12 @@ if (props.src === "antenna") {
connection.on("note", prepend);
} else if (props.src === "home") {
endpoint = "notes/timeline";
connection = stream.useChannel("homeTimeline");
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel("homeTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on("note", prepend);
connection2 = stream.useChannel("main");
@ -102,28 +107,48 @@ if (props.src === "antenna") {
tlHintClosed = defaultStore.state.tlHomeHintClosed;
} else if (props.src === "local") {
endpoint = "notes/local-timeline";
connection = stream.useChannel("localTimeline");
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel("localTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on("note", prepend);
tlHint = i18n.ts._tutorial.step5_4;
tlHintClosed = defaultStore.state.tlLocalHintClosed;
} else if (props.src === "recommended") {
endpoint = "notes/recommended-timeline";
connection = stream.useChannel("recommendedTimeline");
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel("recommendedTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on("note", prepend);
tlHint = i18n.ts._tutorial.step5_6;
tlHintClosed = defaultStore.state.tlRecommendedHintClosed;
} else if (props.src === "social") {
endpoint = "notes/hybrid-timeline";
connection = stream.useChannel("hybridTimeline");
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel("hybridTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on("note", prepend);
tlHint = i18n.ts._tutorial.step5_5;
tlHintClosed = defaultStore.state.tlSocialHintClosed;
} else if (props.src === "global") {
endpoint = "notes/global-timeline";
connection = stream.useChannel("globalTimeline");
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel("globalTimeline", {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on("note", prepend);
tlHint = i18n.ts._tutorial.step5_7;

View File

@ -54,7 +54,7 @@
<FormSwitch v-model="disablePagesScript" class="_formBlock">{{
i18n.ts.disablePagesScript
}}</FormSwitch>
<FormSwitch v-model="profile.showTimelineReplies" class="_formBlock"
<FormSwitch v-model="showTimelineReplies" class="_formBlock"
>{{ i18n.ts.flagShowTimelineReplies
}}<template #caption
>{{ i18n.ts.flagShowTimelineRepliesDescription }}
@ -258,24 +258,6 @@ const lang = ref(localStorage.getItem("lang"));
const fontSize = ref(localStorage.getItem("fontSize"));
const useSystemFont = ref(localStorage.getItem("useSystemFont") != null);
const profile = reactive({
showTimelineReplies: $i?.showTimelineReplies,
});
watch(
() => profile,
() => {
save();
},
{
deep: true,
}
);
function save() {
os.apiWithDialog("i/update", {
showTimelineReplies: !!profile.showTimelineReplies,
});
}
async function reloadAsk() {
const { canceled } = await os.confirm({
type: "info",
@ -360,6 +342,9 @@ const swipeOnDesktop = computed(
const showAdminUpdates = computed(
defaultStore.makeGetterSetter("showAdminUpdates")
);
const showTimelineReplies = computed(
defaultStore.makeGetterSetter("showTimelineReplies")
);
watch(lang, () => {
localStorage.setItem("lang", lang.value as string);

View File

@ -115,6 +115,7 @@ const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [
"enableCustomKaTeXMacro",
"enableEmojiReactions",
"showEmojisInReactionNotifications",
"showTimelineReplies",
];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
"lightTheme",

View File

@ -330,6 +330,10 @@ export const defaultStore = markRaw(
where: "account",
default: true,
},
showTimelineReplies: {
where: "device",
default: true,
}
}),
);

View File

@ -3,13 +3,30 @@ import { markRaw } from "vue";
import { $i } from "@/account";
import { url } from "@/config";
export const stream = markRaw(
new Misskey.Stream(
url,
$i
? {
token: $i.token,
}
: null,
),
);
let stream: Misskey.Stream | null = null;
export function useStream(): Misskey.Stream {
if (stream) return stream;
stream = markRaw(
new Misskey.Stream(
url,
$i
? {
token: $i.token,
}
: null
)
);
window.setTimeout(heartbeat, 1000 * 60);
return stream;
}
function heartbeat(): void {
if (stream != null && document.visibilityState === "visible") {
stream.send("ping");
}
window.setTimeout(heartbeat, 1000 * 60);
}