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 50031f1150
commit 8f61ff7f33
No known key found for this signature in database
GPG Key ID: 8703CACD01000000
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, pub hide_online_status: bool,
#[sea_orm(column_name = "isDeleted")] #[sea_orm(column_name = "isDeleted")]
pub is_deleted: bool, pub is_deleted: bool,
#[sea_orm(column_name = "showTimelineReplies")]
pub show_timeline_replies: bool,
#[sea_orm(column_name = "driveCapacityOverrideMb")] #[sea_orm(column_name = "driveCapacityOverrideMb")]
pub drive_capacity_override_mb: Option<i32>, pub drive_capacity_override_mb: Option<i32>,
#[sea_orm(column_name = "movedToUri")] #[sea_orm(column_name = "movedToUri")]

View File

@ -249,12 +249,6 @@ export class User {
}) })
public followersUri: string | null; 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 }) @Index({ unique: true })
@Column("char", { @Column("char", {
length: 16, length: 16,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,10 @@
<template> <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> <i class="ph-dots-three-outline ph-bold ph-lg"></i>
</button> </button>
<button <button

View File

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

View File

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

View File

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

View File

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

View File

@ -3,13 +3,30 @@ import { markRaw } from "vue";
import { $i } from "@/account"; import { $i } from "@/account";
import { url } from "@/config"; import { url } from "@/config";
export const stream = markRaw( let stream: Misskey.Stream | null = null;
export function useStream(): Misskey.Stream {
if (stream) return stream;
stream = markRaw(
new Misskey.Stream( new Misskey.Stream(
url, url,
$i $i
? { ? {
token: $i.token, token: $i.token,
} }
: null, : 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);
}