summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorPrivateGER <privateger@privateger.me>2024-10-06 23:13:10 +0200
committerPrivateGER <privateger@privateger.me>2024-10-06 23:13:10 +0200
commitfadae347ffd52a3610ef2e6ce160ade32d3dac31 (patch)
tree2731c25215245ba6c9cf4b8738f6868cf33afb78 /packages/frontend/src
parentMove text into translation files (diff)
parentmerge: Add option to reject reports from an instance (Resolves #579, #715, #7... (diff)
downloadsharkey-fadae347ffd52a3610ef2e6ce160ade32d3dac31.tar.gz
sharkey-fadae347ffd52a3610ef2e6ce160ade32d3dac31.tar.bz2
sharkey-fadae347ffd52a3610ef2e6ce160ade32d3dac31.zip
Merge branch 'develop' of https://activitypub.software/TransFem-org/Sharkey into feat/instance-admin-ui
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/boot/main-boot.ts28
-rw-r--r--packages/frontend/src/components/MkLink.vue14
-rw-r--r--packages/frontend/src/components/MkPostForm.vue19
-rw-r--r--packages/frontend/src/components/MkUrlWarningDialog.vue131
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.vue1
-rw-r--r--packages/frontend/src/components/global/MkUrl.vue2
-rw-r--r--packages/frontend/src/const.ts6
-rw-r--r--packages/frontend/src/index.html2
-rw-r--r--packages/frontend/src/pages/admin/moderation.vue9
-rw-r--r--packages/frontend/src/pages/admin/modlog.ModLog.vue8
-rw-r--r--packages/frontend/src/pages/instance-info.vue38
-rw-r--r--packages/frontend/src/pages/user/home.vue29
-rw-r--r--packages/frontend/src/plugin.ts13
-rw-r--r--packages/frontend/src/scripts/warning-external-website.ts51
-rw-r--r--packages/frontend/src/store.ts4
15 files changed, 303 insertions, 52 deletions
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index c10930a038..5ff998fac4 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -216,19 +216,25 @@ export async function mainBoot() {
claimAchievement('collectAchievements30');
}
- window.setInterval(() => {
- if (Math.floor(Math.random() * 20000) === 0) {
- claimAchievement('justPlainLucky');
- }
- }, 1000 * 10);
+ if (!claimedAchievements.includes('justPlainLucky')) {
+ window.setInterval(() => {
+ if (Math.floor(Math.random() * 20000) === 0) {
+ claimAchievement('justPlainLucky');
+ }
+ }, 1000 * 10);
+ }
- window.setTimeout(() => {
- claimAchievement('client30min');
- }, 1000 * 60 * 30);
+ if (!claimedAchievements.includes('client30min')) {
+ window.setTimeout(() => {
+ claimAchievement('client30min');
+ }, 1000 * 60 * 30);
+ }
- window.setTimeout(() => {
- claimAchievement('client60min');
- }, 1000 * 60 * 60);
+ if (!claimedAchievements.includes('client60min')) {
+ window.setTimeout(() => {
+ claimAchievement('client60min');
+ }, 1000 * 60 * 60);
+ }
// 邪魔
//const lastUsed = miLocalStorage.getItem('lastUsed');
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index d2819f9f4c..b04edd1150 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target"
:behavior="props.navigationBehavior"
:title="url"
- @click.prevent="self ? true : promptConfirm()"
+ @click.prevent="self ? true : warningExternalWebsite(url)"
@click.stop
>
<slot></slot>
@@ -23,7 +23,7 @@ import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { MkABehavior } from '@/components/global/MkA.vue';
-import { i18n } from '@/i18n.js';
+import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
const props = withDefaults(defineProps<{
url: string;
@@ -49,16 +49,6 @@ if (isEnabledUrlPreview.value) {
});
});
}
-
-async function promptConfirm() {
- const { canceled } = await os.confirm({
- type: 'question',
- text: i18n.tsx.confirmRemoteUrl({ x: props.url }),
- plain: true,
- });
- if (canceled) return;
- window.open(props.url, '_blank', 'nofollow noopener popup=false');
-}
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index dc3f3aa94c..add5296f0a 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -630,11 +630,22 @@ async function onPaste(ev: ClipboardEvent) {
if (paste.length > 1000) {
ev.preventDefault();
- os.confirm({
- type: 'info',
+ os.actions({
+ type: 'question',
text: i18n.ts.attachAsFileQuestion,
- }).then(({ canceled }) => {
- if (canceled) {
+ actions: [
+ {
+ value: 'yes',
+ text: i18n.ts.yes,
+ primary: true,
+ },
+ {
+ value: 'no',
+ text: i18n.ts.no,
+ },
+ ],
+ }).then(({ result }) => {
+ if (result !== 'yes') {
insertTextAtCursor(textareaEl.value, paste);
return;
}
diff --git a/packages/frontend/src/components/MkUrlWarningDialog.vue b/packages/frontend/src/components/MkUrlWarningDialog.vue
new file mode 100644
index 0000000000..5a37a36ee5
--- /dev/null
+++ b/packages/frontend/src/components/MkUrlWarningDialog.vue
@@ -0,0 +1,131 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
+ <div :class="$style.root" class="_gaps">
+ <div class="_gaps_s">
+ <div :class="$style.header">
+ <div :class="$style.icon">
+ <i class="ti ti-alert-triangle"></i>
+ </div>
+ <div :class="$style.title">{{ i18n.ts._externalNavigationWarning.title }}</div>
+ </div>
+ <div><Mfm :text="i18n.tsx._externalNavigationWarning.description({ host: instanceName })"/></div>
+ <div class="_monospace" :class="$style.urlAddress">{{ url }}</div>
+ <div>
+ <MkSwitch v-model="trustThisDomain">{{ i18n.ts._externalNavigationWarning.trustThisDomain }}</MkSwitch>
+ </div>
+ </div>
+ <div :class="$style.buttons">
+ <MkButton data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ i18n.ts.cancel }}</MkButton>
+ <MkButton data-cy-modal-dialog-ok inline primary rounded @click="ok"><i class="ti ti-external-link"></i> {{ i18n.ts.open }}</MkButton>
+ </div>
+ </div>
+</MkModal>
+</template>
+
+<script lang="ts" setup>
+import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue';
+import { instanceName } from '@/config.js';
+import MkModal from '@/components/MkModal.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import { i18n } from '@/i18n.js';
+import { defaultStore } from '@/store.js';
+
+type Result = string | number | true | null;
+
+const props = defineProps<{
+ url: string;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'done', v: { canceled: true } | { canceled: false, result: Result }): void;
+ (ev: 'closed'): void;
+}>();
+
+const modal = shallowRef<InstanceType<typeof MkModal>>();
+const trustThisDomain = ref(false);
+
+const domain = computed(() => new URL(props.url).hostname);
+
+// overload function を使いたいので lint エラーを無視する
+function done(canceled: true): void;
+function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
+function done(canceled: boolean, result?: Result): void { // eslint-disable-line no-redeclare
+ emit('done', { canceled, result } as { canceled: true } | { canceled: false, result: Result });
+ modal.value?.close();
+}
+
+async function ok() {
+ const result = true;
+ if (!defaultStore.state.trustedDomains.includes(domain.value) && trustThisDomain.value) {
+ await defaultStore.set('trustedDomains', defaultStore.state.trustedDomains.concat(domain.value));
+ }
+ done(false, result);
+}
+
+function cancel() {
+ done(true);
+}
+
+function onKeydown(evt: KeyboardEvent) {
+ if (evt.key === 'Escape') cancel();
+}
+
+onMounted(() => {
+ document.addEventListener('keydown', onKeydown);
+});
+
+onBeforeUnmount(() => {
+ document.removeEventListener('keydown', onKeydown);
+});
+</script>
+
+<style lang="scss" module>
+.root {
+ position: relative;
+ margin: auto;
+ padding: 32px;
+ width: 100%;
+ min-width: 320px;
+ max-width: 480px;
+ box-sizing: border-box;
+ background: var(--panel);
+ border-radius: 16px;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ gap: 0.75em;
+}
+
+.icon {
+ font-size: 18px;
+ color: var(--warn);
+}
+
+.title {
+ font-weight: bold;
+ font-size: 1.1em;
+}
+
+.urlAddress {
+ padding: 10px 14px;
+ border-radius: 8px;
+ border: 1px solid var(--divider);
+ overflow-x: auto;
+ white-space: nowrap;
+}
+
+.buttons {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: right;
+}
+</style>
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index b154f7a5b3..ff2e27aaf8 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -142,6 +142,7 @@ function showMenu(ev: MouseEvent) {
height: 32px;
border-radius: var(--radius-sm);
font-size: 18px;
+ z-index: 50;
}
.mainFg {
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 15595ba515..1dec8ad28c 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target"
:behavior="props.navigationBehavior"
@contextmenu.stop="() => {}"
+ @click.prevent="self ? true : warningExternalWebsite(props.url)"
@click.stop
>
<template v-if="!self">
@@ -34,6 +35,7 @@ import { useTooltip } from '@/scripts/use-tooltip.js';
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { MkABehavior } from '@/components/global/MkA.vue';
+import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
const props = withDefaults(defineProps<{
url: string;
diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts
index c94c0d4408..058db9b981 100644
--- a/packages/frontend/src/const.ts
+++ b/packages/frontend/src/const.ts
@@ -160,9 +160,9 @@ export const ROLE_POLICIES = [
export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM';
-export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://launcher.moe/error.png';
-export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://launcher.moe/missingpage.webp';
-export const DEFAULT_INFO_IMAGE_URL = 'https://launcher.moe/nothinghere.png';
+export const DEFAULT_SERVER_ERROR_IMAGE_URL = '/status/error.png';
+export const DEFAULT_NOT_FOUND_IMAGE_URL = '/status/missingpage.webp';
+export const DEFAULT_INFO_IMAGE_URL = '/status/nothinghere.png';
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime', 'crop', 'fade', 'followmouse'];
export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html
index 733116b75f..fdeb642c70 100644
--- a/packages/frontend/src/index.html
+++ b/packages/frontend/src/index.html
@@ -20,7 +20,7 @@
worker-src 'self';
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh;
style-src 'self' 'unsafe-inline';
- img-src 'self' data: blob: www.google.com xn--931a.moe launcher.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com;
+ img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com;
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.listenbrainz.org;
frame-src *;"
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index 6297b9a182..0a5b06a969 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -50,6 +50,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
</MkTextarea>
+ <MkTextarea v-model="trustedLinkUrlPatterns">
+ <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">
<template #label>{{ i18n.ts.sensitiveWords }}</template>
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
@@ -105,6 +111,7 @@ 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>('');
async function init() {
const meta = await misskeyApi('admin/meta');
@@ -120,6 +127,7 @@ async function init() {
bubbleTimeline.value = meta.bubbleInstances.join('\n');
bubbleTimelineEnabled.value = meta.policies.btlAvailable;
inquiryUrl.value = meta.inquiryUrl;
+ trustedLinkUrlPatterns.value = meta.trustedLinkUrlPatterns.join('\n');
}
function save() {
@@ -135,6 +143,7 @@ function save() {
hiddenTags: hiddenTags.value.split('\n'),
preservedUsernames: preservedUsernames.value.split('\n'),
bubbleInstances: bubbleTimeline.value.split('\n'),
+ trustedLinkUrlPatterns: trustedLinkUrlPatterns.value.split('\n'),
}).then(() => {
fetchInstance(true);
});
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index f6f276de53..9fe804b2bd 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -23,6 +23,10 @@ SPDX-License-Identifier: AGPL-3.0-only
'markSensitiveDriveFile',
'resetPassword',
'suspendRemoteInstance',
+ 'setRemoteInstanceNSFW',
+ 'unsetRemoteInstanceNSFW',
+ 'rejectRemoteInstanceReports',
+ 'acceptRemoteInstanceReports',
].includes(log.type),
[$style.logRed]: [
'suspend',
@@ -61,6 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'unsuspendRemoteInstance'">: {{ log.info.host }}</span>
+ <span v-else-if="log.type === 'setRemoteInstanceNSFW'">: {{ log.info.host }}</span>
+ <span v-else-if="log.type === 'unsetRemoteInstanceNSFW'">: {{ log.info.host }}</span>
+ <span v-else-if="log.type === 'rejectRemoteInstanceReports'">: {{ log.info.host }}</span>
+ <span v-else-if="log.type === 'acceptRemoteInstanceReports'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'createGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>
<span v-else-if="log.type === 'updateGlobalAnnouncement'">: {{ log.info.before.title }}</span>
<span v-else-if="log.type === 'deleteGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index 28ad2da0a7..95371036aa 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -49,10 +49,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-if="suspensionState === 'none'" inline :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton>
<MkButton v-if="suspensionState !== 'none'" inline :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
</div>
- <MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
- <MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
- <MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">Mark as NSFW</MkSwitch>
- <MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
+ <MkInfo v-if="isBaseBlocked" warn>{{ i18n.ts.blockedByBase }}</MkInfo>
+ <MkSwitch v-model="isBlocked" :disabled="!meta || !instance || isBaseBlocked" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
+ <MkInfo v-if="isBaseSilenced" warn>{{ i18n.ts.silencedByBase }}</MkInfo>
+ <MkSwitch v-model="isSilenced" :disabled="!meta || !instance || isBaseSilenced" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
+ <MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">{{ i18n.ts.markInstanceAsNSFW }}</MkSwitch>
+ <MkSwitch v-model="rejectReports" :disabled="!instance" @update:modelValue="toggleRejectReports">{{ i18n.ts.rejectReports }}</MkSwitch>
+ <MkInfo v-if="isBaseMediaSilenced" warn>{{ i18n.ts.mediaSilencedByBase }}</MkInfo>
+ <MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance || isBaseMediaSilenced" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
<MkTextarea v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
@@ -160,6 +164,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
import { dateString } from '@/filters/date.js';
import MkTextarea from '@/components/MkTextarea.vue';
+import MkInfo from '@/components/MkInfo.vue';
const props = defineProps<{
host: string;
@@ -174,10 +179,26 @@ const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'au
const isBlocked = ref(false);
const isSilenced = ref(false);
const isNSFW = ref(false);
+const rejectReports = ref(false);
const isMediaSilenced = ref(false);
const faviconUrl = ref<string | null>(null);
const moderationNote = ref('');
+const baseDomains = computed(() => {
+ const domains: string[] = [];
+
+ const parts = props.host.toLowerCase().split('.');
+ for (let s = 1; s < parts.length; s++) {
+ const domain = parts.slice(s).join('.');
+ domains.push(domain);
+ }
+
+ return domains;
+});
+const isBaseBlocked = computed(() => meta.value && baseDomains.value.some(d => meta.value?.blockedHosts.includes(d)));
+const isBaseSilenced = computed(() => meta.value && baseDomains.value.some(d => meta.value?.silencedHosts.includes(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,
limit: 10,
@@ -204,6 +225,7 @@ async function fetch(): Promise<void> {
isBlocked.value = instance.value?.isBlocked ?? false;
isSilenced.value = instance.value?.isSilenced ?? false;
isNSFW.value = instance.value?.isNSFW ?? false;
+ rejectReports.value = instance.value?.rejectReports ?? false;
isMediaSilenced.value = instance.value?.isMediaSilenced ?? false;
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
moderationNote.value = instance.value?.moderationNote ?? '';
@@ -264,6 +286,14 @@ async function toggleNSFW(): Promise<void> {
});
}
+async function toggleRejectReports(): Promise<void> {
+ if (!instance.value) throw new Error('No instance?');
+ await misskeyApi('admin/federation/update-instance', {
+ host: instance.value.host,
+ rejectReports: rejectReports.value,
+ });
+}
+
function refreshMetadata(): void {
if (!instance.value) throw new Error('No instance?');
misskeyApi('admin/federation/refresh-remote-instance-metadata', {
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index b997fe1c3f..e82ec0cb97 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -30,7 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</div>
</div>
- <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
+ <div v-if="$i && $i.id != user.id" class="info-badges">
+ <span v-if="user.isFollowed">{{ i18n.ts.followsYou }}</span>
+ <span v-if="user.isMuted">{{ i18n.ts.muted }}</span>
+ <span v-if="user.isRenoteMuted">{{ i18n.ts.renoteMuted }}</span>
+ <span v-if="user.isBlocking">{{ i18n.ts.blocked }}</span>
+ </div>
<div class="actions">
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
<MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
@@ -445,15 +450,25 @@ onUnmounted(() => {
background: linear-gradient(transparent, rgba(#000, 0.7));
}
- > .followed {
+ > .info-badges {
position: absolute;
top: 12px;
left: 12px;
- padding: 4px 8px;
- color: #fff;
- background: rgba(0, 0, 0, 0.7);
- font-size: 0.7em;
- border-radius: var(--radius-sm);
+
+ display: flex;
+ flex-direction: row;
+
+ > * {
+ padding: 4px 8px;
+ color: #fff;
+ background: rgba(0, 0, 0, 0.7);
+ font-size: 0.7em;
+ border-radius: var(--radius-sm);
+ }
+
+ > :not(:first-child) {
+ margin-left: 8px;
+ }
}
> .actions {
diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts
index 9640c988eb..c0034d414c 100644
--- a/packages/frontend/src/plugin.ts
+++ b/packages/frontend/src/plugin.ts
@@ -9,6 +9,7 @@ import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js';
+import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
const parser = new Parser();
const pluginContexts = new Map<string, Interpreter>();
@@ -92,16 +93,8 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s
registerPageViewInterruptor({ pluginId: opts.plugin.id, handler });
}),
'Plugin:open_url': values.FN_NATIVE(([url]) => {
- (async () => {
- utils.assertString(url);
- const { canceled } = await os.confirm({
- type: 'question',
- text: i18n.tsx.confirmRemoteUrl({ x: url.value }),
- plain: true,
- });
- if (canceled) return;
- window.open(url.value, '_blank', 'noopener');
- })();
+ utils.assertString(url);
+ warningExternalWebsite(url.value);
}),
'Plugin:config': values.OBJ(config),
};
diff --git a/packages/frontend/src/scripts/warning-external-website.ts b/packages/frontend/src/scripts/warning-external-website.ts
new file mode 100644
index 0000000000..5ef003cb01
--- /dev/null
+++ b/packages/frontend/src/scripts/warning-external-website.ts
@@ -0,0 +1,51 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { instance } from '@/instance.js';
+import { defaultStore } from '@/store.js';
+import * as os from '@/os.js';
+import MkUrlWarningDialog from '@/components/MkUrlWarningDialog.vue';
+
+const extractDomain = /^(https?:\/\/|\/\/)?([^@/\s]+@)?(www\.)?([^:/\s]+)/i;
+const isRegExp = /^\/(.+)\/(.*)$/;
+
+export async function warningExternalWebsite(url: string) {
+ const domain = extractDomain.exec(url)?.[4];
+
+ if (!domain) return false;
+
+ const isTrustedByInstance = instance.trustedLinkUrlPatterns.some(expression => {
+ const r = isRegExp.exec(expression);
+
+ if (r) {
+ return new RegExp(r[1], r[2]).test(url);
+ } else if (expression.includes(' ')) {
+ return expression.split(' ').every(keyword => url.includes(keyword));
+ } else {
+ return domain.endsWith(expression);
+ }
+ });
+
+ const isTrustedByUser = defaultStore.reactiveState.trustedDomains.value.includes(domain);
+
+ if (!isTrustedByInstance && !isTrustedByUser) {
+ const confirm = await new Promise<{ canceled: boolean }>(resolve => {
+ const { dispose } = os.popup(MkUrlWarningDialog, {
+ url,
+ }, {
+ done: result => {
+ resolve(result ?? { canceled: true });
+ },
+ closed: () => dispose(),
+ });
+ });
+
+ if (confirm.canceled) return false;
+
+ return window.open(url, '_blank', 'nofollow noopener popup=false');
+ }
+
+ return window.open(url, '_blank', 'nofollow noopener popup=false');
+}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 036e43a4b6..ab5fbf0dd1 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -165,6 +165,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'account',
default: 'public' as 'public' | 'home' | 'followers',
},
+ trustedDomains: {
+ where: 'account',
+ default: [] as string[],
+ },
menu: {
where: 'deviceAccount',