Merge branch Sharkey:develop into trackeropt

This commit is contained in:
Vavency 2025-05-15 18:07:46 +00:00
commit 249fe253a0
76 changed files with 1789 additions and 577 deletions

View file

@ -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) => {

View file

@ -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 => {

View file

@ -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 FunctionLint
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 {

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -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>

View file

@ -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">

View 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>

View file

@ -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 FunctionLint
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 {

View file

@ -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 {

View file

@ -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

View 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>

View file

@ -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,

View file

@ -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>

View file

@ -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));
});
}

View file

@ -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') ?? '';

View file

@ -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>

View file

@ -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>

View file

@ -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 => {

View file

@ -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',

View file

@ -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';

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -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 = {

View file

@ -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}`,
},
});
}

View file

@ -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',

View 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,
};

View file

@ -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'));

View file

@ -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';

View file

@ -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;

View file

@ -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> {
},
}));
}

View file

@ -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[] = [];

View file

@ -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();

View 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;
}