Merge remote-tracking branch 'misskey/master' into feature/2024.9.0

This commit is contained in:
dakkar 2024-10-09 15:17:22 +01:00
commit f00576bce6
564 changed files with 19993 additions and 8169 deletions

View file

@ -29,7 +29,7 @@ import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkLink from '@/components/MkLink.vue';
import { version } from '@/config.js';
import { version } from '@@/js/config.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';

View file

@ -185,7 +185,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, shallowRef, computed } from 'vue';
import { version } from '@/config.js';
import { version } from '@@/js/config.js';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';

View file

@ -148,7 +148,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
import sanitizeHtml from '@/scripts/sanitize-html.js';
import { host, version } from '@/config.js';
import { host, version } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import number from '@/filters/number.js';

View file

@ -44,6 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
<div v-else-if="tab === 'notes' && info" class="_gaps_m">
<XNotes :fileId="fileId"/>
</div>
<div v-else-if="tab === 'ip' && info" class="_gaps_m">
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
<MkKeyValue v-if="info.requestIp" class="_monospace" :copy="info.requestIp" oneline>
@ -67,7 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@ -88,6 +91,7 @@ const tab = ref('overview');
const file = ref<Misskey.entities.DriveFile | null>(null);
const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null);
const isSensitive = ref<boolean>(false);
const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue'));
const props = defineProps<{
fileId: string,
@ -131,6 +135,10 @@ const headerTabs = computed(() => [{
title: i18n.ts.overview,
icon: 'ti ti-info-circle',
}, iAmModerator ? {
key: 'notes',
title: i18n.ts._fileViewer.attachedNotes,
icon: 'ti ti-pencil',
} : null, iAmModerator ? {
key: 'ip',
title: 'IP',
icon: 'ti ti-password',

View file

@ -208,7 +208,7 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { url } from '@/config.js';
import { url } from '@@/js/config.js';
import { acct } from '@/filters/user.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';

View file

@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, onMounted, onUnmounted, ref, shallowRef, watch, nextTick } from 'vue';
import tinycolor from 'tinycolor2';
import { popupMenu } from '@/os.js';
import { scrollToTop } from '@/scripts/scroll.js';
import { scrollToTop } from '@@/js/scroll.js';
import MkButton from '@/components/MkButton.vue';
import { globalEvents } from '@/events.js';
import { injectReactiveMetadata } from '@/scripts/page-metadata.js';

View file

@ -65,18 +65,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTextarea v-model="ad.memo">
<template #label>{{ i18n.ts.memo }}</template>
</MkTextarea>
<div class="buttons">
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)">
<div class="_buttons">
<MkButton inline primary style="margin-right: 12px;" @click="save(ad)">
<i
class="ti ti-device-floppy"
></i> {{ i18n.ts.save }}
</MkButton>
<MkButton class="button" inline danger @click="remove(ad)">
<MkButton inline danger @click="remove(ad)">
<i class="ti ti-trash"></i> {{ i18n.ts.remove }}
</MkButton>
</div>
</div>
<MkButton class="button" @click="more()">
<MkButton @click="more()">
<i class="ti ti-reload"></i>{{ i18n.ts.more }}
</MkButton>
</div>

View file

@ -29,8 +29,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
</template>
<template #caption>{{ announcement.text }}</template>
<template #footer>
<div class="_buttons">
<MkButton rounded primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="announcement.id != null && announcement.isActive" rounded @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton>
<MkButton v-if="announcement.id != null && !announcement.isActive" rounded @click="unarchive(announcement)"><i class="ti ti-restore"></i> {{ i18n.ts.unarchive }}</MkButton>
<MkButton v-if="announcement.id != null" rounded danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</template>
<div class="_gaps_m">
<div class="_gaps">
<MkInput v-model="announcement.title">
<template #label>{{ i18n.ts.title }}</template>
</MkInput>
@ -64,16 +72,10 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._announcement.needConfirmationToRead }}
</MkSwitch>
<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
<div class="buttons _buttons">
<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="announcement.id != null && announcement.isActive" class="button" inline @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton>
<MkButton v-if="announcement.id != null && !announcement.isActive" class="button" inline @click="unarchive(announcement)"><i class="ti ti-restore"></i> {{ i18n.ts.unarchive }}</MkButton>
<MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
</MkFolder>
<MkLoading v-if="loadingMore"/>
<MkButton class="button" @click="more()">
<MkButton @click="more()">
<i class="ti ti-reload"></i>{{ i18n.ts.more }}
</MkButton>
</template>
@ -170,7 +172,7 @@ function more() {
loadingMore.value = true;
misskeyApi('admin/announcements/list', {
status: announcementsStatus.value,
untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id
untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id,
}).then(announcementResponse => {
announcements.value = announcements.value.concat(announcementResponse);
loadingMore.value = false;

View file

@ -4,145 +4,143 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<FormSuspense :p="init">
<div class="_gaps_m">
<MkRadios v-model="provider">
<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
<option value="hcaptcha">hCaptcha</option>
<option value="mcaptcha">mCaptcha</option>
<option value="recaptcha">reCAPTCHA</option>
<option value="turnstile">Turnstile</option>
</MkRadios>
<MkFolder>
<template #icon><i class="ti ti-shield"></i></template>
<template #label>{{ i18n.ts.botProtection }}</template>
<template v-if="botProtectionForm.savedState.provider === 'hcaptcha'" #suffix>hCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
<template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
<template v-if="botProtectionForm.modified.value" #footer>
<MkFormFooter :form="botProtectionForm"/>
</template>
<template v-if="provider === 'hcaptcha'">
<MkInput v-model="hcaptchaSiteKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="hcaptchaSecretKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
</MkInput>
<FormSlot>
<template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
</FormSlot>
</template>
<template v-else-if="provider === 'mcaptcha'">
<MkInput v-model="mcaptchaSiteKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="mcaptchaSecretKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
</MkInput>
<MkInput v-model="mcaptchaInstanceUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
</MkInput>
<FormSlot v-if="mcaptchaSiteKey && mcaptchaInstanceUrl">
<template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="mcaptcha" :sitekey="mcaptchaSiteKey" :instanceUrl="mcaptchaInstanceUrl"/>
</FormSlot>
</template>
<template v-else-if="provider === 'recaptcha'">
<MkInput v-model="recaptchaSiteKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="recaptchaSecretKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
</MkInput>
<FormSlot v-if="recaptchaSiteKey">
<template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
</FormSlot>
</template>
<template v-else-if="provider === 'turnstile'">
<MkInput v-model="turnstileSiteKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
</MkInput>
<MkInput v-model="turnstileSecretKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
</MkInput>
<FormSlot>
<template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/>
</FormSlot>
</template>
<div class="_gaps_m">
<MkRadios v-model="botProtectionForm.state.provider">
<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
<option value="hcaptcha">hCaptcha</option>
<option value="mcaptcha">mCaptcha</option>
<option value="recaptcha">reCAPTCHA</option>
<option value="turnstile">Turnstile</option>
</MkRadios>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</FormSuspense>
</div>
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
<MkInput v-model="botProtectionForm.state.hcaptchaSiteKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.hcaptchaSecretKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
</MkInput>
<FormSlot>
<template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="hcaptcha" :sitekey="botProtectionForm.state.hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
</FormSlot>
</template>
<template v-else-if="botProtectionForm.state.provider === 'mcaptcha'">
<MkInput v-model="botProtectionForm.state.mcaptchaSiteKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaSecretKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
<template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="mcaptcha" :sitekey="botProtectionForm.state.mcaptchaSiteKey" :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"/>
</FormSlot>
</template>
<template v-else-if="botProtectionForm.state.provider === 'recaptcha'">
<MkInput v-model="botProtectionForm.state.recaptchaSiteKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.recaptchaSecretKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
<template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="recaptcha" :sitekey="botProtectionForm.state.recaptchaSiteKey"/>
</FormSlot>
</template>
<template v-else-if="botProtectionForm.state.provider === 'turnstile'">
<MkInput v-model="botProtectionForm.state.turnstileSiteKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.turnstileSecretKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
</MkInput>
<FormSlot>
<template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="turnstile" :sitekey="botProtectionForm.state.turnstileSiteKey || '1x00000000000000000000AA'"/>
</FormSlot>
</template>
</div>
</MkFolder>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import type { CaptchaProvider } from '@/components/MkCaptcha.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { useForm } from '@/scripts/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue';
import MkFolder from '@/components/MkFolder.vue';
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
const provider = ref<CaptchaProvider | null>(null);
const hcaptchaSiteKey = ref<string | null>(null);
const hcaptchaSecretKey = ref<string | null>(null);
const mcaptchaSiteKey = ref<string | null>(null);
const mcaptchaSecretKey = ref<string | null>(null);
const mcaptchaInstanceUrl = ref<string | null>(null);
const recaptchaSiteKey = ref<string | null>(null);
const recaptchaSecretKey = ref<string | null>(null);
const turnstileSiteKey = ref<string | null>(null);
const turnstileSecretKey = ref<string | null>(null);
const meta = await misskeyApi('admin/meta');
async function init() {
const meta = await misskeyApi('admin/meta');
hcaptchaSiteKey.value = meta.hcaptchaSiteKey;
hcaptchaSecretKey.value = meta.hcaptchaSecretKey;
mcaptchaSiteKey.value = meta.mcaptchaSiteKey;
mcaptchaSecretKey.value = meta.mcaptchaSecretKey;
mcaptchaInstanceUrl.value = meta.mcaptchaInstanceUrl;
recaptchaSiteKey.value = meta.recaptchaSiteKey;
recaptchaSecretKey.value = meta.recaptchaSecretKey;
turnstileSiteKey.value = meta.turnstileSiteKey;
turnstileSecretKey.value = meta.turnstileSecretKey;
provider.value = meta.enableHcaptcha ? 'hcaptcha' :
meta.enableRecaptcha ? 'recaptcha' :
meta.enableTurnstile ? 'turnstile' :
meta.enableMcaptcha ? 'mcaptcha' : null;
}
function save() {
os.apiWithDialog('admin/update-meta', {
enableHcaptcha: provider.value === 'hcaptcha',
hcaptchaSiteKey: hcaptchaSiteKey.value,
hcaptchaSecretKey: hcaptchaSecretKey.value,
enableMcaptcha: provider.value === 'mcaptcha',
mcaptchaSiteKey: mcaptchaSiteKey.value,
mcaptchaSecretKey: mcaptchaSecretKey.value,
mcaptchaInstanceUrl: mcaptchaInstanceUrl.value,
enableRecaptcha: provider.value === 'recaptcha',
recaptchaSiteKey: recaptchaSiteKey.value,
recaptchaSecretKey: recaptchaSecretKey.value,
enableTurnstile: provider.value === 'turnstile',
turnstileSiteKey: turnstileSiteKey.value,
turnstileSecretKey: turnstileSecretKey.value,
}).then(() => {
fetchInstance(true);
const botProtectionForm = useForm({
provider: meta.enableHcaptcha
? 'hcaptcha'
: meta.enableRecaptcha
? 'recaptcha'
: meta.enableTurnstile
? 'turnstile'
: meta.enableMcaptcha
? 'mcaptcha'
: null,
hcaptchaSiteKey: meta.hcaptchaSiteKey,
hcaptchaSecretKey: meta.hcaptchaSecretKey,
mcaptchaSiteKey: meta.mcaptchaSiteKey,
mcaptchaSecretKey: meta.mcaptchaSecretKey,
mcaptchaInstanceUrl: meta.mcaptchaInstanceUrl,
recaptchaSiteKey: meta.recaptchaSiteKey,
recaptchaSecretKey: meta.recaptchaSecretKey,
turnstileSiteKey: meta.turnstileSiteKey,
turnstileSecretKey: meta.turnstileSecretKey,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
enableHcaptcha: state.provider === 'hcaptcha',
hcaptchaSiteKey: state.hcaptchaSiteKey,
hcaptchaSecretKey: state.hcaptchaSecretKey,
enableMcaptcha: state.provider === 'mcaptcha',
mcaptchaSiteKey: state.mcaptchaSiteKey,
mcaptchaSecretKey: state.mcaptchaSecretKey,
mcaptchaInstanceUrl: state.mcaptchaInstanceUrl,
enableRecaptcha: state.provider === 'recaptcha',
recaptchaSiteKey: state.recaptchaSiteKey,
recaptchaSecretKey: state.recaptchaSecretKey,
enableTurnstile: state.provider === 'turnstile',
turnstileSiteKey: state.turnstileSiteKey,
turnstileSecretKey: state.turnstileSecretKey,
});
}
fetchInstance(true);
});
</script>

View file

@ -134,7 +134,7 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
import MkColorInput from '@/components/MkColorInput.vue';
import { host } from '@/config.js';
import { host } from '@@/js/config.js';
const iconUrl = ref<string | null>(null);
const sidebarLogoUrl = ref<string | null>(null);

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<FormSection>
<MkFolder>
<template #label>DeepL Translation</template>
<div class="_gaps_m">
@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="deeplIsPro">
<template #label>Pro account</template>
</MkSwitch>
<MkSwitch v-model="deeplFreeMode">
<template #label>{{ i18n.ts.deeplFreeMode }}</template>
</MkSwitch>
@ -27,17 +28,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>DeepLX-JS URL</template>
<template #caption>{{ i18n.ts.deeplFreeModeDescription }}</template>
</MkInput>
<MkButton primary @click="save_deepl">Save</MkButton>
</div>
</FormSection>
</MkFolder>
</FormSuspense>
</MkSpacer>
<template #footer>
<div :class="$style.footer">
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</MkSpacer>
</div>
</template>
</MkStickyContainer>
</template>
@ -48,12 +44,12 @@ import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkFolder from '@/components/MkFolder.vue';
const deeplAuthKey = ref<string>('');
const deeplIsPro = ref<boolean>(false);
@ -68,7 +64,7 @@ async function init() {
deeplFreeInstance.value = meta.deeplFreeInstance;
}
function save() {
function save_deepl() {
os.apiWithDialog('admin/update-meta', {
deeplAuthKey: deeplAuthKey.value,
deeplIsPro: deeplIsPro.value,
@ -88,10 +84,3 @@ definePageMetadata(() => ({
icon: 'ph-arrow-square-out ph-bold ph-lg',
}));
</script>
<style lang="scss" module>
.footer {
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
</style>

View file

@ -214,16 +214,6 @@ const menuDef = computed(() => [{
text: i18n.ts.relays,
to: '/admin/relays',
active: currentPage.value?.route.name === 'relays',
}, {
icon: 'ti ti-ban',
text: i18n.ts.instanceBlocking,
to: '/admin/instance-block',
active: currentPage.value?.route.name === 'instance-block',
}, {
icon: 'ti ti-ghost',
text: i18n.ts.proxyAccount,
to: '/admin/proxy-account',
active: currentPage.value?.route.name === 'proxy-account',
}, {
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.externalServices,
@ -235,10 +225,10 @@ const menuDef = computed(() => [{
to: '/admin/system-webhook',
active: currentPage.value?.route.name === 'system-webhook',
}, {
icon: 'ti ti-adjustments',
text: i18n.ts.other,
to: '/admin/other-settings',
active: currentPage.value?.route.name === 'other-settings',
icon: 'ti ti-bolt',
text: i18n.ts.performance,
to: '/admin/performance',
active: currentPage.value?.route.name === 'performance',
}],
}, {
title: i18n.ts.info,

View file

@ -1,84 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<template v-if="tab === 'block'">
<MkTextarea v-model="blockedHosts">
<span>{{ i18n.ts.blockedInstances }}</span>
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</MkTextarea>
</template>
<template v-else-if="tab === 'silence'">
<MkTextarea v-model="silencedHosts" class="_formBlock">
<span>{{ i18n.ts.silencedInstances }}</span>
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
</MkTextarea>
<MkTextarea v-model="mediaSilencedHosts" class="_formBlock">
<span>{{ i18n.ts.mediaSilencedInstances }}</span>
<template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template>
</MkTextarea>
</template>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const blockedHosts = ref<string>('');
const silencedHosts = ref<string>('');
const mediaSilencedHosts = ref<string>('');
const tab = ref('block');
async function init() {
const meta = await misskeyApi('admin/meta');
blockedHosts.value = meta.blockedHosts.join('\n');
silencedHosts.value = meta.silencedHosts.join('\n');
mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
}
function save() {
os.apiWithDialog('admin/update-meta', {
blockedHosts: blockedHosts.value.split('\n') || [],
silencedHosts: silencedHosts.value.split('\n') || [],
mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [],
}).then(() => {
fetchInstance(true);
});
}
const headerActions = computed(() => []);
const headerTabs = computed(() => [{
key: 'block',
title: i18n.ts.block,
icon: 'ti ti-ban',
}, {
key: 'silence',
title: i18n.ts.silence,
icon: 'ti ti-eye-off',
}]);
definePageMetadata(() => ({
title: i18n.ts.instanceBlocking,
icon: 'ti ti-ban',
}));
</script>

View file

@ -10,76 +10,130 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<div class="_gaps_m">
<MkSwitch v-model="enableRegistration">
<MkSwitch v-model="enableRegistration" @change="onChange_enableRegistration">
<template #label>{{ i18n.ts.enableRegistration }}</template>
</MkSwitch>
<MkSwitch v-model="emailRequiredForSignup">
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
</MkSwitch>
<MkSwitch v-model="approvalRequiredForSignup">
<MkSwitch v-model="approvalRequiredForSignup" @change="onChange_approvalRequiredForSignup">
<template #label>{{ i18n.ts.approvalRequiredForSignup }}</template>
</MkSwitch>
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
<MkInput v-model="tosUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.tosUrl }}</template>
</MkInput>
<MkInput v-model="privacyPolicyUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
</MkInput>
<MkTextarea v-if="bubbleTimelineEnabled" v-model="bubbleTimeline">
<MkFolder v-if="bubbleTimelineEnabled">
<template #icon><i class="ph-drop ph-bold ph-lg"></i></template>
<template #label>Bubble timeline</template>
<template #caption>Choose which instances should be displayed in the bubble.</template>
</MkTextarea>
<MkInput v-model="inquiryUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.inquiryUrl }}</template>
<template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template>
</MkInput>
<div class="_gaps">
<MkTextarea v-model="bubbleTimeline">
<template #caption>Choose which instances should be displayed in the bubble.</template>
</MkTextarea>
<MkButton primary @click="save_bubbleTimeline">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkTextarea v-model="preservedUsernames">
<template #label>{{ i18n.ts.preservedUsernames }}</template>
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
</MkTextarea>
<MkTextarea v-model="trustedLinkUrlPatterns">
<MkFolder>
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.trustedLinkUrlPatterns }}</template>
<template #caption>{{ i18n.ts.trustedLinkUrlPatternsDescription }}</template>
</MkTextarea>
<MkTextarea v-model="sensitiveWords">
<div class="_gaps">
<MkTextarea v-model="trustedLinkUrlPatterns">
<template #caption>{{ i18n.ts.trustedLinkUrlPatternsDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_trustedLinkUrlPatterns">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-lock-star"></i></template>
<template #label>{{ i18n.ts.preservedUsernames }}</template>
<div class="_gaps">
<MkTextarea v-model="preservedUsernames">
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_preservedUsernames">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-message-exclamation"></i></template>
<template #label>{{ i18n.ts.sensitiveWords }}</template>
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
<MkTextarea v-model="prohibitedWords">
<div class="_gaps">
<MkTextarea v-model="sensitiveWords">
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-message-x"></i></template>
<template #label>{{ i18n.ts.prohibitedWords }}</template>
<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkTextarea v-model="hiddenTags">
<div class="_gaps">
<MkTextarea v-model="prohibitedWords">
<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.hiddenTags }}</template>
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
</MkTextarea>
<div class="_gaps">
<MkTextarea v-model="hiddenTags">
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_hiddenTags">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.silencedInstances }}</template>
<div class="_gaps">
<MkTextarea v-model="silencedHosts">
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_silencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.mediaSilencedInstances }}</template>
<div class="_gaps">
<MkTextarea v-model="mediaSilencedHosts">
<template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_mediaSilencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-ban"></i></template>
<template #label>{{ i18n.ts.blockedInstances }}</template>
<div class="_gaps">
<MkTextarea v-model="blockedHosts">
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_blockedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</div>
</FormSuspense>
</MkSpacer>
<template #footer>
<div :class="$style.footer">
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</MkSpacer>
</div>
</template>
</MkStickyContainer>
</div>
</template>
@ -98,6 +152,7 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue';
const enableRegistration = ref<boolean>(false);
const emailRequiredForSignup = ref<boolean>(false);
@ -108,10 +163,10 @@ const prohibitedWords = ref<string>('');
const hiddenTags = ref<string>('');
const preservedUsernames = ref<string>('');
const bubbleTimeline = ref<string>('');
const tosUrl = ref<string | null>(null);
const privacyPolicyUrl = ref<string | null>(null);
const inquiryUrl = ref<string | null>(null);
const trustedLinkUrlPatterns = ref<string>('');
const blockedHosts = ref<string>('');
const silencedHosts = ref<string>('');
const mediaSilencedHosts = ref<string>('');
async function init() {
const meta = await misskeyApi('admin/meta');
@ -122,33 +177,110 @@ async function init() {
prohibitedWords.value = meta.prohibitedWords.join('\n');
hiddenTags.value = meta.hiddenTags.join('\n');
preservedUsernames.value = meta.preservedUsernames.join('\n');
tosUrl.value = meta.tosUrl;
privacyPolicyUrl.value = meta.privacyPolicyUrl;
bubbleTimeline.value = meta.bubbleInstances.join('\n');
bubbleTimelineEnabled.value = meta.policies.btlAvailable;
inquiryUrl.value = meta.inquiryUrl;
trustedLinkUrlPatterns.value = meta.trustedLinkUrlPatterns.join('\n');
blockedHosts.value = meta.blockedHosts.join('\n');
silencedHosts.value = meta.silencedHosts.join('\n');
mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
}
function save() {
function onChange_enableRegistration(value: boolean) {
os.apiWithDialog('admin/update-meta', {
disableRegistration: !value,
}).then(() => {
fetchInstance(true);
});
}
function onChange_emailRequiredForSignup(value: boolean) {
os.apiWithDialog('admin/update-meta', {
emailRequiredForSignup: value,
}).then(() => {
fetchInstance(true);
});
}
function onChange_approvalRequiredForSignup(value: boolean) {
os.apiWithDialog('admin/update-meta', {
approvalRequiredForSignup: value,
}).then(() => {
fetchInstance(true);
});
}
function save_bubbleTimeline() {
os.apiWithDialog('admin/update-meta', {
disableRegistration: !enableRegistration.value,
emailRequiredForSignup: emailRequiredForSignup.value,
approvalRequiredForSignup: approvalRequiredForSignup.value,
tosUrl: tosUrl.value,
privacyPolicyUrl: privacyPolicyUrl.value,
inquiryUrl: inquiryUrl.value,
sensitiveWords: sensitiveWords.value.split('\n'),
prohibitedWords: prohibitedWords.value.split('\n'),
hiddenTags: hiddenTags.value.split('\n'),
preservedUsernames: preservedUsernames.value.split('\n'),
bubbleInstances: bubbleTimeline.value.split('\n'),
}).then(() => {
fetchInstance(true);
});
}
function save_trustedLinkUrlPatterns() {
os.apiWithDialog('admin/update-meta', {
trustedLinkUrlPatterns: trustedLinkUrlPatterns.value.split('\n'),
}).then(() => {
fetchInstance(true);
});
}
function save_preservedUsernames() {
os.apiWithDialog('admin/update-meta', {
preservedUsernames: preservedUsernames.value.split('\n'),
}).then(() => {
fetchInstance(true);
});
}
function save_sensitiveWords() {
os.apiWithDialog('admin/update-meta', {
sensitiveWords: sensitiveWords.value.split('\n'),
}).then(() => {
fetchInstance(true);
});
}
function save_prohibitedWords() {
os.apiWithDialog('admin/update-meta', {
prohibitedWords: prohibitedWords.value.split('\n'),
}).then(() => {
fetchInstance(true);
});
}
function save_hiddenTags() {
os.apiWithDialog('admin/update-meta', {
hiddenTags: hiddenTags.value.split('\n'),
}).then(() => {
fetchInstance(true);
});
}
function save_blockedHosts() {
os.apiWithDialog('admin/update-meta', {
blockedHosts: blockedHosts.value.split('\n') || [],
}).then(() => {
fetchInstance(true);
});
}
function save_silencedHosts() {
os.apiWithDialog('admin/update-meta', {
silencedHosts: silencedHosts.value.split('\n') || [],
}).then(() => {
fetchInstance(true);
});
}
function save_mediaSilencedHosts() {
os.apiWithDialog('admin/update-meta', {
mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [],
}).then(() => {
fetchInstance(true);
});
}
const headerTabs = computed(() => []);
definePageMetadata(() => ({
@ -156,10 +288,3 @@ definePageMetadata(() => ({
icon: 'ti ti-shield',
}));
</script>
<style lang="scss" module>
.footer {
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
</style>

View file

@ -1,113 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<div class="_gaps">
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableServerMachineStats">
<template #label>{{ i18n.ts.enableServerMachineStats }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableAchievements">
<template #label>{{ i18n.ts.enableAchievements }}</template>
<template #caption>{{ i18n.ts.turnOffAchievements}}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableBotTrending">
<template #label>{{ i18n.ts.enableBotTrending }}</template>
<template #caption>{{ i18n.ts.turnOffBotTrending }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableIdenticonGeneration">
<template #label>{{ i18n.ts.enableIdenticonGeneration }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForRemoteUser">
<template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForFederatedInstances">
<template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</div>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkSwitch from '@/components/MkSwitch.vue';
const enableServerMachineStats = ref<boolean>(false);
const enableAchievements = ref<boolean>(false);
const enableBotTrending = ref<boolean>(false);
const enableIdenticonGeneration = ref<boolean>(false);
const enableChartsForRemoteUser = ref<boolean>(false);
const enableChartsForFederatedInstances = ref<boolean>(false);
async function init() {
const meta = await misskeyApi('admin/meta');
enableServerMachineStats.value = meta.enableServerMachineStats;
enableAchievements.value = meta.enableAchievements;
enableBotTrending.value = meta.enableBotTrending;
enableIdenticonGeneration.value = meta.enableIdenticonGeneration;
enableChartsForRemoteUser.value = meta.enableChartsForRemoteUser;
enableChartsForFederatedInstances.value = meta.enableChartsForFederatedInstances;
}
function save() {
os.apiWithDialog('admin/update-meta', {
enableServerMachineStats: enableServerMachineStats.value,
enableAchievements: enableAchievements.value,
enableBotTrending: enableBotTrending.value,
enableIdenticonGeneration: enableIdenticonGeneration.value,
enableChartsForRemoteUser: enableChartsForRemoteUser.value,
enableChartsForFederatedInstances: enableChartsForFederatedInstances.value,
}).then(() => {
fetchInstance(true);
});
}
const headerActions = computed(() => [{
asFullButton: true,
icon: 'ti ti-check',
text: i18n.ts.save,
handler: save,
}]);
const headerTabs = computed(() => []);
definePageMetadata(() => ({
title: i18n.ts.other,
icon: 'ti ti-adjustments',
}));
</script>

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { StoryObj } from '@storybook/vue3';
import { http, HttpResponse } from 'msw';
import { action } from '@storybook/addon-actions';
import { commonHandlers } from '../../../.storybook/mocks.js';
import overview_ap_requests from './overview.ap-requests.vue';
export const Default = {
render(args) {
return {
components: {
overview_ap_requests,
},
setup() {
return {
args,
};
},
template: '<overview_ap_requests />',
};
},
parameters: {
layout: 'fullscreen',
msw: {
handlers: [
...commonHandlers,
http.post('/api/charts/ap-request', async ({ request }) => {
action('POST /api/charts/ap-request')(await request.json());
return HttpResponse.json({
deliverFailed: [0, 0, 0, 2, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1, 0, 0, 0, 3, 1, 1, 2, 0, 0],
deliverSucceeded: [0, 1, 51, 34, 136, 189, 51, 17, 17, 34, 1, 17, 18, 51, 34, 68, 287, 0, 17, 33, 32, 96, 96, 0, 49, 64, 0, 32, 0, 32, 81, 48, 65, 1, 16, 50, 90, 148, 33, 43, 72, 127, 17, 138, 78, 91, 78, 91, 13, 52],
inboxReceived: [507, 1173, 1096, 871, 958, 937, 908, 1026, 956, 909, 807, 1002, 832, 995, 1039, 1047, 1109, 930, 711, 835, 764, 679, 835, 958, 634, 654, 691, 895, 811, 676, 1044, 1389, 1318, 863, 887, 952, 1011, 1061, 592, 900, 611, 595, 604, 562, 607, 621, 854, 666, 1197, 644],
});
}),
],
},
},
} satisfies StoryObj<typeof overview_ap_requests>;

View file

@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient';
import isChromatic from 'chromatic';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
@ -41,7 +42,7 @@ const { handler: externalTooltipHandler } = useChartTooltip();
const { handler: externalTooltipHandler2 } = useChartTooltip();
onMounted(async () => {
const now = new Date();
const now = isChromatic() ? new Date('2024-08-31T10:00:00Z') : new Date();
const getDate = (ago: number) => {
const y = now.getFullYear();
@ -51,14 +52,14 @@ onMounted(async () => {
return new Date(y, m, d - ago);
};
const format = (arr) => {
const format = (arr: number[]) => {
return arr.map((v, i) => ({
x: getDate(i).getTime(),
y: v,
}));
};
const formatMinus = (arr) => {
const formatMinus = (arr: number[]) => {
return arr.map((v, i) => ({
x: getDate(i).getTime(),
y: -v,
@ -78,7 +79,6 @@ onMounted(async () => {
type: 'line',
data: {
datasets: [{
stack: 'a',
parsing: false,
label: 'Out: Succ',
data: format(raw.deliverSucceeded).slice().reverse(),
@ -92,7 +92,6 @@ onMounted(async () => {
fill: true,
clip: 8,
}, {
stack: 'a',
parsing: false,
label: 'Out: Fail',
data: formatMinus(raw.deliverFailed).slice().reverse(),
@ -137,7 +136,6 @@ onMounted(async () => {
min: getDate(chartLimit).getTime(),
},
y: {
stacked: true,
position: 'left',
suggestedMax: 10,
grid: {
@ -171,6 +169,9 @@ onMounted(async () => {
duration: 0,
},
external: externalTooltipHandler,
callbacks: {
label: context => `${context.dataset.label}: ${Math.abs(context.parsed.y)}`,
},
},
gradient,
},

View file

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import * as Misskey from 'misskey-js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
import { defaultStore } from '@/store.js';

View file

@ -36,7 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import XChart from './overview.queue.chart.vue';
import type { ApQueueDomain } from '@/pages/admin/queue.vue';
import number from '@/filters/number.js';
import { useStream } from '@/stream.js';
@ -52,10 +54,10 @@ const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
const props = defineProps<{
domain: string;
domain: ApQueueDomain;
}>();
const onStats = (stats) => {
function onStats(stats: Misskey.entities.QueueStats) {
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
active.value = stats[props.domain].active;
delayed.value = stats[props.domain].delayed;
@ -65,13 +67,13 @@ const onStats = (stats) => {
chartActive.value.pushData(stats[props.domain].active);
chartDelayed.value.pushData(stats[props.domain].delayed);
chartWaiting.value.pushData(stats[props.domain].waiting);
};
}
const onStatsLog = (statsLog) => {
const dataProcess = [];
const dataActive = [];
const dataDelayed = [];
const dataWaiting = [];
function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
const dataProcess: Misskey.entities.QueueStats[ApQueueDomain]['activeSincePrevTick'][] = [];
const dataActive: Misskey.entities.QueueStats[ApQueueDomain]['active'][] = [];
const dataDelayed: Misskey.entities.QueueStats[ApQueueDomain]['delayed'][] = [];
const dataWaiting: Misskey.entities.QueueStats[ApQueueDomain]['waiting'][] = [];
for (const stats of [...statsLog].reverse()) {
dataProcess.push(stats[props.domain].activeSincePrevTick);
@ -84,7 +86,7 @@ const onStatsLog = (statsLog) => {
chartActive.value.setData(dataActive);
chartDelayed.value.setData(dataDelayed);
chartWaiting.value.setData(dataWaiting);
};
}
onMounted(() => {
connection.on('stats', onStats);

View file

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import * as Misskey from 'misskey-js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { defaultStore } from '@/store.js';
@ -47,14 +47,14 @@ useInterval(fetch, 1000 * 60, {
.root {
&:global {
> .users {
.chart-move {
transition: transform 1s ease;
}
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-gap: 12px;
.chart-move {
transition: transform 1s ease;
}
> .user:hover {
text-decoration: none;
}

View file

@ -0,0 +1,193 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<div class="_gaps">
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableServerMachineStats" @change="onChange_enableServerMachineStats">
<template #label>{{ i18n.ts.enableServerMachineStats }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableIdenticonGeneration" @change="onChange_enableIdenticonGeneration">
<template #label>{{ i18n.ts.enableIdenticonGeneration }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForRemoteUser" @change="onChange_enableChartsForRemoteUser">
<template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances">
<template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-bolt"></i></template>
<template #label>Misskey® Fan-out Timeline Technology (FTT)</template>
<template v-if="fttForm.savedState.enableFanoutTimeline" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="fttForm.modified.value" #footer>
<MkFormFooter :form="fttForm"/>
</template>
<div class="_gaps">
<MkSwitch v-model="fttForm.state.enableFanoutTimeline">
<template #label>{{ i18n.ts.enable }}<span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</div>
<div><MkLink target="_blank" url="https://misskey-hub.net/docs/for-admin/features/ftt/">{{ i18n.ts.details }}</MkLink></div>
</template>
</MkSwitch>
<template v-if="fttForm.state.enableFanoutTimeline">
<MkSwitch v-model="fttForm.state.enableFanoutTimelineDbFallback">
<template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}<span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template>
</MkSwitch>
<MkInput v-model="fttForm.state.perLocalUserUserTimelineCacheMax" type="number">
<template #label>perLocalUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<MkInput v-model="fttForm.state.perRemoteUserUserTimelineCacheMax" type="number">
<template #label>perRemoteUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<MkInput v-model="fttForm.state.perUserHomeTimelineCacheMax" type="number">
<template #label>perUserHomeTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<MkInput v-model="fttForm.state.perUserListTimelineCacheMax" type="number">
<template #label>perUserListTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</template>
</div>
</MkFolder>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-bolt"></i></template>
<template #label>Misskey® Reactions Boost Technology (RBT)<span class="_beta">{{ i18n.ts.beta }}</span></template>
<template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="rbtForm.modified.value" #footer>
<MkFormFooter :form="rbtForm"/>
</template>
<div class="_gaps_m">
<MkSwitch v-model="rbtForm.state.enableReactionsBuffering">
<template #label>{{ i18n.ts.enable }}<span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</template>
</MkSwitch>
</div>
</MkFolder>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkInput from '@/components/MkInput.vue';
import MkLink from '@/components/MkLink.vue';
import { useForm } from '@/scripts/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue';
const meta = await misskeyApi('admin/meta');
const enableServerMachineStats = ref(meta.enableServerMachineStats);
const enableIdenticonGeneration = ref(meta.enableIdenticonGeneration);
const enableChartsForRemoteUser = ref(meta.enableChartsForRemoteUser);
const enableChartsForFederatedInstances = ref(meta.enableChartsForFederatedInstances);
function onChange_enableServerMachineStats(value: boolean) {
os.apiWithDialog('admin/update-meta', {
enableServerMachineStats: value,
}).then(() => {
fetchInstance(true);
});
}
function onChange_enableIdenticonGeneration(value: boolean) {
os.apiWithDialog('admin/update-meta', {
enableIdenticonGeneration: value,
}).then(() => {
fetchInstance(true);
});
}
function onChange_enableChartsForRemoteUser(value: boolean) {
os.apiWithDialog('admin/update-meta', {
enableChartsForRemoteUser: value,
}).then(() => {
fetchInstance(true);
});
}
function onChange_enableChartsForFederatedInstances(value: boolean) {
os.apiWithDialog('admin/update-meta', {
enableChartsForFederatedInstances: value,
}).then(() => {
fetchInstance(true);
});
}
const fttForm = useForm({
enableFanoutTimeline: meta.enableFanoutTimeline,
enableFanoutTimelineDbFallback: meta.enableFanoutTimelineDbFallback,
perLocalUserUserTimelineCacheMax: meta.perLocalUserUserTimelineCacheMax,
perRemoteUserUserTimelineCacheMax: meta.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: meta.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: meta.perUserListTimelineCacheMax,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
enableFanoutTimeline: state.enableFanoutTimeline,
enableFanoutTimelineDbFallback: state.enableFanoutTimelineDbFallback,
perLocalUserUserTimelineCacheMax: state.perLocalUserUserTimelineCacheMax,
perRemoteUserUserTimelineCacheMax: state.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: state.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: state.perUserListTimelineCacheMax,
});
fetchInstance(true);
});
const rbtForm = useForm({
enableReactionsBuffering: meta.enableReactionsBuffering,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
enableReactionsBuffering: state.enableReactionsBuffering,
});
fetchInstance(true);
});
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata(() => ({
title: i18n.ts.other,
icon: 'ti ti-adjustments',
}));
</script>

View file

@ -1,71 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<MkKeyValue>
<template #key>{{ i18n.ts.proxyAccount }}</template>
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
</MkKeyValue>
<MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const proxyAccount = ref<Misskey.entities.UserDetailed | null>(null);
const proxyAccountId = ref<string | null>(null);
async function init() {
const meta = await misskeyApi('admin/meta');
proxyAccountId.value = meta.proxyAccountId;
if (proxyAccountId.value) {
proxyAccount.value = await misskeyApi('users/show', { userId: proxyAccountId.value });
}
}
function chooseProxyAccount() {
os.selectUser({ localOnly: true }).then(user => {
proxyAccount.value = user;
proxyAccountId.value = user.id;
save();
});
}
function save() {
os.apiWithDialog('admin/update-meta', {
proxyAccountId: proxyAccountId.value,
}).then(() => {
fetchInstance(true);
});
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata(() => ({
title: i18n.ts.proxyAccount,
icon: 'ti ti-ghost',
}));
</script>

View file

@ -49,7 +49,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import XChart from './queue.chart.chart.vue';
import type { ApQueueDomain } from '@/pages/admin/queue.vue';
import number from '@/filters/number.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
@ -62,17 +64,17 @@ const activeSincePrevTick = ref(0);
const active = ref(0);
const delayed = ref(0);
const waiting = ref(0);
const jobs = ref<(string | number)[][]>([]);
const jobs = ref<Misskey.Endpoints[`admin/queue/${ApQueueDomain}-delayed`]['res']>([]);
const chartProcess = shallowRef<InstanceType<typeof XChart>>();
const chartActive = shallowRef<InstanceType<typeof XChart>>();
const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
const props = defineProps<{
domain: string;
domain: ApQueueDomain;
}>();
const onStats = (stats) => {
function onStats(stats: Misskey.entities.QueueStats) {
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
active.value = stats[props.domain].active;
delayed.value = stats[props.domain].delayed;
@ -82,13 +84,13 @@ const onStats = (stats) => {
chartActive.value.pushData(stats[props.domain].active);
chartDelayed.value.pushData(stats[props.domain].delayed);
chartWaiting.value.pushData(stats[props.domain].waiting);
};
}
const onStatsLog = (statsLog) => {
const dataProcess = [];
const dataActive = [];
const dataDelayed = [];
const dataWaiting = [];
function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
const dataProcess: Misskey.entities.QueueStats[ApQueueDomain]['activeSincePrevTick'][] = [];
const dataActive: Misskey.entities.QueueStats[ApQueueDomain]['active'][] = [];
const dataDelayed: Misskey.entities.QueueStats[ApQueueDomain]['delayed'][] = [];
const dataWaiting: Misskey.entities.QueueStats[ApQueueDomain]['waiting'][] = [];
for (const stats of [...statsLog].reverse()) {
dataProcess.push(stats[props.domain].activeSincePrevTick);
@ -101,14 +103,12 @@ const onStatsLog = (statsLog) => {
chartActive.value.setData(dataActive);
chartDelayed.value.setData(dataDelayed);
chartWaiting.value.setData(dataWaiting);
};
}
onMounted(() => {
if (props.domain === 'inbox' || props.domain === 'deliver') {
misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => {
jobs.value = result;
});
}
misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => {
jobs.value = result;
});
connection.on('stats', onStats);
connection.on('statsLog', onStatsLog);

View file

@ -16,16 +16,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, type Ref } from 'vue';
import XQueue from './queue.chart.vue';
import XHeader from './_header_.vue';
import * as os from '@/os.js';
import * as config from '@/config.js';
import * as config from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
const tab = ref('deliver');
export type ApQueueDomain = 'deliver' | 'inbox';
const tab: Ref<ApQueueDomain> = ref('deliver');
function clear() {
os.confirm({

View file

@ -630,6 +630,106 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportAntennas, 'canImportAntennas'])">
<template #label>{{ i18n.ts._role._options.canImportAntennas }}</template>
<template #suffix>
<span v-if="role.policies.canImportAntennas.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canImportAntennas.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportAntennas)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canImportAntennas.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canImportAntennas.value" :disabled="role.policies.canImportAntennas.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canImportAntennas.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>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportBlocking, 'canImportBlocking'])">
<template #label>{{ i18n.ts._role._options.canImportBlocking }}</template>
<template #suffix>
<span v-if="role.policies.canImportBlocking.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canImportBlocking.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportBlocking)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canImportBlocking.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canImportBlocking.value" :disabled="role.policies.canImportBlocking.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canImportBlocking.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>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportFollowing, 'canImportFollowing'])">
<template #label>{{ i18n.ts._role._options.canImportFollowing }}</template>
<template #suffix>
<span v-if="role.policies.canImportFollowing.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canImportFollowing.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportFollowing)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canImportFollowing.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canImportFollowing.value" :disabled="role.policies.canImportFollowing.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canImportFollowing.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>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportMuting, 'canImportMuting'])">
<template #label>{{ i18n.ts._role._options.canImportMuting }}</template>
<template #suffix>
<span v-if="role.policies.canImportMuting.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canImportMuting.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportMuting)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canImportMuting.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canImportMuting.value" :disabled="role.policies.canImportMuting.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canImportMuting.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>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportUserLists, 'canImportUserLists'])">
<template #label>{{ i18n.ts._role._options.canImportUserLists }}</template>
<template #suffix>
<span v-if="role.policies.canImportUserLists.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canImportUserLists.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportUserLists)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canImportUserLists.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canImportUserLists.value" :disabled="role.policies.canImportUserLists.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canImportUserLists.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>
@ -648,7 +748,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import FormSlot from '@/components/form/slot.vue';
import { i18n } from '@/i18n.js';
import { ROLE_POLICIES } from '@/const.js';
import { ROLE_POLICIES } from '@@/js/const.js';
import { instance } from '@/instance.js';
import { deepClone } from '@/scripts/clone.js';

View file

@ -11,6 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<MkFolder>
<template #label>{{ i18n.ts._role.baseRole }}</template>
<template #footer>
<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
</template>
<div class="_gaps_s">
<MkInput v-model="baseRoleQ" type="search">
<template #prefix><i class="ti ti-search"></i></template>
@ -233,7 +236,45 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
</MkFolder>
<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportAntennas, 'canImportAntennas'])">
<template #label>{{ i18n.ts._role._options.canImportAntennas }}</template>
<template #suffix>{{ policies.canImportAntennas ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canImportAntennas">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportBlocking, 'canImportBlocking'])">
<template #label>{{ i18n.ts._role._options.canImportBlocking }}</template>
<template #suffix>{{ policies.canImportBlocking ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canImportBlocking">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportFollowing, 'canImportFollowing'])">
<template #label>{{ i18n.ts._role._options.canImportFollowing }}</template>
<template #suffix>{{ policies.canImportFollowing ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canImportFollowing">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportMuting, 'canImportMuting'])">
<template #label>{{ i18n.ts._role._options.canImportMuting }}</template>
<template #suffix>{{ policies.canImportMuting ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canImportMuting">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportUserLists, 'canImportUserList'])">
<template #label>{{ i18n.ts._role._options.canImportUserLists }}</template>
<template #suffix>{{ policies.canImportUserLists ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canImportUserLists">
<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>
@ -259,6 +300,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue';
import { ROLE_POLICIES } from '@@/js/const.js';
import XHeader from './_header_.vue';
import MkInput from '@/components/MkInput.vue';
import MkFolder from '@/components/MkFolder.vue';
@ -273,7 +315,6 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { instance, fetchInstance } from '@/instance.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { ROLE_POLICIES } from '@/const.js';
import { useRouter } from '@/router/supplier.js';
const router = useRouter();

View file

@ -7,76 +7,71 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<div class="_gaps_m">
<MkFolder>
<template #icon><i class="ti ti-shield"></i></template>
<template #label>{{ i18n.ts.botProtection }}</template>
<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
<template v-else-if="enableMcaptcha" #suffix>mCaptcha</template>
<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
<template v-else-if="enableTurnstile" #suffix>Turnstile</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
<div class="_gaps_m">
<XBotProtection/>
<XBotProtection/>
</MkFolder>
<MkFolder>
<template #label>Active Email Validation</template>
<template v-if="emailValidationForm.savedState.enableActiveEmailValidation" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="emailValidationForm.modified.value" #footer>
<MkFormFooter :form="emailValidationForm"/>
</template>
<MkFolder>
<template #label>Active Email Validation</template>
<template v-if="enableActiveEmailValidation" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<div class="_gaps_m">
<span>{{ i18n.ts.activeEmailValidationDescription }}</span>
<MkSwitch v-model="emailValidationForm.state.enableActiveEmailValidation">
<template #label>Enable</template>
</MkSwitch>
<MkSwitch v-model="emailValidationForm.state.enableVerifymailApi">
<template #label>Use Verifymail.io API</template>
</MkSwitch>
<MkInput v-model="emailValidationForm.state.verifymailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Verifymail.io API Auth Key</template>
</MkInput>
<MkSwitch v-model="emailValidationForm.state.enableTruemailApi">
<template #label>Use TrueMail API</template>
</MkSwitch>
<MkInput v-model="emailValidationForm.state.truemailInstance">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>TrueMail API Instance</template>
</MkInput>
<MkInput v-model="emailValidationForm.state.truemailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>TrueMail API Auth Key</template>
</MkInput>
</div>
</MkFolder>
<div class="_gaps_m">
<span>{{ i18n.ts.activeEmailValidationDescription }}</span>
<MkSwitch v-model="enableActiveEmailValidation">
<template #label>Enable</template>
</MkSwitch>
<MkSwitch v-model="enableVerifymailApi">
<template #label>Use Verifymail.io API</template>
</MkSwitch>
<MkInput v-model="verifymailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Verifymail.io API Auth Key</template>
</MkInput>
<MkSwitch v-model="enableTruemailApi">
<template #label>Use TrueMail API</template>
</MkSwitch>
<MkInput v-model="truemailInstance">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>TrueMail API Instance</template>
</MkInput>
<MkInput v-model="truemailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>TrueMail API Auth Key</template>
</MkInput>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #label>Banned Email Domains</template>
<template v-if="bannedEmailDomainsForm.modified.value" #footer>
<MkFormFooter :form="bannedEmailDomainsForm"/>
</template>
<MkFolder>
<template #label>Banned Email Domains</template>
<div class="_gaps_m">
<MkTextarea v-model="bannedEmailDomainsForm.state.bannedEmailDomains">
<template #label>Banned Email Domains List</template>
</MkTextarea>
</div>
</MkFolder>
<div class="_gaps_m">
<MkTextarea v-model="bannedEmailDomains">
<template #label>Banned Email Domains List</template>
</MkTextarea>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #label>Log IP address</template>
<template v-if="ipLoggingForm.savedState.enableIpLogging" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="ipLoggingForm.modified.value" #footer>
<MkFormFooter :form="ipLoggingForm"/>
</template>
<MkFolder>
<template #label>Log IP address</template>
<template v-if="enableIpLogging" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<div class="_gaps_m">
<MkSwitch v-model="enableIpLogging" @update:modelValue="save">
<template #label>Enable</template>
</MkSwitch>
</div>
</MkFolder>
</div>
</FormSuspense>
<div class="_gaps_m">
<MkSwitch v-model="ipLoggingForm.state.enableIpLogging">
<template #label>Enable</template>
</MkSwitch>
</div>
</MkFolder>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
@ -88,60 +83,55 @@ import XHeader from './_header_.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkRange from '@/components/MkRange.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { useForm } from '@/scripts/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue';
const enableHcaptcha = ref<boolean>(false);
const enableMcaptcha = ref<boolean>(false);
const enableRecaptcha = ref<boolean>(false);
const enableTurnstile = ref<boolean>(false);
const enableIpLogging = ref<boolean>(false);
const enableActiveEmailValidation = ref<boolean>(false);
const enableVerifymailApi = ref<boolean>(false);
const verifymailAuthKey = ref<string | null>(null);
const enableTruemailApi = ref<boolean>(false);
const truemailInstance = ref<string | null>(null);
const truemailAuthKey = ref<string | null>(null);
const bannedEmailDomains = ref<string>('');
const meta = await misskeyApi('admin/meta');
async function init() {
const meta = await misskeyApi('admin/meta');
enableHcaptcha.value = meta.enableHcaptcha;
enableMcaptcha.value = meta.enableMcaptcha;
enableRecaptcha.value = meta.enableRecaptcha;
enableTurnstile.value = meta.enableTurnstile;
enableIpLogging.value = meta.enableIpLogging;
enableActiveEmailValidation.value = meta.enableActiveEmailValidation;
enableVerifymailApi.value = meta.enableVerifymailApi;
verifymailAuthKey.value = meta.verifymailAuthKey;
enableTruemailApi.value = meta.enableTruemailApi;
truemailInstance.value = meta.truemailInstance;
truemailAuthKey.value = meta.truemailAuthKey;
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || '';
}
function save() {
os.apiWithDialog('admin/update-meta', {
enableIpLogging: enableIpLogging.value,
enableActiveEmailValidation: enableActiveEmailValidation.value,
enableVerifymailApi: enableVerifymailApi.value,
verifymailAuthKey: verifymailAuthKey.value,
enableTruemailApi: enableTruemailApi.value,
truemailInstance: truemailInstance.value,
truemailAuthKey: truemailAuthKey.value,
bannedEmailDomains: bannedEmailDomains.value.split('\n'),
}).then(() => {
fetchInstance(true);
const ipLoggingForm = useForm({
enableIpLogging: meta.enableIpLogging,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
enableIpLogging: state.enableIpLogging,
});
}
fetchInstance(true);
});
const emailValidationForm = useForm({
enableActiveEmailValidation: meta.enableActiveEmailValidation,
enableVerifymailApi: meta.enableVerifymailApi,
verifymailAuthKey: meta.verifymailAuthKey,
enableTruemailApi: meta.enableTruemailApi,
truemailInstance: meta.truemailInstance,
truemailAuthKey: meta.truemailAuthKey,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
enableActiveEmailValidation: state.enableActiveEmailValidation,
enableVerifymailApi: state.enableVerifymailApi,
verifymailAuthKey: state.verifymailAuthKey,
enableTruemailApi: state.enableTruemailApi,
truemailInstance: state.truemailInstance,
truemailAuthKey: state.truemailAuthKey,
});
fetchInstance(true);
});
const bannedEmailDomainsForm = useForm({
bannedEmailDomains: meta.bannedEmailDomains?.join('\n') || '',
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
bannedEmailDomains: state.bannedEmailDomains.split('\n'),
});
fetchInstance(true);
});
const headerActions = computed(() => []);

View file

@ -8,178 +8,215 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><XHeader :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<div class="_gaps_m">
<MkInput v-model="name">
<template #label>{{ i18n.ts.instanceName }}</template>
</MkInput>
<div class="_gaps_m">
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-info-circle"></i></template>
<template #label>{{ i18n.ts.info }}</template>
<template v-if="infoForm.modified.value" #footer>
<MkFormFooter :form="infoForm"/>
</template>
<MkInput v-model="shortName">
<template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})</template>
<template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template>
</MkInput>
<MkTextarea v-model="description">
<template #label>{{ i18n.ts.instanceDescription }}</template>
</MkTextarea>
<FormSplit :minWidth="300">
<MkInput v-model="maintainerName">
<template #label>{{ i18n.ts.maintainerName }}</template>
<div class="_gaps">
<MkInput v-model="infoForm.state.name">
<template #label>{{ i18n.ts.instanceName }}<span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<MkInput v-model="maintainerEmail" type="email">
<template #prefix><i class="ti ti-mail"></i></template>
<template #label>{{ i18n.ts.maintainerEmail }}</template>
<MkInput v-model="infoForm.state.shortName">
<template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template>
</MkInput>
</FormSplit>
<MkInput v-model="repositoryUrl" type="url">
<template #label>{{ i18n.ts.repositoryUrl }}</template>
<template #prefix><i class="ti ti-link"></i></template>
<template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
</MkInput>
<MkTextarea v-model="infoForm.state.description">
<template #label>{{ i18n.ts.instanceDescription }}<span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkTextarea>
<MkInfo v-if="!instance.providesTarball && !repositoryUrl" warn>
{{ i18n.ts.repositoryUrlOrTarballRequired }}
</MkInfo>
<FormSplit :minWidth="300">
<MkInput v-model="infoForm.state.maintainerName">
<template #label>{{ i18n.ts.maintainerName }}<span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<MkInput v-model="impressumUrl" type="url">
<template #label>{{ i18n.ts.impressumUrl }}</template>
<template #prefix><i class="ti ti-link"></i></template>
<template #caption>{{ i18n.ts.impressumDescription }}</template>
</MkInput>
<MkInput v-model="infoForm.state.maintainerEmail" type="email">
<template #label>{{ i18n.ts.maintainerEmail }}<span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-mail"></i></template>
</MkInput>
</FormSplit>
<MkInput v-model="donationUrl" type="url">
<template #label>{{ i18n.ts.donationUrl }}</template>
<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
</MkInput>
<MkInput v-model="infoForm.state.tosUrl" type="url">
<template #label>{{ i18n.ts.tosUrl }}<span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<MkTextarea v-model="pinnedUsers">
<template #label>{{ i18n.ts.pinnedUsers }}</template>
<MkInput v-model="infoForm.state.privacyPolicyUrl" type="url">
<template #label>{{ i18n.ts.privacyPolicyUrl }}<span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<MkInput v-model="infoForm.state.inquiryUrl" type="url">
<template #label>{{ i18n.ts._serverSettings.inquiryUrl }}<span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<MkInput v-model="infoForm.state.repositoryUrl" type="url">
<template #label>{{ i18n.ts.repositoryUrl }}<span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<MkInfo v-if="!instance.providesTarball && !infoForm.state.repositoryUrl" warn>
{{ i18n.ts.repositoryUrlOrTarballRequired }}
</MkInfo>
<MkInput v-model="infoForm.state.impressumUrl" type="url">
<template #label>{{ i18n.ts.impressumUrl }}<span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.impressumDescription }}</template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<MkInput v-model="infoForm.state.donationUrl" type="url">
<template #label>{{ i18n.ts.donationUrl }}</template>
<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
</MkInput>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-user-star"></i></template>
<template #label>{{ i18n.ts.pinnedUsers }}</template>
<template v-if="pinnedUsersForm.modified.value" #footer>
<MkFormFooter :form="pinnedUsersForm"/>
</template>
<MkTextarea v-model="pinnedUsersForm.state.pinnedUsers">
<template #label>{{ i18n.ts.pinnedUsers }}<span v-if="pinnedUsersForm.modifiedStates.pinnedUsers" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
</MkTextarea>
</MkFolder>
<FormSection>
<template #label>{{ i18n.ts.files }}</template>
<MkFolder>
<template #icon><i class="ti ti-cloud"></i></template>
<template #label>{{ i18n.ts.files }}</template>
<template v-if="filesForm.modified.value" #footer>
<MkFormFooter :form="filesForm"/>
</template>
<div class="_gaps_m">
<MkSwitch v-model="cacheRemoteFiles">
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
<div class="_gaps">
<MkSwitch v-model="filesForm.state.cacheRemoteFiles">
<template #label>{{ i18n.ts.cacheRemoteFiles }}<span v-if="filesForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
</MkSwitch>
<template v-if="filesForm.state.cacheRemoteFiles">
<MkSwitch v-model="filesForm.state.cacheRemoteSensitiveFiles">
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}<span v-if="filesForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
</MkSwitch>
</template>
</div>
</MkFolder>
<template v-if="cacheRemoteFiles">
<MkSwitch v-model="cacheRemoteSensitiveFiles">
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}</template>
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
</MkSwitch>
</template>
</div>
</FormSection>
<MkFolder>
<template #icon><i class="ti ti-world-cog"></i></template>
<template #label>ServiceWorker</template>
<template v-if="serviceWorkerForm.modified.value" #footer>
<MkFormFooter :form="serviceWorkerForm"/>
</template>
<FormSection>
<template #label>ServiceWorker</template>
<div class="_gaps">
<MkSwitch v-model="serviceWorkerForm.state.enableServiceWorker">
<template #label>{{ i18n.ts.enableServiceworker }}<span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
</MkSwitch>
<div class="_gaps_m">
<MkSwitch v-model="enableServiceWorker">
<template #label>{{ i18n.ts.enableServiceworker }}</template>
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
</MkSwitch>
<template v-if="enableServiceWorker">
<MkInput v-model="swPublicKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Public key</template>
</MkInput>
<MkInput v-model="swPrivateKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Private key</template>
</MkInput>
</template>
</div>
</FormSection>
<FormSection>
<template #label>Misskey® Fan-out Timeline Technology (FTT)</template>
<div class="_gaps_m">
<MkSwitch v-model="enableFanoutTimeline">
<template #label>{{ i18n.ts.enable }}</template>
<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</template>
</MkSwitch>
<MkSwitch v-model="enableFanoutTimelineDbFallback">
<template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}</template>
<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template>
</MkSwitch>
<MkInput v-model="perLocalUserUserTimelineCacheMax" type="number">
<template #label>perLocalUserUserTimelineCacheMax</template>
<template v-if="serviceWorkerForm.state.enableServiceWorker">
<MkInput v-model="serviceWorkerForm.state.swPublicKey">
<template #label>Public key<span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
<MkInput v-model="perRemoteUserUserTimelineCacheMax" type="number">
<template #label>perRemoteUserUserTimelineCacheMax</template>
<MkInput v-model="serviceWorkerForm.state.swPrivateKey">
<template #label>Private key<span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
</template>
</div>
</MkFolder>
<MkInput v-model="perUserHomeTimelineCacheMax" type="number">
<template #label>perUserHomeTimelineCacheMax</template>
</MkInput>
<MkFolder>
<template #icon><i class="ph-faders ph-bold ph-lg ti-fw"></i></template>
<template #label>{{ i18n.ts.otherSettings }}</template>
<template v-if="otherForm.modified.value" #footer>
<MkFormFooter :form="otherForm"/>
</template>
<MkInput v-model="perUserListTimelineCacheMax" type="number">
<template #label>perUserListTimelineCacheMax</template>
<div class="_gaps">
<MkSwitch v-model="otherForm.state.enableAchievements">
<template #label>{{ i18n.ts.enableAchievements }}<span v-if="otherForm.modifiedStates.enableAchievements" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.turnOffAchievements}}</template>
</MkSwitch>
<MkSwitch v-model="otherForm.state.enableBotTrending">
<template #label>{{ i18n.ts.enableBotTrending }}<span v-if="otherForm.modifiedStates.enableBotTrending" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.turnOffBotTrending }}</template>
</MkSwitch>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-ad"></i></template>
<template #label>{{ i18n.ts._ad.adsSettings }}</template>
<template v-if="adForm.modified.value" #footer>
<MkFormFooter :form="adForm"/>
</template>
<div class="_gaps">
<div class="_gaps_s">
<MkInput v-model="adForm.state.notesPerOneAd" :min="0" type="number">
<template #label>{{ i18n.ts._ad.notesPerOneAd }}<span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
</MkInput>
<MkInfo v-if="adForm.state.notesPerOneAd > 0 && adForm.state.notesPerOneAd < 20" :warn="true">
{{ i18n.ts._ad.adsTooClose }}
</MkInfo>
</div>
</FormSection>
</div>
</MkFolder>
<FormSection>
<template #label>{{ i18n.ts._ad.adsSettings }}</template>
<MkFolder>
<template #icon><i class="ti ti-world-search"></i></template>
<template #label>{{ i18n.ts._urlPreviewSetting.title }}</template>
<template v-if="urlPreviewForm.modified.value" #footer>
<MkFormFooter :form="urlPreviewForm"/>
</template>
<div class="_gaps_m">
<div class="_gaps_s">
<MkInput v-model="notesPerOneAd" :min="0" type="number">
<template #label>{{ i18n.ts._ad.notesPerOneAd }}</template>
<template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
</MkInput>
<MkInfo v-if="notesPerOneAd > 0 && notesPerOneAd < 20" :warn="true">
{{ i18n.ts._ad.adsTooClose }}
</MkInfo>
</div>
</div>
</FormSection>
<div class="_gaps">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewEnabled">
<template #label>{{ i18n.ts._urlPreviewSetting.enable }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkSwitch>
<FormSection>
<template #label>{{ i18n.ts._urlPreviewSetting.title }}</template>
<div class="_gaps_m">
<MkSwitch v-model="urlPreviewEnabled">
<template #label>{{ i18n.ts._urlPreviewSetting.enable }}</template>
</MkSwitch>
<MkSwitch v-model="urlPreviewRequireContentLength">
<template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}</template>
<template v-if="urlPreviewForm.state.urlPreviewEnabled">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength">
<template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
</MkSwitch>
<MkInput v-model="urlPreviewMaximumContentLength" type="number">
<template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}</template>
<MkInput v-model="urlPreviewForm.state.urlPreviewMaximumContentLength" type="number">
<template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
</MkInput>
<MkInput v-model="urlPreviewTimeout" type="number">
<template #label>{{ i18n.ts._urlPreviewSetting.timeout }}</template>
<MkInput v-model="urlPreviewForm.state.urlPreviewTimeout" type="number">
<template #label>{{ i18n.ts._urlPreviewSetting.timeout }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
</MkInput>
<MkInput v-model="urlPreviewUserAgent" type="text">
<template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}</template>
<MkInput v-model="urlPreviewForm.state.urlPreviewUserAgent" type="text">
<template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
</MkInput>
<div>
<MkInput v-model="urlPreviewSummaryProxyUrl" type="text">
<template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}</template>
<MkInput v-model="urlPreviewForm.state.urlPreviewSummaryProxyUrl" type="text">
<template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
</MkInput>
@ -193,18 +230,51 @@ SPDX-License-Identifier: AGPL-3.0-only
</ul>
</div>
</div>
</div>
</FormSection>
</div>
</FormSuspense>
</MkSpacer>
<template #footer>
<div :class="$style.footer">
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</MkSpacer>
</template>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-planet"></i></template>
<template #label>{{ i18n.ts.federation }}</template>
<template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template>
<template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template>
<template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template>
<template v-if="federationForm.modified.value" #footer>
<MkFormFooter :form="federationForm"/>
</template>
<div class="_gaps">
<MkRadios v-model="federationForm.state.federation">
<template #label>{{ i18n.ts.behavior }}<span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
<option value="all">{{ i18n.ts.all }}</option>
<option value="specified">{{ i18n.ts.specifyHost }}</option>
<option value="none">{{ i18n.ts.none }}</option>
</MkRadios>
<MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts">
<template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
</MkTextarea>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-ghost"></i></template>
<template #label>{{ i18n.ts.proxyAccount }}</template>
<div class="_gaps">
<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<MkKeyValue>
<template #key>{{ i18n.ts.proxyAccount }}</template>
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
</MkKeyValue>
<MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton>
</div>
</MkFolder>
</div>
</template>
</MkSpacer>
</MkStickyContainer>
</div>
</template>
@ -216,9 +286,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkInfo from '@/components/MkInfo.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance, instance } from '@/instance.js';
@ -226,99 +294,136 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import { useForm } from '@/scripts/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue';
import MkRadios from '@/components/MkRadios.vue';
const name = ref<string | null>(null);
const shortName = ref<string | null>(null);
const description = ref<string | null>(null);
const maintainerName = ref<string | null>(null);
const maintainerEmail = ref<string | null>(null);
const repositoryUrl = ref<string | null>(null);
const impressumUrl = ref<string | null>(null);
const donationUrl = ref<string | null>(null);
const pinnedUsers = ref<string>('');
const cacheRemoteFiles = ref<boolean>(false);
const cacheRemoteSensitiveFiles = ref<boolean>(false);
const enableServiceWorker = ref<boolean>(false);
const swPublicKey = ref<string | null>(null);
const swPrivateKey = ref<string | null>(null);
const enableFanoutTimeline = ref<boolean>(false);
const enableFanoutTimelineDbFallback = ref<boolean>(false);
const perLocalUserUserTimelineCacheMax = ref<number>(0);
const perRemoteUserUserTimelineCacheMax = ref<number>(0);
const perUserHomeTimelineCacheMax = ref<number>(0);
const perUserListTimelineCacheMax = ref<number>(0);
const notesPerOneAd = ref<number>(0);
const urlPreviewEnabled = ref<boolean>(true);
const urlPreviewTimeout = ref<number>(10000);
const urlPreviewMaximumContentLength = ref<number>(1024 * 1024 * 10);
const urlPreviewRequireContentLength = ref<boolean>(true);
const urlPreviewUserAgent = ref<string | null>(null);
const urlPreviewSummaryProxyUrl = ref<string | null>(null);
const meta = await misskeyApi('admin/meta');
async function init(): Promise<void> {
const meta = await misskeyApi('admin/meta');
name.value = meta.name;
shortName.value = meta.shortName;
description.value = meta.description;
maintainerName.value = meta.maintainerName;
maintainerEmail.value = meta.maintainerEmail;
repositoryUrl.value = meta.repositoryUrl;
impressumUrl.value = meta.impressumUrl;
donationUrl.value = meta.donationUrl;
pinnedUsers.value = meta.pinnedUsers.join('\n');
cacheRemoteFiles.value = meta.cacheRemoteFiles;
cacheRemoteSensitiveFiles.value = meta.cacheRemoteSensitiveFiles;
enableServiceWorker.value = meta.enableServiceWorker;
swPublicKey.value = meta.swPublickey;
swPrivateKey.value = meta.swPrivateKey;
enableFanoutTimeline.value = meta.enableFanoutTimeline;
enableFanoutTimelineDbFallback.value = meta.enableFanoutTimelineDbFallback;
perLocalUserUserTimelineCacheMax.value = meta.perLocalUserUserTimelineCacheMax;
perRemoteUserUserTimelineCacheMax.value = meta.perRemoteUserUserTimelineCacheMax;
perUserHomeTimelineCacheMax.value = meta.perUserHomeTimelineCacheMax;
perUserListTimelineCacheMax.value = meta.perUserListTimelineCacheMax;
notesPerOneAd.value = meta.notesPerOneAd;
urlPreviewEnabled.value = meta.urlPreviewEnabled;
urlPreviewTimeout.value = meta.urlPreviewTimeout;
urlPreviewMaximumContentLength.value = meta.urlPreviewMaximumContentLength;
urlPreviewRequireContentLength.value = meta.urlPreviewRequireContentLength;
urlPreviewUserAgent.value = meta.urlPreviewUserAgent;
urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl;
}
const proxyAccount = ref(meta.proxyAccountId ? await misskeyApi('users/show', { userId: meta.proxyAccountId }) : null);
async function save() {
const infoForm = useForm({
name: meta.name ?? '',
shortName: meta.shortName ?? '',
description: meta.description ?? '',
maintainerName: meta.maintainerName ?? '',
maintainerEmail: meta.maintainerEmail ?? '',
tosUrl: meta.tosUrl ?? '',
privacyPolicyUrl: meta.privacyPolicyUrl ?? '',
inquiryUrl: meta.inquiryUrl ?? '',
repositoryUrl: meta.repositoryUrl ?? '',
impressumUrl: meta.impressumUrl ?? '',
donationUrl: meta.donationUrl ?? '',
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
name: name.value,
shortName: shortName.value === '' ? null : shortName.value,
description: description.value,
maintainerName: maintainerName.value,
maintainerEmail: maintainerEmail.value,
repositoryUrl: repositoryUrl.value,
impressumUrl: impressumUrl.value,
donationUrl: donationUrl.value,
pinnedUsers: pinnedUsers.value.split('\n'),
cacheRemoteFiles: cacheRemoteFiles.value,
cacheRemoteSensitiveFiles: cacheRemoteSensitiveFiles.value,
enableServiceWorker: enableServiceWorker.value,
swPublicKey: swPublicKey.value,
swPrivateKey: swPrivateKey.value,
enableFanoutTimeline: enableFanoutTimeline.value,
enableFanoutTimelineDbFallback: enableFanoutTimelineDbFallback.value,
perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value,
perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value,
perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value,
perUserListTimelineCacheMax: perUserListTimelineCacheMax.value,
notesPerOneAd: notesPerOneAd.value,
urlPreviewEnabled: urlPreviewEnabled.value,
urlPreviewTimeout: urlPreviewTimeout.value,
urlPreviewMaximumContentLength: urlPreviewMaximumContentLength.value,
urlPreviewRequireContentLength: urlPreviewRequireContentLength.value,
urlPreviewUserAgent: urlPreviewUserAgent.value,
urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value,
name: state.name,
shortName: state.shortName === '' ? null : state.shortName,
description: state.description,
maintainerName: state.maintainerName,
maintainerEmail: state.maintainerEmail,
tosUrl: state.tosUrl,
privacyPolicyUrl: state.privacyPolicyUrl,
inquiryUrl: state.inquiryUrl,
repositoryUrl: state.repositoryUrl,
impressumUrl: state.impressumUrl,
donationUrl: state.donationUrl,
});
fetchInstance(true);
});
const pinnedUsersForm = useForm({
pinnedUsers: meta.pinnedUsers.join('\n'),
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
pinnedUsers: state.pinnedUsers.split('\n'),
});
fetchInstance(true);
});
const filesForm = useForm({
cacheRemoteFiles: meta.cacheRemoteFiles,
cacheRemoteSensitiveFiles: meta.cacheRemoteSensitiveFiles,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
cacheRemoteFiles: state.cacheRemoteFiles,
cacheRemoteSensitiveFiles: state.cacheRemoteSensitiveFiles,
});
fetchInstance(true);
});
const serviceWorkerForm = useForm({
enableServiceWorker: meta.enableServiceWorker,
swPublicKey: meta.swPublickey ?? '',
swPrivateKey: meta.swPrivateKey ?? '',
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
enableServiceWorker: state.enableServiceWorker,
swPublicKey: state.swPublicKey,
swPrivateKey: state.swPrivateKey,
});
fetchInstance(true);
});
const otherForm = useForm({
enableAchievements: meta.enableAchievements,
enableBotTrending: meta.enableBotTrending,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
enableAchievements: state.enableAchievements,
enableBotTrending: state.enableBotTrending,
});
fetchInstance(true);
});
const adForm = useForm({
notesPerOneAd: meta.notesPerOneAd,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
notesPerOneAd: state.notesPerOneAd,
});
fetchInstance(true);
});
const urlPreviewForm = useForm({
urlPreviewEnabled: meta.urlPreviewEnabled,
urlPreviewTimeout: meta.urlPreviewTimeout,
urlPreviewMaximumContentLength: meta.urlPreviewMaximumContentLength,
urlPreviewRequireContentLength: meta.urlPreviewRequireContentLength,
urlPreviewUserAgent: meta.urlPreviewUserAgent ?? '',
urlPreviewSummaryProxyUrl: meta.urlPreviewSummaryProxyUrl ?? '',
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
urlPreviewEnabled: state.urlPreviewEnabled,
urlPreviewTimeout: state.urlPreviewTimeout,
urlPreviewMaximumContentLength: state.urlPreviewMaximumContentLength,
urlPreviewRequireContentLength: state.urlPreviewRequireContentLength,
urlPreviewUserAgent: state.urlPreviewUserAgent,
urlPreviewSummaryProxyUrl: state.urlPreviewSummaryProxyUrl,
});
fetchInstance(true);
});
const federationForm = useForm({
federation: meta.federation,
federationHosts: meta.federationHosts.join('\n'),
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
federation: state.federation,
federationHosts: state.federationHosts.split('\n'),
});
fetchInstance(true);
});
function chooseProxyAccount() {
os.selectUser({ localOnly: true }).then(user => {
proxyAccount.value = user;
os.apiWithDialog('admin/update-meta', {
proxyAccountId: user.id,
}).then(() => {
fetchInstance(true);
});
});
}
const headerTabs = computed(() => []);
@ -330,11 +435,6 @@ definePageMetadata(() => ({
</script>
<style lang="scss" module>
.footer {
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
.subCaption {
font-size: 0.85em;
color: var(--fgTransparentWeak);

View file

@ -4,8 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.main">
<span :class="$style.icon">
<MkFolder>
<template #label>{{ entity.name || entity.url }}</template>
<template #icon>
<i v-if="!entity.isActive" class="ti ti-player-pause"/>
<i v-else-if="entity.latestStatus === null" class="ti ti-circle"/>
<i
@ -14,23 +15,38 @@ SPDX-License-Identifier: AGPL-3.0-only
:style="{ color: 'var(--success)' }"
/>
<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"/>
</span>
<span :class="$style.text">{{ entity.name || entity.url }}</span>
<span :class="$style.suffix">
</template>
<template #suffix>
<MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/>
<button :class="$style.suffixButton" @click="onEditClick">
<i class="ti ti-settings"></i>
</button>
<button :class="$style.suffixButton" @click="onDeleteClick">
<i class="ti ti-trash"></i>
</button>
</span>
</div>
<span v-else>-</span>
</template>
<template #footer>
<div class="_buttons">
<MkButton @click="onEditClick">
<i class="ti ti-settings"></i> {{ i18n.ts.edit }}
</MkButton>
<MkButton danger @click="onDeleteClick">
<i class="ti ti-trash"></i> {{ i18n.ts.delete }}
</MkButton>
</div>
</template>
<div class="_gaps">
<MkKeyValue>
<template #key>latestStatus</template>
<template #value>{{ entity.latestStatus ?? '-' }}</template>
</MkKeyValue>
</div>
</MkFolder>
</template>
<script lang="ts" setup>
import { entities } from 'misskey-js';
import { toRefs } from 'vue';
import MkFolder from '@/components/MkFolder.vue';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
const emit = defineEmits<{
(ev: 'edit', value: entities.SystemWebhook): void;
@ -54,64 +70,10 @@ function onDeleteClick() {
</script>
<style module lang="scss">
.main {
display: flex;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 10px 14px;
background: var(--buttonBg);
border: none;
border-radius: 6px;
font-size: 0.9em;
&:hover {
text-decoration: none;
background: var(--buttonHoverBg);
}
&.active {
color: var(--accent);
background: var(--buttonHoverBg);
}
}
.icon {
margin-right: 0.75em;
flex-shrink: 0;
text-align: center;
color: var(--fgTransparentWeak);
}
.text {
flex-shrink: 1;
white-space: normal;
padding-right: 12px;
text-align: center;
}
.suffix {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gaps: 4px;
margin-left: auto;
margin-right: -8px;
opacity: 0.7;
white-space: nowrap;
}
.suffixButton {
background: transparent;
border: none;
border-radius: 9999px;
margin-top: -8px;
margin-bottom: -8px;
padding: 8px;
&:hover {
background: var(--buttonBg);
}
}
</style>

View file

@ -11,8 +11,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="900">
<div class="_gaps_m">
<MkButton :class="$style.linkButton" full @click="onCreateWebhookClicked">
{{ i18n.ts._webhookSettings.createWebhook }}
<MkButton primary @click="onCreateWebhookClicked">
<i class="ti ti-plus"></i> {{ i18n.ts._webhookSettings.createWebhook }}
</MkButton>
<FormSection>
@ -89,8 +89,5 @@ definePageMetadata(() => ({
</script>
<style module lang="scss">
.linkButton {
text-align: left;
padding: 10px 18px;
}
</style>

View file

@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, watch, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkTimeline from '@/components/MkTimeline.vue';
import { scroll } from '@/scripts/scroll.js';
import { scroll } from '@@/js/scroll.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';

View file

@ -12,19 +12,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ avatarDecoration.name }}</template>
<template #caption>{{ avatarDecoration.description }}</template>
<div class="_gaps_m">
<MkInput v-model="avatarDecoration.name">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
<MkTextarea v-model="avatarDecoration.description">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
<MkInput v-model="avatarDecoration.url">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<div class="buttons _buttons">
<MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
<div :class="$style.editorRoot">
<div :class="$style.editorWrapper">
<div :class="$style.preview">
<div :class="[$style.previewItem, $style.light]">
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/>
</div>
<div :class="[$style.previewItem, $style.dark]">
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/>
</div>
</div>
<div class="_gaps_m">
<MkInput v-model="avatarDecoration.name">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
<MkTextarea v-model="avatarDecoration.description">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
<MkInput v-model="avatarDecoration.url">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<div class="_buttons">
<MkButton inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="avatarDecoration.id != null" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
</div>
</div>
</MkFolder>
@ -39,6 +51,7 @@ import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import { signinRequired } from '@/account.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
@ -47,6 +60,8 @@ import MkFolder from '@/components/MkFolder.vue';
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
const $i = signinRequired();
function add() {
avatarDecorations.value.unshift({
_id: Math.random().toString(36),
@ -99,3 +114,55 @@ definePageMetadata(() => ({
icon: 'ti ti-sparkles',
}));
</script>
<style lang="scss" module>
.editorRoot {
container: editor / inline-size;
}
.editorWrapper {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto auto;
gap: var(--margin);
}
.preview {
display: grid;
place-items: center;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
gap: var(--margin);
}
.previewItem {
width: 100%;
height: 100%;
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
&.light {
background: #eee;
}
&.dark {
background: #222;
}
}
@container editor (min-width: 600px) {
.editorWrapper {
grid-template-columns: 200px 1fr;
grid-template-rows: 1fr;
gap: calc(var(--margin) * 2);
}
.preview {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
}
</style>

View file

@ -82,7 +82,7 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { deviceKind } from '@/scripts/device-kind.js';
import MkNotes from '@/components/MkNotes.vue';
import { url } from '@/config.js';
import { url } from '@@/js/config.js';
import { favoritedChannelsCache } from '@/cache.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@ -341,7 +341,7 @@ definePageMetadata(() => ({
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
}
.bannerStatus {

View file

@ -39,11 +39,13 @@ import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { url } from '@/config.js';
import { url } from '@@/js/config.js';
import MkButton from '@/components/MkButton.vue';
import { clipsCache } from '@/cache.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { genEmbedCode } from '@/scripts/get-embed-code.js';
import type { MenuItem } from '@/types/menu.js';
const props = defineProps<{
clipId: string,
@ -127,21 +129,41 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
clipsCache.delete();
},
}, ...(clip.value.isPublic ? [{
icon: 'ti ti-link',
text: i18n.ts.copyUrl,
handler: async (): Promise<void> => {
copyToClipboard(`${url}/clips/${clip.value.id}`);
os.success();
},
}] : []), ...(clip.value.isPublic && isSupportShare() ? [{
icon: 'ti ti-share',
text: i18n.ts.share,
handler: async (): Promise<void> => {
navigator.share({
title: clip.value.name,
text: clip.value.description,
url: `${url}/clips/${clip.value.id}`,
handler: (ev: MouseEvent): void => {
const menuItems: MenuItem[] = [];
menuItems.push({
icon: 'ti ti-link',
text: i18n.ts.copyUrl,
action: () => {
copyToClipboard(`${url}/clips/${clip.value!.id}`);
os.success();
},
}, {
icon: 'ti ti-code',
text: i18n.ts.genEmbedCode,
action: () => {
genEmbedCode('clips', clip.value!.id);
},
});
if (isSupportShare()) {
menuItems.push({
icon: 'ti ti-share',
text: i18n.ts.share,
action: async () => {
navigator.share({
title: clip.value!.name,
text: clip.value!.description ?? '',
url: `${url}/clips/${clip.value!.id}`,
});
},
});
}
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
},
}] : []), {
icon: 'ti ti-trash',

View file

@ -99,12 +99,12 @@ const file = ref<Misskey.entities.DriveFile>();
const folderHierarchy = computed(() => {
if (!file.value) return [i18n.ts.drive];
const folderNames = [i18n.ts.drive];
function get(folder: Misskey.entities.DriveFolder) {
if (folder.parent) get(folder.parent);
folderNames.push(folder.name);
}
if (file.value.folder) get(file.value.folder);
return folderNames;
});

View file

@ -205,8 +205,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { useInterval } from '@/scripts/use-interval.js';
import { apiUrl } from '@/config.js';
import { useInterval } from '@@/js/use-interval.js';
import { apiUrl } from '@@/js/config.js';
import { $i } from '@/account.js';
import * as sound from '@/scripts/sound.js';
import MkRange from '@/components/MkRange.vue';

View file

@ -11,6 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="title">
<template #label>{{ i18n.ts._play.title }}</template>
</MkInput>
<MkSelect v-model="visibility">
<template #label>{{ i18n.ts.visibility }}</template>
<template #caption>{{ i18n.ts._play.visibilityDescription }}</template>
<option :key="'public'" :value="'public'">{{ i18n.ts.public }}</option>
<option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option>
</MkSelect>
<MkTextarea v-model="summary" :mfmAutocomplete="true" :mfmPreview="true">
<template #label>{{ i18n.ts._play.summary }}</template>
</MkTextarea>
@ -18,19 +24,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCodeEditor v-model="script" lang="is">
<template #label>{{ i18n.ts._play.script }}</template>
</MkCodeEditor>
<MkSelect v-model="visibility">
<template #label>{{ i18n.ts.visibility }}</template>
<template #caption>{{ i18n.ts._play.visibilityDescription }}</template>
<option :key="'public'" :value="'public'">{{ i18n.ts.public }}</option>
<option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option>
</MkSelect>
<div class="_buttons">
<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
<MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton>
<MkButton v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
</MkSpacer>
<template #footer>
<div :class="$style.footer">
<MkSpacer>
<div class="_buttons">
<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
<MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton>
<MkButton v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</MkSpacer>
</div>
</template>
</MkStickyContainer>
</template>
@ -459,3 +465,10 @@ definePageMetadata(() => ({
title: flash.value ? `${i18n.ts._play.edit}: ${flash.value.title}` : i18n.ts._play.new,
}));
</script>
<style lang="scss" module>
.footer {
backdrop-filter: var(--blur, blur(15px));
background: var(--acrylicBg);
border-top: solid .5px var(--divider);
}
</style>

View file

@ -68,7 +68,7 @@ import { Interpreter, Parser, values } from '@syuilo/aiscript';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { url } from '@/config.js';
import { url } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkAsUi from '@/components/MkAsUi.vue';
@ -80,7 +80,7 @@ import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { MenuItem } from '@/types/menu';
import type { MenuItem } from '@/types/menu.js';
import { pleaseLogin } from '@/scripts/please-login.js';
const props = defineProps<{
@ -104,18 +104,23 @@ function fetchFlash() {
function share(ev: MouseEvent) {
if (!flash.value) return;
os.popupMenu([
{
text: i18n.ts.shareWithNote,
icon: 'ph-repeat ph-bold ph-lg ti-fw',
action: shareWithNote,
},
...(isSupportShare() ? [{
const menuItems: MenuItem[] = [];
menuItems.push({
text: i18n.ts.shareWithNote,
icon: 'ph-repeat ph-bold ph-lg ti-fw',
action: shareWithNote,
});
if (isSupportShare()) {
menuItems.push({
text: i18n.ts.share,
icon: 'ti ti-share',
action: shareWithNavigator,
}] : []),
], ev.currentTarget ?? ev.target);
});
}
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
function copyLink() {

View file

@ -72,7 +72,7 @@ import MkContainer from '@/components/MkContainer.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
import { url } from '@/config.js';
import { url } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js';
@ -80,7 +80,7 @@ import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js';
import { MenuItem } from '@/types/menu';
import type { MenuItem } from '@/types/menu.js';
const router = useRouter();
@ -171,35 +171,35 @@ function reportAbuse() {
function showMenu(ev: MouseEvent) {
if (!post.value) return;
const menu: MenuItem[] = [
...($i && $i.id !== post.value.userId ? [
{
icon: 'ti ti-exclamation-circle',
text: i18n.ts.reportAbuse,
action: reportAbuse,
},
...($i.isModerator || $i.isAdmin ? [
{
type: 'divider' as const,
},
{
icon: 'ti ti-trash',
text: i18n.ts.delete,
danger: true,
action: () => os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
}).then(({ canceled }) => {
if (canceled || !post.value) return;
const menuItems: MenuItem[] = [];
os.apiWithDialog('gallery/posts/delete', { postId: post.value.id });
}),
},
] : []),
] : []),
];
if ($i && $i.id !== post.value.userId) {
menuItems.push({
icon: 'ti ti-exclamation-circle',
text: i18n.ts.reportAbuse,
action: reportAbuse,
});
os.popupMenu(menu, ev.currentTarget ?? ev.target);
if ($i.isModerator || $i.isAdmin) {
menuItems.push({
type: 'divider',
}, {
icon: 'ti ti-trash',
text: i18n.ts.delete,
danger: true,
action: () => os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
}).then(({ canceled }) => {
if (canceled || !post.value) return;
os.apiWithDialog('gallery/posts/delete', { postId: post.value.id });
}),
});
}
}
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
watch(() => props.postId, fetchPost, { immediate: true });

View file

@ -173,7 +173,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkChart from '@/components/MkChart.vue';
import MkChart, { type ChartSrc } from '@/components/MkChart.vue';
import MkObjectView from '@/components/MkObjectView.vue';
import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/MkLink.vue';
@ -189,7 +189,7 @@ import { iAmModerator, iAmAdmin } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
import { dateString } from '@/filters/date.js';
@ -202,7 +202,7 @@ const props = defineProps<{
const tab = ref('overview');
const chartSrc = ref('instance-requests');
const chartSrc = ref<ChartSrc>('instance-requests');
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
const instance = ref<Misskey.entities.FederationInstance | null>(null);
const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none');
@ -230,7 +230,7 @@ const isBaseSilenced = computed(() => meta.value && baseDomains.value.some(d =>
const isBaseMediaSilenced = computed(() => meta.value && baseDomains.value.some(d => meta.value?.mediaSilencedHosts.includes(d)));
const usersPagination = {
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
endpoint: iAmModerator ? 'admin/show-users' : 'users',
limit: 10,
params: {
sort: '+updatedAt',
@ -238,7 +238,7 @@ const usersPagination = {
hostname: props.host,
},
offsetMode: true,
};
} satisfies Paging;
const followingPagination = {
endpoint: 'federation/following' as const,
@ -260,9 +260,12 @@ const followersPagination = {
offsetMode: false,
};
watch(moderationNote, async () => {
await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value });
});
if (iAmModerator) {
watch(moderationNote, async () => {
if (instance.value == null) return;
await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value });
});
}
async function fetch(): Promise<void> {
if (iAmAdmin) {
@ -282,6 +285,7 @@ async function fetch(): Promise<void> {
}
async function toggleBlock(): Promise<void> {
if (!iAmAdmin) return;
if (!meta.value) throw new Error('No meta?');
if (!instance.value) throw new Error('No instance?');
const { host } = instance.value;
@ -291,6 +295,7 @@ async function toggleBlock(): Promise<void> {
}
async function toggleSilenced(): Promise<void> {
if (!iAmAdmin) return;
if (!meta.value) throw new Error('No meta?');
if (!instance.value) throw new Error('No instance?');
const { host } = instance.value;
@ -301,6 +306,7 @@ async function toggleSilenced(): Promise<void> {
}
async function toggleMediaSilenced(): Promise<void> {
if (!iAmAdmin) return;
if (!meta.value) throw new Error('No meta?');
if (!instance.value) throw new Error('No instance?');
const { host } = instance.value;
@ -311,6 +317,7 @@ async function toggleMediaSilenced(): Promise<void> {
}
async function stopDelivery(): Promise<void> {
if (!iAmModerator) return;
if (!instance.value) throw new Error('No instance?');
suspensionState.value = 'manuallySuspended';
await misskeyApi('admin/federation/update-instance', {
@ -320,6 +327,7 @@ async function stopDelivery(): Promise<void> {
}
async function resumeDelivery(): Promise<void> {
if (!iAmModerator) return;
if (!instance.value) throw new Error('No instance?');
suspensionState.value = 'none';
await misskeyApi('admin/federation/update-instance', {
@ -345,6 +353,7 @@ async function toggleRejectReports(): Promise<void> {
}
function refreshMetadata(): void {
if (!iAmModerator) return;
if (!instance.value) throw new Error('No instance?');
misskeyApi('admin/federation/refresh-remote-instance-metadata', {
host: instance.value.host,

View file

@ -134,12 +134,14 @@ async function removeUser(item, ev) {
async function showMembershipMenu(item, ev) {
const withRepliesRef = ref(item.withReplies);
os.popupMenu([{
type: 'switch',
text: i18n.ts.showRepliesToOthersInTimeline,
icon: 'ti ti-messages',
ref: withRepliesRef,
}], ev.currentTarget ?? ev.target);
watch(withRepliesRef, withReplies => {
misskeyApi('users/lists/update-membership', {
listId: list.value!.id,

View file

@ -30,7 +30,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { notificationTypes } from '@/const.js';
import { notificationTypes } from '@@/js/const.js';
const tab = ref('all');
const includeTypes = ref<string[] | null>(null);

View file

@ -69,6 +69,7 @@ import MkButton from '@/components/MkButton.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import { url } from '@@/js/config.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { selectFile } from '@/scripts/select-file.js';

View file

@ -104,7 +104,7 @@ import XPage from '@/components/page/page.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { url } from '@/config.js';
import { url } from '@@/js/config.js';
import MkMediaImage from '@/components/MkMediaImage.vue';
import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
@ -121,7 +121,7 @@ import { instance } from '@/instance.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js';
import { MenuItem } from '@/types/menu';
import type { MenuItem } from '@/types/menu.js';
const router = useRouter();
@ -165,18 +165,23 @@ function fetchPage() {
function share(ev: MouseEvent) {
if (!page.value) return;
os.popupMenu([
{
text: i18n.ts.shareWithNote,
icon: 'ti ti-pencil',
action: shareWithNote,
},
...(isSupportShare() ? [{
const menuItems: MenuItem[] = [];
menuItems.push({
text: i18n.ts.shareWithNote,
icon: 'ti ti-pencil',
action: shareWithNote,
});
if (isSupportShare()) {
menuItems.push({
text: i18n.ts.share,
icon: 'ti ti-share',
action: shareWithNavigator,
}] : []),
], ev.currentTarget ?? ev.target);
});
}
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
function copyLink() {
@ -256,51 +261,59 @@ function reportAbuse() {
function showMenu(ev: MouseEvent) {
if (!page.value) return;
const menu: MenuItem[] = [
...($i && $i.id === page.value.userId ? [
{
icon: 'ti ti-code',
text: i18n.ts._pages.viewSource,
action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`),
},
...($i.pinnedPageId === page.value.id ? [{
const menuItems: MenuItem[] = [];
if ($i && $i.id === page.value.userId) {
menuItems.push({
icon: 'ti ti-pencil',
text: i18n.ts.editThisPage,
action: () => router.push(`/pages/edit/${page.value.id}`),
});
if ($i.pinnedPageId === page.value.id) {
menuItems.push({
icon: 'ti ti-pinned-off',
text: i18n.ts.unpin,
action: () => pin(false),
}] : [{
});
} else {
menuItems.push({
icon: 'ti ti-pin',
text: i18n.ts.pin,
action: () => pin(true),
}]),
] : []),
...($i && $i.id !== page.value.userId ? [
{
icon: 'ti ti-exclamation-circle',
text: i18n.ts.reportAbuse,
action: reportAbuse,
},
...($i.isModerator || $i.isAdmin ? [
{
type: 'divider' as const,
},
{
icon: 'ti ti-trash',
text: i18n.ts.delete,
danger: true,
action: () => os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
}).then(({ canceled }) => {
if (canceled || !page.value) return;
});
}
} else if ($i && $i.id !== page.value.userId) {
menuItems.push({
icon: 'ti ti-code',
text: i18n.ts._pages.viewSource,
action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`),
}, {
icon: 'ti ti-exclamation-circle',
text: i18n.ts.reportAbuse,
action: reportAbuse,
});
os.apiWithDialog('pages/delete', { pageId: page.value.id });
}),
},
] : []),
] : []),
];
if ($i.isModerator || $i.isAdmin) {
menuItems.push({
type: 'divider',
}, {
icon: 'ti ti-trash',
text: i18n.ts.delete,
danger: true,
action: () => os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
}).then(({ canceled }) => {
if (canceled || !page.value) return;
os.popupMenu(menu, ev.currentTarget ?? ev.target);
os.apiWithDialog('pages/delete', { pageId: page.value.id });
}),
});
}
}
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
watch(() => path.value, fetchPage, { immediate: true });
@ -433,13 +446,12 @@ definePageMetadata(() => ({
.pageBannerTitleUser {
--height: 32px;
flex-shrink: 0;
line-height: var(--height);
.avatar {
height: var(--height);
width: var(--height);
}
line-height: var(--height);
}
.pageBannerTitleSubActions {

View file

@ -149,9 +149,9 @@ import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { deepClone } from '@/scripts/clone.js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import { signinRequired } from '@/account.js';
import { url } from '@/config.js';
import { url } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { userPage } from '@/filters/user.js';

View file

@ -121,7 +121,7 @@ import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
import { MenuItem } from '@/types/menu.js';
import type { MenuItem } from '@/types/menu.js';
import { useRouter } from '@/router/supplier.js';
const $i = signinRequired();

View file

@ -20,9 +20,9 @@ import { useStream } from '@/stream.js';
import { signinRequired } from '@/account.js';
import { useRouter } from '@/router/supplier.js';
import * as os from '@/os.js';
import { url } from '@/config.js';
import { url } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
const $i = signinRequired();

View file

@ -117,7 +117,7 @@ import { $i } from '@/account.js';
import MkPagination from '@/components/MkPagination.vue';
import { useRouter } from '@/router/supplier.js';
import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import * as sound from '@/scripts/sound.js';

View file

@ -43,7 +43,7 @@ import MkUserList from '@/components/MkUserList.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import MkTimeline from '@/components/MkTimeline.vue';
import { instanceName } from '@/config.js';
import { instanceName } from '@@/js/config.js';
import { serverErrorImageUrl, infoImageUrl } from '@/instance.js';
const props = withDefaults(defineProps<{

View file

@ -30,6 +30,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkContainer>
<MkContainer :foldable="true" :expanded="false">
<template #header>{{ i18n.ts.uiInspector }}</template>
<div :class="$style.uiInspector">
<div v-for="c in components" :key="c.value.id" :class="{ [$style.uiInspectorUnShown]: !showns.has(c.value.id) }">
<div :class="$style.uiInspectorType">{{ c.value.type }}</div>
<div :class="$style.uiInspectorId">{{ c.value.id }}</div>
<button :class="$style.uiInspectorPropsToggle" @click="() => uiInspectorOpenedComponents.set(c, !uiInspectorOpenedComponents.get(c))">
<i v-if="uiInspectorOpenedComponents.get(c)" class="ti ti-chevron-up icon"></i>
<i v-else class="ti ti-chevron-down icon"></i>
</button>
<div v-if="uiInspectorOpenedComponents.get(c)">
<MkTextarea :modelValue="stringifyUiProps(c.value)" code readonly></MkTextarea>
</div>
</div>
<div :class="$style.uiInspectorDescription">{{ i18n.ts.uiInspectorDescription }}</div>
</div>
</MkContainer>
<div class="">
{{ i18n.ts.scratchpadDescription }}
</div>
@ -43,6 +61,7 @@ import { onDeactivated, onUnmounted, Ref, ref, watch, computed } from 'vue';
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
import MkContainer from '@/components/MkContainer.vue';
import MkButton from '@/components/MkButton.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import * as os from '@/os.js';
@ -61,6 +80,7 @@ const logs = ref<any[]>([]);
const root = ref<AsUiRoot>();
const components = ref<Ref<AsUiComponent>[]>([]);
const uiKey = ref(0);
const uiInspectorOpenedComponents = ref(new Map<string, boolean>);
const saved = miLocalStorage.getItem('scratchpad');
if (saved) {
@ -71,6 +91,14 @@ watch(code, () => {
miLocalStorage.setItem('scratchpad', code.value);
});
function stringifyUiProps(uiProps) {
return JSON.stringify(
{ ...uiProps, type: undefined, id: undefined },
(k, v) => typeof v === 'function' ? '<function>' : v,
2
);
}
async function run() {
if (aiscript) aiscript.abort();
root.value = undefined;
@ -152,6 +180,20 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
const showns = computed(() => {
const result = new Set<string>();
(function addChildrenToResult(c: AsUiComponent) {
result.add(c.id);
if (c.children) {
const childComponents = components.value.filter(v => c.children.includes(v.value.id));
for (const child of childComponents) {
addChildrenToResult(child.value);
}
}
})(root.value);
return result;
});
definePageMetadata(() => ({
title: i18n.ts.scratchpad,
icon: 'ti ti-terminal-2',
@ -192,4 +234,39 @@ definePageMetadata(() => ({
}
}
}
.uiInspector {
display: grid;
gap: 8px;
padding: 16px;
}
.uiInspectorUnShown {
color: var(--fgTransparent);
}
.uiInspectorType {
display: inline-block;
border: hidden;
border-radius: 10px;
background-color: var(--panelHighlight);
padding: 2px 8px;
font-size: 12px;
}
.uiInspectorId {
display: inline-block;
padding-left: 8px;
}
.uiInspectorDescription {
display: block;
font-size: 12px;
padding-top: 16px;
}
.uiInspectorPropsToggle {
background: none;
border: none;
}
</style>

View file

@ -103,6 +103,7 @@ const decorationsForPreview = computed(() => {
offsetX: offsetX.value,
offsetY: offsetY.value,
showBelow: showBelow.value,
blink: true,
};
const decorations = [...$i.avatarDecorations];
if (props.usingIndex != null) {

View file

@ -124,10 +124,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<option :value="4">{{ i18n.ts.large }}+</option>
</MkRadios>
<MkSwitch v-model="emojiPickerUseDrawerForMobile">
{{ i18n.ts.useDrawerReactionPickerForMobile }}
<MkSelect v-model="emojiPickerStyle">
<template #label>{{ i18n.ts.style }}</template>
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
</MkSwitch>
<option value="auto">{{ i18n.ts.auto }}</option>
<option value="popup">{{ i18n.ts.popup }}</option>
<option value="drawer">{{ i18n.ts.drawer }}</option>
</MkSelect>
</div>
</FormSection>
</div>
@ -140,7 +143,7 @@ import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import FromSlot from '@/components/form/slot.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
@ -159,7 +162,7 @@ const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmoji
const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale'));
const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth'));
const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight'));
const emojiPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('emojiPickerUseDrawerForMobile'));
const emojiPickerStyle = computed(defaultStore.makeGetterSetter('emojiPickerStyle'));
const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev);
const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev);

View file

@ -24,13 +24,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</MkSelect>
<MkRadios v-model="hemisphere">
<template #label>{{ i18n.ts.hemisphere }}</template>
<option value="N">{{ i18n.ts._hemisphere.N }}</option>
<option value="S">{{ i18n.ts._hemisphere.S }}</option>
<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
</MkRadios>
<MkRadios v-model="overridedDeviceKind">
<template #label>{{ i18n.ts.overridedDeviceKind }}</template>
<option :value="null">{{ i18n.ts.auto }}</option>
@ -194,12 +187,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch>
<MkSwitch v-model="showAvatarDecorations">{{ i18n.ts.showAvatarDecorations }}</MkSwitch>
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
<MkSwitch v-model="oneko">{{ i18n.ts.oneko }}</MkSwitch>
<MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch>
<MkSwitch v-model="useNativeUIForVideoAudioPlayer">{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</MkSwitch>
</div>
<MkSelect v-model="menuStyle">
<template #label>{{ i18n.ts.menuStyle }}</template>
<option value="auto">{{ i18n.ts.auto }}</option>
<option value="popup">{{ i18n.ts.popup }}</option>
<option value="drawer">{{ i18n.ts.drawer }}</option>
</MkSelect>
<div>
<MkRadios v-model="emojiStyle">
<template #label>{{ i18n.ts.emojiStyle }}</template>
@ -314,6 +314,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.other }}</template>
<div class="_gaps">
<MkRadios v-model="hemisphere">
<template #label>{{ i18n.ts.hemisphere }}</template>
<option value="N">{{ i18n.ts._hemisphere.N }}</option>
<option value="S">{{ i18n.ts._hemisphere.S }}</option>
<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
</MkRadios>
<MkFolder>
<template #label>{{ i18n.ts.additionalEmojiDictionary }}</template>
<div class="_buttons">
@ -333,6 +339,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { langs } from '@@/js/config.js';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkRadios from '@/components/MkRadios.vue';
@ -344,12 +351,11 @@ import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/MkLink.vue';
import MkInfo from '@/components/MkInfo.vue';
import { langs } from '@/config.js';
import { searchEngineMap } from '@/scripts/search-engine-map.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { reloadAsk } from '@/scripts/reload-ask.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
@ -364,16 +370,6 @@ const cornerRadius = ref(miLocalStorage.getItem('cornerRadius'));
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
const dataSaver = ref(defaultStore.state.dataSaver);
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
const hemisphere = computed(defaultStore.makeGetterSetter('hemisphere'));
const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
@ -403,7 +399,7 @@ const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount'));
const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
const menuStyle = computed(defaultStore.makeGetterSetter('menuStyle'));
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
const oneko = computed(defaultStore.makeGetterSetter('oneko'));
@ -506,7 +502,7 @@ watch([
confirmWhenRevealingSensitiveMedia,
contextMenu,
], async () => {
await reloadAsk();
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const;

View file

@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
</div>
</MkFolder>
<MkFolder v-if="$i && !$i.movedTo">
<MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportFollowing">
<template #label>{{ i18n.ts.import }}</template>
<template #icon><i class="ti ti-upload"></i></template>
<MkSwitch v-model="withReplies">
@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-download"></i></template>
<MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
</MkFolder>
<MkFolder v-if="$i && !$i.movedTo">
<MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportUserLists">
<template #label>{{ i18n.ts.import }}</template>
<template #icon><i class="ti ti-upload"></i></template>
<MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-download"></i></template>
<MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
</MkFolder>
<MkFolder v-if="$i && !$i.movedTo">
<MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportMuting">
<template #label>{{ i18n.ts.import }}</template>
<template #icon><i class="ti ti-upload"></i></template>
<MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-download"></i></template>
<MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
</MkFolder>
<MkFolder v-if="$i && !$i.movedTo">
<MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportBlocking">
<template #label>{{ i18n.ts.import }}</template>
<template #icon><i class="ti ti-upload"></i></template>
<MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
@ -123,7 +123,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-download"></i></template>
<MkButton primary :class="$style.button" inline @click="exportAntennas()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
</MkFolder>
<MkFolder v-if="$i && !$i.movedTo">
<MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportAntennas">
<template #label>{{ i18n.ts.import }}</template>
<template #icon><i class="ti ti-upload"></i></template>
<MkButton primary :class="$style.button" inline @click="importAntennas($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>

View file

@ -54,7 +54,7 @@ import MkContainer from '@/components/MkContainer.vue';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { defaultStore } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { reloadAsk } from '@/scripts/reload-ask.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@ -67,16 +67,6 @@ const items = ref(defaultStore.state.menu.map(x => ({
const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
async function addItem() {
const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k));
const { canceled, result: item } = await os.select({
@ -100,7 +90,7 @@ function removeItem(index: number) {
async function save() {
defaultStore.set('menu', items.value.map(x => x.type));
await reloadAsk();
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
}
function reset() {
@ -111,7 +101,7 @@ function reset() {
}
watch(menuDisplay, async () => {
await reloadAsk();
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
const headerActions = computed(() => []);

View file

@ -69,12 +69,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import { notificationTypes } from '@/const.js';
import { notificationTypes } from '@@/js/const.js';
const $i = signinRequired();
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted'];
const notificationTypesWithoutSender = ['achievementEarned'];
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[];
const notificationTypesWithoutSender = ['achievementEarned'] as const satisfies (typeof notificationTypes[number])[];
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);

View file

@ -62,8 +62,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.experimentalFeatures }}</template>
<div class="_gaps_m">
<MkSwitch v-model="enableCondensedLineForAcct">
<template #label>Enable condensed line for acct</template>
<MkSwitch v-model="enableCondensedLine">
<template #label>Enable condensed line</template>
</MkSwitch>
</div>
</MkFolder>
@ -109,13 +109,13 @@ import { defaultStore } from '@/store.js';
import { signout, signinRequired } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { reloadAsk } from '@/scripts/reload-ask.js';
import FormSection from '@/components/form/section.vue';
const $i = signinRequired();
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct'));
const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine'));
const devMode = computed(defaultStore.makeGetterSetter('devMode'));
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
@ -143,16 +143,6 @@ async function deleteAccount() {
await signout();
}
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
async function updateRepliesAll(withReplies: boolean) {
const { canceled } = await os.confirm({
type: 'warning',
@ -178,9 +168,9 @@ const exportData = () => {
};
watch([
enableCondensedLineForAcct,
enableCondensedLineForAcct,
], async () => {
await reloadAsk();
await reloadAsk();
});
const headerActions = computed(() => []);

View file

@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<div :class="$style.buttons">
<MkButton inline primary @click="saveNew">{{ ts._preferencesBackups.saveNew }}</MkButton>
<MkButton inline @click="loadFile">{{ ts._preferencesBackups.loadFile }}</MkButton>
<MkButton inline primary @click="saveNew">{{ i18n.ts._preferencesBackups.saveNew }}</MkButton>
<MkButton inline @click="loadFile">{{ i18n.ts._preferencesBackups.loadFile }}</MkButton>
</div>
<FormSection>
<template #label>{{ ts._preferencesBackups.list }}</template>
<template #label>{{ i18n.ts._preferencesBackups.list }}</template>
<template v-if="profiles && Object.keys(profiles).length > 0">
<div class="_gaps_s">
<div
@ -23,13 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.prevent.stop="$event => menu($event, id)"
>
<div :class="$style.profileName">{{ profile.name }}</div>
<div :class="$style.profileTime">{{ t('_preferencesBackups.createdAt', { date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div>
<div v-if="profile.updatedAt" :class="$style.profileTime">{{ t('_preferencesBackups.updatedAt', { date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div>
<div :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.createdAt({ date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div>
<div v-if="profile.updatedAt" :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.updatedAt({ date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div>
</div>
</div>
</template>
<div v-else-if="profiles">
<MkInfo>{{ ts._preferencesBackups.noBackups }}</MkInfo>
<MkInfo>{{ i18n.ts._preferencesBackups.noBackups }}</MkInfo>
</div>
<MkLoading v-else/>
</FormSection>
@ -39,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import { version, host } from '@@/js/config.js';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
@ -49,10 +50,8 @@ import { unisonReload } from '@/scripts/unison-reload.js';
import { useStream } from '@/stream.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { version, host } from '@/config.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
const { t, ts } = i18n;
const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'collapseRenotes',
@ -80,7 +79,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'disableCatSpeak',
'disableShowingAnimatedImages',
'emojiStyle',
'disableDrawer',
'menuStyle',
'useBlurEffectForModal',
'useBlurEffect',
'showFixedPostForm',
@ -92,7 +91,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'emojiPickerScale',
'emojiPickerWidth',
'emojiPickerHeight',
'emojiPickerUseDrawerForMobile',
'emojiPickerStyle',
'defaultSideView',
'menuDisplay',
'reportError',
@ -110,7 +109,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'mediaListWithOneImageAppearance',
'notificationPosition',
'notificationStackAxis',
'enableCondensedLineForAcct',
'keepScreenOn',
'defaultWithReplies',
'disableStreamingTimeline',
@ -211,15 +209,15 @@ async function saveNew(): Promise<void> {
if (!profiles.value) return;
const { canceled, result: name } = await os.inputText({
title: ts._preferencesBackups.inputName,
title: i18n.ts._preferencesBackups.inputName,
default: '',
});
if (canceled) return;
if (Object.values(profiles.value).some(x => x.name === name)) {
return os.alert({
title: ts._preferencesBackups.cannotSave,
text: t('_preferencesBackups.nameAlreadyExists', { name }),
title: i18n.ts._preferencesBackups.cannotSave,
text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }),
});
}
@ -248,8 +246,8 @@ function loadFile(): void {
if (file.type !== 'application/json') {
return os.alert({
type: 'error',
title: ts._preferencesBackups.cannotLoad,
text: ts._preferencesBackups.invalidFile,
title: i18n.ts._preferencesBackups.cannotLoad,
text: i18n.ts._preferencesBackups.invalidFile,
});
}
@ -260,7 +258,7 @@ function loadFile(): void {
} catch (err) {
return os.alert({
type: 'error',
title: ts._preferencesBackups.cannotLoad,
title: i18n.ts._preferencesBackups.cannotLoad,
text: (err as any)?.message ?? '',
});
}
@ -286,8 +284,8 @@ async function applyProfile(id: string): Promise<void> {
const { canceled: cancel1 } = await os.confirm({
type: 'warning',
title: ts._preferencesBackups.apply,
text: t('_preferencesBackups.applyConfirm', { name: profile.name }),
title: i18n.ts._preferencesBackups.apply,
text: i18n.tsx._preferencesBackups.applyConfirm({ name: profile.name }),
});
if (cancel1) return;
@ -346,7 +344,7 @@ async function applyProfile(id: string): Promise<void> {
const { canceled: cancel2 } = await os.confirm({
type: 'info',
text: ts.reloadToApplySetting,
text: i18n.ts.reloadToApplySetting,
});
if (cancel2) return;
@ -358,8 +356,8 @@ async function deleteProfile(id: string): Promise<void> {
const { canceled } = await os.confirm({
type: 'info',
title: ts.delete,
text: t('deleteAreYouSure', { x: profiles.value[id].name }),
title: i18n.ts.delete,
text: i18n.tsx.deleteAreYouSure({ x: profiles.value[id].name }),
});
if (canceled) return;
@ -374,8 +372,8 @@ async function save(id: string): Promise<void> {
const { canceled } = await os.confirm({
type: 'info',
title: ts._preferencesBackups.save,
text: t('_preferencesBackups.saveConfirm', { name }),
title: i18n.ts._preferencesBackups.save,
text: i18n.tsx._preferencesBackups.saveConfirm({ name }),
});
if (canceled) return;
@ -394,15 +392,15 @@ async function rename(id: string): Promise<void> {
if (!profiles.value) return;
const { canceled: cancel1, result: name } = await os.inputText({
title: ts._preferencesBackups.inputName,
title: i18n.ts._preferencesBackups.inputName,
default: '',
});
if (cancel1 || profiles.value[id].name === name) return;
if (Object.values(profiles.value).some(x => x.name === name)) {
return os.alert({
title: ts._preferencesBackups.cannotSave,
text: t('_preferencesBackups.nameAlreadyExists', { name }),
title: i18n.ts._preferencesBackups.cannotSave,
text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }),
});
}
@ -410,8 +408,8 @@ async function rename(id: string): Promise<void> {
const { canceled: cancel2 } = await os.confirm({
type: 'info',
title: ts.rename,
text: t('_preferencesBackups.renameConfirm', { old: registry.name, new: name }),
title: i18n.ts.rename,
text: i18n.tsx._preferencesBackups.renameConfirm({ old: registry.name, new: name }),
});
if (cancel2) return;
@ -423,25 +421,25 @@ function menu(ev: MouseEvent, profileId: string) {
if (!profiles.value) return;
return os.popupMenu([{
text: ts._preferencesBackups.apply,
text: i18n.ts._preferencesBackups.apply,
icon: 'ti ti-check',
action: () => applyProfile(profileId),
}, {
type: 'a',
text: ts.download,
text: i18n.ts.download,
icon: 'ti ti-download',
href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })),
download: `${profiles.value[profileId].name}.json`,
}, { type: 'divider' }, {
text: ts.rename,
text: i18n.ts.rename,
icon: 'ti ti-forms',
action: () => rename(profileId),
}, {
text: ts._preferencesBackups.save,
text: i18n.ts._preferencesBackups.save,
icon: 'ti ti-device-floppy',
action: () => save(profileId),
}, { type: 'divider' }, {
text: ts.delete,
text: i18n.ts.delete,
icon: 'ti ti-trash',
action: () => deleteProfile(profileId),
danger: true,
@ -463,7 +461,7 @@ onUnmounted(() => {
});
definePageMetadata(() => ({
title: ts.preferencesBackups,
title: i18n.ts.preferencesBackups,
icon: 'ti ti-device-floppy',
}));
</script>

View file

@ -94,15 +94,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
</FormSlot>
<MkFolder>
<template #label>{{ i18n.ts.advancedSettings }}</template>
<div class="_gaps_m">
<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
<MkSwitch v-if="profile.isCat" v-model="profile.speakAsCat">{{ i18n.ts.flagSpeakAsCat }}<template #caption>{{ i18n.ts.flagSpeakAsCatDescription }}</template></MkSwitch>
<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
</div>
</MkFolder>
<MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false">
<template #label>{{ i18n.ts._profile.followedMessage }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption>
<div>{{ i18n.ts._profile.followedMessageDescription }}</div>
<div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
</template>
</MkInput>
<MkSelect v-model="reactionAcceptance">
<template #label>{{ i18n.ts.reactionAcceptance }}</template>
@ -112,6 +110,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option>
<option value="likeOnly">{{ i18n.ts.likeOnly }}</option>
</MkSelect>
<MkFolder>
<template #label>{{ i18n.ts.advancedSettings }}</template>
<div class="_gaps_m">
<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
<MkSwitch v-if="profile.isCat" v-model="profile.speakAsCat">{{ i18n.ts.flagSpeakAsCat }}<template #caption>{{ i18n.ts.flagSpeakAsCatDescription }}</template></MkSwitch>
<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
</div>
</MkFolder>
</div>
</template>
@ -153,6 +161,7 @@ const setMaxBirthDate = () => {
const profile = reactive({
name: $i.name,
description: $i.description,
followedMessage: $i.followedMessage,
location: $i.location,
birthday: $i.birthday,
listenbrainz: $i.listenbrainz,
@ -209,6 +218,8 @@ function save() {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
description: profile.description || null,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
followedMessage: profile.followedMessage || null,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
location: profile.location || null,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
birthday: profile.birthday || null,

View file

@ -88,19 +88,9 @@ import { uniqueBy } from '@/scripts/array.js';
import { fetchThemes, getThemes } from '@/theme-store.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { reloadAsk } from '@/scripts/reload-ask.js';
import * as os from '@/os.js';
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
const installedThemes = ref(getThemes());
const builtinThemes = getBuiltinThemesRef();
@ -148,13 +138,13 @@ watch(syncDeviceDarkMode, () => {
}
});
watch(wallpaper, () => {
watch(wallpaper, async () => {
if (wallpaper.value == null) {
miLocalStorage.removeItem('wallpaper');
} else {
miLocalStorage.setItem('wallpaper', wallpaper.value);
}
reloadAsk();
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
onActivated(() => {

View file

@ -21,14 +21,41 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection>
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
<div class="_gaps_s">
<MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
<MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch>
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
<div class="_gaps">
<div class="_gaps_s">
<div :class="$style.switchBox">
<MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_follow)" @click="test('follow')"><i class="ti ti-send"></i></MkButton>
</div>
<div :class="$style.switchBox">
<MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch>
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_followed)" @click="test('followed')"><i class="ti ti-send"></i></MkButton>
</div>
<div :class="$style.switchBox">
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_note)" @click="test('note')"><i class="ti ti-send"></i></MkButton>
</div>
<div :class="$style.switchBox">
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reply)" @click="test('reply')"><i class="ti ti-send"></i></MkButton>
</div>
<div :class="$style.switchBox">
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
</div>
<div :class="$style.switchBox">
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
</div>
<div :class="$style.switchBox">
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_mention)" @click="test('mention')"><i class="ti ti-send"></i></MkButton>
</div>
</div>
<div :class="$style.description">
{{ i18n.ts._webhookSettings.testRemarks }}
</div>
</div>
</FormSection>
@ -43,6 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import FormSection from '@/components/form/section.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@ -76,8 +104,8 @@ const event_renote = ref(webhook.on.includes('renote'));
const event_reaction = ref(webhook.on.includes('reaction'));
const event_mention = ref(webhook.on.includes('mention'));
async function save(): Promise<void> {
const events = [];
function save() {
const events: Misskey.entities.UserWebhook['on'] = [];
if (event_follow.value) events.push('follow');
if (event_followed.value) events.push('followed');
if (event_note.value) events.push('note');
@ -110,8 +138,21 @@ async function del(): Promise<void> {
router.push('/settings/webhook');
}
async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise<void> {
await os.apiWithDialog('i/webhooks/test', {
webhookId: props.webhookId,
type,
override: {
secret: secret.value,
url: url.value,
},
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const headerActions = computed(() => []);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const headerTabs = computed(() => []);
definePageMetadata(() => ({
@ -119,3 +160,30 @@ definePageMetadata(() => ({
icon: 'ti ti-webhook',
}));
</script>
<style module lang="scss">
.switchBox {
display: flex;
align-items: center;
justify-content: start;
.testButton {
$buttonSize: 28px;
padding: 0;
width: $buttonSize;
min-width: $buttonSize;
max-width: $buttonSize;
height: $buttonSize;
margin-left: auto;
line-height: inherit;
font-size: 90%;
border-radius: 9999px;
}
}
.description {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
}
</style>

View file

@ -28,6 +28,7 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import { genEmbedCode } from '@/scripts/get-embed-code.js';
const props = defineProps<{
tag: string;
@ -51,7 +52,19 @@ async function post() {
notes.value?.pagingComponent?.reload();
}
const headerActions = computed(() => []);
const headerActions = computed(() => [{
icon: 'ti ti-dots',
label: i18n.ts.more,
handler: (ev: MouseEvent) => {
os.popupMenu([{
text: i18n.ts.genEmbedCode,
icon: 'ti ti-code',
action: () => {
genEmbedCode('tags', props.tag);
},
}], ev.currentTarget ?? ev.target);
}
}]);
const headerTabs = computed(() => []);

View file

@ -79,6 +79,8 @@ import tinycolor from 'tinycolor2';
import { v4 as uuid } from 'uuid';
import JSON5 from 'json5';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import MkButton from '@/components/MkButton.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@ -86,9 +88,7 @@ import MkFolder from '@/components/MkFolder.vue';
import { $i } from '@/account.js';
import { Theme, applyTheme } from '@/scripts/theme.js';
import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5';
import { host } from '@/config.js';
import { host } from '@@/js/config.js';
import * as os from '@/os.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { addTheme } from '@/theme-store.js';

View file

@ -41,7 +41,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { scroll } from '@/scripts/scroll.js';
import { scroll } from '@@/js/scroll.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
@ -51,11 +51,10 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js';
import { deviceKind } from '@/scripts/device-kind.js';
import { deepMerge } from '@/scripts/merge.js';
import { MenuItem } from '@/types/menu.js';
import type { MenuItem } from '@/types/menu.js';
import { miLocalStorage } from '@/local-storage.js';
import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
import type { BasicTimelineType } from '@/timelines.js';
import { useRouter } from '@/router/supplier.js';
provide('shouldOmitHeaderTitle', true);
@ -198,7 +197,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
}),
(channels.length === 0 ? undefined : { type: 'divider' }),
{
type: 'link' as const,
type: 'link',
icon: 'ti ti-plus',
text: i18n.ts.createNew,
to: '/channels',
@ -267,16 +266,24 @@ const headerActions = computed(() => {
icon: 'ti ti-dots',
text: i18n.ts.options,
handler: (ev) => {
os.popupMenu([{
const menuItems: MenuItem[] = [];
menuItems.push({
type: 'switch',
text: i18n.ts.showRenotes,
ref: withRenotes,
}, isBasicTimeline(src.value) && hasWithReplies(src.value) ? {
type: 'switch',
text: i18n.ts.showRepliesToOthersInTimeline,
ref: withReplies,
disabled: onlyFiles,
} : undefined, {
});
if (isBasicTimeline(src.value) && hasWithReplies(src.value)) {
menuItems.push({
type: 'switch',
text: i18n.ts.showRepliesToOthersInTimeline,
ref: withReplies,
disabled: onlyFiles,
});
}
menuItems.push({
type: 'switch',
text: i18n.ts.withSensitive,
ref: withSensitive,
@ -285,7 +292,9 @@ const headerActions = computed(() => {
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,
}], ev.currentTarget ?? ev.target);
});
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
},
},
];

View file

@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, watch, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkTimeline from '@/components/MkTimeline.vue';
import { scroll } from '@/scripts/scroll.js';
import { scroll } from '@@/js/scroll.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';

View file

@ -51,6 +51,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
</div>
</div>
<div v-if="user.followedMessage != null" class="followedMessage">
<div style="border: solid 1px var(--love); border-radius: 6px; background: color-mix(in srgb, var(--love), transparent 90%); padding: 6px 8px;">
<Mfm :text="user.followedMessage" :author="user"/>
</div>
</div>
<div v-if="user.roles.length > 0" class="roles">
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
<MkA v-adaptive-bg :to="`/roles/${role.id}`">
@ -180,7 +185,7 @@ import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkButton from '@/components/MkButton.vue';
import { getScrollPosition } from '@/scripts/scroll.js';
import { getScrollPosition } from '@@/js/scroll.js';
import { getUserMenu } from '@/scripts/get-user-menu.js';
import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js';
@ -567,6 +572,11 @@ onUnmounted(() => {
filter: drop-shadow(1px 1px 3px rgba(#000, 0.2));
}
> .followedMessage {
padding: 24px 24px 0 154px;
font-size: 0.9em;
}
> .roles {
padding: 24px 24px 0 154px;
font-size: 0.95em;
@ -749,6 +759,10 @@ onUnmounted(() => {
margin: auto;
}
> .followedMessage {
padding: 16px 16px 0 16px;
}
> .roles {
padding: 16px 16px 0 16px;
justify-content: center;

View file

@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import { host, version } from '@/config.js';
import { host, version } from '@@/js/config.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { login } from '@/account.js';

View file

@ -84,7 +84,7 @@ onUpdated(() => {
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
}
}

View file

@ -24,7 +24,7 @@ import * as Misskey from 'misskey-js';
import { onUpdated, ref, shallowRef } from 'vue';
import XNote from '@/pages/welcome.timeline.note.vue';
import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { getScrollContainer } from '@/scripts/scroll.js';
import { getScrollContainer } from '@@/js/scroll.js';
const notes = ref<Misskey.entities.Note[]>([]);
const isScrolling = ref(false);

View file

@ -15,7 +15,7 @@ import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XSetup from './welcome.setup.vue';
import XEntrance from './welcome.entrance.a.vue';
import { instanceName } from '@/config.js';
import { instanceName } from '@@/js/config.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { fetchInstance } from '@/instance.js';