mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-07-07 20:44:34 +00:00
factor out shared word mute logic
This commit is contained in:
parent
1a3c6f25a2
commit
b52db71e18
8 changed files with 107 additions and 297 deletions
|
@ -171,24 +171,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<I18n v-else-if="prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" tag="small">
|
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
|
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
<template #word>
|
|
||||||
{{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!--
|
<!--
|
||||||
|
@ -224,7 +207,7 @@ import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { pleaseLogin } from '@/utility/please-login.js';
|
import { pleaseLogin } from '@/utility/please-login.js';
|
||||||
import { checkWordMute } from '@/utility/check-word-mute.js';
|
import { checkMutes } from '@/utility/check-word-mute.js';
|
||||||
import { notePage } from '@/filters/note.js';
|
import { notePage } from '@/filters/note.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
|
@ -253,6 +236,7 @@ import { prefer } from '@/preferences.js';
|
||||||
import { getPluginHandlers } from '@/plugin.js';
|
import { getPluginHandlers } from '@/plugin.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/router.js';
|
||||||
|
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -272,8 +256,6 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const inTimeline = inject<boolean>('inTimeline', false);
|
|
||||||
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
|
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject('inChannel', null);
|
||||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||||
|
|
||||||
|
@ -327,8 +309,7 @@ const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
||||||
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const renoted = ref(false);
|
const renoted = ref(false);
|
||||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
const { muted, hardMuted } = checkMutes(appearNote.value, props.withHardMute);
|
||||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
|
|
||||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||||
|
@ -353,31 +334,6 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
||||||
|
|
||||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||||
|
|
||||||
/* 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): Array<string | string[]> | false | 'sensitiveMute';
|
|
||||||
*/
|
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
|
|
||||||
if (mutedWords != null) {
|
|
||||||
const result = checkWordMute(noteToCheck, $i, mutedWords);
|
|
||||||
if (Array.isArray(result)) return result;
|
|
||||||
|
|
||||||
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
|
|
||||||
if (Array.isArray(replyResult)) return replyResult;
|
|
||||||
|
|
||||||
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
|
|
||||||
if (Array.isArray(renoteResult)) return renoteResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkOnly) return false;
|
|
||||||
|
|
||||||
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
|
|
||||||
return 'sensitiveMute';
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let renoting = false;
|
let renoting = false;
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
|
|
|
@ -230,28 +230,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
||||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<I18n v-else-if="prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" 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.userSaysSomethingAbout" tag="small">
|
|
||||||
<template #name>
|
|
||||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</MkA>
|
|
||||||
</template>
|
|
||||||
<template #word>
|
|
||||||
{{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -262,7 +241,6 @@ import * as Misskey from 'misskey-js';
|
||||||
import { isLink } from '@@/js/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
import { host } from '@@/js/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { computeMergedCw } from '@@/js/compute-merged-cw.js';
|
import { computeMergedCw } from '@@/js/compute-merged-cw.js';
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { Paging } from '@/components/MkPagination.vue';
|
||||||
import type { Keymap } from '@/utility/hotkey.js';
|
import type { Keymap } from '@/utility/hotkey.js';
|
||||||
|
@ -278,7 +256,7 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||||
import { pleaseLogin } from '@/utility/please-login.js';
|
import { pleaseLogin } from '@/utility/please-login.js';
|
||||||
import { checkWordMute } from '@/utility/check-word-mute.js';
|
import { checkMutes } from '@/utility/check-word-mute.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import { notePage } from '@/filters/note.js';
|
import { notePage } from '@/filters/note.js';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
|
@ -308,6 +286,7 @@ import { getAppearNote } from '@/utility/get-appear-note.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { getPluginHandlers } from '@/plugin.js';
|
import { getPluginHandlers } from '@/plugin.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -375,34 +354,7 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
||||||
|
|
||||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||||
|
|
||||||
const inTimeline = inject<boolean>('inTimeline', false);
|
const { muted } = checkMutes(appearNote.value);
|
||||||
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
|
|
||||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
|
||||||
|
|
||||||
/* 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): Array<string | string[]> | false | 'sensitiveMute';
|
|
||||||
*/
|
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
|
|
||||||
if (mutedWords != null) {
|
|
||||||
const result = checkWordMute(noteToCheck, $i, mutedWords);
|
|
||||||
if (Array.isArray(result)) return result;
|
|
||||||
|
|
||||||
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
|
|
||||||
if (Array.isArray(replyResult)) return replyResult;
|
|
||||||
|
|
||||||
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
|
|
||||||
if (Array.isArray(renoteResult)) return renoteResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkOnly) return false;
|
|
||||||
|
|
||||||
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
|
|
||||||
return 'sensitiveMute';
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.expandAllCws, (expandAllCws) => {
|
watch(() => props.expandAllCws, (expandAllCws) => {
|
||||||
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
||||||
|
|
|
@ -73,33 +73,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else :class="$style.muted" @click="muted = false">
|
<div v-else :class="$style.muted" @click="muted = false">
|
||||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<I18n v-else-if="prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" tag="small">
|
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
|
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
<template #word>
|
|
||||||
{{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, ref, shallowRef, watch } from 'vue';
|
import { computed, ref, shallowRef, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { computeMergedCw } from '@@/js/compute-merged-cw.js';
|
import { computeMergedCw } from '@@/js/compute-merged-cw.js';
|
||||||
import { host } from '@@/js/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import type { Visibility } from '@/utility/boost-quote.js';
|
import type { Visibility } from '@/utility/boost-quote.js';
|
||||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||||
import MkNoteHeader from '@/components/MkNoteHeader.vue';
|
import MkNoteHeader from '@/components/MkNoteHeader.vue';
|
||||||
|
@ -113,7 +95,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import { checkWordMute } from '@/utility/check-word-mute.js';
|
import { checkMutes } from '@/utility/check-word-mute.js';
|
||||||
import { pleaseLogin } from '@/utility/please-login.js';
|
import { pleaseLogin } from '@/utility/please-login.js';
|
||||||
import { showMovedDialog } from '@/utility/show-moved-dialog.js';
|
import { showMovedDialog } from '@/utility/show-moved-dialog.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
|
@ -123,6 +105,7 @@ import { getNoteMenu } from '@/utility/get-note-menu.js';
|
||||||
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
|
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { useNoteCapture } from '@/use/use-note-capture.js';
|
import { useNoteCapture } from '@/use/use-note-capture.js';
|
||||||
|
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -183,34 +166,7 @@ async function removeReply(id: Misskey.entities.Note['id']) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inTimeline = inject<boolean>('inTimeline', false);
|
const { muted } = checkMutes(appearNote.value);
|
||||||
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
|
|
||||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
|
||||||
|
|
||||||
/* 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): Array<string | string[]> | false | 'sensitiveMute';
|
|
||||||
*/
|
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
|
|
||||||
if (mutedWords != null) {
|
|
||||||
const result = checkWordMute(noteToCheck, $i, mutedWords);
|
|
||||||
if (Array.isArray(result)) return result;
|
|
||||||
|
|
||||||
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
|
|
||||||
if (Array.isArray(replyResult)) return replyResult;
|
|
||||||
|
|
||||||
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
|
|
||||||
if (Array.isArray(renoteResult)) return renoteResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkOnly) return false;
|
|
||||||
|
|
||||||
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
|
|
||||||
return 'sensitiveMute';
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
rootEl: el,
|
rootEl: el,
|
||||||
|
|
46
packages/frontend/src/components/SkMutedNote.vue
Normal file
46
packages/frontend/src/components/SkMutedNote.vue
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
||||||
|
<template #name>
|
||||||
|
<MkUserName :user="note.user"/>
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
<I18n v-else-if="prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" tag="small">
|
||||||
|
<template #name>
|
||||||
|
<MkUserName :user="note.user"/>
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
|
||||||
|
<template #name>
|
||||||
|
<MkUserName :user="note.user"/>
|
||||||
|
</template>
|
||||||
|
<template #word>
|
||||||
|
{{ mutedWords }}
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { prefer } from '@/preferences.js';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
muted: false | 'sensitiveMute' | (string | string[])[];
|
||||||
|
note: Misskey.entities.Note;
|
||||||
|
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const mutedWords = computed(() => Array.isArray(props.muted)
|
||||||
|
? props.muted.map(words => Array.isArray(words) ? words.join() : words).join(' ')
|
||||||
|
: props.muted);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
|
||||||
|
</style>
|
|
@ -172,24 +172,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<I18n v-else-if="prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" tag="small">
|
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
|
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
<template #word>
|
|
||||||
{{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!--
|
<!--
|
||||||
|
@ -224,7 +207,7 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { pleaseLogin } from '@/utility/please-login.js';
|
import { pleaseLogin } from '@/utility/please-login.js';
|
||||||
import { checkWordMute } from '@/utility/check-word-mute.js';
|
import { checkMutes } from '@/utility/check-word-mute.js';
|
||||||
import { notePage } from '@/filters/note.js';
|
import { notePage } from '@/filters/note.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
|
@ -253,6 +236,7 @@ import { prefer } from '@/preferences.js';
|
||||||
import { getPluginHandlers } from '@/plugin.js';
|
import { getPluginHandlers } from '@/plugin.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/router.js';
|
||||||
|
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -272,8 +256,6 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const inTimeline = inject<boolean>('inTimeline', false);
|
|
||||||
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
|
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject('inChannel', null);
|
||||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||||
|
|
||||||
|
@ -327,8 +309,7 @@ const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
||||||
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const renoted = ref(false);
|
const renoted = ref(false);
|
||||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
const { muted, hardMuted } = checkMutes(appearNote.value, props.withHardMute);
|
||||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
|
|
||||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||||
|
@ -353,31 +334,6 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
||||||
|
|
||||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||||
|
|
||||||
/* 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): Array<string | string[]> | false | 'sensitiveMute';
|
|
||||||
*/
|
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
|
|
||||||
if (mutedWords != null) {
|
|
||||||
const result = checkWordMute(noteToCheck, $i, mutedWords);
|
|
||||||
if (Array.isArray(result)) return result;
|
|
||||||
|
|
||||||
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
|
|
||||||
if (Array.isArray(replyResult)) return replyResult;
|
|
||||||
|
|
||||||
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
|
|
||||||
if (Array.isArray(renoteResult)) return renoteResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkOnly) return false;
|
|
||||||
|
|
||||||
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
|
|
||||||
return 'sensitiveMute';
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let renoting = false;
|
let renoting = false;
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
|
|
|
@ -235,28 +235,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
||||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<I18n v-else-if="prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" 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.userSaysSomethingAbout" tag="small">
|
|
||||||
<template #name>
|
|
||||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</MkA>
|
|
||||||
</template>
|
|
||||||
<template #word>
|
|
||||||
{{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -267,7 +246,6 @@ import * as Misskey from 'misskey-js';
|
||||||
import { isLink } from '@@/js/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
import { host } from '@@/js/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { computeMergedCw } from '@@/js/compute-merged-cw.js';
|
import { computeMergedCw } from '@@/js/compute-merged-cw.js';
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { Paging } from '@/components/MkPagination.vue';
|
||||||
import type { Keymap } from '@/utility/hotkey.js';
|
import type { Keymap } from '@/utility/hotkey.js';
|
||||||
|
@ -283,7 +261,7 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import SkInstanceTicker from '@/components/SkInstanceTicker.vue';
|
import SkInstanceTicker from '@/components/SkInstanceTicker.vue';
|
||||||
import { pleaseLogin } from '@/utility/please-login.js';
|
import { pleaseLogin } from '@/utility/please-login.js';
|
||||||
import { checkWordMute } from '@/utility/check-word-mute.js';
|
import { checkMutes } from '@/utility/check-word-mute.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import { notePage } from '@/filters/note.js';
|
import { notePage } from '@/filters/note.js';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
|
@ -313,6 +291,7 @@ import { getAppearNote } from '@/utility/get-appear-note.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { getPluginHandlers } from '@/plugin.js';
|
import { getPluginHandlers } from '@/plugin.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -381,34 +360,7 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
||||||
|
|
||||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||||
|
|
||||||
const inTimeline = inject<boolean>('inTimeline', false);
|
const { muted } = checkMutes(appearNote.value);
|
||||||
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
|
|
||||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
|
||||||
|
|
||||||
/* 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): Array<string | string[]> | false | 'sensitiveMute';
|
|
||||||
*/
|
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
|
|
||||||
if (mutedWords != null) {
|
|
||||||
const result = checkWordMute(noteToCheck, $i, mutedWords);
|
|
||||||
if (Array.isArray(result)) return result;
|
|
||||||
|
|
||||||
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
|
|
||||||
if (Array.isArray(replyResult)) return replyResult;
|
|
||||||
|
|
||||||
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
|
|
||||||
if (Array.isArray(renoteResult)) return renoteResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkOnly) return false;
|
|
||||||
|
|
||||||
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
|
|
||||||
return 'sensitiveMute';
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.expandAllCws, (expandAllCws) => {
|
watch(() => props.expandAllCws, (expandAllCws) => {
|
||||||
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
||||||
|
|
|
@ -81,24 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else :class="$style.muted" @click="muted = false">
|
<div v-else :class="$style.muted" @click="muted = false">
|
||||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<I18n v-else-if="prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" tag="small">
|
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
|
|
||||||
<template #name>
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</template>
|
|
||||||
<template #word>
|
|
||||||
{{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -107,7 +90,6 @@ import { computed, inject, ref, shallowRef, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { computeMergedCw } from '@@/js/compute-merged-cw.js';
|
import { computeMergedCw } from '@@/js/compute-merged-cw.js';
|
||||||
import { host } from '@@/js/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import type { Visibility } from '@/utility/boost-quote.js';
|
import type { Visibility } from '@/utility/boost-quote.js';
|
||||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||||
import SkNoteHeader from '@/components/SkNoteHeader.vue';
|
import SkNoteHeader from '@/components/SkNoteHeader.vue';
|
||||||
|
@ -121,7 +103,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import { checkWordMute } from '@/utility/check-word-mute.js';
|
import { checkMutes } from '@/utility/check-word-mute.js';
|
||||||
import { pleaseLogin } from '@/utility/please-login.js';
|
import { pleaseLogin } from '@/utility/please-login.js';
|
||||||
import { showMovedDialog } from '@/utility/show-moved-dialog.js';
|
import { showMovedDialog } from '@/utility/show-moved-dialog.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
|
@ -131,6 +113,7 @@ import { getNoteMenu } from '@/utility/get-note-menu.js';
|
||||||
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
|
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { useNoteCapture } from '@/use/use-note-capture.js';
|
import { useNoteCapture } from '@/use/use-note-capture.js';
|
||||||
|
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -197,34 +180,7 @@ async function removeReply(id: Misskey.entities.Note['id']) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inTimeline = inject<boolean>('inTimeline', false);
|
const { muted } = checkMutes(appearNote.value);
|
||||||
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
|
|
||||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
|
||||||
|
|
||||||
/* 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): Array<string | string[]> | false | 'sensitiveMute';
|
|
||||||
*/
|
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
|
|
||||||
if (mutedWords != null) {
|
|
||||||
const result = checkWordMute(noteToCheck, $i, mutedWords);
|
|
||||||
if (Array.isArray(result)) return result;
|
|
||||||
|
|
||||||
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
|
|
||||||
if (Array.isArray(replyResult)) return replyResult;
|
|
||||||
|
|
||||||
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
|
|
||||||
if (Array.isArray(renoteResult)) return renoteResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkOnly) return false;
|
|
||||||
|
|
||||||
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
|
|
||||||
return 'sensitiveMute';
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
rootEl: el,
|
rootEl: el,
|
||||||
|
|
|
@ -3,6 +3,42 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { $i } from '@/i';
|
||||||
|
|
||||||
|
export function checkMutes(noteToCheck: Misskey.entities.Note, withHardMute = false) {
|
||||||
|
const muted = ref(checkMute(noteToCheck, $i?.mutedWords));
|
||||||
|
const hardMuted = ref(withHardMute && checkMute(noteToCheck, $i?.hardMutedWords, true));
|
||||||
|
return { muted, hardMuted };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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): Array<string | string[]> | false | 'sensitiveMute';
|
||||||
|
*/
|
||||||
|
export function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
|
||||||
|
if (mutedWords != null) {
|
||||||
|
const result = checkWordMute(noteToCheck, $i, mutedWords);
|
||||||
|
if (Array.isArray(result)) return result;
|
||||||
|
|
||||||
|
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
|
||||||
|
if (Array.isArray(replyResult)) return replyResult;
|
||||||
|
|
||||||
|
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
|
||||||
|
if (Array.isArray(renoteResult)) return renoteResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkOnly) return false;
|
||||||
|
|
||||||
|
const inTimeline = inject<boolean>('inTimeline', false);
|
||||||
|
const tl_withSensitive = inject<Ref<boolean> | null>('tl_withSensitive', null);
|
||||||
|
if (inTimeline && tl_withSensitive?.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
|
||||||
|
return 'sensitiveMute';
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function checkWordMute(note: string | Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): Array<string | string[]> | false {
|
export function checkWordMute(note: string | Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): Array<string | string[]> | false {
|
||||||
// 自分自身
|
// 自分自身
|
||||||
|
|
Loading…
Add table
Reference in a new issue