enhance: タイムラインからRenoteを除外するオプションを追加

This commit is contained in:
syuilo 2023-09-28 11:41:41 +09:00
parent d854942a1f
commit eb740e2c72
14 changed files with 94 additions and 28 deletions

View File

@ -15,6 +15,7 @@
## next ## next
### General ### General
- Enhance: タイムラインからRenoteを除外するオプションを追加
- Enhance: ユーザーページのート一覧でRenoteを除外できるように - Enhance: ユーザーページのート一覧でRenoteを除外できるように
### Client ### Client

1
locales/index.d.ts vendored
View File

@ -1124,6 +1124,7 @@ export interface Locale {
"authentication": string; "authentication": string;
"authenticationRequiredToContinue": string; "authenticationRequiredToContinue": string;
"dateAndTime": string; "dateAndTime": string;
"showRenotes": string;
"_announcement": { "_announcement": {
"forExistingUsers": string; "forExistingUsers": string;
"forExistingUsersDescription": string; "forExistingUsersDescription": string;

View File

@ -1121,6 +1121,7 @@ unnotifyNotes: "投稿の通知を解除"
authentication: "認証" authentication: "認証"
authenticationRequiredToContinue: "続けるには認証を行ってください" authenticationRequiredToContinue: "続けるには認証を行ってください"
dateAndTime: "日時" dateAndTime: "日時"
showRenotes: "リノートを表示"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"

View File

@ -49,6 +49,8 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true }, includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withFiles: { withFiles: {
type: 'boolean', type: 'boolean',
default: false, default: false,
@ -130,6 +132,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
})); }));
} }
if (!ps.withReplies) {
query.andWhere('note.replyId IS NULL');
}
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
if (ps.withFiles) { if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\''); query.andWhere('note.fileIds != \'{}\'');
} }

View File

