Merge branch Sharkey:develop into trackeropt
This commit is contained in:
commit
249fe253a0
76 changed files with 1789 additions and 577 deletions
|
|
@ -63,11 +63,10 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
|
|||
return new Promise((done, fail) => {
|
||||
window.fetch(`${apiUrl}/i`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
i: token,
|
||||
}),
|
||||
body: '{}',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.then(res => new Promise<Misskey.entities.MeDetailed | { error: Record<string, any> }>((done2, fail2) => {
|
||||
|
|
|
|||
|
|
@ -73,12 +73,12 @@ const ok = async () => {
|
|||
const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender });
|
||||
croppedCanvas?.toBlob(blob => {
|
||||
if (!blob) return;
|
||||
if (!$i) return;
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob);
|
||||
formData.append('name', `cropped_${props.file.name}`);
|
||||
formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false');
|
||||
if (props.file.comment) { formData.append('comment', props.file.comment);}
|
||||
formData.append('i', $i!.token);
|
||||
if (props.uploadFolder) {
|
||||
formData.append('folderId', props.uploadFolder);
|
||||
} else if (props.uploadFolder !== null && prefer.s.uploadFolder) {
|
||||
|
|
@ -88,6 +88,9 @@ const ok = async () => {
|
|||
window.fetch(apiUrl + '/drive/files/create', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${$i.token}`,
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(f => {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
|
||||
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
|
||||
<MkAvatar :class="[$style.avatar, prefer.s.useStickyIcons ? $style.useSticky : null]" :user="appearNote.user" :link="!mock" :preview="!mock"/>
|
||||
<div :class="[$style.main, { [$style.clickToOpen]: store.s.clickToOpen }]" @click.stop="store.s.clickToOpen ? noteclick(appearNote.id) : undefined">
|
||||
<div :class="[$style.main, { [$style.clickToOpen]: prefer.s.clickToOpen }]" @click.stop="prefer.s.clickToOpen ? noteclick(appearNote.id) : undefined">
|
||||
<MkNoteHeader :note="appearNote" :mini="true" @click.stop/>
|
||||
<MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
|
||||
<div style="container-type: inline-size;">
|
||||
|
|
@ -171,30 +171,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</article>
|
||||
</div>
|
||||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||
<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-if="showSoftWordMutedWord !== true" :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>
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
<div v-else>
|
||||
<!--
|
||||
|
|
@ -230,7 +207,7 @@ import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
|||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
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 { userPage } from '@/filters/user.js';
|
||||
import number from '@/filters/number.js';
|
||||
|
|
@ -259,7 +236,7 @@ import { prefer } from '@/preferences.js';
|
|||
import { getPluginHandlers } from '@/plugin.js';
|
||||
import { DI } from '@/di.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { store } from '@/store';
|
||||
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -279,8 +256,6 @@ const emit = defineEmits<{
|
|||
|
||||
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 currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||
|
||||
|
|
@ -334,9 +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 isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
|
||||
const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
|
||||
const { muted, hardMuted } = checkMutes(appearNote.value, props.withHardMute);
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||
const translating = ref(false);
|
||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||
|
|
@ -361,31 +334,6 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
|||
|
||||
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;
|
||||
|
||||
const keymap = {
|
||||
|
|
@ -1389,6 +1337,11 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
padding: 8px;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
|
||||
.reactionOmitted {
|
||||
|
|
|
|||
|
|
@ -230,13 +230,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
||||
<I18n :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>
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -262,7 +256,7 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
|||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
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 { notePage } from '@/filters/note.js';
|
||||
import number from '@/filters/number.js';
|
||||
|
|
@ -292,6 +286,7 @@ import { getAppearNote } from '@/utility/get-appear-note.js';
|
|||
import { prefer } from '@/preferences.js';
|
||||
import { getPluginHandlers } from '@/plugin.js';
|
||||
import { DI } from '@/di.js';
|
||||
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -342,7 +337,6 @@ const isMyRenote = $i && ($i.id === note.value.userId);
|
|||
const showContent = ref(prefer.s.uncollapseCW);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||
const translating = ref(false);
|
||||
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
||||
|
|
@ -360,6 +354,8 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
|||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
|
||||
const { muted } = checkMutes(appearNote.value);
|
||||
|
||||
watch(() => props.expandAllCws, (expandAllCws) => {
|
||||
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
||||
});
|
||||
|
|
@ -1199,5 +1195,10 @@ function animatedMFM() {
|
|||
padding: 8px;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="depth < store.s.numberOfReplies">
|
||||
<template v-if="depth < prefer.s.numberOfReplies">
|
||||
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply"/>
|
||||
</template>
|
||||
<div v-else :class="$style.more">
|
||||
|
|
@ -73,13 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="note.userId" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -101,7 +95,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/i.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 { showMovedDialog } from '@/utility/show-moved-dialog.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
|
|
@ -111,7 +105,7 @@ import { getNoteMenu } from '@/utility/get-note-menu.js';
|
|||
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { useNoteCapture } from '@/use/use-note-capture.js';
|
||||
import { store } from '@/store.js';
|
||||
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -129,7 +123,6 @@ const props = withDefaults(defineProps<{
|
|||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i?.id);
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
const muted = computed(() => $i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
|
||||
const translation = ref<any>(null);
|
||||
const translating = ref(false);
|
||||
const isDeleted = ref(false);
|
||||
|
|
@ -173,13 +166,15 @@ async function removeReply(id: Misskey.entities.Note['id']) {
|
|||
}
|
||||
}
|
||||
|
||||
const { muted } = checkMutes(appearNote.value);
|
||||
|
||||
useNoteCapture({
|
||||
rootEl: el,
|
||||
note: appearNote,
|
||||
isDeletedRef: isDeleted,
|
||||
// only update replies if we are, in fact, showing replies
|
||||
onReplyCallback: props.detail && props.depth < store.s.numberOfReplies ? addReplyTo : undefined,
|
||||
onDeleteCallback: props.detail && props.depth < store.s.numberOfReplies ? props.onDeleteCallback : undefined,
|
||||
onReplyCallback: props.detail && props.depth < prefer.s.numberOfReplies ? addReplyTo : undefined,
|
||||
onDeleteCallback: props.detail && props.depth < prefer.s.numberOfReplies ? props.onDeleteCallback : undefined,
|
||||
});
|
||||
|
||||
if ($i) {
|
||||
|
|
@ -384,7 +379,7 @@ function menu(): void {
|
|||
if (props.detail) {
|
||||
misskeyApi('notes/children', {
|
||||
noteId: props.note.id,
|
||||
limit: store.s.numberOfReplies,
|
||||
limit: prefer.s.numberOfReplies,
|
||||
showQuotes: false,
|
||||
}).then(res => {
|
||||
replies.value = res;
|
||||
|
|
@ -519,5 +514,10 @@ if (props.detail) {
|
|||
border: 1px solid var(--MI_THEME-divider);
|
||||
margin: 8px 8px 0 8px;
|
||||
border-radius: var(--MI-radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -280,18 +280,16 @@ const fetchMore = async (): Promise<void> => {
|
|||
|
||||
if (res.length === 0) {
|
||||
if (props.pagination.reversed) {
|
||||
reverseConcat(res).then(() => {
|
||||
more.value = false;
|
||||
});
|
||||
await reverseConcat(res);
|
||||
more.value = false;
|
||||
} else {
|
||||
items.value = concatMapWithArray(items.value, res);
|
||||
more.value = false;
|
||||
}
|
||||
} else {
|
||||
if (props.pagination.reversed) {
|
||||
reverseConcat(res).then(() => {
|
||||
more.value = true;
|
||||
});
|
||||
await reverseConcat(res);
|
||||
more.value = true;
|
||||
} else {
|
||||
items.value = concatMapWithArray(items.value, res);
|
||||
more.value = true;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root" @click="$emit('select', note.user)">
|
||||
<div v-if="!hardMuted" :class="$style.root" @click="$emit('select', note.user)">
|
||||
<div :class="$style.avatar">
|
||||
<MkAvatar :class="$style.icon" :user="note.user" indictor/>
|
||||
</div>
|
||||
|
|
@ -18,11 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkA>
|
||||
</header>
|
||||
<div>
|
||||
<div v-if="isMuted" :class="[$style.text, $style.muted]">({{ i18n.ts.postFiltered }})</div>
|
||||
<div v-if="muted" :class="[$style.text, $style.muted]">
|
||||
<SkMutedNote :muted="muted" :note="note"></SkMutedNote>
|
||||
</div>
|
||||
<Mfm v-else :class="$style.text" :text="getNoteSummary(note)" :isBlock="true" :plain="true" :nowrap="false" :isNote="true" nyaize="respect" :author="note.user"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<!--
|
||||
MkDateSeparatedList uses TransitionGroup which requires single element in the child elements
|
||||
so MkNote create empty div instead of no elements
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -30,19 +38,19 @@ import * as Misskey from 'misskey-js';
|
|||
import { getNoteSummary } from '@/utility/get-note-summary.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { checkMutes } from '@/utility/check-word-mute';
|
||||
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||
|
||||
withDefaults(defineProps<{
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note,
|
||||
isMuted: boolean
|
||||
}>(), {
|
||||
isMuted: false,
|
||||
});
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(event: 'select', user: Misskey.entities.UserLite): void
|
||||
}>();
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const { muted, hardMuted } = checkMutes(props.note);
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template #default="{ items: notes }">
|
||||
<MkDateSeparatedList v-slot="{ item: note }" :items="notes" :class="$style.panel" :noGap="true">
|
||||
<SkFollowingFeedEntry v-if="!isHardMuted(note)" :isMuted="isSoftMuted(note)" :note="note" :class="props.selectedUserId == note.userId && $style.selected" @select="u => selectUser(u.id)"/>
|
||||
<SkFollowingFeedEntry :note="note" :class="props.selectedUserId == note.userId && $style.selected" @select="u => selectUser(u.id)"/>
|
||||
</MkDateSeparatedList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
|
|
@ -23,17 +23,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import type { FollowingFeedTab } from '@/utility/following-feed-utils.js';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import type { FollowingFeedTab } from '@/types/following-feed.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import SkFollowingFeedEntry from '@/components/SkFollowingFeedEntry.vue';
|
||||
import { $i } from '@/i.js';
|
||||
import { checkWordMute } from '@/utility/check-word-mute.js';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -84,37 +81,6 @@ const latestNotesPagination: Paging<'notes/following'> = {
|
|||
};
|
||||
|
||||
const latestNotesPaging = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
function isSoftMuted(note: Misskey.entities.Note): boolean {
|
||||
return isMuted(note, $i?.mutedWords);
|
||||
}
|
||||
|
||||
function isHardMuted(note: Misskey.entities.Note): boolean {
|
||||
return isMuted(note, $i?.hardMutedWords);
|
||||
}
|
||||
|
||||
// Match the typing used by Misskey
|
||||
type Mutes = (string | string[])[] | null | undefined;
|
||||
|
||||
// Adapted from MkNote.ts
|
||||
function isMuted(note: Misskey.entities.Note, mutes: Mutes): boolean {
|
||||
return checkMute(note, mutes)
|
||||
|| checkMute(note.reply, mutes)
|
||||
|| checkMute(note.renote, mutes);
|
||||
}
|
||||
|
||||
// Adapted from check-word-mute.ts
|
||||
function checkMute(note: Misskey.entities.Note | undefined | null, mutes: Mutes): boolean {
|
||||
if (!note) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mutes || mutes.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!checkWordMute(note, $i, mutes);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
|
|
|
|||
45
packages/frontend/src/components/SkMutedNote.vue
Normal file
45
packages/frontend/src/components/SkMutedNote.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<!--
|
||||
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[];
|
||||
note: Misskey.entities.Note;
|
||||
}>();
|
||||
|
||||
const mutedWords = computed(() => Array.isArray(props.muted)
|
||||
? props.muted.join(', ')
|
||||
: props.muted);
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
|
||||
</style>
|
||||
|
|
@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<SkNoteHeader :note="appearNote" :mini="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="[{ [$style.clickToOpen]: store.s.clickToOpen }]" @click.stop="store.s.clickToOpen ? noteclick(appearNote.id) : undefined">
|
||||
<div :class="[{ [$style.clickToOpen]: prefer.s.clickToOpen }]" @click.stop="prefer.s.clickToOpen ? noteclick(appearNote.id) : undefined">
|
||||
<div style="container-type: inline-size;">
|
||||
<p v-if="mergedCW != null" :class="$style.cw">
|
||||
<Mfm
|
||||
|
|
@ -172,30 +172,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</article>
|
||||
</div>
|
||||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||
<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-if="showSoftWordMutedWord !== true" :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>
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
<div v-else>
|
||||
<!--
|
||||
|
|
@ -230,7 +207,7 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
|||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
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 { userPage } from '@/filters/user.js';
|
||||
import number from '@/filters/number.js';
|
||||
|
|
@ -259,7 +236,7 @@ import { prefer } from '@/preferences.js';
|
|||
import { getPluginHandlers } from '@/plugin.js';
|
||||
import { DI } from '@/di.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { store } from '@/store';
|
||||
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -279,8 +256,6 @@ const emit = defineEmits<{
|
|||
|
||||
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 currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||
|
||||
|
|
@ -334,9 +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 isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
|
||||
const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
|
||||
const { muted, hardMuted } = checkMutes(appearNote.value, props.withHardMute);
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||
const translating = ref(false);
|
||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||
|
|
@ -361,31 +334,6 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
|||
|
||||
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;
|
||||
|
||||
const keymap = {
|
||||
|
|
@ -1452,6 +1400,11 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
padding: 8px;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
|
||||
.reactionOmitted {
|
||||
|
|
|
|||
|
|
@ -235,13 +235,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
||||
<I18n :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>
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -267,7 +261,7 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
|||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import SkInstanceTicker from '@/components/SkInstanceTicker.vue';
|
||||
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 { notePage } from '@/filters/note.js';
|
||||
import number from '@/filters/number.js';
|
||||
|
|
@ -297,6 +291,7 @@ import { getAppearNote } from '@/utility/get-appear-note.js';
|
|||
import { prefer } from '@/preferences.js';
|
||||
import { getPluginHandlers } from '@/plugin.js';
|
||||
import { DI } from '@/di.js';
|
||||
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -348,7 +343,6 @@ const isMyRenote = $i && ($i.id === note.value.userId);
|
|||
const showContent = ref(prefer.s.uncollapseCW);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||
const translating = ref(false);
|
||||
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
||||
|
|
@ -366,6 +360,8 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
|||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
|
||||
const { muted } = checkMutes(appearNote.value);
|
||||
|
||||
watch(() => props.expandAllCws, (expandAllCws) => {
|
||||
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
||||
});
|
||||
|
|
@ -1273,6 +1269,11 @@ onUnmounted(() => {
|
|||
padding: 8px;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
|
||||
.badgeRoles {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="depth < store.s.numberOfReplies">
|
||||
<template v-if="depth < prefer.s.numberOfReplies">
|
||||
<SkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="[$style.reply, { [$style.single]: replies.length === 1 }]" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply" :isReply="props.isReply"/>
|
||||
</template>
|
||||
<div v-else :class="$style.more">
|
||||
|
|
@ -81,18 +81,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="note.userId" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, shallowRef, watch } from 'vue';
|
||||
import { computed, inject, ref, shallowRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computeMergedCw } from '@@/js/compute-merged-cw.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
|
|
@ -109,7 +103,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/i.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 { showMovedDialog } from '@/utility/show-moved-dialog.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
|
|
@ -119,7 +113,7 @@ import { getNoteMenu } from '@/utility/get-note-menu.js';
|
|||
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { useNoteCapture } from '@/use/use-note-capture.js';
|
||||
import { store } from '@/store.js';
|
||||
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -143,7 +137,6 @@ const canRenote = computed(() => ['public', 'home'].includes(props.note.visibili
|
|||
const hideLine = computed(() => props.detail);
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
|
||||
const translation = ref<any>(null);
|
||||
const translating = ref(false);
|
||||
const isDeleted = ref(false);
|
||||
|
|
@ -187,13 +180,15 @@ async function removeReply(id: Misskey.entities.Note['id']) {
|
|||
}
|
||||
}
|
||||
|
||||
const { muted } = checkMutes(appearNote.value);
|
||||
|
||||
useNoteCapture({
|
||||
rootEl: el,
|
||||
note: appearNote,
|
||||
isDeletedRef: isDeleted,
|
||||
// only update replies if we are, in fact, showing replies
|
||||
onReplyCallback: props.detail && props.depth < store.s.numberOfReplies ? addReplyTo : undefined,
|
||||
onDeleteCallback: props.detail && props.depth < store.s.numberOfReplies ? props.onDeleteCallback : undefined,
|
||||
onReplyCallback: props.detail && props.depth < prefer.s.numberOfReplies ? addReplyTo : undefined,
|
||||
onDeleteCallback: props.detail && props.depth < prefer.s.numberOfReplies ? props.onDeleteCallback : undefined,
|
||||
});
|
||||
|
||||
if ($i) {
|
||||
|
|
@ -398,7 +393,7 @@ function menu(): void {
|
|||
if (props.detail) {
|
||||
misskeyApi('notes/children', {
|
||||
noteId: props.note.id,
|
||||
limit: store.s.numberOfReplies,
|
||||
limit: prefer.s.numberOfReplies,
|
||||
showQuotes: false,
|
||||
}).then(res => {
|
||||
replies.value = res;
|
||||
|
|
@ -607,6 +602,11 @@ if (props.detail) {
|
|||
border: 1px solid var(--MI_THEME-divider);
|
||||
margin: 8px 8px 0 8px;
|
||||
border-radius: var(--MI-radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
|
||||
// avatar container with line
|
||||
|
|
|
|||
57
packages/frontend/src/components/SkPatternTest.vue
Normal file
57
packages/frontend/src/components/SkPatternTest.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.wordMuteTestLabel }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkTextarea v-model="testWords">
|
||||
<template #caption>{{ i18n.ts.wordMuteTestDescription }}</template>
|
||||
</MkTextarea>
|
||||
<div><MkButton :disabled="!testWords" @click="testWordMutes">{{ i18n.ts.wordMuteTestTest }}</MkButton></div>
|
||||
<div v-if="testMatches == null">{{ i18n.ts.wordMuteTestNoResults }}</div>
|
||||
<div v-else-if="testMatches === ''">{{ i18n.ts.wordMuteTestNoMatch }}</div>
|
||||
<div v-else>{{ i18n.tsx.wordMuteTestMatch({ words: testMatches }) }}</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import { parseMutes } from '@/utility/parse-mutes';
|
||||
import { checkWordMute } from '@/utility/check-word-mute';
|
||||
|
||||
const props = defineProps<{
|
||||
mutedWords?: string | null,
|
||||
}>();
|
||||
|
||||
const testWords = ref<string | null>(null);
|
||||
const testMatches = ref<string | null>(null);
|
||||
|
||||
function testWordMutes() {
|
||||
if (!testWords.value || !props.mutedWords) {
|
||||
testMatches.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const mutes = parseMutes(props.mutedWords);
|
||||
const matches = checkWordMute(testWords.value, null, mutes);
|
||||
testMatches.value = matches ? matches.join(', ') : '';
|
||||
} catch {
|
||||
// Error is displayed by above function
|
||||
testMatches.value = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
|
||||
</style>
|
||||
|
|
@ -11,10 +11,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { FollowingFeedModel } from '@/utility/following-feed-utils.js';
|
||||
import type { FollowingFeedModel } from '@/types/following-feed.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { followersTab } from '@/utility/following-feed-utils.js';
|
||||
import { followersTab } from '@/types/following-feed.js';
|
||||
|
||||
const props = defineProps<{
|
||||
model: FollowingFeedModel,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSelect v-model="type" :class="$style.typeSelect">
|
||||
<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
|
||||
<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option>
|
||||
<option value="isFromInstance">{{ i18n.ts._role._condition.isFromInstance }}</option>
|
||||
<option value="fromBubbleInstance">{{ i18n.ts._role._condition.fromBubbleInstance }}</option>
|
||||
<option value="isSuspended">{{ i18n.ts._role._condition.isSuspended }}</option>
|
||||
<option value="isLocked">{{ i18n.ts._role._condition.isLocked }}</option>
|
||||
<option value="isBot">{{ i18n.ts._role._condition.isBot }}</option>
|
||||
|
|
@ -21,6 +23,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="followersMoreThanOrEq">{{ i18n.ts._role._condition.followersMoreThanOrEq }}</option>
|
||||
<option value="followingLessThanOrEq">{{ i18n.ts._role._condition.followingLessThanOrEq }}</option>
|
||||
<option value="followingMoreThanOrEq">{{ i18n.ts._role._condition.followingMoreThanOrEq }}</option>
|
||||
<option value="localFollowersLessThanOrEq">{{ i18n.ts._role._condition.localFollowersLessThanOrEq }}</option>
|
||||
<option value="localFollowersMoreThanOrEq">{{ i18n.ts._role._condition.localFollowersMoreThanOrEq }}</option>
|
||||
<option value="localFollowingLessThanOrEq">{{ i18n.ts._role._condition.localFollowingLessThanOrEq }}</option>
|
||||
<option value="localFollowingMoreThanOrEq">{{ i18n.ts._role._condition.localFollowingMoreThanOrEq }}</option>
|
||||
<option value="remoteFollowersLessThanOrEq">{{ i18n.ts._role._condition.remoteFollowersLessThanOrEq }}</option>
|
||||
<option value="remoteFollowersMoreThanOrEq">{{ i18n.ts._role._condition.remoteFollowersMoreThanOrEq }}</option>
|
||||
<option value="remoteFollowingLessThanOrEq">{{ i18n.ts._role._condition.remoteFollowingLessThanOrEq }}</option>
|
||||
<option value="remoteFollowingMoreThanOrEq">{{ i18n.ts._role._condition.remoteFollowingMoreThanOrEq }}</option>
|
||||
<option value="notesLessThanOrEq">{{ i18n.ts._role._condition.notesLessThanOrEq }}</option>
|
||||
<option value="notesMoreThanOrEq">{{ i18n.ts._role._condition.notesMoreThanOrEq }}</option>
|
||||
<option value="and">{{ i18n.ts._role._condition.and }}</option>
|
||||
|
|
@ -55,12 +65,44 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #suffix>sec</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
|
||||
<MkInput
|
||||
v-else-if="[
|
||||
'followersLessThanOrEq',
|
||||
'followersMoreThanOrEq',
|
||||
'followingLessThanOrEq',
|
||||
'followingMoreThanOrEq',
|
||||
'localFollowersLessThanOrEq',
|
||||
'localFollowersMoreThanOrEq',
|
||||
'localFollowingLessThanOrEq',
|
||||
'localFollowingMoreThanOrEq',
|
||||
'remoteFollowersLessThanOrEq',
|
||||
'remoteFollowersMoreThanOrEq',
|
||||
'remoteFollowingLessThanOrEq',
|
||||
'remoteFollowingMoreThanOrEq',
|
||||
'notesLessThanOrEq',
|
||||
'notesMoreThanOrEq'
|
||||
].includes(type)"
|
||||
v-model="v.value"
|
||||
type="number"
|
||||
>
|
||||
</MkInput>
|
||||
|
||||
<MkSelect v-else-if="type === 'roleAssignedTo'" v-model="v.roleId">
|
||||
<option v-for="role in roles.filter(r => r.target === 'manual')" :key="role.id" :value="role.id">{{ role.name }}</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkInput v-else-if="type === 'isFromInstance'" v-model="v.host" type="text">
|
||||
<template #label>{{ i18n.ts._role._condition.isFromInstanceHost }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkSwitch v-if="type === 'isFromInstance'" v-model="v.subdomains">
|
||||
<template #label>{{ i18n.ts._role._condition.isFromInstanceSubdomains }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<div v-if="['remoteFollowersLessThanOrEq', 'remoteFollowersMoreThanOrEq', 'remoteFollowingLessThanOrEq', 'remoteFollowingMoreThanOrEq'].includes(type)" :class="$style.warningBanner">
|
||||
<i class="ti ti-alert-triangle"></i>
|
||||
{{ i18n.ts._role.remoteDataWarning }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -73,6 +115,7 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import { rolesCache } from '@/cache.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
|
|
@ -102,6 +145,7 @@ watch(v, () => {
|
|||
const type = computed({
|
||||
get: () => v.value.type,
|
||||
set: (t) => {
|
||||
// TODO there's a bug here: switching types leaves extra properties in the JSON
|
||||
if (t === 'and') v.value.values = [];
|
||||
if (t === 'or') v.value.values = [];
|
||||
if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' };
|
||||
|
|
@ -112,8 +156,20 @@ const type = computed({
|
|||
if (t === 'followersMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'followingLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'followingMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'localFollowersLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'localFollowersMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'localFollowingLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'localFollowingMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'remoteFollowersLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'remoteFollowersMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'remoteFollowingLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'remoteFollowingMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'notesLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'notesMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'isFromInstance') {
|
||||
v.value.host = '';
|
||||
v.value.subdomains = true;
|
||||
}
|
||||
v.value.type = t;
|
||||
},
|
||||
});
|
||||
|
|
@ -163,4 +219,14 @@ function removeSelf() {
|
|||
border-color: var(--MI_THEME-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.warningBanner {
|
||||
color: var(--MI_THEME-warn);
|
||||
width: 100%;
|
||||
padding: 0 6px;
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="translationTimeout" type="number" manualSave @update:modelValue="saveTranslationTimeout">
|
||||
<template #label>{{ i18n.ts.translationTimeoutLabel }}</template>
|
||||
<template #caption>{{ i18n.ts.translationTimeoutCaption }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>DeepL Translation</template>
|
||||
|
||||
|
|
@ -69,6 +74,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePage } from '@/page.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const translationTimeout = ref(0);
|
||||
const deeplAuthKey = ref<string | null>('');
|
||||
const deeplIsPro = ref<boolean>(false);
|
||||
const deeplFreeMode = ref<boolean>(false);
|
||||
|
|
@ -78,6 +84,7 @@ const libreTranslateKey = ref<string | null>('');
|
|||
|
||||
async function init() {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
translationTimeout.value = meta.translationTimeout;
|
||||
deeplAuthKey.value = meta.deeplAuthKey;
|
||||
deeplIsPro.value = meta.deeplIsPro;
|
||||
deeplFreeMode.value = meta.deeplFreeMode;
|
||||
|
|
@ -86,6 +93,13 @@ async function init() {
|
|||
libreTranslateKey.value = meta.libreTranslateKey;
|
||||
}
|
||||
|
||||
async function saveTranslationTimeout() {
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
translationTimeout: translationTimeout.value,
|
||||
});
|
||||
await os.promiseDialog(fetchInstance(true));
|
||||
}
|
||||
|
||||
function save_deepl() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
deeplAuthKey: deeplAuthKey.value,
|
||||
|
|
@ -93,7 +107,7 @@ function save_deepl() {
|
|||
deeplFreeMode: deeplFreeMode.value,
|
||||
deeplFreeInstance: deeplFreeInstance.value,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
os.promiseDialog(fetchInstance(true));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +116,7 @@ function save_libre() {
|
|||
libreTranslateURL: libreTranslateURL.value,
|
||||
libreTranslateKey: libreTranslateKey.value,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
os.promiseDialog(fetchInstance(true));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,15 +26,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||
|
||||
<!-- TODO translate -->
|
||||
<MkFolder v-if="bubbleTimelineEnabled">
|
||||
<MkFolder>
|
||||
<template #icon><i class="ph-drop ph-bold ph-lg"></i></template>
|
||||
<template #label>Bubble timeline</template>
|
||||
<template #label>{{ i18n.ts.bubbleTimeline }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<div v-if="!$i.policies.btlAvailable">
|
||||
<i class="ti ti-alert-triangle"></i> {{ i18n.ts.bubbleTimelineMustBeEnabled }}
|
||||
</div>
|
||||
|
||||
<MkTextarea v-model="bubbleTimeline">
|
||||
<template #caption>Choose which instances should be displayed in the bubble.</template>
|
||||
<template #caption>{{ i18n.ts.bubbleTimelineDescription }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkButton primary @click="save_bubbleTimeline">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
|
@ -47,6 +51,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTextarea v-model="trustedLinkUrlPatterns">
|
||||
<template #caption>{{ i18n.ts.trustedLinkUrlPatternsDescription }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<SkPatternTest :mutedWords="trustedLinkUrlPatterns"></SkPatternTest>
|
||||
|
||||
<MkButton primary @click="save_trustedLinkUrlPatterns">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
|
@ -71,6 +78,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTextarea v-model="sensitiveWords">
|
||||
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<SkPatternTest :mutedWords="sensitiveWords"></SkPatternTest>
|
||||
|
||||
<MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
|
@ -83,6 +93,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTextarea v-model="prohibitedWords">
|
||||
<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<SkPatternTest :mutedWords="prohibitedWords"></SkPatternTest>
|
||||
|
||||
<MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
|
@ -95,6 +108,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTextarea v-model="prohibitedWordsForNameOfUser">
|
||||
<template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<SkPatternTest :mutedWords="prohibitedWordsForNameOfUser"></SkPatternTest>
|
||||
|
||||
<MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
|
@ -166,11 +182,12 @@ import { definePage } from '@/page.js';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import SkPatternTest from '@/components/SkPatternTest.vue';
|
||||
import { $i } from '@/i';
|
||||
|
||||
const enableRegistration = ref<boolean>(false);
|
||||
const emailRequiredForSignup = ref<boolean>(false);
|
||||
const approvalRequiredForSignup = ref<boolean>(false);
|
||||
const bubbleTimelineEnabled = ref<boolean>(false);
|
||||
const sensitiveWords = ref<string>('');
|
||||
const prohibitedWords = ref<string>('');
|
||||
const prohibitedWordsForNameOfUser = ref<string>('');
|
||||
|
|
@ -193,7 +210,6 @@ async function init() {
|
|||
hiddenTags.value = meta.hiddenTags.join('\n');
|
||||
preservedUsernames.value = meta.preservedUsernames.join('\n');
|
||||
bubbleTimeline.value = meta.bubbleInstances.join('\n');
|
||||
bubbleTimelineEnabled.value = meta.policies.btlAvailable;
|
||||
trustedLinkUrlPatterns.value = meta.trustedLinkUrlPatterns.join('\n');
|
||||
blockedHosts.value = meta.blockedHosts.join('\n');
|
||||
silencedHosts.value = meta.silencedHosts?.join('\n') ?? '';
|
||||
|
|
|
|||
|
|
@ -797,6 +797,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canTrend, 'canTrend'])">
|
||||
<template #label>{{ i18n.ts._role._options.canTrend }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.canTrend.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canTrend.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canTrend)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.canTrend.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="role.policies.canTrend.value" :disabled="role.policies.canTrend.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="role.policies.canTrend.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSlot>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -300,6 +300,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canTrend, 'canTrend'])">
|
||||
<template #label>{{ i18n.ts._role._options.canTrend }}</template>
|
||||
<template #suffix>{{ policies.canTrend ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canTrend">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
|
||||
|
|
|
|||
|
|
@ -908,7 +908,6 @@ function getGameImageDriveFile() {
|
|||
formData.append('file', blob);
|
||||
formData.append('name', `bubble-game-${Date.now()}.png`);
|
||||
formData.append('isSensitive', 'false');
|
||||
formData.append('i', $i.token);
|
||||
if (prefer.s.uploadFolder) {
|
||||
formData.append('folderId', prefer.s.uploadFolder);
|
||||
}
|
||||
|
|
@ -916,6 +915,9 @@ function getGameImageDriveFile() {
|
|||
window.fetch(apiUrl + '/drive/files/create', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${$i.token}`,
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(f => {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template v-if="tag == null">
|
||||
<MkFoldableSection class="_margin">
|
||||
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.tsx.popularUsersLocal({ name: instance.name ?? host }) }}</template>
|
||||
<MkUserList :pagination="popularUsersLocalF"/>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection class="_margin">
|
||||
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsersGlobal }}</template>
|
||||
<MkUserList :pagination="popularUsersF"/>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection class="_margin">
|
||||
|
|
@ -65,6 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { watch, ref, useTemplateRef, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { host } from '@@/js/config';
|
||||
import MkUserList from '@/components/MkUserList.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
|
|
@ -73,7 +78,7 @@ import { instance } from '@/instance.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
tag?: string | undefined;
|
||||
}>();
|
||||
|
||||
const origin = ref('local');
|
||||
|
|
@ -86,43 +91,48 @@ watch(() => props.tag, () => {
|
|||
});
|
||||
|
||||
const tagUsers = computed(() => ({
|
||||
endpoint: 'hashtags/users' as const,
|
||||
endpoint: 'hashtags/users',
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: props.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
},
|
||||
}));
|
||||
} as const));
|
||||
|
||||
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
|
||||
const pinnedUsers = { endpoint: 'pinned-users', limit: 10, noPaging: true } as const;
|
||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
} };
|
||||
} } as const;
|
||||
const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
} } as const;
|
||||
const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
state: 'alive',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
} } as const;
|
||||
const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'remote',
|
||||
sort: '+follower',
|
||||
} };
|
||||
} } as const;
|
||||
const popularUsersLocalF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'remote',
|
||||
sort: '+localFollower',
|
||||
} } as const;
|
||||
const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
} } as const;
|
||||
const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
} } as const;
|
||||
|
||||
misskeyApi('hashtags/list', {
|
||||
sort: '+attachedLocalUsers',
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ import { i18n } from '@/i18n.js';
|
|||
import MkSwiper from '@/components/MkSwiper.vue';
|
||||
import MkPageHeader from '@/components/global/MkPageHeader.vue';
|
||||
import SkUserRecentNotes from '@/components/SkUserRecentNotes.vue';
|
||||
import { createModel, createHeaderItem, followingFeedTabs, followingTabIcon, followingTabName, followingTab } from '@/utility/following-feed-utils.js';
|
||||
import { createModel, createHeaderItem, followingTabIcon, followingTabName } from '@/utility/following-feed-utils.js';
|
||||
import { followingTab, followingFeedTabs } from '@/types/following-feed.js';
|
||||
import SkFollowingRecentNotes from '@/components/SkFollowingRecentNotes.vue';
|
||||
import SkRemoteFollowersWarning from '@/components/SkRemoteFollowersWarning.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
|
||||
<SkPatternTest :mutedWords="mutedWords"></SkPatternTest>
|
||||
|
||||
<MkButton primary inline :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -19,8 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ref, watch } from 'vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { parseMutes } from '@/utility/parse-mutes';
|
||||
import SkPatternTest from '@/components/SkPatternTest.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
muted: (string[] | string)[];
|
||||
|
|
@ -30,7 +34,7 @@ const emit = defineEmits<{
|
|||
(ev: 'save', value: (string[] | string)[]): void;
|
||||
}>();
|
||||
|
||||
const render = (mutedWords) => mutedWords.map(x => {
|
||||
const render = (mutedWords: (string | string[])[]) => mutedWords.map(x => {
|
||||
if (Array.isArray(x)) {
|
||||
return x.join(' ');
|
||||
} else {
|
||||
|
|
@ -46,47 +50,15 @@ watch(mutedWords, () => {
|
|||
});
|
||||
|
||||
async function save() {
|
||||
const parseMutes = (mutes) => {
|
||||
// split into lines, remove empty lines and unnecessary whitespace
|
||||
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
|
||||
|
||||
// check each line if it is a RegExp or not
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const regexp = line.match(/^\/(.+)\/(.*)$/);
|
||||
if (regexp) {
|
||||
// check that the RegExp is valid
|
||||
try {
|
||||
new RegExp(regexp[1], regexp[2]);
|
||||
// note that regex lines will not be split by spaces!
|
||||
} catch (err: any) {
|
||||
// invalid syntax: do not save, do not reset changed flag
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.regexpError,
|
||||
text: i18n.tsx.regexpErrorDescription({ tab: 'word mute', line: i + 1 }) + '\n' + err.toString(),
|
||||
});
|
||||
// re-throw error so these invalid settings are not saved
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
lines[i] = line.split(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
};
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseMutes(mutedWords.value);
|
||||
} catch (err) {
|
||||
const parsed = parseMutes(mutedWords.value);
|
||||
|
||||
emit('save', parsed);
|
||||
|
||||
changed.value = false;
|
||||
} catch {
|
||||
// already displayed error message in parseMutes
|
||||
return;
|
||||
}
|
||||
|
||||
emit('save', parsed);
|
||||
|
||||
changed.value = false;
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -117,18 +117,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<hr>
|
||||
|
||||
<SearchMarker :keywords="['replies']">
|
||||
<FormSection>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="defaultWithReplies"><SearchLabel>{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</SearchLabel></MkSwitch>
|
||||
<MkButton danger @click="updateRepliesAll(true)"><i class="ph-chats ph-bold ph-lg"></i> {{ i18n.ts.showRepliesToOthersInTimelineAll }}</MkButton>
|
||||
<MkButton danger @click="updateRepliesAll(false)"><i class="ph-chat ph-bold ph-lg"></i> {{ i18n.ts.hideRepliesToOthersInTimelineAll }}</MkButton>
|
||||
</div>
|
||||
</FormSection>
|
||||
</SearchMarker>
|
||||
|
||||
<hr>
|
||||
|
||||
<FormSlot>
|
||||
<MkButton danger @click="migrate"><i class="ti ti-refresh"></i> {{ i18n.ts.migrateOldSettings }}</MkButton>
|
||||
<template #caption>{{ i18n.ts.migrateOldSettings_description }}</template>
|
||||
|
|
|
|||
|
|
@ -814,6 +814,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label><SearchLabel>{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</SearchLabel></template>
|
||||
</MkSwitch>
|
||||
</MkPreferenceContainer>
|
||||
<div class="_buttons">
|
||||
<MkButton danger @click="updateRepliesAll(true)"><i class="ph-chats ph-bold ph-lg"></i> {{ i18n.ts.showRepliesToOthersInTimelineAll }}</MkButton>
|
||||
<MkButton danger @click="updateRepliesAll(false)"><i class="ph-chat ph-bold ph-lg"></i> {{ i18n.ts.hideRepliesToOthersInTimelineAll }}</MkButton>
|
||||
</div>
|
||||
</SearchMarker>
|
||||
</div>
|
||||
|
||||
|
|
@ -824,6 +828,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
|
||||
<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
|
||||
<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
|
||||
<option value="disabled">{{ i18n.ts._serverDisconnectedBehavior.disabled }}</option>
|
||||
</MkSelect>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
|
@ -1217,6 +1222,16 @@ async function testNotificationDot() {
|
|||
}
|
||||
}
|
||||
|
||||
async function updateRepliesAll(withReplies: boolean) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: withReplies ? i18n.ts.confirmShowRepliesAll : i18n.ts.confirmHideRepliesAll,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
misskeyApi('following/update-all', { withReplies });
|
||||
}
|
||||
|
||||
function save() {
|
||||
misskeyApi('i/update', {
|
||||
defaultCWPriority: defaultCWPriority.value,
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ import type { Plugin } from '@/plugin.js';
|
|||
import type { DeviceKind } from '@/utility/device-kind.js';
|
||||
import type { DeckProfile } from '@/deck.js';
|
||||
import type { PreferencesDefinition } from './manager.js';
|
||||
import type { FollowingFeedState } from '@/utility/following-feed-utils.js';
|
||||
import type { FollowingFeedState } from '@/types/following-feed.js';
|
||||
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
|
||||
import { searchEngineMap } from '@/utility/search-engine-map.js';
|
||||
import { defaultFollowingFeedState } from '@/utility/following-feed-utils.js';
|
||||
import { defaultFollowingFeedState } from '@/types/following-feed.js';
|
||||
|
||||
/** サウンド設定 */
|
||||
export type SoundStore = {
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ export async function signout() {
|
|||
await window.fetch(`${apiUrl}/sw/unregister`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
i: $i.token,
|
||||
endpoint: push.endpoint,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${$i.token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ import darkTheme from '@@/themes/d-green-lime.json5';
|
|||
import { hemisphere } from '@@/js/intl-const.js';
|
||||
import type { DeviceKind } from '@/utility/device-kind.js';
|
||||
import type { Plugin } from '@/plugin.js';
|
||||
import type { FollowingFeedState } from '@/types/following-feed.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { Pizzax } from '@/lib/pizzax.js';
|
||||
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
|
||||
import { defaultFollowingFeedState } from '@/utility/following-feed-utils.js';
|
||||
import type { FollowingFeedState } from '@/utility/following-feed-utils.js';
|
||||
import { defaultFollowingFeedState } from '@/types/following-feed.js';
|
||||
import { searchEngineMap } from '@/utility/search-engine-map.js';
|
||||
|
||||
/**
|
||||
|
|
@ -457,7 +457,7 @@ export const store = markRaw(new Pizzax('base', {
|
|||
},
|
||||
sound_note: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/n-aec', volume: 1 },
|
||||
default: { type: 'syuilo/n-aec', volume: 0 },
|
||||
},
|
||||
sound_noteMy: {
|
||||
where: 'device',
|
||||
|
|
|
|||
36
packages/frontend/src/types/following-feed.ts
Normal file
36
packages/frontend/src/types/following-feed.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { WritableComputedRef } from 'vue';
|
||||
|
||||
export const followingTab = 'following' as const;
|
||||
export const mutualsTab = 'mutuals' as const;
|
||||
export const followersTab = 'followers' as const;
|
||||
export const followingFeedTabs = [followingTab, mutualsTab, followersTab] as const;
|
||||
export type FollowingFeedTab = typeof followingFeedTabs[number];
|
||||
|
||||
export type FollowingFeedState = {
|
||||
withNonPublic: boolean,
|
||||
withQuotes: boolean,
|
||||
withBots: boolean,
|
||||
withReplies: boolean,
|
||||
onlyFiles: boolean,
|
||||
userList: FollowingFeedTab,
|
||||
remoteWarningDismissed: boolean,
|
||||
};
|
||||
|
||||
export type FollowingFeedModel = {
|
||||
[Key in keyof FollowingFeedState]: WritableComputedRef<FollowingFeedState[Key]>;
|
||||
};
|
||||
|
||||
export const defaultFollowingFeedState: FollowingFeedState = {
|
||||
withNonPublic: false,
|
||||
withQuotes: false,
|
||||
withBots: true,
|
||||
withReplies: false,
|
||||
onlyFiles: false,
|
||||
userList: followingTab,
|
||||
remoteWarningDismissed: false,
|
||||
};
|
||||
|
|
@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div v-if="$i && $i.isBot" id="botWarn"><span>{{ i18n.ts.loggedInAsBot }}</span></div>
|
||||
|
||||
<SkOneko v-if="store.r.oneko.value"/>
|
||||
<SkOneko v-if="prefer.r.oneko.value"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -115,7 +115,6 @@ import { i18n } from '@/i18n.js';
|
|||
import { prefer } from '@/preferences.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
|
||||
import { store } from '@/store.js';
|
||||
|
||||
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
|
||||
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
|
||||
|
|
|
|||
|
|
@ -19,18 +19,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts">
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import type { Column } from '@/deck.js';
|
||||
import type { FollowingFeedState } from '@/utility/following-feed-utils.js';
|
||||
import type { FollowingFeedState } from '@/types/following-feed.js';
|
||||
export type FollowingColumn = Column & Partial<FollowingFeedState>;
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FollowingFeedTab } from '@/utility/following-feed-utils.js';
|
||||
import type { FollowingFeedTab } from '@/types/following-feed.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { getColumn, updateColumn } from '@/deck.js';
|
||||
import XColumn from '@/ui/deck/column.vue';
|
||||
import SkFollowingRecentNotes from '@/components/SkFollowingRecentNotes.vue';
|
||||
import SkRemoteFollowersWarning from '@/components/SkRemoteFollowersWarning.vue';
|
||||
import { createModel, createOptionsMenu, followingTab, followingTabName, followingTabIcon, followingFeedTabs } from '@/utility/following-feed-utils.js';
|
||||
import { followingTab, followingFeedTabs } from '@/types/following-feed.js';
|
||||
import { createModel, createOptionsMenu, followingTabName, followingTabIcon } from '@/utility/following-feed-utils.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
|
|
|||
|
|
@ -3,40 +3,88 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
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 };
|
||||
}
|
||||
|
||||
export function checkMute(note: Misskey.entities.Note, mutes: undefined | null): false;
|
||||
export function checkMute(note: Misskey.entities.Note, mutes: undefined | null, checkOnly: false): false;
|
||||
export function checkMute(note: Misskey.entities.Note, mutes: undefined | null, checkOnly?: boolean): false | 'sensitiveMute';
|
||||
export function checkMute(note: Misskey.entities.Note, mutes: Array<string | string[]> | undefined | null): string[] | false;
|
||||
export function checkMute(note: Misskey.entities.Note, mutes: Array<string | string[]> | undefined | null, checkOnly: false): string[] | false;
|
||||
export function checkMute(note: Misskey.entities.Note, mutes: Array<string | string[]> | undefined | null, checkOnly?: boolean): string[] | false | 'sensitiveMute';
|
||||
export function checkMute(note: Misskey.entities.Note, mutes: Array<string | string[]> | undefined | null, checkOnly = false): string[] | false | 'sensitiveMute' {
|
||||
if (mutes != null) {
|
||||
const result =
|
||||
checkWordMute(note, $i, mutes)
|
||||
|| checkWordMute(note.reply, $i, mutes)
|
||||
|| checkWordMute(note.renote, $i, mutes);
|
||||
|
||||
// Only continue to sensitiveMute if we don't match any *actual* mutes
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
const inTimeline = inject<boolean>('inTimeline', false);
|
||||
const tl_withSensitive = inject<Ref<boolean> | null>('tl_withSensitive', null);
|
||||
if (inTimeline && tl_withSensitive?.value === false && note.files?.some((v) => v.isSensitive)) {
|
||||
return 'sensitiveMute';
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function checkWordMute(note: string | Misskey.entities.Note | undefined | null, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): string[] | false {
|
||||
if (note == null) return false;
|
||||
|
||||
export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): Array<string | string[]> | false {
|
||||
// 自分自身
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
if (me && typeof(note) === 'object' && (note.userId === me.id)) return false;
|
||||
|
||||
if (mutedWords.length > 0) {
|
||||
const text = getNoteText(note);
|
||||
const text = typeof(note) === 'object' ? getNoteText(note) : note;
|
||||
|
||||
if (text === '') return false;
|
||||
|
||||
const matched = mutedWords.filter(filter => {
|
||||
const matched = mutedWords.reduce((matchedWords, filter) => {
|
||||
if (Array.isArray(filter)) {
|
||||
// Clean up
|
||||
const filteredFilter = filter.filter(keyword => keyword !== '');
|
||||
if (filteredFilter.length === 0) return false;
|
||||
|
||||
return filteredFilter.every(keyword => text.includes(keyword));
|
||||
if (filteredFilter.length > 0 && filteredFilter.every(keyword => text.includes(keyword))) {
|
||||
const fullFilter = filteredFilter.join(' ');
|
||||
matchedWords.add(fullFilter);
|
||||
}
|
||||
} else {
|
||||
// represents RegExp
|
||||
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
||||
|
||||
// This should never happen due to input sanitisation.
|
||||
if (!regexp) return false;
|
||||
|
||||
try {
|
||||
return new RegExp(regexp[1], regexp[2]).test(text);
|
||||
} catch (err) {
|
||||
// This should never happen due to input sanitisation.
|
||||
return false;
|
||||
if (regexp) {
|
||||
try {
|
||||
const flags = regexp[2].includes('g') ? regexp[2] : (regexp[2] + 'g');
|
||||
const matches = text.matchAll(new RegExp(regexp[1], flags));
|
||||
for (const match of matches) {
|
||||
matchedWords.add(match[0]);
|
||||
}
|
||||
} catch {
|
||||
// This should never happen due to input sanitisation.
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (matched.length > 0) return matched;
|
||||
return matchedWords;
|
||||
}, new Set<string>());
|
||||
|
||||
// Nested arrays are intentional, otherwise the note components will join with space (" ") and it's confusing.
|
||||
if (matched.size > 0) return Array.from(matched);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -7,16 +7,12 @@ import { computed } from 'vue';
|
|||
import type { Ref, WritableComputedRef } from 'vue';
|
||||
import type { PageHeaderItem } from '@/types/page-header.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import type { FollowingFeedTab, FollowingFeedState, FollowingFeedModel } from '@/types/following-feed.js';
|
||||
import { deepMerge } from '@/utility/merge.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { popupMenu } from '@/os.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
export const followingTab = 'following' as const;
|
||||
export const mutualsTab = 'mutuals' as const;
|
||||
export const followersTab = 'followers' as const;
|
||||
export const followingFeedTabs = [followingTab, mutualsTab, followersTab] as const;
|
||||
export type FollowingFeedTab = typeof followingFeedTabs[number];
|
||||
import { followingTab, followersTab, mutualsTab, defaultFollowingFeedState } from '@/types/following-feed.js';
|
||||
|
||||
export function followingTabName(tab: FollowingFeedTab): string;
|
||||
export function followingTabName(tab: FollowingFeedTab | null | undefined): null;
|
||||
|
|
@ -33,30 +29,6 @@ export function followingTabIcon(tab: FollowingFeedTab | null | undefined): stri
|
|||
return 'ph-user-check ph-bold ph-lg';
|
||||
}
|
||||
|
||||
export type FollowingFeedModel = {
|
||||
[Key in keyof FollowingFeedState]: WritableComputedRef<FollowingFeedState[Key]>;
|
||||
};
|
||||
|
||||
export type FollowingFeedState = {
|
||||
withNonPublic: boolean,
|
||||
withQuotes: boolean,
|
||||
withBots: boolean,
|
||||
withReplies: boolean,
|
||||
onlyFiles: boolean,
|
||||
userList: FollowingFeedTab,
|
||||
remoteWarningDismissed: boolean,
|
||||
};
|
||||
|
||||
export const defaultFollowingFeedState: FollowingFeedState = {
|
||||
withNonPublic: false,
|
||||
withQuotes: false,
|
||||
withBots: true,
|
||||
withReplies: false,
|
||||
onlyFiles: false,
|
||||
userList: followingTab,
|
||||
remoteWarningDismissed: false,
|
||||
};
|
||||
|
||||
interface StorageInterface {
|
||||
readonly state: Ref<Partial<FollowingFeedState>>;
|
||||
save(updated: Partial<FollowingFeedState>): void;
|
||||
|
|
@ -177,3 +149,4 @@ function createDefaultStorage(): Ref<StorageInterface> {
|
|||
},
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -293,12 +293,12 @@ export function getNoteMenu(props: {
|
|||
async function translate(): Promise<void> {
|
||||
if (props.translation.value != null) return;
|
||||
props.translating.value = true;
|
||||
const res = await misskeyApi('notes/translate', {
|
||||
props.translation.value = await misskeyApi('notes/translate', {
|
||||
noteId: appearNote.id,
|
||||
targetLang: miLocalStorage.getItem('lang') ?? navigator.language,
|
||||
}).finally(() => {
|
||||
props.translating.value = false;
|
||||
});
|
||||
props.translating.value = false;
|
||||
props.translation.value = res;
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function misskeyApi<
|
|||
_ResT = ResT extends void ? Response<E, P> : ResT,
|
||||
>(
|
||||
endpoint: E,
|
||||
data: P & { i?: string | null; } = {} as any,
|
||||
data: P & { i?: string | null; } = {} as P & {},
|
||||
token?: string | null | undefined,
|
||||
signal?: AbortSignal,
|
||||
): Promise<_ResT> {
|
||||
|
|
@ -41,9 +41,23 @@ export function misskeyApi<
|
|||
};
|
||||
|
||||
const promise = new Promise<_ResT>((resolve, reject) => {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// Append a credential
|
||||
if ($i) data.i = $i.token;
|
||||
if (token !== undefined) data.i = token;
|
||||
const auth = token !== undefined
|
||||
? token
|
||||
: data.i !== undefined
|
||||
? data.i
|
||||
: $i?.token;
|
||||
|
||||
if (auth) {
|
||||
headers['Authorization'] = `Bearer ${auth}`;
|
||||
}
|
||||
|
||||
// Don't let the body value leak through
|
||||
delete data.i;
|
||||
|
||||
// Send request
|
||||
window.fetch(`${apiUrl}/${endpoint}`, {
|
||||
|
|
@ -51,9 +65,7 @@ export function misskeyApi<
|
|||
body: JSON.stringify(data),
|
||||
credentials: 'omit',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers,
|
||||
signal,
|
||||
}).then(async (res) => {
|
||||
const body = res.status === 204 ? null : await res.json();
|
||||
|
|
@ -81,7 +93,9 @@ export function misskeyApiGet<
|
|||
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
|
||||
>(
|
||||
endpoint: E,
|
||||
data: P = {} as any,
|
||||
data: P & { i?: string | null; } = {} as P & {},
|
||||
token?: string | null | undefined,
|
||||
signal?: AbortSignal,
|
||||
): Promise<_ResT> {
|
||||
pendingApiRequestsCount.value++;
|
||||
|
||||
|
|
@ -92,11 +106,27 @@ export function misskeyApiGet<
|
|||
const query = new URLSearchParams(data as any);
|
||||
|
||||
const promise = new Promise<_ResT>((resolve, reject) => {
|
||||
// Append a credential
|
||||
const auth = token !== undefined
|
||||
? token
|
||||
: data.i !== undefined
|
||||
? data.i
|
||||
: $i?.token;
|
||||
|
||||
const headers = auth
|
||||
? { 'Authorization': `Bearer ${auth}` }
|
||||
: undefined;
|
||||
|
||||
// Don't let the body value leak through
|
||||
query.delete('i');
|
||||
|
||||
// Send request
|
||||
window.fetch(`${apiUrl}/${endpoint}?${query}`, {
|
||||
method: 'GET',
|
||||
credentials: 'omit',
|
||||
cache: 'default',
|
||||
headers,
|
||||
signal,
|
||||
}).then(async (res) => {
|
||||
const body = res.status === 204 ? null : await res.json();
|
||||
|
||||
|
|
|
|||
41
packages/frontend/src/utility/parse-mutes.ts
Normal file
41
packages/frontend/src/utility/parse-mutes.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export type Mutes = (string | string[])[];
|
||||
|
||||
export function parseMutes(mutes: string): Mutes {
|
||||
// split into lines, remove empty lines and unnecessary whitespace
|
||||
const lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
|
||||
const outLines: Mutes = Array.from(lines);
|
||||
|
||||
// check each line if it is a RegExp or not
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const regexp = line.match(/^\/(.+)\/(.*)$/);
|
||||
if (regexp) {
|
||||
// check that the RegExp is valid
|
||||
try {
|
||||
new RegExp(regexp[1], regexp[2]);
|
||||
// note that regex lines will not be split by spaces!
|
||||
} catch (err: any) {
|
||||
// invalid syntax: do not save, do not reset changed flag
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.regexpError,
|
||||
text: i18n.tsx.regexpErrorDescription({ tab: 'word mute', line: i + 1 }) + '\n' + err.toString(),
|
||||
});
|
||||
// re-throw error so these invalid settings are not saved
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
outLines[i] = line.split(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return outLines;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue