enhance(frontend): タイムラインフィルターの設定を保持+センシティブなノートを隠せるように (#12848)
* (enhance) タイムラインフィルターの状態を記憶するように * fix * (enhance) センシティブな投稿をミュート形式で表示する(TLのみ) * fix * Update Changelog * Fix changelog * Lintエラーを潰す * Update locales/ja-JP.yml * hideSensitive -> withSensitive * Update CHANGELOG.md * Update ja-JP.yml --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
		
							parent
							
								
									fb309f3d4f
								
							
						
					
					
						commit
						0580ba1fb5
					
				
					 7 changed files with 110 additions and 27 deletions
				
			
		| 
						 | 
				
			
			@ -38,6 +38,9 @@
 | 
			
		|||
- Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように
 | 
			
		||||
- Enhance: Playの説明欄にMFMを使えるように
 | 
			
		||||
- Enhance: チャンネルノートの場合は詳細ページからその前後のノートを見れるように
 | 
			
		||||
- Enhance: タイムラインフィルターの設定をすべて保持できるように
 | 
			
		||||
	- 今までの「TLに他の人への返信を含める」設定は一旦リセットされます
 | 
			
		||||
- Enhance: タイムラインフィルターに「センシティブなファイルを含むノートを表示」を追加
 | 
			
		||||
- Enhance: ノート作成画面のファイル添付メニューから直接ファイルを削除できるように
 | 
			
		||||
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
 | 
			
		||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -4824,6 +4824,8 @@ export interface Locale extends ILocale {
 | 
			
		|||
     * タイトルへ
 | 
			
		||||
     */
 | 
			
		||||
    "backToTitle": string;
 | 
			
		||||
    "withSensitive": string;
 | 
			
		||||
    "userSaysSomethingSensitive": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * スワイプしてタブを切り替える
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1202,6 +1202,8 @@ replaying: "リプレイ中"
 | 
			
		|||
ranking: "ランキング"
 | 
			
		||||
lastNDays: "直近{n}日"
 | 
			
		||||
backToTitle: "タイトルへ"
 | 
			
		||||
withSensitive: "センシティブなファイルを含むノートを表示"
 | 
			
		||||
userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿"
 | 
			
		||||
enableHorizontalSwipe: "スワイプしてタブを切り替える"
 | 
			
		||||
 | 
			
		||||
_bubbleGame:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,6 +49,7 @@ const emit = defineEmits<{
 | 
			
		|||
	(ev: 'queue', count: number): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
provide('inTimeline', true);
 | 
			
		||||
provide('inChannel', computed(() => props.src === 'channel'));
 | 
			
		||||
 | 
			
		||||
type TimelineQueryType = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,19 +66,53 @@ 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);
 | 
			
		||||
 | 
			
		||||
		// これだけはクライアント側で完結する処理なので手動でリロード
 | 
			
		||||
		tlComponent.value?.reloadTimeline();
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(src, () => {
 | 
			
		||||
	queue.value = 0;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(withReplies, (x) => {
 | 
			
		||||
	if ($i) defaultStore.set('tlWithReplies', x);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function queueUpdated(q: number): void {
 | 
			
		||||
	queue.value = q;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			@ -202,6 +256,10 @@ const headerActions = computed(() => {
 | 
			
		|||
					ref: withReplies,
 | 
			
		||||
					disabled: onlyFiles,
 | 
			
		||||
				} : undefined, {
 | 
			
		||||
					type: 'switch',
 | 
			
		||||
					text: i18n.ts.withSensitive,
 | 
			
		||||
					ref: withSensitive,
 | 
			
		||||
				}, {
 | 
			
		||||
					type: 'switch',
 | 
			
		||||
					text: i18n.ts.fileAttachedOnly,
 | 
			
		||||
					ref: onlyFiles,
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue