summaryrefslogtreecommitdiff
path: root/packages/frontend
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--packages/frontend/src/components/MkNote.vue30
-rw-r--r--packages/frontend/src/components/MkTimeline.vue1
-rw-r--r--packages/frontend/src/pages/timeline.vue89
-rw-r--r--packages/frontend/src/store.ts10
4 files changed, 103 insertions, 27 deletions
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index d7bb64661b..7448fd34e3 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
- v-if="!hardMuted && !muted"
+ v-if="!hardMuted && muted === false"
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
@@ -134,7 +134,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</article>
</div>
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
- <I18n :src="i18n.ts.userSaysSomething" tag="small">
+ <I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
+ <template #name>
+ <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
+ <MkUserName :user="appearNote.user"/>
+ </MkA>
+ </template>
+ </I18n>
+ <I18n v-else :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
<MkUserName :user="appearNote.user"/>
@@ -203,6 +210,7 @@ const emit = defineEmits<{
(ev: 'removeReaction', emoji: string): void;
}>();
+const inTimeline = inject<boolean>('inTimeline', false);
const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
@@ -250,19 +258,27 @@ const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
const collapsed = ref(appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
-const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords));
+const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null)));
-function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
+/* Overload FunctionにLintが対応していないのでコメントアウト
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
+*/
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
if (mutedWords == null) return false;
- if (checkWordMute(note, $i, mutedWords)) return true;
- if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
- if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
+ if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
+ if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
+ if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
+
+ if (checkOnly) return false;
+
+ if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
return false;
}
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 8a5076ea1d..9ea654213e 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -49,6 +49,7 @@ const emit = defineEmits<{
(ev: 'queue', count: number): void;
}>();
+provide('inTimeline', true);
provide('inChannel', computed(() => props.src === 'channel'));
type TimelineQueryType = {
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 666a9968b2..3481113f87 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -66,17 +66,51 @@ const rootEl = shallowRef<HTMLElement>();
const queue = ref(0);
const srcWhenNotSignin = ref(isLocalTimelineAvailable ? 'local' : 'global');
-const src = computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value), set: (x) => saveSrc(x) });
-const withRenotes = ref(true);
-const withReplies = ref($i ? defaultStore.state.tlWithReplies : false);
-const onlyFiles = ref(false);
+const src = computed({
+ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value),
+ set: (x) => saveSrc(x),
+});
+const withRenotes = computed({
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ get: () => (defaultStore.reactiveState.tl.value.filter?.withRenotes ?? saveTlFilter('withRenotes', true)),
+ set: (x) => saveTlFilter('withRenotes', x),
+});
+const withReplies = computed({
+ get: () => {
+ if (!$i) return false;
+ if (['local', 'social'].includes(src.value) && onlyFiles.value) {
+ return false;
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ return defaultStore.reactiveState.tl.value.filter?.withReplies ?? saveTlFilter('withReplies', true);
+ }
+ },
+ set: (x) => saveTlFilter('withReplies', x),
+});
+const onlyFiles = computed({
+ get: () => {
+ if (['local', 'social'].includes(src.value) && withReplies.value) {
+ return false;
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ return defaultStore.reactiveState.tl.value.filter?.onlyFiles ?? saveTlFilter('onlyFiles', false);
+ }
+ },
+ set: (x) => saveTlFilter('onlyFiles', x),
+});
+const withSensitive = computed({
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ get: () => (defaultStore.reactiveState.tl.value.filter?.withSensitive ?? saveTlFilter('withSensitive', true)),
+ set: (x) => {
+ saveTlFilter('withSensitive', x);
-watch(src, () => {
- queue.value = 0;
+ // これだけはクライアント側で完結する処理なので手動でリロード
+ tlComponent.value?.reloadTimeline();
+ },
});
-watch(withReplies, (x) => {
- if ($i) defaultStore.set('tlWithReplies', x);
+watch(src, () => {
+ queue.value = 0;
});
function queueUpdated(q: number): void {
@@ -154,18 +188,38 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
}
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void {
- let userList = null;
+ const out = {
+ ...defaultStore.state.tl,
+ src: newSrc,
+ };
+
if (newSrc.startsWith('userList:')) {
const id = newSrc.substring('userList:'.length);
- userList = defaultStore.reactiveState.pinnedUserLists.value.find(l => l.id === id);
+ out.userList = defaultStore.reactiveState.pinnedUserLists.value.find(l => l.id === id) ?? null;
}
- defaultStore.set('tl', {
- src: newSrc,
- userList,
- });
+
+ defaultStore.set('tl', out);
srcWhenNotSignin.value = newSrc;
}
+function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) {
+ if (key !== 'withReplies' || $i) {
+ const out = { ...defaultStore.state.tl };
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!out.filter) {
+ out.filter = {
+ withRenotes: true,
+ withReplies: true,
+ withSensitive: true,
+ onlyFiles: false,
+ };
+ }
+ out.filter[key] = newValue;
+ defaultStore.set('tl', out);
+ }
+ return newValue;
+}
+
async function timetravel(): Promise<void> {
const { canceled, result: date } = await os.inputDate({
title: i18n.ts.date,
@@ -203,6 +257,10 @@ const headerActions = computed(() => {
disabled: onlyFiles,
} : undefined, {
type: 'switch',
+ text: i18n.ts.withSensitive,
+ ref: withSensitive,
+ }, {
+ type: 'switch',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
disabled: src.value === 'local' || src.value === 'social' ? withReplies : false,
@@ -215,8 +273,7 @@ const headerActions = computed(() => {
icon: 'ti ti-refresh',
text: i18n.ts.reload,
handler: (ev: Event) => {
- console.log('called');
- tlComponent.value.reloadTimeline();
+ tlComponent.value?.reloadTimeline();
},
});
}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 21b796caa1..5635e99650 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -184,6 +184,12 @@ export const defaultStore = markRaw(new Storage('base', {
default: {
src: 'home' as 'home' | 'local' | 'social' | 'global' | `list:${string}`,
userList: null as Misskey.entities.UserList | null,
+ filter: {
+ withReplies: true,
+ withRenotes: true,
+ withSensitive: true,
+ onlyFiles: false,
+ },
},
},
pinnedUserLists: {
@@ -391,10 +397,6 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
- tlWithReplies: {
- where: 'device',
- default: false,
- },
defaultWithReplies: {
where: 'account',
default: false,