summaryrefslogtreecommitdiff
path: root/packages/frontend/src/pages
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2024-06-01 11:27:03 +0900
committerGitHub <noreply@github.com>2024-06-01 11:27:03 +0900
commitfce66b85b603caac79e1bfa87b5f4621b1ba9d4e (patch)
treed22952ee3f8e30057977a99a33823f4d52990fbc /packages/frontend/src/pages
parentMerge pull request #13493 from misskey-dev/develop (diff)
parentfix(backend): use insertOne insteadof insert/findOneOrFail combination (#13908) (diff)
downloadmisskey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.tar.gz
misskey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.tar.bz2
misskey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.zip
Merge pull request #13917 from misskey-dev/develop
Release 2024.5.0 (master)
Diffstat (limited to 'packages/frontend/src/pages')
-rw-r--r--packages/frontend/src/pages/about-misskey.vue20
-rw-r--r--packages/frontend/src/pages/admin-user.vue2
-rw-r--r--packages/frontend/src/pages/admin/RolesEditorFormula.vue5
-rw-r--r--packages/frontend/src/pages/admin/federation.vue14
-rw-r--r--packages/frontend/src/pages/admin/files.vue27
-rw-r--r--packages/frontend/src/pages/admin/index.vue37
-rw-r--r--packages/frontend/src/pages/admin/moderation.vue9
-rw-r--r--packages/frontend/src/pages/admin/roles.role.vue2
-rw-r--r--packages/frontend/src/pages/admin/security.vue16
-rw-r--r--packages/frontend/src/pages/admin/settings.vue74
-rw-r--r--packages/frontend/src/pages/admin/users.vue2
-rw-r--r--packages/frontend/src/pages/announcement.vue142
-rw-r--r--packages/frontend/src/pages/announcements.vue25
-rw-r--r--packages/frontend/src/pages/channel.vue3
-rw-r--r--packages/frontend/src/pages/clip.vue13
-rw-r--r--packages/frontend/src/pages/contact.vue40
-rw-r--r--packages/frontend/src/pages/explore.featured.vue3
-rw-r--r--packages/frontend/src/pages/flash/flash-edit.vue62
-rw-r--r--packages/frontend/src/pages/flash/flash.vue82
-rw-r--r--packages/frontend/src/pages/instance-info.vue29
-rw-r--r--packages/frontend/src/pages/my-antennas/create.vue1
-rw-r--r--packages/frontend/src/pages/my-antennas/editor.vue6
-rw-r--r--packages/frontend/src/pages/my-clips/index.vue10
-rw-r--r--packages/frontend/src/pages/note.vue7
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue2
-rw-r--r--packages/frontend/src/pages/page.vue390
-rw-r--r--packages/frontend/src/pages/reversi/game.board.vue3
-rw-r--r--packages/frontend/src/pages/settings/2fa.qrdialog.vue21
-rw-r--r--packages/frontend/src/pages/settings/2fa.vue12
-rw-r--r--packages/frontend/src/pages/settings/drive.vue5
-rw-r--r--packages/frontend/src/pages/settings/general.vue12
-rw-r--r--packages/frontend/src/pages/settings/plugin.vue20
-rw-r--r--packages/frontend/src/pages/settings/preferences-backups.vue1
-rw-r--r--packages/frontend/src/pages/share.vue29
-rw-r--r--packages/frontend/src/pages/timeline.vue6
-rw-r--r--packages/frontend/src/pages/welcome.entrance.a.vue35
-rw-r--r--packages/frontend/src/pages/welcome.vue12
37 files changed, 875 insertions, 304 deletions
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 1a49dbf1d5..b55ae220d8 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -222,6 +222,24 @@ const patronsWithIcon = [{
}, {
name: '有栖かずみ',
icon: 'https://assets.misskey-hub.net/patrons/9240e8e0ba294a8884143e99ac7ed6a0.jpg',
+}, {
+ name: 'イカロ(コアラ)',
+ icon: 'https://assets.misskey-hub.net/patrons/50b9bdc03735412c80807dbdf32cecb6.jpg',
+}, {
+ name: 'ハチノス3号',
+ icon: 'https://assets.misskey-hub.net/patrons/030347a6f8ce4e82bc5184b5aad09a18.jpg',
+}, {
+ name: 'Takeno',
+ icon: 'https://assets.misskey-hub.net/patrons/6fba81536aea48fe94a30909c502dfa1.jpg',
+}, {
+ name: 'くびすじ',
+ icon: 'https://assets.misskey-hub.net/patrons/aa5789850b2149aeb5b89ebe2e9083db.jpg',
+}, {
+ name: '古道京紗@ぷらいべったー',
+ icon: 'https://assets.misskey-hub.net/patrons/18346d0519704963a4beabe6abc170af.jpg',
+}, {
+ name: '越貝鯛丸',
+ icon: 'https://assets.misskey-hub.net/patrons/86c7374de37849b882d8ebbc833dc968.jpg',
}];
const patrons = [
@@ -324,6 +342,8 @@ const patrons = [
'てば',
'たっくん',
'SHO SEKIGUCHI',
+ '塩キャベツ',
+ 'はとぽぷさん',
];
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 2cef55df6c..f57aa51b5b 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -416,7 +416,7 @@ async function assignRole() {
if (canceled) return;
const { canceled: canceled2, result: period } = await os.select({
- title: i18n.ts.period,
+ title: i18n.ts.period + ': ' + roles.find(r => r.id === roleId)!.name,
items: [{
value: 'indefinitely', text: i18n.ts.indefinitely,
}, {
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index 2f5b4c47d8..f001a4ac20 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -9,6 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-model="type" :class="$style.typeSelect">
<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option>
+ <option value="isSuspended">{{ i18n.ts._role._condition.isSuspended }}</option>
+ <option value="isLocked">{{ i18n.ts._role._condition.isLocked }}</option>
+ <option value="isBot">{{ i18n.ts._role._condition.isBot }}</option>
+ <option value="isCat">{{ i18n.ts._role._condition.isCat }}</option>
+ <option value="isExplorable">{{ i18n.ts._role._condition.isExplorable }}</option>
<option value="roleAssignedTo">{{ i18n.ts._role._condition.roleAssignedTo }}</option>
<option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>
<option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option>
diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue
index de27e1f67a..0aaa398584 100644
--- a/packages/frontend/src/pages/admin/federation.vue
+++ b/packages/frontend/src/pages/admin/federation.vue
@@ -58,6 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import * as Misskey from 'misskey-js';
import { computed, ref } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/MkInput.vue';
@@ -90,8 +91,17 @@ const pagination = {
})),
};
-function getStatus(instance) {
- if (instance.isSuspended) return 'Suspended';
+function getStatus(instance: Misskey.entities.FederationInstance) {
+ switch (instance.suspensionState) {
+ case 'manuallySuspended':
+ return 'Manually Suspended';
+ case 'goneSuspended':
+ return 'Automatically Suspended (Gone)';
+ case 'autoSuspendedForNotResponding':
+ return 'Automatically Suspended (Not Responding)';
+ case 'none':
+ break;
+ }
if (instance.isBlocked) return 'Blocked';
if (instance.isSilenced) return 'Silenced';
if (instance.isNotResponding) return 'Error';
diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue
index 3fe021e771..5132b85c64 100644
--- a/packages/frontend/src/pages/admin/files.vue
+++ b/packages/frontend/src/pages/admin/files.vue
@@ -42,7 +42,7 @@ import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { lookupFile } from '@/scripts/admin-lookup.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -73,33 +73,10 @@ function clear() {
});
}
-function show(file) {
- os.pageWindow(`/admin/file/${file.id}`);
-}
-
-async function find() {
- const { canceled, result: q } = await os.inputText({
- title: i18n.ts.fileIdOrUrl,
- minLength: 1,
- });
- if (canceled) return;
-
- misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
- show(file);
- }).catch(err => {
- if (err.code === 'NO_SUCH_FILE') {
- os.alert({
- type: 'error',
- text: i18n.ts.notFound,
- });
- }
- });
-}
-
const headerActions = computed(() => [{
text: i18n.ts.lookup,
icon: 'ti ti-search',
- handler: find,
+ handler: lookupFile,
}, {
text: i18n.ts.clearCachedFiles,
icon: 'ti ti-trash',
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index d4a41c66cc..794feae202 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -12,10 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<img :src="instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
</div>
- <MkInfo v-if="thereIsUnresolvedAbuseReport" warn class="info">{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
- <MkInfo v-if="noMaintainerInformation" warn class="info">{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
- <MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
- <MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
+ <div class="_gaps_s">
+ <MkInfo v-if="thereIsUnresolvedAbuseReport" warn>{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
+ <MkInfo v-if="noMaintainerInformation" warn>{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
+ <MkInfo v-if="noInquiryUrl" warn>{{ i18n.ts.noInquiryUrlWarning }} <MkA to="/admin/moderation" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
+ <MkInfo v-if="noBotProtection" warn>{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
+ <MkInfo v-if="noEmailServer" warn>{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
+ </div>
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
</div>
@@ -33,9 +36,10 @@ import { i18n } from '@/i18n.js';
import MkSuperMenu from '@/components/MkSuperMenu.vue';
import MkInfo from '@/components/MkInfo.vue';
import { instance } from '@/instance.js';
+import { lookup } from '@/scripts/lookup.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
-import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js';
+import { lookupUser, lookupUserByEmail, lookupFile } from '@/scripts/admin-lookup.js';
import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { useRouter } from '@/router/supplier.js';
@@ -60,6 +64,7 @@ const pageProps = ref({});
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile;
let noEmailServer = !instance.enableEmail;
+let noInquiryUrl = isEmpty(instance.inquiryUrl);
const thereIsUnresolvedAbuseReport = ref(false);
const currentPage = computed(() => router.currentRef.value.child);
@@ -82,7 +87,7 @@ const menuDef = computed(() => [{
type: 'button',
icon: 'ti ti-search',
text: i18n.ts.lookup,
- action: lookup,
+ action: adminLookup,
}, ...(instance.disableRegistration ? [{
type: 'button',
icon: 'ti ti-user-plus',
@@ -282,7 +287,7 @@ function invite() {
});
}
-function lookup(ev: MouseEvent) {
+function adminLookup(ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts.user,
icon: 'ti ti-user',
@@ -296,22 +301,16 @@ function lookup(ev: MouseEvent) {
lookupUserByEmail();
},
}, {
- text: i18n.ts.note,
- icon: 'ti ti-pencil',
- action: () => {
- alert('TODO');
- },
- }, {
text: i18n.ts.file,
icon: 'ti ti-cloud',
action: () => {
- alert('TODO');
+ lookupFile();
},
}, {
- text: i18n.ts.instance,
- icon: 'ti ti-planet',
+ text: i18n.ts.lookup,
+ icon: 'ti ti-world-search',
action: () => {
- alert('TODO');
+ lookup();
},
}], ev.currentTarget ?? ev.target);
}
@@ -353,10 +352,6 @@ defineExpose({
> .nav {
.lxpfedzu {
- > .info {
- margin: 16px 0;
- }
-
> .banner {
margin: 16px;
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index 9efb34ac9a..a75799696d 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -30,6 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
</MkInput>
+ <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>
+
<MkTextarea v-model="preservedUsernames">
<template #label>{{ i18n.ts.preservedUsernames }}</template>
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
@@ -86,6 +92,7 @@ const hiddenTags = ref<string>('');
const preservedUsernames = ref<string>('');
const tosUrl = ref<string | null>(null);
const privacyPolicyUrl = ref<string | null>(null);
+const inquiryUrl = ref<string | null>(null);
async function init() {
const meta = await misskeyApi('admin/meta');
@@ -97,6 +104,7 @@ async function init() {
preservedUsernames.value = meta.preservedUsernames.join('\n');
tosUrl.value = meta.tosUrl;
privacyPolicyUrl.value = meta.privacyPolicyUrl;
+ inquiryUrl.value = meta.inquiryUrl;
}
function save() {
@@ -105,6 +113,7 @@ function save() {
emailRequiredForSignup: emailRequiredForSignup.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'),
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index ab8005045b..8b3c906d8a 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -119,7 +119,7 @@ async function assign() {
const user = await os.selectUser({ includeSelf: true });
const { canceled: canceled2, result: period } = await os.select({
- title: i18n.ts.period,
+ title: i18n.ts.period + ': ' + role.name,
items: [{
value: 'indefinitely', text: i18n.ts.indefinitely,
}, {
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index c4745978df..9bccee89a5 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -118,19 +118,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</div>
</MkFolder>
-
- <MkFolder>
- <template #label>Summaly Proxy</template>
-
- <div class="_gaps_m">
- <MkInput v-model="summalyProxy">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>Summaly Proxy URL</template>
- </MkInput>
-
- <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
- </div>
- </MkFolder>
</div>
</FormSuspense>
</MkSpacer>
@@ -155,7 +142,6 @@ import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-const summalyProxy = ref<string>('');
const enableHcaptcha = ref<boolean>(false);
const enableMcaptcha = ref<boolean>(false);
const enableRecaptcha = ref<boolean>(false);
@@ -175,7 +161,6 @@ const bannedEmailDomains = ref<string>('');
async function init() {
const meta = await misskeyApi('admin/meta');
- summalyProxy.value = meta.summalyProxy;
enableHcaptcha.value = meta.enableHcaptcha;
enableMcaptcha.value = meta.enableMcaptcha;
enableRecaptcha.value = meta.enableRecaptcha;
@@ -201,7 +186,6 @@ async function init() {
function save() {
os.apiWithDialog('admin/update-meta', {
- summalyProxy: summalyProxy.value,
sensitiveMediaDetection: sensitiveMediaDetection.value,
sensitiveMediaDetectionSensitivity:
sensitiveMediaDetectionSensitivity.value === 0 ? 'veryLow' :
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 9a198ee8a3..6f45c212ec 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -143,6 +143,53 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</FormSection>
+
+ <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 #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
+ </MkSwitch>
+
+ <MkInput v-model="urlPreviewMaximumContentLength" type="number">
+ <template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}</template>
+ <template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
+ </MkInput>
+
+ <MkInput v-model="urlPreviewTimeout" type="number">
+ <template #label>{{ i18n.ts._urlPreviewSetting.timeout }}</template>
+ <template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
+ </MkInput>
+
+ <MkInput v-model="urlPreviewUserAgent" type="text">
+ <template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}</template>
+ <template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
+ </MkInput>
+
+ <div>
+ <MkInput v-model="urlPreviewSummaryProxyUrl" type="text">
+ <template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}</template>
+ <template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
+ </MkInput>
+
+ <div :class="$style.subCaption">
+ {{ i18n.ts._urlPreviewSetting.summaryProxyDescription2 }}
+ <ul style="padding-left: 20px; margin: 4px 0">
+ <li>{{ i18n.ts._urlPreviewSetting.timeout }} / key:timeout</li>
+ <li>{{ i18n.ts._urlPreviewSetting.maximumContentLength }} / key:contentLengthLimit</li>
+ <li>{{ i18n.ts._urlPreviewSetting.requireContentLength }} / key:contentLengthRequired</li>
+ <li>{{ i18n.ts._urlPreviewSetting.userAgent }} / key:userAgent</li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </FormSection>
</div>
</FormSuspense>
</MkSpacer>
@@ -173,6 +220,8 @@ import { fetchInstance, instance } from '@/instance.js';
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';
const name = ref<string | null>(null);
const shortName = ref<string | null>(null);
@@ -194,6 +243,12 @@ 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);
async function init(): Promise<void> {
const meta = await misskeyApi('admin/meta');
@@ -217,9 +272,15 @@ async function init(): Promise<void> {
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;
}
-async function save(): void {
+async function save() {
await os.apiWithDialog('admin/update-meta', {
name: name.value,
shortName: shortName.value === '' ? null : shortName.value,
@@ -241,6 +302,12 @@ async function save(): void {
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,
});
fetchInstance(true);
@@ -259,4 +326,9 @@ definePageMetadata(() => ({
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
+
+.subCaption {
+ font-size: 0.85em;
+ color: var(--fgTransparentWeak);
+}
</style>
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index 06317760d2..7d87b97a36 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -63,7 +63,7 @@ import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkPagination from '@/components/MkPagination.vue';
import * as os from '@/os.js';
-import { lookupUser } from '@/scripts/lookup-user.js';
+import { lookupUser } from '@/scripts/admin-lookup.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue
new file mode 100644
index 0000000000..85ae9062d4
--- /dev/null
+++ b/packages/frontend/src/pages/announcement.vue
@@ -0,0 +1,142 @@
+<!--
+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="800">
+ <Transition
+ :enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
+ mode="out-in"
+ >
+ <div v-if="announcement" :key="announcement.id" class="_panel" :class="$style.announcement">
+ <div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
+ <div :class="$style.header">
+ <span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
+ <span style="margin-right: 0.5em;">
+ <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
+ <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
+ <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
+ <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+ </span>
+ <Mfm :text="announcement.title"/>
+ </div>
+ <div :class="$style.content">
+ <Mfm :text="announcement.text"/>
+ <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
+ <div style="margin-top: 8px; opacity: 0.7; font-size: 85%;">
+ {{ i18n.ts.createdAt }}: <MkTime :time="announcement.createdAt" mode="detail"/>
+ </div>
+ <div v-if="announcement.updatedAt" style="opacity: 0.7; font-size: 85%;">
+ {{ i18n.ts.updatedAt }}: <MkTime :time="announcement.updatedAt" mode="detail"/>
+ </div>
+ </div>
+ <div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer">
+ <MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
+ </div>
+ </div>
+ <MkError v-else-if="error" @retry="fetch()"/>
+ <MkLoading v-else/>
+ </Transition>
+ </MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, watch } from 'vue';
+import * as Misskey from 'misskey-js';
+import MkButton from '@/components/MkButton.vue';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { $i, updateAccount } from '@/account.js';
+import { defaultStore } from '@/store.js';
+
+const props = defineProps<{
+ announcementId: string;
+}>();
+
+const announcement = ref<Misskey.entities.Announcement | null>(null);
+const error = ref<any>(null);
+const path = computed(() => props.announcementId);
+
+function fetch() {
+ announcement.value = null;
+ misskeyApi('announcements/show', {
+ announcementId: props.announcementId,
+ }).then(async _announcement => {
+ announcement.value = _announcement;
+ }).catch(err => {
+ error.value = err;
+ });
+}
+
+async function read(target: Misskey.entities.Announcement): Promise<void> {
+ if (target.needConfirmationToRead) {
+ const confirm = await os.confirm({
+ type: 'question',
+ title: i18n.ts._announcement.readConfirmTitle,
+ text: i18n.tsx._announcement.readConfirmText({ title: target.title }),
+ });
+ if (confirm.canceled) return;
+ }
+
+ target.isRead = true;
+ await misskeyApi('i/read-announcement', { announcementId: target.id });
+ if ($i) {
+ updateAccount({
+ unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
+ });
+ }
+}
+
+watch(() => path.value, fetch, { immediate: true });
+
+const headerActions = computed(() => []);
+
+const headerTabs = computed(() => []);
+
+definePageMetadata(() => ({
+ title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements,
+ icon: 'ti ti-speakerphone',
+}));
+</script>
+
+<style lang="scss" module>
+.announcement {
+ padding: 16px;
+}
+
+.forYou {
+ display: flex;
+ align-items: center;
+ line-height: 24px;
+ font-size: 90%;
+ white-space: pre;
+ color: #d28a3f;
+}
+
+.header {
+ margin-bottom: 16px;
+ font-weight: bold;
+ font-size: 120%;
+}
+
+.content {
+ > img {
+ display: block;
+ max-height: 300px;
+ max-width: 100%;
+ }
+}
+
+.footer {
+ margin-top: 16px;
+}
+</style>
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index bcd6eb7c0f..e50b208775 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -21,14 +21,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
</span>
- <span>{{ announcement.title }}</span>
+ <MkA :to="`/announcements/${announcement.id}`"><span>{{ announcement.title }}</span></MkA>
</div>
<div :class="$style.content">
<Mfm :text="announcement.text"/>
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
- <div style="opacity: 0.7; font-size: 85%;">
- <MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
- </div>
+ <MkA :to="`/announcements/${announcement.id}`">
+ <div style="margin-top: 8px; opacity: 0.7; font-size: 85%;">
+ {{ i18n.ts.createdAt }}: <MkTime :time="announcement.createdAt" mode="detail"/>
+ </div>
+ <div v-if="announcement.updatedAt" style="opacity: 0.7; font-size: 85%;">
+ {{ i18n.ts.updatedAt }}: <MkTime :time="announcement.updatedAt" mode="detail"/>
+ </div>
+ </MkA>
</div>
<div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
@@ -73,24 +78,24 @@ const paginationEl = ref<InstanceType<typeof MkPagination>>();
const tab = ref('current');
-async function read(announcement) {
- if (announcement.needConfirmationToRead) {
+async function read(target) {
+ if (target.needConfirmationToRead) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts._announcement.readConfirmTitle,
- text: i18n.tsx._announcement.readConfirmText({ title: announcement.title }),
+ text: i18n.tsx._announcement.readConfirmText({ title: target.title }),
});
if (confirm.canceled) return;
}
if (!paginationEl.value) return;
- paginationEl.value.updateItem(announcement.id, a => {
+ paginationEl.value.updateItem(target.id, a => {
a.isRead = true;
return a;
});
- misskeyApi('i/read-announcement', { announcementId: announcement.id });
+ misskeyApi('i/read-announcement', { announcementId: target.id });
updateAccount({
- unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== announcement.id),
+ unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
});
}
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 611ae6feca..a895df76e8 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -83,6 +83,7 @@ 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 { favoritedChannelsCache } from '@/cache.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import { defaultStore } from '@/store.js';
@@ -153,6 +154,7 @@ function favorite() {
channelId: channel.value.id,
}).then(() => {
favorited.value = true;
+ favoritedChannelsCache.delete();
});
}
@@ -168,6 +170,7 @@ async function unfavorite() {
channelId: channel.value.id,
}).then(() => {
favorited.value = false;
+ favoritedChannelsCache.delete();
});
}
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index c38cc117bc..fd64a55c65 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -9,11 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="800">
<div v-if="clip" class="_gaps">
<div class="_panel">
- <div v-if="clip.description" :class="$style.description">
- <Mfm :text="clip.description" :isNote="false"/>
+ <div class="_gaps_s" :class="$style.description">
+ <div v-if="clip.description">
+ <Mfm :text="clip.description" :isNote="false"/>
+ </div>
+ <div v-else>({{ i18n.ts.noDescription }})</div>
+ <div>
+ <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
+ <MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
+ </div>
</div>
- <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
- <MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
<div :class="$style.user">
<MkAvatar :user="clip.user" :class="$style.avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
</div>
diff --git a/packages/frontend/src/pages/contact.vue b/packages/frontend/src/pages/contact.vue
new file mode 100644
index 0000000000..bcdcf43275
--- /dev/null
+++ b/packages/frontend/src/pages/contact.vue
@@ -0,0 +1,40 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+ <template #header><MkPageHeader/></template>
+ <MkSpacer :contentMax="600" :marginMin="20">
+ <div class="_gaps">
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.inquiry }}</template>
+ <template #value>
+ <MkLink :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
+ </template>
+ </MkKeyValue>
+
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.email }}</template>
+ <template #value>
+ <div>{{ instance.maintainerEmail }}</div>
+ </template>
+ </MkKeyValue>
+ </div>
+ </MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { i18n } from '@/i18n.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { instance } from '@/instance.js';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import MkLink from '@/components/MkLink.vue';
+
+definePageMetadata(() => ({
+ title: i18n.ts.inquiry,
+ icon: 'ti ti-help-circle',
+}));
+</script>
diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue
index b5c8e70166..cfdb235d3a 100644
--- a/packages/frontend/src/pages/explore.featured.vue
+++ b/packages/frontend/src/pages/explore.featured.vue
@@ -29,6 +29,9 @@ const paginationForPolls = {
endpoint: 'notes/polls/recommendation' as const,
limit: 10,
offsetMode: true,
+ params: {
+ excludeChannels: true,
+ },
};
const tab = ref('notes');
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index 4418172e62..3445da26a2 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCodeEditor v-model="script" lang="is">
<template #label>{{ i18n.ts._play.script }}</template>
</MkCodeEditor>
- <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>
<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>
</MkStickyContainer>
@@ -47,7 +48,7 @@ import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import { useRouter } from '@/router/supplier.js';
-const PRESET_DEFAULT = `/// @ 0.16.0
+const PRESET_DEFAULT = `/// @ 0.18.0
var name = ""
@@ -59,13 +60,13 @@ Ui:render([
Ui:C:button({
text: "Hello"
onClick: @() {
- Mk:dialog(null \`Hello, {name}!\`)
+ Mk:dialog(null, \`Hello, {name}!\`)
}
})
])
`;
-const PRESET_OMIKUJI = `/// @ 0.16.0
+const PRESET_OMIKUJI = `/// @ 0.18.0
// ユーザーごとに日替わりのおみくじのプリセット
// 選択肢
@@ -80,11 +81,11 @@ let choices = [
"大凶"
]
-// シードが「ユーザーID+今日の日付」である乱数生成器を用意
-let random = Math:gen_rng(\`{USER_ID}{Date:year()}{Date:month()}{Date:day()}\`)
+// シードが「PlayID+ユーザーID+今日の日付」である乱数生成器を用意
+let random = Math:gen_rng(\`{THIS_ID}{USER_ID}{Date:year()}{Date:month()}{Date:day()}\`)
// ランダムに選択肢を選ぶ
-let chosen = choices[random(0 (choices.len - 1))]
+let chosen = choices[random(0, (choices.len - 1))]
// 結果のテキスト
let result = \`今日のあなたの運勢は **{chosen}** です。\`
@@ -108,7 +109,7 @@ Ui:render([
])
`;
-const PRESET_SHUFFLE = `/// @ 0.16.0
+const PRESET_SHUFFLE = `/// @ 0.18.0
// 巻き戻し可能な文字シャッフルのプリセット
let string = "ペペロンチーノ"
@@ -122,13 +123,13 @@ var cursor = 0
@do() {
if (cursor != 0) {
- results = results.slice(0 (cursor + 1))
+ results = results.slice(0, (cursor + 1))
cursor = 0
}
let chars = []
for (let i, length) {
- let r = Math:rnd(0 (length - 1))
+ let r = Math:rnd(0, (length - 1))
chars.push(string.pick(r))
}
let result = chars.join("")
@@ -162,11 +163,11 @@ var cursor = 0
text: "←"
disabled: !(results.len > 1 && (results.len - cursor) > 1)
onClick: back
- } {
+ }, {
text: "→"
disabled: !(results.len > 1 && cursor > 0)
onClick: forward
- } {
+ }, {
text: "引き直す"
onClick: do
}]
@@ -187,27 +188,27 @@ var cursor = 0
do()
`;
-const PRESET_QUIZ = `/// @ 0.16.0
+const PRESET_QUIZ = `/// @ 0.18.0
let title = '地理クイズ'
let qas = [{
q: 'オーストラリアの首都は?'
- choices: ['シドニー' 'キャンベラ' 'メルボルン']
+ choices: ['シドニー', 'キャンベラ', 'メルボルン']
a: 'キャンベラ'
aDescription: '最大の都市はシドニーですが首都はキャンベラです。'
-} {
+}, {
q: '国土面積2番目の国は?'
- choices: ['カナダ' 'アメリカ' '中国']
+ choices: ['カナダ', 'アメリカ', '中国']
a: 'カナダ'
aDescription: '大きい順にロシア、カナダ、アメリカ、中国です。'
-} {
+}, {
q: '二重内陸国ではないのは?'
- choices: ['リヒテンシュタイン' 'ウズベキスタン' 'レソト']
+ choices: ['リヒテンシュタイン', 'ウズベキスタン', 'レソト']
a: 'レソト'
aDescription: 'レソトは(一重)内陸国です。'
-} {
+}, {
q: '閘門がない運河は?'
- choices: ['キール運河' 'スエズ運河' 'パナマ運河']
+ choices: ['キール運河', 'スエズ運河', 'パナマ運河']
a: 'スエズ運河'
aDescription: 'スエズ運河は高低差がないので閘門はありません。'
}]
@@ -243,9 +244,9 @@ each (let qa, qas) {
})
Ui:C:container({
children: []
- } \`{qa.id}:a\`)
+ }, \`{qa.id}:a\`)
]
- } qa.id))
+ }, qa.id))
}
@finish() {
@@ -295,12 +296,12 @@ qaEls.push(Ui:C:container({
onClick: finish
})
]
-} 'footer'))
+}, 'footer'))
Ui:render(qaEls)
`;
-const PRESET_TIMELINE = `/// @ 0.16.0
+const PRESET_TIMELINE = `/// @ 0.18.0
// APIリクエストを行いローカルタイムラインを表示するプリセット
@fetch() {
@@ -314,7 +315,7 @@ const PRESET_TIMELINE = `/// @ 0.16.0
])
// タイムライン取得
- let notes = Mk:api("notes/local-timeline" {})
+ let notes = Mk:api("notes/local-timeline", {})
// それぞれのノートごとにUI要素作成
let noteEls = []
@@ -367,7 +368,7 @@ const props = defineProps<{
}>();
const flash = ref<Misskey.entities.Flash | null>(null);
-const visibility = ref<Misskey.entities.FlashUpdateRequest['visibility']>('public');
+const visibility = ref<'private' | 'public'>('public');
if (props.id) {
flash.value = await misskeyApi('flash/show', {
@@ -420,6 +421,7 @@ async function save() {
summary: summary.value,
permissions: permissions.value,
script: script.value,
+ visibility: visibility.value,
});
router.push('/play/' + created.id + '/edit');
}
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 4aa3ce1672..40499fde0e 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -15,11 +15,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAsUi v-if="root" :component="root" :components="components"/>
</div>
<div class="actions _panel">
- <MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
- <MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
- <MkButton v-tooltip="i18n.ts.shareWithNote" class="button" rounded @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></MkButton>
- <MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ti ti-link ti-fw"></i></MkButton>
- <MkButton v-if="isSupportShare()" v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
+ <div class="items">
+ <MkButton v-tooltip="i18n.ts.reload" class="button" rounded @click="reset"><i class="ti ti-reload"></i></MkButton>
+ </div>
+ <div class="items">
+ <MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ti ti-heart"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
+ <MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
+ <MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ti ti-link ti-fw"></i></MkButton>
+ <MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
+ </div>
</div>
</div>
<div v-else :class="$style.ready">
@@ -49,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--accent);">{{ i18n.ts._play.editThisPage }}</MkA>
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
</div>
- <MkError v-else-if="error" @retry="fetchPage()"/>
+ <MkError v-else-if="error" @retry="fetchFlash()"/>
<MkLoading v-else/>
</Transition>
</MkSpacer>
@@ -94,12 +98,33 @@ function fetchFlash() {
});
}
+function share(ev: MouseEvent) {
+ if (!flash.value) return;
+
+ os.popupMenu([
+ {
+ text: i18n.ts.shareWithNote,
+ icon: 'ti ti-pencil',
+ action: shareWithNote,
+ },
+ ...(isSupportShare() ? [{
+ text: i18n.ts.share,
+ icon: 'ti ti-share',
+ action: shareWithNavigator,
+ }] : []),
+ ], ev.currentTarget ?? ev.target);
+}
+
function copyLink() {
+ if (!flash.value) return;
+
copyToClipboard(`${url}/play/${flash.value.id}`);
os.success();
}
-function share() {
+function shareWithNavigator() {
+ if (!flash.value) return;
+
navigator.share({
title: flash.value.title,
text: flash.value.summary,
@@ -108,21 +133,28 @@ function share() {
}
function shareWithNote() {
+ if (!flash.value) return;
+
os.post({
- initialText: `${flash.value.title} ${url}/play/${flash.value.id}`,
+ initialText: `${flash.value.title}\n${url}/play/${flash.value.id}`,
+ instant: true,
});
}
function like() {
+ if (!flash.value) return;
+
os.apiWithDialog('flash/like', {
flashId: flash.value.id,
}).then(() => {
- flash.value.isLiked = true;
- flash.value.likedCount++;
+ flash.value!.isLiked = true;
+ flash.value!.likedCount++;
});
}
async function unlike() {
+ if (!flash.value) return;
+
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.unlikeConfirm,
@@ -131,8 +163,8 @@ async function unlike() {
os.apiWithDialog('flash/unlike', {
flashId: flash.value.id,
}).then(() => {
- flash.value.isLiked = false;
- flash.value.likedCount--;
+ flash.value!.isLiked = false;
+ flash.value!.likedCount--;
});
}
@@ -152,6 +184,7 @@ function start() {
async function run() {
if (aiscript.value) aiscript.value.abort();
+ if (!flash.value) return;
aiscript.value = new Interpreter({
...createAiScriptEnv({
@@ -193,12 +226,17 @@ async function run() {
}
}
-onDeactivated(() => {
+function reset() {
if (aiscript.value) aiscript.value.abort();
+ started.value = false;
+}
+
+onDeactivated(() => {
+ reset();
});
onUnmounted(() => {
- if (aiscript.value) aiscript.value.abort();
+ reset();
});
const headerActions = computed(() => []);
@@ -265,11 +303,19 @@ definePageMetadata(() => ({
}
> .actions {
- display: flex;
- justify-content: center;
- gap: 12px;
margin-top: 16px;
- padding: 16px;
+
+ > .items {
+ display: flex;
+ justify-content: center;
+ gap: 12px;
+ padding: 16px;
+ border-bottom: 1px solid var(--divider);
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
}
}
}
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index cb7fe2866c..26797ba85e 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -35,7 +35,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection v-if="iAmModerator">
<template #label>Moderation</template>
<div class="_gaps_s">
- <MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
+ <MkKeyValue>
+ <template #key>
+ {{ i18n.ts._delivery.status }}
+ </template>
+ <template #value>
+ {{ i18n.ts._delivery._type[suspensionState] }}
+ </template>
+ </MkKeyValue>
+ <MkButton v-if="suspensionState === 'none'" :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton>
+ <MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
<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>
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
@@ -155,7 +164,7 @@ const tab = ref('overview');
const chartSrc = ref('instance-requests');
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
const instance = ref<Misskey.entities.FederationInstance | null>(null);
-const suspended = ref(false);
+const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none');
const isBlocked = ref(false);
const isSilenced = ref(false);
const faviconUrl = ref<string | null>(null);
@@ -183,7 +192,7 @@ async function fetch(): Promise<void> {
instance.value = await misskeyApi('federation/show-instance', {
host: props.host,
});
- suspended.value = instance.value?.isSuspended ?? false;
+ suspensionState.value = instance.value?.suspensionState ?? 'none';
isBlocked.value = instance.value?.isBlocked ?? false;
isSilenced.value = instance.value?.isSilenced ?? false;
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
@@ -209,11 +218,21 @@ async function toggleSilenced(): Promise<void> {
});
}
-async function toggleSuspend(): Promise<void> {
+async function stopDelivery(): Promise<void> {
if (!instance.value) throw new Error('No instance?');
+ suspensionState.value = 'manuallySuspended';
await misskeyApi('admin/federation/update-instance', {
host: instance.value.host,
- isSuspended: suspended.value,
+ isSuspended: true,
+ });
+}
+
+async function resumeDelivery(): Promise<void> {
+ if (!instance.value) throw new Error('No instance?');
+ suspensionState.value = 'none';
+ await misskeyApi('admin/federation/update-instance', {
+ host: instance.value.host,
+ isSuspended: false,
});
}
diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue
index 8b3b3cfbfd..2d026d2fa9 100644
--- a/packages/frontend/src/pages/my-antennas/create.vue
+++ b/packages/frontend/src/pages/my-antennas/create.vue
@@ -26,6 +26,7 @@ const draft = ref({
users: [],
keywords: [],
excludeKeywords: [],
+ excludeBots: false,
withReplies: false,
caseSensitive: false,
localOnly: false,
diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue
index c6dcbadd9b..2949bfc02c 100644
--- a/packages/frontend/src/pages/my-antennas/editor.vue
+++ b/packages/frontend/src/pages/my-antennas/editor.vue
@@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.users }}</template>
<template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template>
</MkTextarea>
+ <MkSwitch v-model="excludeBots">{{ i18n.ts.antennaExcludeBots }}</MkSwitch>
<MkSwitch v-model="withReplies">{{ i18n.ts.withReplies }}</MkSwitch>
<MkTextarea v-model="keywords">
<template #label>{{ i18n.ts.antennaKeywords }}</template>
@@ -38,7 +39,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
<MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch>
<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
- <MkSwitch v-model="notify">{{ i18n.ts.notifyAntenna }}</MkSwitch>
</div>
<div :class="$style.actions">
<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
@@ -78,9 +78,9 @@ const keywords = ref<string>(props.antenna.keywords.map(x => x.join(' ')).join('
const excludeKeywords = ref<string>(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
const caseSensitive = ref<boolean>(props.antenna.caseSensitive);
const localOnly = ref<boolean>(props.antenna.localOnly);
+const excludeBots = ref<boolean>(props.antenna.excludeBots);
const withReplies = ref<boolean>(props.antenna.withReplies);
const withFile = ref<boolean>(props.antenna.withFile);
-const notify = ref<boolean>(props.antenna.notify);
const userLists = ref<Misskey.entities.UserList[] | null>(null);
watch(() => src.value, async () => {
@@ -94,9 +94,9 @@ async function saveAntenna() {
name: name.value,
src: src.value,
userListId: userListId.value,
+ excludeBots: excludeBots.value,
withReplies: withReplies.value,
withFile: withFile.value,
- notify: notify.value,
caseSensitive: caseSensitive.value,
localOnly: localOnly.value,
users: users.value.trim().split('\n').map(x => x.trim()),
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index 803b28899a..1a0d7177fc 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -11,16 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="tab === 'my'" key="my" class="_gaps">
<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
- <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps">
- <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">
- <MkClipPreview :clip="item"/>
- </MkA>
+ <MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps">
+ <MkClipPreview v-for="item in items" :key="item.id" :clip="item"/>
</MkPagination>
</div>
<div v-else-if="tab === 'favorites'" key="favorites" class="_gaps">
- <MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`">
- <MkClipPreview :clip="item"/>
- </MkA>
+ <MkClipPreview v-for="item in favorites" :key="item.id" :clip="item"/>
</div>
</MkHorizontalSwipe>
</MkSpacer>
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 4c985b96e6..97f32d35cd 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -21,14 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div class="_margin _gaps_s">
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
- <MkNoteDetailed :key="note.id" v-model:note="note" :class="$style.note"/>
+ <MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note"/>
</div>
<div v-if="clips && clips.length > 0" class="_margin">
<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
<div class="_gaps">
- <MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`">
- <MkClipPreview :clip="item"/>
- </MkA>
+ <MkClipPreview v-for="item in clips" :key="item.id" :clip="item"/>
</div>
</div>
<div v-if="!showPrev" class="_buttons" :class="$style.loadPrev">
@@ -66,6 +64,7 @@ import { defaultStore } from '@/store.js';
const props = defineProps<{
noteId: string;
+ initialTab?: string;
}>();
const note = ref<null | Misskey.entities.Note>();
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
index 194a276f89..0a28386986 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XContainer :draggable="true" @remove="() => $emit('remove')">
<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
- <section style="padding: 0 16px 0 16px;">
+ <section style="padding: 16px;" class="_gaps_s">
<MkInput v-model="id">
<template #label>{{ i18n.ts._pages.blocks._note.id }}</template>
<template #caption>{{ i18n.ts._pages.blocks._note.idDescription }}</template>
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index bece32fc11..e73d032000 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -6,48 +6,80 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :contentMax="700">
- <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
- <div v-if="page" :key="page.id" class="xcukqgmh">
- <div class="main">
- <!--
- <div class="header">
- <h1>{{ page.title }}</h1>
- </div>
- -->
- <div class="banner">
- <MkMediaImage
- v-if="page.eyeCatchingImageId"
- :image="page.eyeCatchingImage"
- :cover="true"
- :disableImageLink="true"
- class="thumbnail"
- />
+ <MkSpacer :contentMax="800">
+ <Transition
+ :enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
+ mode="out-in"
+ >
+ <div v-if="page" :key="page.id" class="_gaps">
+ <div :class="$style.pageMain">
+ <div :class="$style.pageBanner">
+ <div :class="$style.pageBannerBgRoot">
+ <MkImgWithBlurhash
+ v-if="page.eyeCatchingImageId"
+ :class="$style.pageBannerBg"
+ :hash="page.eyeCatchingImage?.blurhash"
+ :cover="true"
+ :forceBlurhash="true"
+ />
+ <img
+ v-else-if="instance.backgroundImageUrl || instance.bannerUrl"
+ :class="[$style.pageBannerBg, $style.pageBannerBgFallback1]"
+ :src="getStaticImageUrl(instance.backgroundImageUrl ?? instance.bannerUrl!)"
+ />
+ <div v-else :class="[$style.pageBannerBg, $style.pageBannerBgFallback2]"></div>
+ </div>
+ <div v-if="page.eyeCatchingImageId" :class="$style.pageBannerImage">
+ <MkMediaImage
+ :image="page.eyeCatchingImage!"
+ :cover="true"
+ :disableImageLink="true"
+ :class="$style.thumbnail"
+ />
+ </div>
+ <div :class="$style.pageBannerTitle" class="_gaps_s">
+ <h1>{{ page.title || page.name }}</h1>
+ <div :class="$style.pageBannerTitleSub">
+ <div v-if="page.user" :class="$style.pageBannerTitleUser">
+ <MkAvatar :user="page.user" :class="$style.avatar" indicator link preview/> <MkA :to="`/@${username}`"><MkUserName :user="page.user" :nowrap="false"/></MkA>
+ </div>
+ <div :class="$style.pageBannerTitleSubActions">
+ <MkA v-if="page.userId === $i?.id" v-tooltip="i18n.ts._pages.editThisPage" :to="`/pages/edit/${page.id}`" class="_button" :class="$style.generalActionButton"><i class="ti ti-pencil ti-fw"></i></MkA>
+ <button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button>
+ </div>
+ </div>
+ </div>
</div>
- <div class="content">
+ <div :class="$style.pageContent">
<XPage :page="page"/>
</div>
- <div class="actions">
- <div class="like">
+ <div :class="$style.pageActions">
+ <div>
<MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
<MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
</div>
- <div class="other">
- <button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button>
- <button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
- <button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ti ti-share ti-fw"></i></button>
+ <div :class="$style.other">
+ <button v-tooltip="i18n.ts.copyLink" class="_button" :class="$style.generalActionButton" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
+ <button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button>
</div>
</div>
- <div class="user">
- <MkAvatar :user="page.user" class="avatar" link preview/>
- <div class="name">
- <MkUserName :user="page.user" style="display: block;"/>
- <MkAcct :user="page.user"/>
- </div>
- <MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
+ <div :class="$style.pageUser">
+ <MkAvatar :user="page.user" :class="$style.avatar" link preview/>
+ <MkA :to="`/@${username}`">
+ <MkUserName :user="page.user" :class="$style.name"/>
+ <MkAcct :user="page.user" :class="$style.acct"/>
+ </MkA>
+ <MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user!" :inline="true" :transparent="false" :full="true" :class="$style.follow"/>
</div>
- <div class="links">
- <MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
+ <div :class="$style.pageDate">
+ <div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
+ <div v-if="page.createdAt != page.updatedAt"><i class="ti ti-clock-edit"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
+ </div>
+ <div :class="$style.pageLinks">
+ <MkA v-if="!$i || $i.id !== page.userId" :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
<template v-if="$i && $i.id === page.userId">
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ i18n.ts._pages.editThisPage }}</MkA>
<button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button>
@@ -55,10 +87,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</div>
</div>
- <div class="footer">
- <div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
- <div v-if="page.createdAt != page.updatedAt"><i class="ti ti-clock"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
- </div>
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
<MkContainer :max-height="300" :foldable="true" class="other">
<template #icon><i class="ti ti-clock"></i></template>
@@ -84,6 +112,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { url } from '@/config.js';
import MkMediaImage from '@/components/MkMediaImage.vue';
+import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
import MkContainer from '@/components/MkContainer.vue';
import MkPagination from '@/components/MkPagination.vue';
@@ -94,6 +123,8 @@ import { pageViewInterruptors, defaultStore } from '@/store.js';
import { deepClone } from '@/scripts/clone.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
+import { instance } from '@/instance.js';
+import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{
@@ -133,35 +164,63 @@ function fetchPage() {
});
}
-function share() {
- navigator.share({
- title: page.value.title ?? page.value.name,
- text: page.value.summary,
- url: `${url}/@${page.value.user.username}/pages/${page.value.name}`,
- });
+function share(ev: MouseEvent) {
+ if (!page.value) return;
+
+ os.popupMenu([
+ {
+ text: i18n.ts.shareWithNote,
+ icon: 'ti ti-pencil',
+ action: shareWithNote,
+ },
+ ...(isSupportShare() ? [{
+ text: i18n.ts.share,
+ icon: 'ti ti-share',
+ action: shareWithNavigator,
+ }] : []),
+ ], ev.currentTarget ?? ev.target);
}
function copyLink() {
+ if (!page.value) return;
+
copyToClipboard(`${url}/@${page.value.user.username}/pages/${page.value.name}`);
os.success();
}
function shareWithNote() {
+ if (!page.value) return;
+
os.post({
- initialText: `${page.value.title || page.value.name} ${url}/@${page.value.user.username}/pages/${page.value.name}`,
+ initialText: `${page.value.title || page.value.name}\n${url}/@${page.value.user.username}/pages/${page.value.name}`,
+ instant: true,
+ });
+}
+
+function shareWithNavigator() {
+ if (!page.value) return;
+
+ navigator.share({
+ title: page.value.title ?? page.value.name,
+ text: page.value.summary ?? undefined,
+ url: `${url}/@${page.value.user.username}/pages/${page.value.name}`,
});
}
function like() {
+ if (!page.value) return;
+
os.apiWithDialog('pages/like', {
pageId: page.value.id,
}).then(() => {
- page.value.isLiked = true;
- page.value.likedCount++;
+ page.value!.isLiked = true;
+ page.value!.likedCount++;
});
}
async function unlike() {
+ if (!page.value) return;
+
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.unlikeConfirm,
@@ -170,12 +229,14 @@ async function unlike() {
os.apiWithDialog('pages/unlike', {
pageId: page.value.id,
}).then(() => {
- page.value.isLiked = false;
- page.value.likedCount--;
+ page.value!.isLiked = false;
+ page.value!.likedCount--;
});
}
function pin(pin) {
+ if (!page.value) return;
+
os.apiWithDialog('i/update', {
pinnedPageId: pin ? page.value.id : null,
});
@@ -200,109 +261,200 @@ definePageMetadata(() => ({
}));
</script>
-<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
+<style lang="scss" module>
+.fadeEnterActive,
+.fadeLeaveActive {
transition: opacity 0.125s ease;
}
-.fade-enter-from,
-.fade-leave-to {
+.fadeEnterFrom,
+.fadeLeaveTo {
opacity: 0;
}
-.xcukqgmh {
- > .main {
- padding: 32px;
+.generalActionButton {
+ height: 2.5rem;
+ width: 2.5rem;
+ text-align: center;
+ border-radius: 99rem;
- > .header {
- padding: 16px;
+ & :global(.ti) {
+ line-height: 2.5rem;
+ }
- > h1 {
- margin: 0;
- }
+ &:hover,
+ &:focus-visible {
+ background-color: var(--accentedBg);
+ color: var(--accent);
+ text-decoration: none;
+ }
+}
+
+.pageMain {
+ border-radius: var(--radius);
+ padding: 2rem;
+ background: var(--panel);
+ box-sizing: border-box;
+}
+
+.pageBanner {
+ width: calc(100% + 4rem);
+ margin: -2rem -2rem 1.5rem;
+ border-radius: var(--radius) var(--radius) 0 0;
+ overflow: hidden;
+ position: relative;
+
+ > .pageBannerBgRoot {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+
+ .pageBannerBg {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ opacity: .2;
+ filter: brightness(1.2);
}
- > .banner {
- > .thumbnail {
- // TODO: 良い感じのアスペクト比で表示
- display: block;
- width: 100%;
- height: auto;
- aspect-ratio: 3/1;
- border-radius: var(--radius);
- overflow: hidden;
- object-fit: cover;
- }
+ .pageBannerBgFallback1 {
+ filter: blur(20px);
}
- > .content {
- margin-top: 16px;
- padding: 16px 0 0 0;
+ .pageBannerBgFallback2 {
+ background-color: var(--accentedBg);
}
- > .actions {
- display: flex;
- align-items: center;
- margin-top: 16px;
- padding: 16px 0 0 0;
- border-top: solid 0.5px var(--divider);
+ &::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100px;
+ background: linear-gradient(0deg, var(--panel), transparent);
+ }
+ }
- > .other {
- margin-left: auto;
+ > .pageBannerImage {
+ position: relative;
+ padding-top: 56.25%;
- > button {
- padding: 8px;
- margin: 0 8px;
+ > .thumbnail {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+ }
- &:hover {
- color: var(--fgHighlighted);
- }
- }
- }
+ > .pageBannerTitle {
+ position: relative;
+ padding: 1.5rem 2rem;
+
+ h1 {
+ font-size: 2rem;
+ font-weight: 700;
+ color: var(--fg);
+ margin: 0;
}
- > .user {
- margin-top: 16px;
- padding: 16px 0 0 0;
- border-top: solid 0.5px var(--divider);
+ .pageBannerTitleSub {
display: flex;
align-items: center;
+ width: 100%;
+ }
- > .avatar {
- width: 52px;
- height: 52px;
- }
+ .pageBannerTitleUser {
+ --height: 32px;
+ flex-shrink: 0;
- > .name {
- margin: 0 0 0 12px;
- font-size: 90%;
+ .avatar {
+ height: var(--height);
+ width: var(--height);
}
- > .koudoku {
- margin-left: auto;
- }
+ line-height: var(--height);
}
- > .links {
- margin-top: 16px;
- padding: 24px 0 0 0;
- border-top: solid 0.5px var(--divider);
-
- > .link {
- margin-right: 0.75em;
- }
+ .pageBannerTitleSubActions {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ gap: var(--marginHalf);
+ margin-left: auto;
}
}
+}
+
+.pageContent {
+ margin-bottom: 1.5rem;
+}
+
+.pageActions {
+ display: flex;
+ align-items: center;
+
+ border-top: 1px solid var(--divider);
+ padding-top: 1.5rem;
+ margin-bottom: 1.5rem;
+
+ > .other {
+ margin-left: auto;
+ display: flex;
+ gap: var(--marginHalf);
+ }
+}
+
+.pageUser {
+ display: flex;
+ align-items: center;
+
+ border-top: 1px solid var(--divider);
+ padding-top: 1.5rem;
+ margin-bottom: 1.5rem;
+
+ .avatar,
+ .name,
+ .acct {
+ display: block;
+ }
+
+ .avatar {
+ width: 4rem;
+ height: 4rem;
+ margin-right: 1rem;
+ }
- > .footer {
- margin: var(--margin) 0 var(--margin) 0;
- font-size: 85%;
- opacity: 0.75;
+ .name {
+ font-size: 110%;
+ font-weight: 700;
+ }
+
+ .acct {
+ font-size: 90%;
+ opacity: 0.7;
+ }
+
+ .follow {
+ margin-left: auto;
}
}
-</style>
-<style module>
+.pageDate {
+ margin-bottom: 1.5rem;
+}
+
+.pageLinks {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: var(--marginHalf);
+}
+
.relatedPagesRoot {
padding: var(--margin);
}
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 5259dfa29a..175ea62411 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -151,6 +151,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import { deepClone } from '@/scripts/clone.js';
import { useInterval } from '@/scripts/use-interval.js';
import { signinRequired } from '@/account.js';
+import { url } from '@/config.js';
import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { userPage } from '@/filters/user.js';
@@ -442,7 +443,7 @@ function autoplay() {
function share() {
os.post({
- initialText: `#MisskeyReversi ${location.href}`,
+ initialText: `#MisskeyReversi\n${url}/reversi/g/${game.value.id}`,
instant: true,
});
}
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 2608560cc4..2244047b31 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -25,6 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div style="height: 100cqh; overflow: auto; text-align: center;">
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps">
+ <MkInfo><MkLink url="https://misskey-hub.net/docs/for-users/stepped-guides/how-to-enable-2fa/" target="_blank">{{ i18n.ts._2fa.moreDetailedGuideHere }}</MkLink></MkInfo>
+
<I18n :src="i18n.ts._2fa.step1" tag="div">
<template #a>
<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
@@ -33,8 +35,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a>
</template>
</I18n>
- <div>{{ i18n.ts._2fa.step2 }}<br>{{ i18n.ts._2fa.step2Click }}</div>
- <a :href="twoFactorData.url"><img :class="$style.qr" :src="twoFactorData.qr"></a>
+ <div>{{ i18n.ts._2fa.step2 }}</div>
+ <div>
+ <a :class="$style.qrRoot" :href="twoFactorData.url"><img :class="$style.qr" :src="twoFactorData.qr"></a>
+ <!-- QRコード側にマージンが入っているので直下でOK -->
+ <div><MkButton inline rounded link :to="twoFactorData.url" :linkBehavior="'browser'">{{ i18n.ts.launchApp }}</MkButton></div>
+ </div>
<MkKeyValue :copy="twoFactorData.url">
<template #key>{{ i18n.ts._2fa.step2Uri }}</template>
<template #value>{{ twoFactorData.url }}</template>
@@ -52,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps">
<div>{{ i18n.ts._2fa.step3Title }}</div>
- <MkInput v-model="token" autocomplete="one-time-code"></MkInput>
+ <MkInput v-model="token" autocomplete="one-time-code" inputmode="numeric"></MkInput>
<div>{{ i18n.ts._2fa.step3 }}</div>
</div>
<div class="_buttonsCenter" style="margin-top: 16px;">
@@ -109,6 +115,7 @@ import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
+import MkLink from '@/components/MkLink.vue';
import { confetti } from '@/scripts/confetti.js';
import { signinRequired } from '@/account.js';
@@ -177,8 +184,14 @@ function allDone() {
transform: translateX(-50px);
}
-.qr {
+.qrRoot {
+ display: block;
+ margin: 0 auto;
width: 200px;
max-width: 100%;
}
+
+.qr {
+ width: 100%;
+}
</style>
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index d8c5f848fe..b7d648c1a4 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -30,7 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-else danger @click="unregisterTOTP">{{ i18n.ts.unregister }}</MkButton>
</div>
- <MkButton v-else-if="!$i.twoFactorEnabled" primary gradate @click="registerTOTP">{{ i18n.ts._2fa.registerTOTP }}</MkButton>
+ <div v-else-if="!$i.twoFactorEnabled" class="_gaps_s">
+ <MkButton primary gradate @click="registerTOTP">{{ i18n.ts._2fa.registerTOTP }}</MkButton>
+ <MkLink url="https://misskey-hub.net/docs/for-users/stepped-guides/how-to-enable-2fa/" target="_blank"><i class="ti ti-help-circle"></i> {{ i18n.ts.learnMore }}</MkLink>
+ </div>
</MkFolder>
<MkFolder>
@@ -79,8 +82,9 @@ import MkInfo from '@/components/MkInfo.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
+import MkLink from '@/components/MkLink.vue';
import * as os from '@/os.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired, updateAccount } from '@/account.js';
import { i18n } from '@/i18n.js';
const $i = signinRequired();
@@ -116,6 +120,10 @@ async function unregisterTOTP(): Promise<void> {
os.apiWithDialog('i/2fa/unregister', {
password: auth.result.password,
token: auth.result.token,
+ }).then(res => {
+ updateAccount({
+ twoFactorEnabled: false,
+ });
}).catch(error => {
os.alert({
type: 'error',
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 1919f80864..81a8d474d2 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -44,6 +44,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.keepOriginalUploading }}</template>
<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
</MkSwitch>
+ <MkSwitch v-model="keepOriginalFilename">
+ <template #label>{{ i18n.ts.keepOriginalFilename }}</template>
+ <template #caption>{{ i18n.ts.keepOriginalFilenameDescription }}</template>
+ </MkSwitch>
<MkSwitch v-model="alwaysMarkNsfw" @update:modelValue="saveProfile()">
<template #label>{{ i18n.ts.alwaysMarkSensitive }}</template>
</MkSwitch>
@@ -96,6 +100,7 @@ const meterStyle = computed(() => {
});
const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading'));
+const keepOriginalFilename = computed(defaultStore.makeGetterSetter('keepOriginalFilename'));
misskeyApi('drive').then(info => {
capacity.value = info.capacity;
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index d13b6884bd..cfc63f2a08 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -50,12 +50,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<div class="_gaps_s">
+ <MkSwitch v-model="collapseRenotes">
+ <template #label>{{ i18n.ts.collapseRenotes }}</template>
+ <template #caption>{{ i18n.ts.collapseRenotesDescription }}</template>
+ </MkSwitch>
<MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch>
<MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch>
- <MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch>
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
<MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch>
+ <MkSwitch v-model="showReactionsCount">{{ i18n.ts.showReactionsCount }}</MkSwitch>
<MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
<MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
<MkRadios v-model="reactionsDisplaySize">
@@ -131,6 +135,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
<MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch>
+ <MkSwitch v-model="useNativeUIForVideoAudioPlayer">{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</MkSwitch>
</div>
<div>
<MkRadios v-model="emojiStyle">
@@ -163,6 +168,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
+ <MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</MkSwitch>
</div>
<MkSelect v-model="serverDisconnectedBehavior">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@@ -281,6 +287,7 @@ const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm'));
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'));
@@ -306,6 +313,8 @@ const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disable
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect'));
const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
+const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer'));
+const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
@@ -347,6 +356,7 @@ watch([
keepScreenOn,
disableStreamingTimeline,
enableSeasonalScreenEffect,
+ alwaysConfirmFollow,
], async () => {
await reloadAsk();
});
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index 0ab75b95a2..9804454e66 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -42,12 +42,25 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkFolder>
+ <template #icon><i class="ti ti-terminal-2"></i></template>
+ <template #label>{{ i18n.ts._plugin.viewLog }}</template>
+
+ <div class="_gaps_s">
+ <div class="_buttons">
+ <MkButton inline @click="copy(pluginLogs.get(plugin.id)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+ </div>
+
+ <MkCode :code="pluginLogs.get(plugin.id)?.join('\n') ?? ''"/>
+ </div>
+ </MkFolder>
+
+ <MkFolder>
<template #icon><i class="ti ti-code"></i></template>
<template #label>{{ i18n.ts._plugin.viewSource }}</template>
<div class="_gaps_s">
<div class="_buttons">
- <MkButton inline @click="copy(plugin)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+ <MkButton inline @click="copy(plugin.src)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
</div>
<MkCode :code="plugin.src ?? ''" lang="is"/>
@@ -74,6 +87,7 @@ import { ColdDeviceStorage } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { pluginLogs } from '@/plugin.js';
const plugins = ref(ColdDeviceStorage.get('plugins'));
@@ -87,8 +101,8 @@ async function uninstall(plugin) {
});
}
-function copy(plugin) {
- copyToClipboard(plugin.src ?? '');
+function copy(text) {
+ copyToClipboard(text ?? '');
os.success();
}
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index 942de19d82..b6f1043154 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -70,6 +70,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'animation',
'animatedMfm',
'advancedMfm',
+ 'showReactionsCount',
'loadRawImages',
'imageNewTab',
'dataSaver',
diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue
index 680934e7ce..37f6558d64 100644
--- a/packages/frontend/src/pages/share.vue
+++ b/packages/frontend/src/pages/share.vue
@@ -64,7 +64,34 @@ async function init() {
// Googleニュース対策
if (text?.startsWith(`${title.value}.\n`)) noteText += text.replace(`${title.value}.\n`, '');
else if (text && title.value !== text) noteText += `${text}\n`;
- if (url) noteText += `${url}`;
+ if (url) {
+ try {
+ // Normalize the URL to URL-encoded and puny-coded from with the URL constructor.
+ //
+ // It's common to use unicode characters in the URL for better visibility of URL
+ // like: https://ja.wikipedia.org/wiki/ミスキー
+ // or like: https://藍.moe/
+ // However, in the MFM, the unicode characters must be URL-encoded to be parsed as `url` node
+ // like: https://ja.wikipedia.org/wiki/%E3%83%9F%E3%82%B9%E3%82%AD%E3%83%BC
+ // or like: https://xn--931a.moe/
+ // Therefore, we need to normalize the URL to URL-encoded form.
+ //
+ // The URL constructor will parse the URL and normalize unicode characters
+ // in the host to punycode and in the path component to URL-encoded form.
+ // (see url.spec.whatwg.org)
+ //
+ // In addition, the current MFM renderer decodes the URL-encoded path and / punycode encoded host name so
+ // this normalization doesn't make the visible URL ugly.
+ // (see MkUrl.vue)
+
+ noteText += new URL(url).href;
+ } catch {
+ // fallback to original URL if the URL is invalid.
+ // note that this is extremely rare since the `url` parameter is designed to share a URL and
+ // the URL constructor will throw TypeError only if failure, which means the URL is not valid.
+ noteText += url;
+ }
+ }
initialText.value = noteText.trim();
if (visibility.value === 'specified') {
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 48dfc1fd44..98744c6318 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -48,7 +48,7 @@ import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { $i } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { antennasCache, userListsCache } from '@/cache.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';
@@ -173,9 +173,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> {
}
async function chooseChannel(ev: MouseEvent): Promise<void> {
- const channels = await misskeyApi('channels/my-favorites', {
- limit: 100,
- });
+ const channels = await favoritedChannelsCache.fetch();
const items: MenuItem[] = [
...channels.map(channel => {
const lastReadedAt = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.id}`) ?? null;
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index 89bb010dd6..d6ba397f1b 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -9,7 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<XTimeline class="tl"/>
<div class="shape1"></div>
<div class="shape2"></div>
- <img :src="misskeysvg" class="misskey"/>
+ <div class="logo-wrapper">
+ <div class="powered-by">Powered by</div>
+ <img :src="misskeysvg" class="misskey"/>
+ </div>
<div class="emojis">
<MkEmoji :normal="true" :noStyle="true" emoji="👍"/>
<MkEmoji :normal="true" :noStyle="true" emoji="❤"/>
@@ -39,11 +42,11 @@ import XTimeline from './welcome.timeline.vue';
import MarqueeText from '@/components/MkMarquee.vue';
import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue';
import misskeysvg from '/client-assets/misskey.svg';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { instance as meta } from '@/instance.js';
-const meta = ref<Misskey.entities.MetaResponse>();
const instances = ref<Misskey.entities.FederationInstance[]>();
function getInstanceIcon(instance: Misskey.entities.FederationInstance): string {
@@ -53,10 +56,6 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string
return getProxiedImageUrl(instance.iconUrl, 'preview');
}
-misskeyApi('meta', { detail: true }).then(_meta => {
- meta.value = _meta;
-});
-
misskeyApiGet('federation/instances', {
sort: '+pubSub',
limit: 20,
@@ -113,14 +112,24 @@ misskeyApiGet('federation/instances', {
opacity: 0.5;
}
- > .misskey {
+ > .logo-wrapper {
position: fixed;
- top: 42px;
- left: 42px;
- width: 140px;
+ top: 36px;
+ left: 36px;
+ flex: auto;
+ color: #fff;
+ user-select: none;
+ pointer-events: none;
+
+ > .powered-by {
+ margin-bottom: 2px;
+ }
- @media (max-width: 450px) {
- width: 130px;
+ > .misskey {
+ width: 140px;
+ @media (max-width: 450px) {
+ width: 130px;
+ }
}
}
diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue
index 9ba6a5885e..915fe35025 100644
--- a/packages/frontend/src/pages/welcome.vue
+++ b/packages/frontend/src/pages/welcome.vue
@@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div v-if="meta">
- <XSetup v-if="meta.requireSetup"/>
+<div v-if="instance">
+ <XSetup v-if="instance.requireSetup"/>
<XEntrance v-else/>
</div>
</template>
@@ -16,13 +16,13 @@ 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 { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { fetchInstance } from '@/instance.js';
-const meta = ref<Misskey.entities.MetaResponse | null>(null);
+const instance = ref<Misskey.entities.MetaDetailed | null>(null);
-misskeyApi('meta', { detail: true }).then(res => {
- meta.value = res;
+fetchInstance(true).then((res) => {
+ instance.value = res;
});
const headerActions = computed(() => []);