@ -41,8 +41,8 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
userId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' },
includeReplies: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false },
includeRenotes: { type: 'boolean', default: true }, withRenotes: { type: 'boolean', default: true },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
@ -115,11 +115,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
} }
if (!ps.includeReplies) { if (!ps.withReplies) {
query.andWhere('note.replyId IS NULL'); query.andWhere('note.replyId IS NULL');
} }
if (ps.includeRenotes === false) { if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => { query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL'); qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => { qb.orWhere(new Brackets(qb => {

View File

@ -19,6 +19,7 @@ class GlobalTimelineChannel extends Channel {
public static shouldShare = true; public static shouldShare = true;
public static requireCredential = false; public static requireCredential = false;
private withReplies: boolean; private withReplies: boolean;
private withRenotes: boolean;
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
@ -37,7 +38,8 @@ class GlobalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.gtlAvailable) return; if (!policies.gtlAvailable) return;
this.withReplies = params.withReplies as boolean; this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
@ -68,6 +70,8 @@ class GlobalTimelineChannel extends Channel {
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
} }
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// Ignore notes from instances the user has muted // Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return; if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;

View File

@ -17,6 +17,7 @@ class HomeTimelineChannel extends Channel {
public static shouldShare = true; public static shouldShare = true;
public static requireCredential = true; public static requireCredential = true;
private withReplies: boolean; private withReplies: boolean;
private withRenotes: boolean;
constructor( constructor(
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
@ -30,7 +31,8 @@ class HomeTimelineChannel extends Channel {
@bindThis @bindThis
public async init(params: any) { public async init(params: any) {
this.withReplies = params.withReplies as boolean; this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
} }
@ -77,6 +79,8 @@ class HomeTimelineChannel extends Channel {
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
} }
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return; if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View File

@ -19,6 +19,7 @@ class HybridTimelineChannel extends Channel {
public static shouldShare = true; public static shouldShare = true;
public static requireCredential = true; public static requireCredential = true;
private withReplies: boolean; private withReplies: boolean;
private withRenotes: boolean;
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
@ -37,7 +38,8 @@ class HybridTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return; if (!policies.ltlAvailable) return;
this.withReplies = params.withReplies as boolean; this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
@ -89,6 +91,8 @@ class HybridTimelineChannel extends Channel {
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
} }
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return; if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View File

@ -18,6 +18,7 @@ class LocalTimelineChannel extends Channel {
public static shouldShare = true; public static shouldShare = true;
public static requireCredential = false; public static requireCredential = false;
private withReplies: boolean; private withReplies: boolean;
private withRenotes: boolean;
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
@ -36,7 +37,8 @@ class LocalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return; if (!policies.ltlAvailable) return;
this.withReplies = params.withReplies as boolean; this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
@ -68,6 +70,8 @@ class LocalTimelineChannel extends Channel {
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
} }
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return; if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View File

@ -15,14 +15,19 @@ import * as sound from '@/scripts/sound.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
const props = defineProps<{ const props = withDefaults(defineProps<{
src: string; src: string;
list?: string; list?: string;
antenna?: string; antenna?: string;
channel?: string; channel?: string;
role?: string; role?: string;
sound?: boolean; sound?: boolean;
}>(); withRenotes?: boolean;
withReplies?: boolean;
}>(), {
withRenotes: true,
withReplies: false,
});
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'note'): void; (ev: 'note'): void;
@ -62,10 +67,12 @@ if (props.src === 'antenna') {
} else if (props.src === 'home') { } else if (props.src === 'home') {
endpoint = 'notes/timeline'; endpoint = 'notes/timeline';
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}; };
connection = stream.useChannel('homeTimeline', { connection = stream.useChannel('homeTimeline', {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}); });
connection.on('note', prepend); connection.on('note', prepend);
@ -73,28 +80,34 @@ if (props.src === 'antenna') {
} else if (props.src === 'local') { } else if (props.src === 'local') {
endpoint = 'notes/local-timeline'; endpoint = 'notes/local-timeline';
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}; };
connection = stream.useChannel('localTimeline', { connection = stream.useChannel('localTimeline', {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}); });
connection.on('note', prepend); connection.on('note', prepend);
} else if (props.src === 'social') { } else if (props.src === 'social') {
endpoint = 'notes/hybrid-timeline'; endpoint = 'notes/hybrid-timeline';
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}; };
connection = stream.useChannel('hybridTimeline', { connection = stream.useChannel('hybridTimeline', {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}); });
connection.on('note', prepend); connection.on('note', prepend);
} else if (props.src === 'global') { } else if (props.src === 'global') {
endpoint = 'notes/global-timeline'; endpoint = 'notes/global-timeline';
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}; };
connection = stream.useChannel('globalTimeline', { connection = stream.useChannel('globalTimeline', {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}); });
connection.on('note', prepend); connection.on('note', prepend);
} else if (props.src === 'mentions') { } else if (props.src === 'mentions') {
@ -116,9 +129,13 @@ if (props.src === 'antenna') {
} else if (props.src === 'list') { } else if (props.src === 'list') {
endpoint = 'notes/user-list-timeline'; endpoint = 'notes/user-list-timeline';
query = { query = {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
listId: props.list, listId: props.list,
}; };
connection = stream.useChannel('userList', { connection = stream.useChannel('userList', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
listId: props.list, listId: props.list,
}); });
connection.on('note', prepend); connection.on('note', prepend);

View File

@ -29,7 +29,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s"> <div class="_gaps_s">
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch> <MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch> <MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
<MkSwitch v-model="showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts.pinnedList }}</template> <template #label>{{ i18n.ts.pinnedList }}</template>
<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ --> <!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
@ -249,7 +248,6 @@ const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance')); const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance'));
const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition')); const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis')); const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
const showTimelineReplies = computed(defaultStore.makeGetterSetter('showTimelineReplies'));
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn')); const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
watch(lang, () => { watch(lang, () => {

View File

@ -15,9 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tl"> <div :class="$style.tl">
<MkTimeline <MkTimeline
ref="tlComponent" ref="tlComponent"
:key="src" :key="src + withRenotes + withReplies"
:src="src.split(':')[0]" :src="src.split(':')[0]"
:list="src.split(':')[1]" :list="src.split(':')[1]"
:withRenotes="withRenotes"
:withReplies="withReplies"
:sound="true" :sound="true"
@queue="queueUpdated" @queue="queueUpdated"
/> />
@ -58,6 +60,8 @@ const rootEl = $shallowRef<HTMLElement>();
let queue = $ref(0); let queue = $ref(0);
let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global'); let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global');
const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) }); const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) });
const withRenotes = $ref(true);
const withReplies = $ref(false);
watch($$(src), () => queue = 0); watch($$(src), () => queue = 0);
@ -129,7 +133,23 @@ function focus(): void {
tlComponent.focus(); tlComponent.focus();
} }
const headerActions = $computed(() => []); const headerActions = $computed(() => [{
icon: 'ti ti-dots',
text: i18n.ts.options,
handler: (ev) => {
os.popupMenu([{
type: 'switch',
text: i18n.ts.showRenotes,
icon: 'ti ti-repeat',
ref: $$(withRenotes),
}, {
type: 'switch',
text: i18n.ts.withReplies,
icon: 'ti ti-arrow-back-up',
ref: $$(withReplies),
}], ev.currentTarget ?? ev.target);
},
}]);
const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({ const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
key: 'list:' + l.id, key: 'list:' + l.id,

View File

@ -36,8 +36,8 @@ const pagination = {
limit: 10, limit: 10,
params: computed(() => ({ params: computed(() => ({
userId: props.user.id, userId: props.user.id,
includeRenotes: include.value === 'all', withRenotes: include.value === 'all',
includeReplies: include.value === 'all' || include.value === 'files', withReplies: include.value === 'all' || include.value === 'files',
withFiles: include.value === 'files', withFiles: include.value === 'files',
})), })),
}; };

View File

@ -109,10 +109,6 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'account', where: 'account',
default: [] as string[], default: [] as string[],
}, },
showTimelineReplies: {
where: 'account',
default: false,
},
menu: { menu: {
where: 'deviceAccount', where: 'deviceAccount',