summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/_boot_.ts2
-rw-r--r--packages/frontend/src/account.ts87
-rw-r--r--packages/frontend/src/boot/common.ts29
-rw-r--r--packages/frontend/src/boot/main-boot.ts58
-rw-r--r--packages/frontend/src/components/MkAbuseReport.vue4
-rw-r--r--packages/frontend/src/components/MkAnnouncementDialog.vue4
-rw-r--r--packages/frontend/src/components/MkAntennaEditor.vue2
-rw-r--r--packages/frontend/src/components/MkAuthConfirm.stories.impl.ts7
-rw-r--r--packages/frontend/src/components/MkAuthConfirm.vue450
-rw-r--r--packages/frontend/src/components/MkCaptcha.vue4
-rw-r--r--packages/frontend/src/components/MkChannelPreview.vue3
-rw-r--r--packages/frontend/src/components/MkContainer.vue24
-rw-r--r--packages/frontend/src/components/MkCropperDialog.vue2
-rw-r--r--packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue2
-rw-r--r--packages/frontend/src/components/MkDateSeparatedList.vue8
-rw-r--r--packages/frontend/src/components/MkDialog.vue4
-rw-r--r--packages/frontend/src/components/MkDrive.vue50
-rw-r--r--packages/frontend/src/components/MkEmbedCodeGenDialog.vue2
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.section.vue2
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.vue4
-rw-r--r--packages/frontend/src/components/MkExtensionInstaller.vue2
-rw-r--r--packages/frontend/src/components/MkFoldableSection.vue52
-rw-r--r--packages/frontend/src/components/MkFolder.vue38
-rw-r--r--packages/frontend/src/components/MkFollowButton.vue14
-rw-r--r--packages/frontend/src/components/MkFormDialog.vue2
-rw-r--r--packages/frontend/src/components/MkInput.vue8
-rw-r--r--packages/frontend/src/components/MkMediaVideo.vue43
-rw-r--r--packages/frontend/src/components/MkModalWindow.vue10
-rw-r--r--packages/frontend/src/components/MkNote.vue34
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue37
-rw-r--r--packages/frontend/src/components/MkNotificationSelectWindow.vue2
-rw-r--r--packages/frontend/src/components/MkObjectView.value.vue10
-rw-r--r--packages/frontend/src/components/MkPageWindow.vue10
-rw-r--r--packages/frontend/src/components/MkPoll.vue6
-rw-r--r--packages/frontend/src/components/MkPopupMenu.vue2
-rw-r--r--packages/frontend/src/components/MkPostForm.vue40
-rw-r--r--packages/frontend/src/components/MkPostFormAttaches.vue8
-rw-r--r--packages/frontend/src/components/MkPostFormDialog.vue16
-rw-r--r--packages/frontend/src/components/MkRadio.vue8
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.details.vue3
-rw-r--r--packages/frontend/src/components/MkSelect.vue82
-rw-r--r--packages/frontend/src/components/MkSignin.password.vue10
-rw-r--r--packages/frontend/src/components/MkSignupDialog.form.vue4
-rw-r--r--packages/frontend/src/components/MkSignupDialog.vue12
-rw-r--r--packages/frontend/src/components/MkSuperMenu.vue33
-rw-r--r--packages/frontend/src/components/MkTimeline.vue6
-rw-r--r--packages/frontend/src/components/MkTokenGenerateWindow.vue2
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue10
-rw-r--r--packages/frontend/src/components/MkUserAnnouncementEditDialog.vue18
-rw-r--r--packages/frontend/src/components/MkUserSelectDialog.vue2
-rw-r--r--packages/frontend/src/components/MkUsersTooltip.vue4
-rw-r--r--packages/frontend/src/components/MkWidgets.vue2
-rw-r--r--packages/frontend/src/components/MkWindow.vue13
-rw-r--r--packages/frontend/src/components/form/suspense.vue6
-rw-r--r--packages/frontend/src/components/global/MkAd.vue6
-rw-r--r--packages/frontend/src/components/global/MkCustomEmoji.vue29
-rw-r--r--packages/frontend/src/components/global/MkMfm.ts4
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.tabs.vue27
-rw-r--r--packages/frontend/src/directives/adaptive-bg.ts12
-rw-r--r--packages/frontend/src/directives/adaptive-border.ts12
-rw-r--r--packages/frontend/src/directives/panel.ts12
-rw-r--r--packages/frontend/src/navbar.ts1
-rw-r--r--packages/frontend/src/nirax.ts12
-rw-r--r--packages/frontend/src/os.ts37
-rw-r--r--packages/frontend/src/pages/about-misskey.vue9
-rw-r--r--packages/frontend/src/pages/admin-user.vue1
-rw-r--r--packages/frontend/src/pages/admin/index.vue2
-rw-r--r--packages/frontend/src/pages/admin/moderation.vue21
-rw-r--r--packages/frontend/src/pages/admin/users.vue6
-rw-r--r--packages/frontend/src/pages/announcement.vue6
-rw-r--r--packages/frontend/src/pages/announcements.vue4
-rw-r--r--packages/frontend/src/pages/auth.vue2
-rw-r--r--packages/frontend/src/pages/avatar-decoration-edit-dialog.vue220
-rw-r--r--packages/frontend/src/pages/avatar-decorations.vue172
-rw-r--r--packages/frontend/src/pages/clip.vue14
-rw-r--r--packages/frontend/src/pages/custom-emojis-manager.vue26
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue33
-rw-r--r--packages/frontend/src/pages/emojis.emoji.vue28
-rw-r--r--packages/frontend/src/pages/follow-requests.vue109
-rw-r--r--packages/frontend/src/pages/invite.vue8
-rw-r--r--packages/frontend/src/pages/list.vue6
-rw-r--r--packages/frontend/src/pages/lookup.vue2
-rw-r--r--packages/frontend/src/pages/miauth.vue147
-rw-r--r--packages/frontend/src/pages/my-clips/index.vue6
-rw-r--r--packages/frontend/src/pages/my-lists/list.vue4
-rw-r--r--packages/frontend/src/pages/not-found.vue2
-rw-r--r--packages/frontend/src/pages/note.vue17
-rw-r--r--packages/frontend/src/pages/oauth.vue111
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue7
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue15
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue16
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue9
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.blocks.vue2
-rw-r--r--packages/frontend/src/pages/registry.keys.vue2
-rw-r--r--packages/frontend/src/pages/reversi/game.setting.vue6
-rw-r--r--packages/frontend/src/pages/role.vue24
-rw-r--r--packages/frontend/src/pages/scratchpad.vue6
-rw-r--r--packages/frontend/src/pages/settings/2fa.qrdialog.vue7
-rw-r--r--packages/frontend/src/pages/settings/2fa.vue4
-rw-r--r--packages/frontend/src/pages/settings/accounts.vue22
-rw-r--r--packages/frontend/src/pages/settings/apps.vue3
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.decoration.vue7
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.dialog.vue12
-rw-r--r--packages/frontend/src/pages/settings/navbar.vue4
-rw-r--r--packages/frontend/src/pages/settings/notifications.notification-config.vue50
-rw-r--r--packages/frontend/src/pages/settings/notifications.vue15
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue138
-rw-r--r--packages/frontend/src/pages/settings/webhook.edit.vue2
-rw-r--r--packages/frontend/src/pages/settings/webhook.new.vue2
-rw-r--r--packages/frontend/src/pages/timeline.vue8
-rw-r--r--packages/frontend/src/pages/user/home.vue2
-rw-r--r--packages/frontend/src/pages/user/index.vue18
-rw-r--r--packages/frontend/src/pizzax.ts12
-rw-r--r--packages/frontend/src/router/definition.ts4
-rw-r--r--packages/frontend/src/router/main.ts8
-rw-r--r--packages/frontend/src/scripts/check-word-mute.ts3
-rw-r--r--packages/frontend/src/scripts/device-kind.ts24
-rw-r--r--packages/frontend/src/scripts/form.ts16
-rw-r--r--packages/frontend/src/scripts/fullscreen.ts46
-rw-r--r--packages/frontend/src/scripts/get-bg-color.ts18
-rw-r--r--packages/frontend/src/scripts/misskey-api.ts6
-rw-r--r--packages/frontend/src/scripts/please-login.ts14
-rw-r--r--packages/frontend/src/scripts/select-file.ts6
-rw-r--r--packages/frontend/src/scripts/shuffle.ts5
-rw-r--r--packages/frontend/src/scripts/upload.ts8
-rw-r--r--packages/frontend/src/server-context.ts23
-rw-r--r--packages/frontend/src/store.ts18
-rw-r--r--packages/frontend/src/style.scss15
-rw-r--r--packages/frontend/src/types/post-form.ts22
-rw-r--r--packages/frontend/src/ui/_common_/navbar.vue67
-rw-r--r--packages/frontend/src/ui/_common_/statusbar-rss.vue2
-rw-r--r--packages/frontend/src/ui/deck/deck-store.ts1
-rw-r--r--packages/frontend/src/ui/deck/tl-column.vue12
-rw-r--r--packages/frontend/src/widgets/WidgetPhotos.vue4
-rw-r--r--packages/frontend/src/widgets/WidgetRss.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetRssTicker.vue2
136 files changed, 2235 insertions, 889 deletions
diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts
index 13a97e433c..c90cc6bdd0 100644
--- a/packages/frontend/src/_boot_.ts
+++ b/packages/frontend/src/_boot_.ts
@@ -12,7 +12,7 @@ import '@/style.scss';
import { mainBoot } from '@/boot/main-boot.js';
import { subBoot } from '@/boot/sub-boot.js';
-const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete'];
+const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete'];
if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
subBoot();
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index b91834b94f..36186ecac1 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -5,12 +5,12 @@
import { defineAsyncComponent, reactive, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { apiUrl } from '@@/js/config.js';
+import type { MenuItem, MenuButton } from '@/types/menu.js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
-import type { MenuItem, MenuButton } from '@/types/menu.js';
import { del, get, set } from '@/scripts/idb-proxy.js';
-import { apiUrl } from '@@/js/config.js';
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
@@ -165,7 +165,18 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
});
}
-export function updateAccount(accountData: Partial<Account>) {
+export function updateAccount(accountData: Account) {
+ if (!$i) return;
+ for (const key of Object.keys($i)) {
+ delete $i[key];
+ }
+ for (const [key, value] of Object.entries(accountData)) {
+ $i[key] = value;
+ }
+ miLocalStorage.setItem('account', JSON.stringify($i));
+}
+
+export function updateAccountPartial(accountData: Partial<Account>) {
if (!$i) return;
for (const [key, value] of Object.entries(accountData)) {
$i[key] = value;
@@ -224,26 +235,6 @@ export async function openAccountMenu(opts: {
}, ev: MouseEvent) {
if (!$i) return;
- function showSigninDialog() {
- const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
- done: (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
- addAccount(res.id, res.i);
- success();
- },
- closed: () => dispose(),
- });
- }
-
- function createAccount() {
- const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
- done: (res: Misskey.entities.SignupResponse) => {
- addAccount(res.id, res.token);
- switchAccountWithToken(res.token);
- },
- closed: () => dispose(),
- });
- }
-
async function switchAccount(account: Misskey.entities.UserDetailed) {
const storedAccounts = await getAccounts();
const found = storedAccounts.find(x => x.id === account.id);
@@ -312,10 +303,22 @@ export async function openAccountMenu(opts: {
text: i18n.ts.addAccount,
children: [{
text: i18n.ts.existingAccount,
- action: () => { showSigninDialog(); },
+ action: () => {
+ getAccountWithSigninDialog().then(res => {
+ if (res != null) {
+ success();
+ }
+ });
+ },
}, {
text: i18n.ts.createAccount,
- action: () => { createAccount(); },
+ action: () => {
+ getAccountWithSignupDialog().then(res => {
+ if (res != null) {
+ switchAccountWithToken(res.token);
+ }
+ });
+ },
}],
}, {
type: 'link',
@@ -336,6 +339,40 @@ export async function openAccountMenu(opts: {
});
}
+export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
+ return new Promise((resolve) => {
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
+ done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
+ await addAccount(res.id, res.i);
+ resolve({ id: res.id, token: res.i });
+ },
+ cancelled: () => {
+ resolve(null);
+ },
+ closed: () => {
+ dispose();
+ },
+ });
+ });
+}
+
+export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> {
+ return new Promise((resolve) => {
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
+ done: async (res: Misskey.entities.SignupResponse) => {
+ await addAccount(res.id, res.token);
+ resolve({ id: res.id, token: res.token });
+ },
+ cancelled: () => {
+ resolve(null);
+ },
+ closed: () => {
+ dispose();
+ },
+ });
+ });
+}
+
if (_DEV_) {
(window as any).$i = $i;
}
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index 1145891b71..bfe5c4f5f7 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -11,11 +11,11 @@ import directives from '@/directives/index.js';
import components from '@/components/index.js';
import { applyTheme } from '@/scripts/theme.js';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
-import { updateI18n } from '@/i18n.js';
+import { updateI18n, i18n } from '@/i18n.js';
import { $i, refreshAccount, login } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js';
import { fetchInstance, instance } from '@/instance.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js';
import { reloadChannel } from '@/scripts/unison-reload.js';
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
@@ -185,6 +185,10 @@ export async function common(createVue: () => App<Element>) {
}
});
+ watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => {
+ updateDeviceKind(kind);
+ }, { immediate: true });
+
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });
@@ -269,6 +273,27 @@ export async function common(createVue: () => App<Element>) {
removeSplash();
+ //#region Self-XSS 対策メッセージ
+ console.log(
+ `%c${i18n.ts._selfXssPrevention.warning}`,
+ 'color: #f00; background-color: #ff0; font-size: 36px; padding: 4px;',
+ );
+ console.log(
+ `%c${i18n.ts._selfXssPrevention.title}`,
+ 'color: #f00; font-weight: 900; font-family: "Hiragino Sans W9", "Hiragino Kaku Gothic ProN", sans-serif; font-size: 24px;',
+ );
+ console.log(
+ `%c${i18n.ts._selfXssPrevention.description1}`,
+ 'font-size: 16px; font-weight: 700;',
+ );
+ console.log(
+ `%c${i18n.ts._selfXssPrevention.description2}`,
+ 'font-size: 16px;',
+ 'font-size: 20px; font-weight: 700; color: #f00;',
+ );
+ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hub.net/docs/for-users/resources/self-xss/' }));
+ //#endregion
+
return {
isClientUpdated,
app,
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 76459ab330..2bf9029479 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -4,14 +4,14 @@
*/
import { createApp, defineAsyncComponent, markRaw } from 'vue';
+import { ui } from '@@/js/config.js';
import { common } from './common.js';
import type * as Misskey from 'misskey-js';
-import { ui } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js';
-import { $i, signout, updateAccount } from '@/account.js';
+import { $i, signout, updateAccountPartial } from '@/account.js';
import { instance } from '@/instance.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
@@ -231,11 +231,41 @@ export async function mainBoot() {
}
if (!claimedAchievements.includes('justPlainLucky')) {
- window.setInterval(() => {
+ let justPlainLuckyTimer: number | null = null;
+ let lastVisibilityChangedAt = Date.now();
+
+ function claimPlainLucky() {
+ if (document.visibilityState !== 'visible') {
+ if (justPlainLuckyTimer != null) window.clearTimeout(justPlainLuckyTimer);
+ return;
+ }
+
if (Math.floor(Math.random() * 20000) === 0) {
claimAchievement('justPlainLucky');
+ } else {
+ justPlainLuckyTimer = window.setTimeout(claimPlainLucky, 1000 * 10);
}
- }, 1000 * 10);
+ }
+
+ window.addEventListener('visibilitychange', () => {
+ const now = Date.now();
+
+ if (document.visibilityState === 'visible') {
+ // タブを高速で切り替えたら取得処理が何度も走るのを防ぐ
+ if ((now - lastVisibilityChangedAt) < 1000 * 10) {
+ justPlainLuckyTimer = window.setTimeout(claimPlainLucky, 1000 * 10);
+ } else {
+ claimPlainLucky();
+ }
+ } else if (justPlainLuckyTimer != null) {
+ window.clearTimeout(justPlainLuckyTimer);
+ justPlainLuckyTimer = null;
+ }
+
+ lastVisibilityChangedAt = now;
+ }, { passive: true });
+
+ claimPlainLucky();
}
if (!claimedAchievements.includes('client30min')) {
@@ -291,11 +321,11 @@ export async function mainBoot() {
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
- updateAccount(i);
+ updateAccountPartial(i);
});
main.on('readAllNotifications', () => {
- updateAccount({
+ updateAccountPartial({
hasUnreadNotification: false,
unreadNotificationsCount: 0,
});
@@ -303,39 +333,39 @@ export async function mainBoot() {
main.on('unreadNotification', () => {
const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
- updateAccount({
+ updateAccountPartial({
hasUnreadNotification: true,
unreadNotificationsCount,
});
});
main.on('unreadMention', () => {
- updateAccount({ hasUnreadMentions: true });
+ updateAccountPartial({ hasUnreadMentions: true });
});
main.on('readAllUnreadMentions', () => {
- updateAccount({ hasUnreadMentions: false });
+ updateAccountPartial({ hasUnreadMentions: false });
});
main.on('unreadSpecifiedNote', () => {
- updateAccount({ hasUnreadSpecifiedNotes: true });
+ updateAccountPartial({ hasUnreadSpecifiedNotes: true });
});
main.on('readAllUnreadSpecifiedNotes', () => {
- updateAccount({ hasUnreadSpecifiedNotes: false });
+ updateAccountPartial({ hasUnreadSpecifiedNotes: false });
});
main.on('readAllAntennas', () => {
- updateAccount({ hasUnreadAntenna: false });
+ updateAccountPartial({ hasUnreadAntenna: false });
});
main.on('unreadAntenna', () => {
- updateAccount({ hasUnreadAntenna: true });
+ updateAccountPartial({ hasUnreadAntenna: true });
sound.playMisskeySfx('antenna');
});
main.on('readAllAnnouncements', () => {
- updateAccount({ hasUnreadAnnouncement: false });
+ updateAccountPartial({ hasUnreadAnnouncement: false });
});
// 個人宛てお知らせが発行されたとき
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index b9413270ae..e48b6ef781 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
- <div :class="$style.root" class="_gaps_s">
+ <div class="_gaps_s">
<MkFolder :withSpacer="false">
<template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template>
<template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template>
@@ -151,6 +151,4 @@ function showMenu(ev: MouseEvent) {
</script>
<style lang="scss" module>
-.root {
-}
</style>
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index 1adb244c9e..3045a47585 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -29,7 +29,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
-import { $i, updateAccount } from '@/account.js';
+import { $i, updateAccountPartial } from '@/account.js';
const props = withDefaults(defineProps<{
announcement: Misskey.entities.Announcement;
@@ -51,7 +51,7 @@ async function ok() {
modal.value?.close();
misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
- updateAccount({
+ updateAccountPartial({
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
});
}
diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue
index 2386ba6fa7..e622d57f1e 100644
--- a/packages/frontend/src/components/MkAntennaEditor.vue
+++ b/packages/frontend/src/components/MkAntennaEditor.vue
@@ -160,7 +160,7 @@ async function deleteAntenna() {
function addUser() {
os.selectUser({ includeSelf: true }).then(user => {
users.value = users.value.trim();
- users.value += '\n@' + Misskey.acct.toString(user as any);
+ users.value += '\n@' + Misskey.acct.toString(user);
users.value = users.value.trim();
});
}
diff --git a/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts b/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts
new file mode 100644
index 0000000000..0adc44e204
--- /dev/null
+++ b/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkAuthConfirm from './MkAuthConfirm.vue';
+void MkAuthConfirm;
diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue
new file mode 100644
index 0000000000..f78d2d38f0
--- /dev/null
+++ b/packages/frontend/src/components/MkAuthConfirm.vue
@@ -0,0 +1,450 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper">
+ <Transition
+ mode="out-in"
+ :enterActiveClass="$style.transition_enterActive"
+ :leaveActiveClass="$style.transition_leaveActive"
+ :enterFromClass="$style.transition_enterFrom"
+ :leaveToClass="$style.transition_leaveTo"
+
+ :inert="_waiting"
+ >
+ <div v-if="phase === 'accountSelect'" key="accountSelect" :class="$style.root" class="_gaps">
+ <div :class="$style.header" class="_gaps_s">
+ <div :class="$style.iconFallback">
+ <i class="ti ti-user"></i>
+ </div>
+ <div :class="$style.headerText">{{ i18n.ts.pleaseSelectAccount }}</div>
+ </div>
+ <div>
+ <div :class="$style.accountSelectorLabel">{{ i18n.ts.selectAccount }}</div>
+ <div :class="$style.accountSelectorList">
+ <template v-for="[id, user] in users">
+ <input :id="'account-' + id" v-model="selectedUser" type="radio" name="accountSelector" :value="id" :class="$style.accountSelectorRadio"/>
+ <label :for="'account-' + id" :class="$style.accountSelectorItem">
+ <MkAvatar :user="user" :class="$style.accountSelectorAvatar"/>
+ <div :class="$style.accountSelectorBody">
+ <MkUserName :user="user" :class="$style.accountSelectorName"/>
+ <MkAcct :user="user" :class="$style.accountSelectorAcct"/>
+ </div>
+ </label>
+ </template>
+ <button class="_button" :class="[$style.accountSelectorItem, $style.accountSelectorAddAccountRoot]" @click="clickAddAccount">
+ <div :class="[$style.accountSelectorAvatar, $style.accountSelectorAddAccountAvatar]">
+ <i class="ti ti-user-plus"></i>
+ </div>
+ <div :class="[$style.accountSelectorBody, $style.accountSelectorName]">{{ i18n.ts.addAccount }}</div>
+ </button>
+ </div>
+ </div>
+ <div class="_buttonsCenter">
+ <MkButton rounded gradate :disabled="selectedUser === null" @click="clickChooseAccount">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </div>
+ </div>
+ <div v-else-if="phase === 'consent'" key="consent" :class="$style.root" class="_gaps">
+ <div :class="$style.header" class="_gaps_s">
+ <img v-if="icon" :class="$style.icon" :src="getProxiedImageUrl(icon, 'preview')"/>
+ <div v-else :class="$style.iconFallback">
+ <i class="ti ti-apps"></i>
+ </div>
+ <div :class="$style.headerText">{{ name ? i18n.tsx._auth.shareAccess({ name }) : i18n.ts._auth.shareAccessAsk }}</div>
+ </div>
+ <div v-if="permissions && permissions.length > 0" class="_gaps_s" :class="$style.permissionRoot">
+ <div>{{ name ? i18n.tsx._auth.permission({ name }) : i18n.ts._auth.permissionAsk }}</div>
+ <div :class="$style.permissionListWrapper">
+ <ul :class="$style.permissionList">
+ <li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
+ </ul>
+ </div>
+ </div>
+ <slot name="consentAdditionalInfo"></slot>
+ <div>
+ <div :class="$style.accountSelectorLabel">
+ {{ i18n.ts._auth.scopeUser }} <button class="_textButton" @click="clickBackToAccountSelect">{{ i18n.ts.switchAccount }}</button>
+ </div>
+ <div :class="$style.accountSelectorList">
+ <div :class="[$style.accountSelectorItem, $style.static]">
+ <MkAvatar :user="users.get(selectedUser!)!" :class="$style.accountSelectorAvatar"/>
+ <div :class="$style.accountSelectorBody">
+ <MkUserName :user="users.get(selectedUser!)!" :class="$style.accountSelectorName"/>
+ <MkAcct :user="users.get(selectedUser!)!" :class="$style.accountSelectorAcct"/>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="_buttonsCenter">
+ <MkButton rounded @click="clickCancel">{{ i18n.ts.reject }}</MkButton>
+ <MkButton rounded gradate @click="clickAccept">{{ i18n.ts.accept }}</MkButton>
+ </div>
+ </div>
+ <div v-else-if="phase === 'success'" key="success" :class="$style.root" class="_gaps_s">
+ <div :class="$style.header" class="_gaps_s">
+ <div :class="$style.iconFallback">
+ <i class="ti ti-check"></i>
+ </div>
+ <div :class="$style.headerText">{{ i18n.ts._auth.accepted }}</div>
+ <div :class="$style.headerTextSub">{{ i18n.ts._auth.pleaseGoBack }}</div>
+ </div>
+ </div>
+ <div v-else-if="phase === 'denied'" key="denied" :class="$style.root" class="_gaps_s">
+ <div :class="$style.header" class="_gaps_s">
+ <div :class="$style.iconFallback">
+ <i class="ti ti-x"></i>
+ </div>
+ <div :class="$style.headerText">{{ i18n.ts._auth.denied }}</div>
+ </div>
+ </div>
+ <div v-else-if="phase === 'failed'" key="failed" :class="$style.root" class="_gaps_s">
+ <div :class="$style.header" class="_gaps_s">
+ <div :class="$style.iconFallback">
+ <i class="ti ti-x"></i>
+ </div>
+ <div :class="$style.headerText">{{ i18n.ts.somethingHappened }}</div>
+ </div>
+ </div>
+ </Transition>
+ <div v-if="_waiting" :class="$style.waitingRoot">
+ <MkLoading/>
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import * as Misskey from 'misskey-js';
+
+import MkButton from '@/components/MkButton.vue';
+
+import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+
+const props = defineProps<{
+ name?: string;
+ icon?: string;
+ permissions?: (typeof Misskey.permissions[number])[];
+ manualWaiting?: boolean;
+ waitOnDeny?: boolean;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'accept', token: string): void;
+ (ev: 'deny', token: string): void;
+}>();
+
+const waiting = ref(true);
+const _waiting = computed(() => waiting.value || props.manualWaiting);
+const phase = ref<'accountSelect' | 'consent' | 'success' | 'denied' | 'failed'>('accountSelect');
+
+const selectedUser = ref<string | null>(null);
+
+const users = ref(new Map<string, Misskey.entities.UserDetailed & { token: string; }>());
+
+async function init() {
+ waiting.value = true;
+
+ users.value.clear();
+
+ if ($i) {
+ users.value.set($i.id, $i);
+ }
+
+ const accounts = await getAccounts();
+
+ const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id));
+
+ if (accountIdsToFetch.length > 0) {
+ const usersRes = await misskeyApi('users/show', {
+ userIds: accountIdsToFetch,
+ });
+
+ for (const user of usersRes) {
+ if (users.value.has(user.id)) continue;
+
+ users.value.set(user.id, {
+ ...user,
+ token: accounts.find(a => a.id === user.id)!.token,
+ });
+ }
+ }
+
+ waiting.value = false;
+}
+
+init();
+
+function clickAddAccount(ev: MouseEvent) {
+ selectedUser.value = null;
+
+ os.popupMenu([{
+ text: i18n.ts.existingAccount,
+ action: () => {
+ getAccountWithSigninDialog().then(async (res) => {
+ if (res != null) {
+ os.success();
+ await init();
+ if (users.value.has(res.id)) {
+ selectedUser.value = res.id;
+ }
+ }
+ });
+ },
+ }, {
+ text: i18n.ts.createAccount,
+ action: () => {
+ getAccountWithSignupDialog().then(async (res) => {
+ if (res != null) {
+ os.success();
+ await init();
+ if (users.value.has(res.id)) {
+ selectedUser.value = res.id;
+ }
+ }
+ });
+ },
+ }], ev.currentTarget ?? ev.target);
+}
+
+function clickChooseAccount() {
+ if (selectedUser.value === null) return;
+
+ phase.value = 'consent';
+}
+
+function clickBackToAccountSelect() {
+ selectedUser.value = null;
+ phase.value = 'accountSelect';
+}
+
+function clickCancel() {
+ if (selectedUser.value === null) return;
+
+ const user = users.value.get(selectedUser.value)!;
+
+ const token = user.token;
+
+ if (props.waitOnDeny) {
+ waiting.value = true;
+ }
+ emit('deny', token);
+}
+
+async function clickAccept() {
+ if (selectedUser.value === null) return;
+
+ const user = users.value.get(selectedUser.value)!;
+
+ const token = user.token;
+
+ waiting.value = true;
+ emit('accept', token);
+}
+
+function showUI(state: 'success' | 'denied' | 'failed') {
+ phase.value = state;
+ waiting.value = false;
+}
+
+defineExpose({
+ showUI,
+});
+</script>
+
+<style lang="scss" module>
+.transition_enterActive,
+.transition_leaveActive {
+ transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
+}
+.transition_enterFrom {
+ opacity: 0;
+ transform: translateX(50px);
+}
+.transition_leaveTo {
+ opacity: 0;
+ transform: translateX(-50px);
+}
+
+.wrapper {
+ overflow-x: hidden;
+ overflow-x: clip;
+
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+.waitingRoot {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1;
+ cursor: wait;
+}
+
+.root {
+ position: relative;
+ box-sizing: border-box;
+ width: 100%;
+ padding: 48px 24px;
+}
+
+.header {
+ margin: 0 auto;
+ max-width: 320px;
+}
+
+.icon,
+.iconFallback {
+ display: block;
+ margin: 0 auto;
+ width: 54px;
+ height: 54px;
+}
+
+.icon {
+ border-radius: 50%;
+ border: 1px solid var(--MI_THEME-divider);
+ background-color: #fff;
+ object-fit: contain;
+}
+
+.iconFallback {
+ border-radius: 50%;
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
+ text-align: center;
+ line-height: 54px;
+ font-size: 18px;
+}
+
+.headerText,
+.headerTextSub {
+ text-align: center;
+ word-break: normal;
+ word-break: auto-phrase;
+}
+
+.headerText {
+ font-size: 16px;
+ font-weight: 700;
+}
+
+.permissionRoot {
+ padding: 16px;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-bg);
+}
+
+.permissionListWrapper {
+ max-height: 350px;
+ overflow-y: auto;
+ padding: 12px;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-panel);
+}
+
+.permissionList {
+ margin: 0 0 0 1.5em;
+ padding: 0;
+ font-size: 90%;
+}
+
+.accountSelectorLabel {
+ font-size: 0.85em;
+ opacity: 0.7;
+ margin-bottom: 8px;
+}
+
+.accountSelectorList {
+ border-radius: var(--MI-radius);
+ border: 1px solid var(--MI_THEME-divider);
+ overflow: hidden;
+ overflow: clip;
+}
+
+.accountSelectorRadio {
+ position: absolute;
+ clip: rect(0, 0, 0, 0);
+ pointer-events: none;
+
+ &:focus-visible + .accountSelectorItem {
+ outline: 2px solid var(--MI_THEME-accent);
+ outline-offset: -4px;
+ }
+
+ &:checked:focus-visible + .accountSelectorItem {
+ outline-color: #fff;
+ }
+
+ &:checked + .accountSelectorItem {
+ background: var(--MI_THEME-accent);
+ color: #fff;
+ }
+}
+
+.accountSelectorItem {
+ display: flex;
+ align-items: center;
+ padding: 8px;
+ font-size: 14px;
+ -webkit-tap-highlight-color: transparent;
+ cursor: pointer;
+
+ &:hover {
+ background: var(--MI_THEME-buttonHoverBg);
+ }
+
+ &.static {
+ cursor: unset;
+
+ &:hover {
+ background: none;
+ }
+ }
+}
+
+.accountSelectorAddAccountRoot {
+ width: 100%;
+}
+
+.accountSelectorBody {
+ padding: 0 8px;
+ min-width: 0;
+}
+
+.accountSelectorAvatar {
+ width: 45px;
+ height: 45px;
+}
+
+.accountSelectorAddAccountAvatar {
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
+ font-size: 16px;
+ line-height: 45px;
+ text-align: center;
+ border-radius: 50%;
+}
+
+.accountSelectorName {
+ display: block;
+ font-weight: bold;
+}
+
+.accountSelectorAcct {
+ opacity: 0.5;
+}
+</style>
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index 82fc89e51c..264cf9af06 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -117,8 +117,8 @@ async function requestRender() {
sitekey: props.sitekey,
theme: defaultStore.state.darkMode ? 'dark' : 'light',
callback: callback,
- 'expired-callback': callback,
- 'error-callback': callback,
+ 'expired-callback': () => callback(undefined),
+ 'error-callback': () => callback(undefined),
});
} else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) {
const { default: Widget } = await import('@mcaptcha/vanilla-glue');
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index 99580df5e2..c470042b79 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -47,11 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
const props = defineProps<{
- channel: Record<string, any>;
+ channel: Misskey.entities.Channel;
}>();
const getLastReadedAt = (): number | null => {
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index 8ab01d7db8..f513795c56 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -64,26 +64,30 @@ const showBody = ref(props.expanded);
const ignoreOmit = ref(false);
const omitted = ref(false);
-function enter(el) {
+function enter(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height;
- el.style.height = 0;
+ el.style.height = '0';
el.offsetHeight; // reflow
- el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px';
+ el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`;
}
-function afterEnter(el) {
- el.style.height = null;
+function afterEnter(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
+ el.style.height = '';
}
-function leave(el) {
+function leave(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height;
- el.style.height = elementHeight + 'px';
+ el.style.height = `${elementHeight}px`;
el.offsetHeight; // reflow
- el.style.height = 0;
+ el.style.height = '0';
}
-function afterLeave(el) {
- el.style.height = null;
+function afterLeave(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
+ el.style.height = '';
}
const calcOmit = () => {
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index c2a1aaf29a..0186cfc2c0 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:withOkButton="true"
@close="cancel()"
@ok="ok()"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<template #header>{{ i18n.ts.cropImage }}</template>
<template #default="{ width, height }">
diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
index 949adc6a8e..ecbee864dc 100644
--- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
+++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
+<MkModalWindow ref="dialogEl" @close="cancel()" @closed="emit('closed')">
<template #header>:{{ emoji.name }}:</template>
<template #default>
<MkSpacer>
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index f04e5cf7c6..9c75f91cb2 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -128,14 +128,14 @@ export default defineComponent({
return children;
};
- function onBeforeLeave(element: Element) {
- const el = element as HTMLElement;
+ function onBeforeLeave(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
el.style.top = `${el.offsetTop}px`;
el.style.left = `${el.offsetLeft}px`;
}
- function onLeaveCancelled(element: Element) {
- const el = element as HTMLElement;
+ function onLeaveCancelled(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
el.style.top = '';
el.style.left = '';
}
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 22130d4fab..b095a1cd4a 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
- <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
+ <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
</div>
<div v-if="actions" :class="$style.buttons">
@@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
text: string;
primary?: boolean,
danger?: boolean,
- callback: (...args: any[]) => void;
+ callback: (...args: unknown[]) => void;
}[];
showOkButton?: boolean;
showCancelButton?: boolean;
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 23883a44e9..8be6d6f53d 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -157,7 +157,12 @@ const ilFilesObserver = new IntersectionObserver(
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
);
+const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
+
watch(folder, () => emit('cd', folder.value));
+watch(sortModeSelect, () => {
+ fetch();
+});
function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
addFile(file, true);
@@ -193,7 +198,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
removeFolder(folderId);
}
-function onDragover(ev: DragEvent): any {
+function onDragover(ev: DragEvent) {
if (!ev.dataTransfer) return;
// ドラッグ元が自分自身の所有するアイテムだったら
@@ -238,7 +243,7 @@ function onDragleave() {
draghover.value = false;
}
-function onDrop(ev: DragEvent): any {
+function onDrop(ev: DragEvent) {
draghover.value = false;
if (!ev.dataTransfer) return;
@@ -327,7 +332,7 @@ function createFolder() {
title: i18n.ts.createFolder,
placeholder: i18n.ts.folderName,
}).then(({ canceled, result: name }) => {
- if (canceled) return;
+ if (canceled || name == null) return;
misskeyApi('drive/folders/create', {
name: name,
parentId: folder.value ? folder.value.id : undefined,
@@ -558,6 +563,7 @@ async function fetch() {
folderId: folder.value ? folder.value.id : null,
type: props.type,
limit: filesMax + 1,
+ sort: sortModeSelect.value,
}).then(fetchedFiles => {
if (fetchedFiles.length === filesMax + 1) {
moreFiles.value = true;
@@ -607,6 +613,7 @@ function fetchMoreFiles() {
type: props.type,
untilId: files.value.at(-1)?.id,
limit: max + 1,
+ sort: sortModeSelect.value,
}).then(files => {
if (files.length === max + 1) {
moreFiles.value = true;
@@ -642,6 +649,43 @@ function getMenu() {
type: 'label',
});
+ menu.push({
+ type: 'parent',
+ text: i18n.ts.sort,
+ icon: 'ti ti-arrows-sort',
+ children: [{
+ text: `${i18n.ts.registeredDate} (${i18n.ts.descendingOrder})`,
+ icon: 'ti ti-sort-descending-letters',
+ action: () => { sortModeSelect.value = '+createdAt'; },
+ active: sortModeSelect.value === '+createdAt',
+ }, {
+ text: `${i18n.ts.registeredDate} (${i18n.ts.ascendingOrder})`,
+ icon: 'ti ti-sort-ascending-letters',
+ action: () => { sortModeSelect.value = '-createdAt'; },
+ active: sortModeSelect.value === '-createdAt',
+ }, {
+ text: `${i18n.ts.size} (${i18n.ts.descendingOrder})`,
+ icon: 'ti ti-sort-descending-letters',
+ action: () => { sortModeSelect.value = '+size'; },
+ active: sortModeSelect.value === '+size',
+ }, {
+ text: `${i18n.ts.size} (${i18n.ts.ascendingOrder})`,
+ icon: 'ti ti-sort-ascending-letters',
+ action: () => { sortModeSelect.value = '-size'; },
+ active: sortModeSelect.value === '-size',
+ }, {
+ text: `${i18n.ts.name} (${i18n.ts.descendingOrder})`,
+ icon: 'ti ti-sort-descending-letters',
+ action: () => { sortModeSelect.value = '+name'; },
+ active: sortModeSelect.value === '+name',
+ }, {
+ text: `${i18n.ts.name} (${i18n.ts.ascendingOrder})`,
+ icon: 'ti ti-sort-ascending-letters',
+ action: () => { sortModeSelect.value = '-name'; },
+ active: sortModeSelect.value === '-name',
+ }],
+ });
+
if (folder.value) {
menu.push({
text: i18n.ts.renameFolder,
diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
index c2bb516c7c..6e9eb75920 100644
--- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
+++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:scroll="false"
:withOkButton="false"
@close="cancel()"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index f4caa730bf..b418ed3ae6 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -90,7 +90,7 @@ function computeButtonTitle(ev: MouseEvent): void {
elm.title = getEmojiName(emoji);
}
-function nestedChosen(emoji: any, ev: MouseEvent) {
+function nestedChosen(emoji: string, ev: MouseEvent) {
emit('chosen', emoji, ev);
}
</script>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 219950f135..8187d991e7 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -409,7 +409,7 @@ function computeButtonTitle(ev: MouseEvent): void {
elm.title = getEmojiName(emoji);
}
-function chosen(emoji: any, ev?: MouseEvent) {
+function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -426,7 +426,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
// 最近使った絵文字更新
if (!pinned.value?.includes(key)) {
let recents = defaultStore.state.recentlyUsedEmojis;
- recents = recents.filter((emoji: any) => emoji !== key);
+ recents = recents.filter((emoji) => emoji !== key);
recents.unshift(key);
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
}
diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue
index b41604b2c3..d59b20435e 100644
--- a/packages/frontend/src/components/MkExtensionInstaller.vue
+++ b/packages/frontend/src/components/MkExtensionInstaller.vue
@@ -73,7 +73,7 @@ export type Extension = {
author: string;
description?: string;
permissions?: string[];
- config?: Record<string, any>;
+ config?: Record<string, unknown>;
};
} | {
type: 'theme';
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index 1717f8fc98..fb1b5220fb 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div ref="rootEl" :class="$style.root">
- <header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody">
+ <header :class="$style.header" class="_button" @click="showBody = !showBody">
<div :class="$style.title"><div><slot name="header"></slot></div></div>
<div :class="$style.divider"></div>
<button class="_button" :class="$style.button">
@@ -32,21 +32,23 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, ref, shallowRef, watch } from 'vue';
-import tinycolor from 'tinycolor2';
import { miLocalStorage } from '@/local-storage.js';
import { defaultStore } from '@/store.js';
+import { getBgColor } from '@/scripts/get-bg-color.js';
const miLocalStoragePrefix = 'ui:folder:' as const;
const props = withDefaults(defineProps<{
expanded?: boolean;
- persistKey?: string;
+ persistKey?: string | null;
}>(), {
expanded: true,
+ persistKey: null,
});
-const rootEl = shallowRef<HTMLDivElement>();
-const bg = ref<string>();
+const rootEl = shallowRef<HTMLElement>();
+const parentBg = ref<string | null>(null);
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
watch(showBody, () => {
@@ -55,47 +57,34 @@ watch(showBody, () => {
}
});
-function enter(element: Element) {
- const el = element as HTMLElement;
+function enter(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height;
el.style.height = '0';
el.offsetHeight; // reflow
- el.style.height = elementHeight + 'px';
+ el.style.height = `${elementHeight}px`;
}
-function afterEnter(element: Element) {
- const el = element as HTMLElement;
- el.style.height = 'unset';
+function afterEnter(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
+ el.style.height = '';
}
-function leave(element: Element) {
- const el = element as HTMLElement;
+function leave(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height;
- el.style.height = elementHeight + 'px';
+ el.style.height = `${elementHeight}px`;
el.offsetHeight; // reflow
el.style.height = '0';
}
-function afterLeave(element: Element) {
- const el = element as HTMLElement;
- el.style.height = 'unset';
+function afterLeave(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
+ el.style.height = '';
}
onMounted(() => {
- function getParentBg(el?: HTMLElement | null): string {
- if (el == null || el.tagName === 'BODY') return 'var(--MI_THEME-bg)';
- const background = el.style.background || el.style.backgroundColor;
- if (background) {
- return background;
- } else {
- return getParentBg(el.parentElement);
- }
- }
-
- const rawBg = getParentBg(rootEl.value);
- const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
- _bg.setAlpha(0.85);
- bg.value = _bg.toRgbString();
+ parentBg.value = getBgColor(rootEl.value?.parentElement);
});
</script>
@@ -121,6 +110,7 @@ onMounted(() => {
top: var(--MI-stickyTop, 0px);
-webkit-backdrop-filter: var(--MI-blur, blur(8px));
backdrop-filter: var(--MI-blur, blur(20px));
+ background-color: color(from v-bind("parentBg ?? 'var(--bg)'") srgb r g b / 0.85);
}
.title {
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 5f9500d923..7bdc06a8b4 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -56,8 +56,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick, onMounted, shallowRef, ref } from 'vue';
+import { nextTick, onMounted, ref, shallowRef } from 'vue';
import { defaultStore } from '@/store.js';
+import { getBgColor } from '@/scripts/get-bg-color.js';
const props = withDefaults(defineProps<{
defaultOpen?: boolean;
@@ -69,40 +70,35 @@ const props = withDefaults(defineProps<{
withSpacer: true,
});
-const getBgColor = (el: HTMLElement) => {
- const style = window.getComputedStyle(el);
- if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
- return style.backgroundColor;
- } else {
- return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
- }
-};
-
const rootEl = shallowRef<HTMLElement>();
const bgSame = ref(false);
const opened = ref(props.defaultOpen);
const openedAtLeastOnce = ref(props.defaultOpen);
-function enter(el) {
+function enter(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height;
- el.style.height = 0;
+ el.style.height = '0';
el.offsetHeight; // reflow
- el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px';
+ el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`;
}
-function afterEnter(el) {
- el.style.height = null;
+function afterEnter(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
+ el.style.height = '';
}
-function leave(el) {
+function leave(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height;
- el.style.height = elementHeight + 'px';
+ el.style.height = `${elementHeight}px`;
el.offsetHeight; // reflow
- el.style.height = 0;
+ el.style.height = '0';
}
-function afterLeave(el) {
- el.style.height = null;
+function afterLeave(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
+ el.style.height = '';
}
function toggle() {
@@ -117,7 +113,7 @@ function toggle() {
onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement);
- const parentBg = getBgColor(rootEl.value!.parentElement!);
+ const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent';
const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
bgSame.value = parentBg === myBg;
});
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index ccea7cd453..c1dc67f776 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -37,13 +37,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { host } from '@@/js/config.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { pleaseLogin } from '@/scripts/please-login.js';
-import { host } from '@@/js/config.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
@@ -80,7 +80,7 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
}
async function onClick() {
- pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` });
+ pleaseLogin({ openOnRemote: { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` } });
wait.value = true;
@@ -91,7 +91,10 @@ async function onClick() {
text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }),
});
- if (canceled) return;
+ if (canceled) {
+ wait.value = false;
+ return;
+ }
await misskeyApi('following/delete', {
userId: props.user.id,
@@ -125,7 +128,10 @@ async function onClick() {
});
hasPendingFollowRequestFromYou.value = true;
- if ($i == null) return;
+ if ($i == null) {
+ wait.value = false;
+ return;
+ }
claimAchievement('following1');
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 124f114111..a639eae208 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@click="cancel()"
@ok="ok()"
@close="cancel()"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<template #header>
{{ title }}
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index e01ff86c5a..08817fd6a8 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
+import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs, InputHTMLAttributes } from 'vue';
import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue';
import { useInterval } from '@@/js/use-interval.js';
@@ -53,7 +53,7 @@ import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
const props = defineProps<{
modelValue: string | number | null;
- type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
+ type?: InputHTMLAttributes['type'];
required?: boolean;
readonly?: boolean;
disabled?: boolean;
@@ -64,8 +64,8 @@ const props = defineProps<{
mfmAutocomplete?: boolean | SuggestionType[],
autocapitalize?: string;
spellcheck?: boolean;
- inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal';
- step?: any;
+ inputmode?: InputHTMLAttributes['inputmode'];
+ step?: InputHTMLAttributes['step'];
datalist?: string[];
min?: number;
max?: number;
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index d3a12ca734..65e4a1eb12 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -118,7 +118,7 @@ import { hms } from '@/filters/hms.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
-import { isFullscreenNotSupported } from '@/scripts/device-kind.js';
+import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js';
import hasAudio from '@/scripts/media-has-audio.js';
import MkMediaRange from '@/components/MkMediaRange.vue';
import { $i, iAmModerator } from '@/account.js';
@@ -334,26 +334,21 @@ function togglePlayPause() {
}
function toggleFullscreen() {
- if (isFullscreenNotSupported && videoEl.value) {
- if (isFullscreen.value) {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- //@ts-ignore
- videoEl.value.webkitExitFullscreen();
- isFullscreen.value = false;
- } else {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- //@ts-ignore
- videoEl.value.webkitEnterFullscreen();
- isFullscreen.value = true;
- }
- } else if (playerEl.value) {
- if (isFullscreen.value) {
- document.exitFullscreen();
- isFullscreen.value = false;
- } else {
- playerEl.value.requestFullscreen({ navigationUI: 'hide' });
- isFullscreen.value = true;
- }
+ if (playerEl.value == null || videoEl.value == null) return;
+ if (isFullscreen.value) {
+ exitFullscreen({
+ videoEl: videoEl.value,
+ });
+ isFullscreen.value = false;
+ } else {
+ requestFullscreen({
+ videoEl: videoEl.value,
+ playerEl: playerEl.value,
+ options: {
+ navigationUI: 'hide',
+ },
+ });
+ isFullscreen.value = true;
}
}
@@ -454,8 +449,10 @@ watch(loop, (to) => {
});
watch(hide, (to) => {
- if (to && isFullscreen.value) {
- document.exitFullscreen();
+ if (videoEl.value && to && isFullscreen.value) {
+ exitFullscreen({
+ videoEl: videoEl.value,
+ });
isFullscreen.value = false;
}
});
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index fe9e1ce088..f06cfffee4 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -26,11 +26,11 @@ import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
import MkModal from './MkModal.vue';
const props = withDefaults(defineProps<{
- withOkButton: boolean;
- withCloseButton: boolean;
- okButtonDisabled: boolean;
- width: number;
- height: number;
+ withOkButton?: boolean;
+ withCloseButton?: boolean;
+ okButtonDisabled?: boolean;
+ width?: number;
+ height?: number;
}>(), {
withOkButton: false,
withCloseButton: true,
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 828ad2e872..1a8814b7cb 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -187,6 +187,7 @@ import MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { checkWordMute } from '@/scripts/check-word-mute.js';
+import { notePage } from '@/filters/note.js';
import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import * as os from '@/os.js';
@@ -227,6 +228,7 @@ const emit = defineEmits<{
}>();
const inTimeline = inject<boolean>('inTimeline', false);
+const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
@@ -291,15 +293,18 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
*/
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
- if (mutedWords == null) return false;
-
- if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
- if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
- if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
+ if (mutedWords != null) {
+ if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
+ if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
+ if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
+ }
if (checkOnly) return false;
- if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
+ if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
+ return 'sensitiveMute';
+ }
+
return false;
}
@@ -419,7 +424,7 @@ if (!props.mock) {
}
function renote(viaKeyboard = false) {
- pleaseLogin(undefined, pleaseLoginContext.value);
+ pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog();
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
@@ -429,7 +434,7 @@ function renote(viaKeyboard = false) {
}
function reply(): void {
- pleaseLogin(undefined, pleaseLoginContext.value);
+ pleaseLogin({ openOnRemote: pleaseLoginContext.value });
if (props.mock) {
return;
}
@@ -442,7 +447,7 @@ function reply(): void {
}
function react(): void {
- pleaseLogin(undefined, pleaseLoginContext.value);
+ pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.playMisskeySfx('reaction');
@@ -562,15 +567,24 @@ function showRenoteMenu(): void {
};
}
+ const renoteDetailsMenu: MenuItem = {
+ type: 'link',
+ text: i18n.ts.renoteDetails,
+ icon: 'ti ti-info-circle',
+ to: notePage(note.value),
+ };
+
if (isMyRenote) {
- pleaseLogin(undefined, pleaseLoginContext.value);
+ pleaseLogin({ openOnRemote: pleaseLoginContext.value });
os.popupMenu([
+ renoteDetailsMenu,
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
{ type: 'divider' },
getUnrenote(),
], renoteTime.value);
} else {
os.popupMenu([
+ renoteDetailsMenu,
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
{ type: 'divider' },
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 6d53685651..4a350388c2 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -62,7 +62,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
</div>
</div>
- <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div>
+ <div :class="$style.noteHeaderUsernameAndBadgeRoles">
+ <div :class="$style.noteHeaderUsername">
+ <MkAcct :user="appearNote.user"/>
+ </div>
+ <div v-if="appearNote.user.badgeRoles" :class="$style.noteHeaderBadgeRoles">
+ <img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/>
+ </div>
+ </div>
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
</div>
</header>
@@ -207,6 +214,7 @@ import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
+import { host } from '@@/js/config.js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@@ -230,7 +238,6 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
-import { host } from '@@/js/config.js';
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js';
@@ -404,7 +411,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
}
function renote() {
- pleaseLogin(undefined, pleaseLoginContext.value);
+ pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog();
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
@@ -412,7 +419,7 @@ function renote() {
}
function reply(): void {
- pleaseLogin(undefined, pleaseLoginContext.value);
+ pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog();
os.post({
reply: appearNote.value,
@@ -423,7 +430,7 @@ function reply(): void {
}
function react(): void {
- pleaseLogin(undefined, pleaseLoginContext.value);
+ pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.playMisskeySfx('reaction');
@@ -499,7 +506,7 @@ async function clip(): Promise<void> {
function showRenoteMenu(): void {
if (!isMyRenote) return;
- pleaseLogin(undefined, pleaseLoginContext.value);
+ pleaseLogin({ openOnRemote: pleaseLoginContext.value });
os.popupMenu([{
text: i18n.ts.unrenote,
icon: 'ti ti-trash',
@@ -679,12 +686,30 @@ function loadConversation() {
float: right;
}
+.noteHeaderUsernameAndBadgeRoles {
+ display: flex;
+}
+
.noteHeaderUsername {
margin-bottom: 2px;
+ margin-right: 0.5em;
line-height: 1.3;
word-wrap: anywhere;
}
+.noteHeaderBadgeRoles {
+ margin: 0 .5em 0 0;
+}
+
+.noteHeaderBadgeRole {
+ height: 1.3em;
+ vertical-align: -20%;
+
+ & + .noteHeaderBadgeRole {
+ margin-left: 0.2em;
+ }
+}
+
.noteContent {
container-type: inline-size;
overflow-wrap: break-word;
diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue
index 47a9c79e45..d07827d11a 100644
--- a/packages/frontend/src/components/MkNotificationSelectWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue
@@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
-const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any);
+const typesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as TypesMap);
function ok() {
emit('done', {
diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue
index dabdd324fd..7fa8c23c6c 100644
--- a/packages/frontend/src/components/MkObjectView.value.vue
+++ b/packages/frontend/src/components/MkObjectView.value.vue
@@ -39,7 +39,7 @@ import number from '@/filters/number.js';
import XValue from '@/components/MkObjectView.value.vue';
const props = defineProps<{
- value: any;
+ value: unknown;
}>();
const collapsed = reactive({});
@@ -50,19 +50,19 @@ if (isObject(props.value)) {
}
}
-function isObject(v): boolean {
+function isObject(v: unknown): v is Record<PropertyKey, unknown> {
return typeof v === 'object' && !Array.isArray(v) && v !== null;
}
-function isArray(v): boolean {
+function isArray(v: unknown): v is unknown[] {
return Array.isArray(v);
}
-function isEmpty(v): boolean {
+function isEmpty(v: unknown): v is Record<PropertyKey, never> | never[] {
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
}
-function collapsable(v): boolean {
+function collapsable(v: unknown): boolean {
return (isObject(v) || isArray(v)) && !isEmpty(v);
}
</script>
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 4777da2848..9547423227 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:buttonsLeft="buttonsLeft"
:buttonsRight="buttonsRight"
:contextmenu="contextmenu"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<template #header>
<template v-if="pageMetadata">
@@ -30,17 +30,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
+import { url } from '@@/js/config.js';
+import { getScrollContainer } from '@@/js/scroll.js';
import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { url } from '@@/js/config.js';
import { useScrollPositionManager } from '@/nirax.js';
import { i18n } from '@/i18n.js';
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { openingWindowsCount } from '@/os.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import { getScrollContainer } from '@@/js/scroll.js';
import { useRouterFactory } from '@/router/supplier.js';
import { mainRouter } from '@/router/main.js';
@@ -48,7 +48,7 @@ const props = defineProps<{
initialPath: string;
}>();
-defineEmits<{
+const emit = defineEmits<{
(ev: 'closed'): void;
}>();
@@ -58,7 +58,7 @@ const windowRouter = routerFactory(props.initialPath);
const contents = shallowRef<HTMLElement | null>(null);
const pageMetadata = ref<null | PageMetadata>(null);
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
-const history = ref<{ path: string; key: any; }[]>([{
+const history = ref<{ path: string; key: string; }[]>([{
path: windowRouter.getCurrentPath(),
key: windowRouter.getCurrentKey(),
}]);
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 48913004e0..e70ac7ff1a 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { host } from '@@/js/config.js';
+import { useInterval } from '@@/js/use-interval.js';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { sum } from '@/scripts/array.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
-import { host } from '@@/js/config.js';
-import { useInterval } from '@@/js/use-interval.js';
const props = defineProps<{
noteId: string;
@@ -85,7 +85,7 @@ if (props.poll.expiresAt) {
const vote = async (id) => {
if (props.readOnly || closed.value || isVoted.value) return;
- pleaseLogin(undefined, pleaseLoginContext.value);
+ pleaseLogin({ openOnRemote: pleaseLoginContext.value });
const { canceled } = await os.confirm({
type: 'question',
diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue
index 26c251a8d2..df664e49f7 100644
--- a/packages/frontend/src/components/MkPopupMenu.vue
+++ b/packages/frontend/src/components/MkPopupMenu.vue
@@ -19,7 +19,7 @@ defineProps<{
items: MenuItem[];
align?: 'center' | string;
width?: number;
- src?: any;
+ src?: HTMLElement | null;
returnFocusTo?: HTMLElement | null;
}>();
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 76a6e4212a..0b5794d1e3 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -65,10 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
- <input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
+ <input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd">
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
- <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
+ <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
@@ -129,25 +129,13 @@ import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
+import type { PostFormProps } from '@/types/post-form.js';
const $i = signinRequired();
const modal = inject('modal');
-const props = withDefaults(defineProps<{
- reply?: Misskey.entities.Note;
- renote?: Misskey.entities.Note;
- channel?: Misskey.entities.Channel; // TODO
- mention?: Misskey.entities.User;
- specified?: Misskey.entities.UserDetailed;
- initialText?: string;
- initialCw?: string;
- initialVisibility?: (typeof Misskey.noteVisibilities)[number];
- initialFiles?: Misskey.entities.DriveFile[];
- initialLocalOnly?: boolean;
- initialVisibleUsers?: Misskey.entities.UserDetailed[];
- initialNote?: Misskey.entities.Note;
- instant?: boolean;
+const props = withDefaults(defineProps<PostFormProps & {
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
@@ -201,6 +189,7 @@ const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'
const imeText = ref('');
const showingOptions = ref(false);
const textAreaReadOnly = ref(false);
+const justEndedComposition = ref(false);
const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : '';
@@ -573,7 +562,13 @@ function clear() {
function onKeydown(ev: KeyboardEvent) {
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
- if (ev.key === 'Escape') emit('esc');
+ // justEndedComposition.value is for Safari, which keyDown occurs after compositionend.
+ // ev.isComposing is for another browsers.
+ if (ev.key === 'Escape' && !justEndedComposition.value && !ev.isComposing) emit('esc');
+}
+
+function onKeyup(ev: KeyboardEvent) {
+ justEndedComposition.value = false;
}
function onCompositionUpdate(ev: CompositionEvent) {
@@ -582,6 +577,7 @@ function onCompositionUpdate(ev: CompositionEvent) {
function onCompositionEnd(ev: CompositionEvent) {
imeText.value = '';
+ justEndedComposition.value = true;
}
async function onPaste(ev: ClipboardEvent) {
@@ -947,8 +943,8 @@ function showActions(ev: MouseEvent) {
action.handler({
text: text.value,
cw: cw.value,
- }, (key, value: any) => {
- if (typeof key !== 'string') return;
+ }, (key, value) => {
+ if (typeof key !== 'string' || typeof value !== 'string') return;
if (key === 'text') { text.value = value; }
if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
});
@@ -1112,7 +1108,7 @@ defineExpose({
&:focus-visible {
outline: none;
- .submitInner {
+ > .submitInner {
outline: 2px solid var(--MI_THEME-fgOnAccent);
outline-offset: -4px;
}
@@ -1127,13 +1123,13 @@ defineExpose({
}
&:not(:disabled):hover {
- > .inner {
+ > .submitInner {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
&:not(:disabled):active {
- > .inner {
+ > .submitInner {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index ee7038df64..56e026aa3c 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div v-show="props.modelValue.length != 0" :class="$style.root">
<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
- <template #item="{element}">
+ <template #item="{ element }">
<div
:class="$style.file"
role="button"
@@ -38,14 +38,14 @@ import type { MenuItem } from '@/types/menu.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const props = defineProps<{
- modelValue: any[];
+ modelValue: Misskey.entities.DriveFile[];
detachMediaFn?: (id: string) => void;
}>();
const mock = inject<boolean>('mock', false);
const emit = defineEmits<{
- (ev: 'update:modelValue', value: any[]): void;
+ (ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void;
(ev: 'detach', id: string): void;
(ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void;
(ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void;
@@ -113,7 +113,7 @@ async function rename(file) {
});
}
-async function describe(file) {
+async function describe(file: Misskey.entities.DriveFile) {
if (mock) return;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index d6bca29050..32d8df1504 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -11,23 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { shallowRef } from 'vue';
-import * as Misskey from 'misskey-js';
import MkModal from '@/components/MkModal.vue';
import MkPostForm from '@/components/MkPostForm.vue';
+import type { PostFormProps } from '@/types/post-form.js';
-const props = withDefaults(defineProps<{
- reply?: Misskey.entities.Note;
- renote?: Misskey.entities.Note;
- channel?: any; // TODO
- mention?: Misskey.entities.User;
- specified?: Misskey.entities.UserDetailed;
- initialText?: string;
- initialCw?: string;
- initialVisibility?: (typeof Misskey.noteVisibilities)[number];
- initialFiles?: Misskey.entities.DriveFile[];
- initialLocalOnly?: boolean;
- initialVisibleUsers?: Misskey.entities.UserDetailed[];
- initialNote?: Misskey.entities.Note;
+const props = withDefaults(defineProps<PostFormProps & {
instant?: boolean;
fixed?: boolean;
autofocus?: boolean;
diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue
index e735d9fff8..f16c8f6c2a 100644
--- a/packages/frontend/src/components/MkRadio.vue
+++ b/packages/frontend/src/components/MkRadio.vue
@@ -24,17 +24,17 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
-<script lang="ts" setup>
+<script lang="ts" setup generic="T extends unknown">
import { computed } from 'vue';
const props = defineProps<{
- modelValue: any;
- value: any;
+ modelValue: T;
+ value: T;
disabled?: boolean;
}>();
const emit = defineEmits<{
- (ev: 'update:modelValue', value: any): void;
+ (ev: 'update:modelValue', value: T): void;
}>();
const checked = computed(() => props.modelValue === props.value);
diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue
index f4c3643ba8..d24e0b15bf 100644
--- a/packages/frontend/src/components/MkReactionsViewer.details.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.details.vue
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { } from 'vue';
+import * as Misskey from 'misskey-js';
import { getEmojiName } from '@@/js/emojilist.js';
import MkTooltip from './MkTooltip.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
@@ -30,7 +31,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
defineProps<{
showing: boolean;
reaction: string;
- users: any[]; // TODO
+ users: Misskey.entities.UserLite[];
count: number;
targetElement: HTMLElement;
}>();
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index a2ec384ac5..eeadd49936 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
@keydown.space.enter="show"
>
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
- <select
+ <div
ref="inputEl"
- v-model="v"
v-adaptive-border
tabindex="-1"
:class="$style.inputCore"
@@ -26,55 +25,48 @@ SPDX-License-Identifier: AGPL-3.0-only
:required="required"
:readonly="readonly"
:placeholder="placeholder"
- @input="onInput"
@mousedown.prevent="() => {}"
@keydown.prevent="() => {}"
>
- <slot></slot>
- </select>
+ <div style="pointer-events: none;">{{ currentValueText ?? '' }}</div>
+ <div style="display: none;">
+ <slot></slot>
+ </div>
+ </div>
<div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
</div>
<div :class="$style.caption"><slot name="caption"></slot></div>
-
- <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
<script lang="ts" setup>
import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
-import MkButton from '@/components/MkButton.vue';
-import * as os from '@/os.js';
import { useInterval } from '@@/js/use-interval.js';
-import { i18n } from '@/i18n.js';
import type { MenuItem } from '@/types/menu.js';
+import * as os from '@/os.js';
const props = defineProps<{
- modelValue: string | null;
+ modelValue: string | number | null;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
placeholder?: string;
autofocus?: boolean;
inline?: boolean;
- manualSave?: boolean;
small?: boolean;
large?: boolean;
}>();
const emit = defineEmits<{
- (ev: 'changeByUser', value: string | null): void;
- (ev: 'update:modelValue', value: string | null): void;
+ (ev: 'update:modelValue', value: string | number | null): void;
}>();
const slots = useSlots();
const { modelValue, autofocus } = toRefs(props);
-const v = ref(modelValue.value);
const focused = ref(false);
const opening = ref(false);
-const changed = ref(false);
-const invalid = ref(false);
-const filled = computed(() => v.value !== '' && v.value != null);
+const currentValueText = ref<string | null>(null);
const inputEl = ref<HTMLObjectElement | null>(null);
const prefixEl = ref<HTMLElement | null>(null);
const suffixEl = ref<HTMLElement | null>(null);
@@ -85,26 +77,6 @@ const height =
36;
const focus = () => container.value?.focus();
-const onInput = (ev) => {
- changed.value = true;
-};
-
-const updated = () => {
- changed.value = false;
- emit('update:modelValue', v.value);
-};
-
-watch(modelValue, newValue => {
- v.value = newValue;
-});
-
-watch(v, () => {
- if (!props.manualSave) {
- updated();
- }
-
- invalid.value = inputEl.value?.validity.badInput ?? true;
-});
// このコンポーネントが作成された時、非表示状態である場合がある
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
@@ -134,6 +106,31 @@ onMounted(() => {
});
});
+watch(modelValue, () => {
+ const scanOptions = (options: VNodeChild[]) => {
+ for (const vnode of options) {
+ if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
+ if (vnode.type === 'optgroup') {
+ const optgroup = vnode;
+ if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
+ } else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
+ const fragment = vnode;
+ if (Array.isArray(fragment.children)) scanOptions(fragment.children);
+ } else if (vnode.props == null) { // v-if で条件が false のときにこうなる
+ // nop?
+ } else {
+ const option = vnode;
+ if (option.props?.value === modelValue.value) {
+ currentValueText.value = option.children as string;
+ break;
+ }
+ }
+ }
+ };
+
+ scanOptions(slots.default!());
+}, { immediate: true });
+
function show() {
if (opening.value) return;
focus();
@@ -146,11 +143,9 @@ function show() {
const pushOption = (option: VNode) => {
menu.push({
text: option.children as string,
- active: computed(() => v.value === option.props?.value),
+ active: computed(() => modelValue.value === option.props?.value),
action: () => {
- v.value = option.props?.value;
- changed.value = true;
- emit('changeByUser', v.value);
+ emit('update:modelValue', option.props?.value);
},
});
};
@@ -248,7 +243,8 @@ function show() {
.inputCore {
appearance: none;
-webkit-appearance: none;
- display: block;
+ display: flex;
+ align-items: center;
height: v-bind("height + 'px'");
width: 100%;
margin: 0;
diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue
index 5608122a39..cd003a39df 100644
--- a/packages/frontend/src/components/MkSignin.password.vue
+++ b/packages/frontend/src/components/MkSignin.password.vue
@@ -24,11 +24,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
<div v-if="needCaptcha">
- <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
- <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
- <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
- <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
- <MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
+ <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
+ <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
+ <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
+ <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+ <MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha"/>
</div>
<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 3d1c44fc90..e1f4e26d62 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -277,7 +277,7 @@ async function onSubmit(): Promise<void> {
return null;
});
- if (res) {
+ if (res && res.ok) {
if (res.status === 204 || instance.emailRequiredForSignup) {
os.alert({
type: 'success',
@@ -295,6 +295,8 @@ async function onSubmit(): Promise<void> {
await login(resJson.token);
}
}
+ } else {
+ onSignupApiError();
}
submitting.value = false;
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index f240e6dc46..6fb9d77837 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="dialog"
:width="500"
:height="600"
- @close="dialog?.close()"
- @closed="$emit('closed')"
+ @close="onClose"
+ @closed="emit('closed')"
>
<template #header>{{ i18n.ts.signup }}</template>
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveToClass="$style.transition_x_leaveTo"
>
<template v-if="!isAcceptedServerRule">
- <XServerRules @done="isAcceptedServerRule = true" @cancel="dialog?.close()"/>
+ <XServerRules @done="isAcceptedServerRule = true" @cancel="onClose"/>
</template>
<template v-else>
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
@@ -48,6 +48,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
(ev: 'done', res: Misskey.entities.SignupResponse): void;
+ (ev: 'cancelled'): void;
(ev: 'closed'): void;
}>();
@@ -55,6 +56,11 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const isAcceptedServerRule = ref(false);
+function onClose() {
+ emit('cancelled');
+ dialog.value?.close();
+}
+
function onSignup(res: Misskey.entities.SignupResponse) {
emit('done', res);
dialog.value?.close();
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 6e7a875dec..0caaed6f39 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -28,11 +28,38 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
-<script lang="ts" setup>
-import { } from 'vue';
+<script lang="ts">
+export type SuperMenuDef = {
+ title?: string;
+ items: ({
+ type: 'a';
+ href: string;
+ target?: string;
+ icon?: string;
+ text: string;
+ danger?: boolean;
+ active?: boolean;
+ } | {
+ type: 'button';
+ icon?: string;
+ text: string;
+ danger?: boolean;
+ active?: boolean;
+ action: (ev: MouseEvent) => void;
+ } | {
+ type: 'link';
+ to: string;
+ icon?: string;
+ text: string;
+ danger?: boolean;
+ active?: boolean;
+ })[];
+};
+</script>
+<script lang="ts" setup>
defineProps<{
- def: any[];
+ def: SuperMenuDef[];
grid?: boolean;
}>();
</script>
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index ca87316bf7..fb8eb4ae37 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -38,10 +38,12 @@ const props = withDefaults(defineProps<{
sound?: boolean;
withRenotes?: boolean;
withReplies?: boolean;
+ withSensitive?: boolean;
onlyFiles?: boolean;
}>(), {
withRenotes: true,
withReplies: false,
+ withSensitive: true,
onlyFiles: false,
});
@@ -51,6 +53,7 @@ const emit = defineEmits<{
}>();
provide('inTimeline', true);
+provide('tl_withSensitive', computed(() => props.withSensitive));
provide('inChannel', computed(() => props.src === 'channel'));
type TimelineQueryType = {
@@ -248,6 +251,9 @@ function refreshEndpointAndChannel() {
// IDが切り替わったら切り替え先のTLを表示させたい
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
+// withSensitiveはクライアントで完結する処理のため、単にリロードするだけでOK
+watch(() => props.withSensitive, reloadTimeline);
+
// 初回表示用
refreshEndpointAndChannel();
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index a7bc3f37f1..73aef68964 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:okButtonDisabled="false"
:canClose="false"
@close="dialog?.close()"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
@ok="ok()"
>
<template #header>{{ title || i18n.ts.generateAccessToken }}</template>
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index c287effadc..f0da8fd3f2 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -180,7 +180,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
sensitive.value = info.sensitive ?? false;
});
-function adjustTweetHeight(message: any) {
+function adjustTweetHeight(message: MessageEvent) {
if (message.origin !== 'https://platform.twitter.com') return;
const embed = message.data?.['twttr.embed'];
if (embed?.method !== 'twttr.private.resize') return;
@@ -193,14 +193,16 @@ function openPlayer(): void {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
url: requestUrl.href,
}, {
- // TODO
+ closed: () => {
+ dispose();
+ },
});
}
-(window as any).addEventListener('message', adjustTweetHeight);
+window.addEventListener('message', adjustTweetHeight);
onUnmounted(() => {
- (window as any).removeEventListener('message', adjustTweetHeight);
+ window.removeEventListener('message', adjustTweetHeight);
});
</script>
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 7a2b5f5ddc..fe499fabbf 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="dialog"
:width="400"
@close="dialog?.close()"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<template v-if="announcement" #header>:{{ announcement.title }}:</template>
<template v-else #header>New announcement</template>
@@ -62,9 +62,16 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRadios from '@/components/MkRadios.vue';
+type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; }
+
const props = defineProps<{
user: Misskey.entities.User,
- announcement?: Misskey.entities.Announcement,
+ announcement?: Required<AdminAnnouncementType>,
+}>();
+
+const emit = defineEmits<{
+ (ev: 'done', v: { deleted?: boolean; updated?: AdminAnnouncementType; created?: AdminAnnouncementType; }): void,
+ (ev: 'closed'): void
}>();
const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
@@ -74,11 +81,6 @@ const icon = ref(props.announcement ? props.announcement.icon : 'info');
const display = ref(props.announcement ? props.announcement.display : 'dialog');
const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
-const emit = defineEmits<{
- (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
- (ev: 'closed'): void
-}>();
-
async function done() {
const params = {
title: title.value,
@@ -88,7 +90,7 @@ async function done() {
display: display.value,
needConfirmationToRead: needConfirmationToRead.value,
userId: props.user.id,
- };
+ } satisfies Misskey.entities.AdminAnnouncementsCreateRequest;
if (props.announcement) {
await os.apiWithDialog('admin/announcements/update', {
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 8e58a6c5a2..764bf74f21 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@click="cancel()"
@close="cancel()"
@ok="ok()"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<template #header>{{ i18n.ts.selectUser }}</template>
<div>
diff --git a/packages/frontend/src/components/MkUsersTooltip.vue b/packages/frontend/src/components/MkUsersTooltip.vue
index 054a503257..0cb7f22e93 100644
--- a/packages/frontend/src/components/MkUsersTooltip.vue
+++ b/packages/frontend/src/components/MkUsersTooltip.vue
@@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import * as Misskey from 'misskey-js';
import MkTooltip from './MkTooltip.vue';
defineProps<{
showing: boolean;
- users: any[]; // TODO
+ users: Misskey.entities.UserLite[];
count: number;
targetElement: HTMLElement;
}>();
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index 492dd4cdc0..ba619f6063 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
</MkSelect>
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
- <MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
+ <MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
</header>
<Sortable
:modelValue="props.widgets"
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 056b6a37ed..2953f656d4 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
:leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
appear
- @afterLeave="$emit('closed')"
+ @afterLeave="emit('closed')"
>
<div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]">
<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
@@ -60,6 +60,13 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
+type WindowButton = {
+ title: string;
+ icon: string;
+ onClick: () => void;
+ highlighted?: boolean;
+};
+
const minHeight = 50;
const minWidth = 250;
@@ -87,8 +94,8 @@ const props = withDefaults(defineProps<{
mini?: boolean;
front?: boolean;
contextmenu?: MenuItem[] | null;
- buttonsLeft?: any[];
- buttonsRight?: any[];
+ buttonsLeft?: WindowButton[];
+ buttonsRight?: WindowButton[];
}>(), {
initialWidth: 400,
initialHeight: null,
diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue
index 5226c61d68..821f07510b 100644
--- a/packages/frontend/src/components/form/suspense.vue
+++ b/packages/frontend/src/components/form/suspense.vue
@@ -18,19 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
-<script lang="ts" setup>
+<script lang="ts" setup generic="T extends unknown">
import { ref, watch } from 'vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
const props = defineProps<{
- p: () => Promise<any>;
+ p: () => Promise<T>;
}>();
const pending = ref(true);
const resolved = ref(false);
const rejected = ref(false);
-const result = ref<any>(null);
+const result = ref<T | null>(null);
const process = () => {
if (props.p == null) {
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 0d68d02e35..08a78c8d81 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div v-if="chosen && !shouldHide" :class="$style.root">
+<div v-if="chosen && !shouldHide">
<div
v-if="!showMenu"
:class="[$style.main, {
@@ -120,10 +120,6 @@ function reduceFrequency(): void {
</script>
<style lang="scss" module>
-.root {
-
-}
-
.main {
text-align: center;
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 66f82a7898..ec1d859080 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -25,17 +25,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, inject, ref } from 'vue';
+import { computed, defineAsyncComponent, inject, ref } from 'vue';
+import type { MenuItem } from '@/types/menu.js';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
import { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.js';
import * as os from '@/os.js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import * as sound from '@/scripts/sound.js';
import { i18n } from '@/i18n.js';
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
-import type { MenuItem } from '@/types/menu.js';
+import { $i } from '@/account.js';
const props = defineProps<{
name: string;
@@ -125,9 +126,31 @@ function onClick(ev: MouseEvent) {
},
});
+ if ($i?.isModerator ?? $i?.isAdmin) {
+ menuItems.push({
+ text: i18n.ts.edit,
+ icon: 'ti ti-pencil',
+ action: async () => {
+ await edit(props.name);
+ },
+ });
+ }
+
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
}
+
+async function edit(name: string) {
+ const emoji = await misskeyApi('emoji', {
+ name: name,
+ });
+ const { dispose } = os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), {
+ emoji: emoji,
+ }, {
+ closed: () => dispose(),
+ });
+}
+
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts
index 0d4ae8cacb..0d138d1f1c 100644
--- a/packages/frontend/src/components/global/MkMfm.ts
+++ b/packages/frontend/src/components/global/MkMfm.ts
@@ -467,8 +467,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
}
default: {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- console.error('unrecognized ast type:', (token as any).type);
+ // @ts-expect-error 存在しないASTタイプ
+ console.error('unrecognized ast type:', token.type);
return [];
}
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index adf8638dae..aaef8b8fca 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -53,7 +53,7 @@ export type Tab = {
</script>
<script lang="ts" setup>
-import { onMounted, onUnmounted, watch, nextTick, shallowRef } from 'vue';
+import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{
@@ -120,14 +120,14 @@ function onTabWheel(ev: WheelEvent) {
let entering = false;
-async function enter(element: Element) {
+async function enter(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
entering = true;
- const el = element as HTMLElement;
const elementWidth = el.getBoundingClientRect().width;
el.style.width = '0';
el.style.paddingLeft = '0';
- el.offsetWidth; // force reflow
- el.style.width = elementWidth + 'px';
+ el.offsetWidth; // reflow
+ el.style.width = `${elementWidth}px`;
el.style.paddingLeft = '';
nextTick(() => {
entering = false;
@@ -136,22 +136,23 @@ async function enter(element: Element) {
setTimeout(renderTab, 170);
}
-function afterEnter(element: Element) {
- //el.style.width = '';
+function afterEnter(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
+ // element.style.width = '';
}
-async function leave(element: Element) {
- const el = element as HTMLElement;
+async function leave(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
const elementWidth = el.getBoundingClientRect().width;
- el.style.width = elementWidth + 'px';
+ el.style.width = `${elementWidth}px`;
el.style.paddingLeft = '';
- el.offsetWidth; // force reflow
+ el.offsetWidth; // reflow
el.style.width = '0';
el.style.paddingLeft = '0';
}
-function afterLeave(element: Element) {
- const el = element as HTMLElement;
+function afterLeave(el: Element) {
+ if (!(el instanceof HTMLElement)) return;
el.style.width = '';
}
diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts
index 45891de889..f88996019f 100644
--- a/packages/frontend/src/directives/adaptive-bg.ts
+++ b/packages/frontend/src/directives/adaptive-bg.ts
@@ -4,19 +4,11 @@
*/
import { Directive } from 'vue';
+import { getBgColor } from '@/scripts/get-bg-color.js';
export default {
mounted(src, binding, vn) {
- const getBgColor = (el: HTMLElement) => {
- const style = window.getComputedStyle(el);
- if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
- return style.backgroundColor;
- } else {
- return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
- }
- };
-
- const parentBg = getBgColor(src.parentElement);
+ const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const myBg = window.getComputedStyle(src).backgroundColor;
diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts
index 685ca38e96..1305f312bd 100644
--- a/packages/frontend/src/directives/adaptive-border.ts
+++ b/packages/frontend/src/directives/adaptive-border.ts
@@ -4,19 +4,11 @@
*/
import { Directive } from 'vue';
+import { getBgColor } from '@/scripts/get-bg-color.js';
export default {
mounted(src, binding, vn) {
- const getBgColor = (el: HTMLElement) => {
- const style = window.getComputedStyle(el);
- if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
- return style.backgroundColor;
- } else {
- return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
- }
- };
-
- const parentBg = getBgColor(src.parentElement);
+ const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const myBg = window.getComputedStyle(src).backgroundColor;
diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts
index 7b5969c679..aa26b94d0b 100644
--- a/packages/frontend/src/directives/panel.ts
+++ b/packages/frontend/src/directives/panel.ts
@@ -4,19 +4,11 @@
*/
import { Directive } from 'vue';
+import { getBgColor } from '@/scripts/get-bg-color.js';
export default {
mounted(src, binding, vn) {
- const getBgColor = (el: HTMLElement) => {
- const style = window.getComputedStyle(el);
- if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
- return style.backgroundColor;
- } else {
- return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
- }
- };
-
- const parentBg = getBgColor(src.parentElement);
+ const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel');
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index ac730f8021..096d404a57 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -40,7 +40,6 @@ export const navbarItemDef = reactive({
followRequests: {
title: i18n.ts.followRequests,
icon: 'ti ti-user-plus',
- show: computed(() => $i != null && $i.isLocked),
indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
to: '/my/follow-requests',
},
diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts
index 25f853453a..965bd6f0bc 100644
--- a/packages/frontend/src/nirax.ts
+++ b/packages/frontend/src/nirax.ts
@@ -36,6 +36,8 @@ interface RouteDefWithRedirect extends RouteDefBase {
export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
+export type RouterFlag = 'forcePage';
+
type ParsedPath = (string | {
name: string;
startsWith?: string;
@@ -107,7 +109,7 @@ export interface IRouter extends EventEmitter<RouterEvent> {
current: Resolved;
currentRef: ShallowRef<Resolved>;
currentRoute: ShallowRef<RouteDef>;
- navHook: ((path: string, flag?: any) => boolean) | null;
+ navHook: ((path: string, flag?: RouterFlag) => boolean) | null;
/**
* ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
@@ -116,11 +118,11 @@ export interface IRouter extends EventEmitter<RouterEvent> {
resolve(path: string): Resolved | null;
- getCurrentPath(): any;
+ getCurrentPath(): string;
getCurrentKey(): string;
- push(path: string, flag?: any): void;
+ push(path: string, flag?: RouterFlag): void;
replace(path: string, key?: string | null): void;
@@ -197,7 +199,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
private currentKey = Date.now().toString();
private redirectCount = 0;
- public navHook: ((path: string, flag?: any) => boolean) | null = null;
+ public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null;
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
super();
@@ -404,7 +406,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
return this.currentKey;
}
- public push(path: string, flag?: any) {
+ public push(path: string, flag?: RouterFlag) {
const beforePath = this.currentPath;
if (path === beforePath) {
this.emit('same');
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 4d41cf5bc0..ea1b673de9 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -28,12 +28,13 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
import { focusParent } from '@/scripts/focus.js';
+import type { PostFormProps } from '@/types/post-form.js';
export const openingWindowsCount = ref(0);
-export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
+export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
endpoint: E,
- data: P = {} as any,
+ data: P,
token?: string | null | undefined,
customErrors?: Record<string, { title?: string; text: string; }>,
) => {
@@ -94,7 +95,7 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
export function promiseDialog<T extends Promise<any>>(
promise: T,
- onSuccess?: ((res: any) => void) | null,
+ onSuccess?: ((res: Awaited<T>) => void) | null,
onFailure?: ((err: Misskey.api.APIError) => void) | null,
text?: string,
): T {
@@ -136,12 +137,12 @@ export function promiseDialog<T extends Promise<any>>(
}
let popupIdCount = 0;
-export const popups = ref([]) as Ref<{
+export const popups = ref<{
id: number;
component: Component;
props: Record<string, any>;
events: Record<string, any>;
-}[]>;
+}[]>([]);
const zIndexes = {
veryLow: 500000,
@@ -458,7 +459,7 @@ type SelectItem<C> = {
};
// default が指定されていたら result は null になり得ないことを保証する overload function
-export function select<C = any>(props: {
+export function select<C = unknown>(props: {
title?: string;
text?: string;
default: string;
@@ -471,7 +472,7 @@ export function select<C = any>(props: {
} | {
canceled: false; result: C;
}>;
-export function select<C = any>(props: {
+export function select<C = unknown>(props: {
title?: string;
text?: string;
default?: string | null;
@@ -484,7 +485,7 @@ export function select<C = any>(props: {
} | {
canceled: false; result: C | null;
}>;
-export function select<C = any>(props: {
+export function select<C = unknown>(props: {
title?: string;
text?: string;
default?: string | null;
@@ -687,15 +688,17 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
}));
}
-export function post(props: Record<string, any> = {}): Promise<void> {
- pleaseLogin(undefined, (props.initialText || props.initialNote ? {
- type: 'share',
- params: {
- text: props.initialText ?? props.initialNote.text,
- visibility: props.initialVisibility ?? props.initialNote?.visibility,
- localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
- },
- } : undefined));
+export function post(props: PostFormProps = {}): Promise<void> {
+ pleaseLogin({
+ openOnRemote: (props.initialText || props.initialNote ? {
+ type: 'share',
+ params: {
+ text: props.initialText ?? props.initialNote?.text ?? '',
+ visibility: props.initialVisibility ?? props.initialNote?.visibility ?? 'public',
+ localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
+ },
+ } : undefined),
+ });
showMovedDialog();
return new Promise(resolve => {
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 68b98c2ab7..f2becfd8f5 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -269,6 +269,12 @@ const patronsWithIcon = [{
}, {
name: '如月ユカ',
icon: 'https://assets.misskey-hub.net/patrons/f24a042076a041b6811a2f124eb620ca.jpg',
+}, {
+ name: 'Yatoigawa',
+ icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
+}, {
+ name: '秋瀬カヲル',
+ icon: 'https://assets.misskey-hub.net/patrons/0f22aeb866484f4fa51db6721e3f9847.jpg',
}];
const patrons = [
@@ -375,6 +381,9 @@ const patrons = [
'はとぽぷさん',
'100の人 (エスパー・イーシア)',
'ケモナーのケシン',
+ 'こまつぶり',
+ 'まゆつな空高',
+ 'asata',
];
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 948e7a3cce..30d7e38638 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -627,6 +627,7 @@ definePageMetadata(() => ({
<style lang="scss" module>
.ip {
display: flex;
+ word-break: break-all;
> :global(.date) {
opacity: 0.7;
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 8a206a2f79..fd15ae1d66 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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="noInquiryUrl" warn>{{ i18n.ts.noInquiryUrlWarning }} <MkA to="/admin/settings" 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>
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index 5d8a581b2e..ac1fe7783c 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -10,9 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<div class="_gaps_m">
- <MkSwitch v-model="enableRegistration" @change="onChange_enableRegistration">
- <template #label>{{ i18n.ts.enableRegistration }}</template>
- <template #caption>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</template>
+ <MkSwitch :modelValue="enableRegistration" @update:modelValue="onChange_enableRegistration">
+ <template #label>{{ i18n.ts._serverSettings.openRegistration }}</template>
+ <template #caption>
+ <div>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</div>
+ <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.openRegistrationWarning }}</div>
+ </template>
</MkSwitch>
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
@@ -164,7 +167,17 @@ async function init() {
mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
}
-function onChange_enableRegistration(value: boolean) {
+async function onChange_enableRegistration(value: boolean) {
+ if (value) {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.acknowledgeNotesAndEnable,
+ });
+ if (canceled) return;
+ }
+
+ enableRegistration.value = value;
+
os.apiWithDialog('admin/update-meta', {
disableRegistration: !value,
}).then(() => {
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index d1bbb5b734..870c3ce88b 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -99,19 +99,19 @@ async function addUser() {
const { canceled: canceled1, result: username } = await os.inputText({
title: i18n.ts.username,
});
- if (canceled1) return;
+ if (canceled1 || username == null) return;
const { canceled: canceled2, result: password } = await os.inputText({
title: i18n.ts.password,
type: 'password',
});
- if (canceled2) return;
+ if (canceled2 || password == null) return;
os.apiWithDialog('admin/accounts/create', {
username: username,
password: password,
}).then(res => {
- paginationComponent.value.reload();
+ paginationComponent.value?.reload();
});
}
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue
index 3840e6a494..56c10fb292 100644
--- a/packages/frontend/src/pages/announcement.vue
+++ b/packages/frontend/src/pages/announcement.vue
@@ -55,7 +55,7 @@ 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 { $i, updateAccountPartial } from '@/account.js';
import { defaultStore } from '@/store.js';
const props = defineProps<{
@@ -90,7 +90,7 @@ async function read(target: Misskey.entities.Announcement): Promise<void> {
target.isRead = true;
await misskeyApi('i/read-announcement', { announcementId: target.id });
if ($i) {
- updateAccount({
+ updateAccountPartial({
unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
});
}
@@ -103,7 +103,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata(() => ({
- title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements,
+ title: announcement.value ? announcement.value.title : i18n.ts.announcements,
icon: 'ti ti-speakerphone',
}));
</script>
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index 688a542988..75c0fd98dc 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -56,7 +56,7 @@ 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 { $i, updateAccountPartial } from '@/account.js';
const paginationCurrent = {
endpoint: 'announcements' as const,
@@ -94,7 +94,7 @@ async function read(target) {
return a;
});
misskeyApi('i/read-announcement', { announcementId: target.id });
- updateAccount({
+ updateAccountPartial({
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
});
}
diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue
index d8f8d0b428..4170b4f73e 100644
--- a/packages/frontend/src/pages/auth.vue
+++ b/packages/frontend/src/pages/auth.vue
@@ -62,7 +62,7 @@ function accepted() {
state.value = 'accepted';
if (session.value && session.value.app.callbackUrl) {
const url = new URL(session.value.app.callbackUrl);
- if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url');
+ if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(url.protocol)) throw new Error('invalid url');
location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
}
}
diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
new file mode 100644
index 0000000000..a834f1c5fd
--- /dev/null
+++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
@@ -0,0 +1,220 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkWindow
+ ref="windowEl"
+ :initialWidth="400"
+ :initialHeight="500"
+ :canResize="true"
+ @close="windowEl?.close()"
+ @closed="emit('closed')"
+>
+ <template v-if="avatarDecoration" #header>{{ avatarDecoration.name }}</template>
+ <template v-else #header>New decoration</template>
+
+ <div style="display: flex; flex-direction: column; min-height: 100%;">
+ <MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
+ <div class="_gaps_m">
+ <div :class="$style.preview">
+ <div :class="[$style.previewItem, $style.light]">
+ <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
+ </div>
+ <div :class="[$style.previewItem, $style.dark]">
+ <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
+ </div>
+ </div>
+ <MkInput v-model="name">
+ <template #label>{{ i18n.ts.name }}</template>
+ </MkInput>
+ <MkInput v-model="url">
+ <template #label>{{ i18n.ts.imageUrl }}</template>
+ </MkInput>
+ <MkTextarea v-model="description">
+ <template #label>{{ i18n.ts.description }}</template>
+ </MkTextarea>
+ <MkFolder>
+ <template #label>{{ i18n.ts.availableRoles }}</template>
+ <template #suffix>{{ rolesThatCanBeUsedThisDecoration.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisDecoration.length }}</template>
+
+ <div class="_gaps">
+ <MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+
+ <div v-for="role in rolesThatCanBeUsedThisDecoration" :key="role.id" :class="$style.roleItem">
+ <MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
+ <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
+ <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
+ </div>
+ </div>
+ </MkFolder>
+ <MkButton v-if="avatarDecoration" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+ </div>
+ </MkSpacer>
+ <div :class="$style.footer">
+ <MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.avatarDecoration ? i18n.ts.update : i18n.ts.create }}</MkButton>
+ </div>
+ </div>
+</MkWindow>
+</template>
+
+<script lang="ts" setup>
+import { computed, watch, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import MkWindow from '@/components/MkWindow.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkRolePreview from '@/components/MkRolePreview.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
+import { signinRequired } from '@/account.js';
+
+const $i = signinRequired();
+
+const props = defineProps<{
+ avatarDecoration?: any,
+}>();
+
+const emit = defineEmits<{
+ (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
+ (ev: 'closed'): void
+}>();
+
+const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
+const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
+const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : '');
+const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : '');
+const roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []);
+const rolesThatCanBeUsedThisDecoration = ref<Misskey.entities.Role[]>([]);
+
+watch(roleIdsThatCanBeUsedThisDecoration, async () => {
+ rolesThatCanBeUsedThisDecoration.value = (await Promise.all(roleIdsThatCanBeUsedThisDecoration.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
+}, { immediate: true });
+
+async function addRole() {
+ const roles = await misskeyApi('admin/roles/list');
+ const currentRoleIds = rolesThatCanBeUsedThisDecoration.value.map(x => x.id);
+
+ const { canceled, result: role } = await os.select({
+ items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
+ });
+ if (canceled || role == null) return;
+
+ rolesThatCanBeUsedThisDecoration.value.push(role);
+}
+
+async function removeRole(role, ev) {
+ rolesThatCanBeUsedThisDecoration.value = rolesThatCanBeUsedThisDecoration.value.filter(x => x.id !== role.id);
+}
+
+async function done() {
+ const params = {
+ url: url.value,
+ name: name.value,
+ description: description.value,
+ roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id),
+ };
+
+ if (props.avatarDecoration) {
+ await os.apiWithDialog('admin/avatar-decorations/update', {
+ id: props.avatarDecoration.id,
+ ...params,
+ });
+
+ emit('done', {
+ updated: {
+ id: props.avatarDecoration.id,
+ ...params,
+ },
+ });
+
+ windowEl.value?.close();
+ } else {
+ const created = await os.apiWithDialog('admin/avatar-decorations/create', params);
+
+ emit('done', {
+ created: created,
+ });
+
+ windowEl.value?.close();
+ }
+}
+
+async function del() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.tsx.removeAreYouSure({ x: name.value }),
+ });
+ if (canceled) return;
+
+ misskeyApi('admin/avatar-decorations/delete', {
+ id: props.avatarDecoration.id,
+ }).then(() => {
+ emit('done', {
+ deleted: true,
+ });
+ windowEl.value?.close();
+ });
+}
+</script>
+
+<style lang="scss" module>
+.preview {
+ display: grid;
+ place-items: center;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 1fr;
+ gap: var(--MI-margin);
+}
+
+.previewItem {
+ width: 100%;
+ height: 100%;
+ min-height: 160px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--MI-radius);
+
+ &.light {
+ background: #eee;
+ }
+
+ &.dark {
+ background: #222;
+ }
+}
+
+.roleItem {
+ display: flex;
+}
+
+.role {
+ flex: 1;
+}
+
+.roleUnassign {
+ width: 32px;
+ height: 32px;
+ margin-left: 8px;
+ align-self: center;
+}
+
+.footer {
+ position: sticky;
+ z-index: 10000;
+ bottom: 0;
+ left: 0;
+ padding: 12px;
+ border-top: solid 0.5px var(--MI_THEME-divider);
+ background: var(--MI_THEME-acrylicBg);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
+}
+</style>
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index b97e7c0eea..a5cafb1678 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -5,92 +5,38 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
- <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
+ <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="900">
<div class="_gaps">
- <MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
- <template #label>{{ avatarDecoration.name }}</template>
- <template #caption>{{ avatarDecoration.description }}</template>
-
- <div :class="$style.editorRoot">
- <div :class="$style.editorWrapper">
- <div :class="$style.preview">
- <div :class="[$style.previewItem, $style.light]">
- <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/>
- </div>
- <div :class="[$style.previewItem, $style.dark]">
- <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/>
- </div>
- </div>
- <div class="_gaps_m">
- <MkInput v-model="avatarDecoration.name">
- <template #label>{{ i18n.ts.name }}</template>
- </MkInput>
- <MkTextarea v-model="avatarDecoration.description">
- <template #label>{{ i18n.ts.description }}</template>
- </MkTextarea>
- <MkInput v-model="avatarDecoration.url">
- <template #label>{{ i18n.ts.imageUrl }}</template>
- </MkInput>
- <div class="_buttons">
- <MkButton inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
- <MkButton v-if="avatarDecoration.id != null" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
- </div>
- </div>
- </div>
+ <div :class="$style.decorations">
+ <div
+ v-for="avatarDecoration in avatarDecorations"
+ :key="avatarDecoration.id"
+ v-panel
+ :class="$style.decoration"
+ @click="edit(avatarDecoration)"
+ >
+ <div :class="$style.decorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
+ <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/>
</div>
- </MkFolder>
+ </div>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/MkInput.vue';
-import MkTextarea from '@/components/MkTextarea.vue';
import { signinRequired } from '@/account.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import MkFolder from '@/components/MkFolder.vue';
-
-const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
const $i = signinRequired();
-function add() {
- avatarDecorations.value.unshift({
- _id: Math.random().toString(36),
- id: null,
- name: '',
- description: '',
- url: '',
- });
-}
-
-function del(avatarDecoration) {
- os.confirm({
- type: 'warning',
- text: i18n.tsx.deleteAreYouSure({ x: avatarDecoration.name }),
- }).then(({ canceled }) => {
- if (canceled) return;
- avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration);
- misskeyApi('admin/avatar-decorations/delete', avatarDecoration);
- });
-}
-
-async function save(avatarDecoration) {
- if (avatarDecoration.id == null) {
- await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration);
- load();
- } else {
- os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration);
- }
-}
+const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
function load() {
misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
@@ -100,6 +46,37 @@ function load() {
load();
+async function add(ev: MouseEvent) {
+ const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
+ }, {
+ done: result => {
+ if (result.created) {
+ avatarDecorations.value.unshift(result.created);
+ }
+ },
+ closed: () => dispose(),
+ });
+}
+
+function edit(avatarDecoration) {
+ const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
+ avatarDecoration: avatarDecoration,
+ }, {
+ done: result => {
+ if (result.updated) {
+ const index = avatarDecorations.value.findIndex(x => x.id === avatarDecoration.id);
+ avatarDecorations.value[index] = {
+ ...avatarDecorations.value[index],
+ ...result.updated,
+ };
+ } else if (result.deleted) {
+ avatarDecorations.value = avatarDecorations.value.filter(x => x.id !== avatarDecoration.id);
+ }
+ },
+ closed: () => dispose(),
+ });
+}
+
const headerActions = computed(() => [{
asFullButton: true,
icon: 'ti ti-plus',
@@ -116,53 +93,26 @@ definePageMetadata(() => ({
</script>
<style lang="scss" module>
-.editorRoot {
- container: editor / inline-size;
-}
-
-.editorWrapper {
+.decorations {
display: grid;
- grid-template-columns: 1fr;
- grid-template-rows: auto auto;
- gap: var(--MI-margin);
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ grid-gap: 12px;
}
-.preview {
- display: grid;
- place-items: center;
- grid-template-columns: 1fr 1fr;
- grid-template-rows: 1fr;
- gap: var(--MI-margin);
-}
-
-.previewItem {
- width: 100%;
- height: 100%;
- min-height: 160px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: var(--MI-radius);
-
- &.light {
- background: #eee;
- }
-
- &.dark {
- background: #222;
- }
+.decoration {
+ cursor: pointer;
+ padding: 16px 16px 28px 16px;
+ border-radius: 8px;
+ text-align: center;
+ font-size: 90%;
+ overflow: clip;
+ contain: content;
}
-@container editor (min-width: 600px) {
- .editorWrapper {
- grid-template-columns: 200px 1fr;
- grid-template-rows: 1fr;
- gap: calc(var(--MI-margin) * 2);
- }
-
- .preview {
- grid-template-columns: 1fr;
- grid-template-rows: 1fr 1fr;
- }
+.decorationName {
+ position: relative;
+ z-index: 10;
+ font-weight: bold;
+ margin-bottom: 20px;
}
</style>
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 7b1737fece..891d59d605 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -33,25 +33,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, provide, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
+import type { MenuItem } from '@/types/menu.js';
import MkNotes from '@/components/MkNotes.vue';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { url } from '@@/js/config.js';
import MkButton from '@/components/MkButton.vue';
import { clipsCache } from '@/cache.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { genEmbedCode } from '@/scripts/get-embed-code.js';
-import type { MenuItem } from '@/types/menu.js';
+import { getServerContext } from '@/server-context.js';
+
+const CTX_CLIP = getServerContext('clip');
const props = defineProps<{
clipId: string,
}>();
-const clip = ref<Misskey.entities.Clip | null>(null);
+const clip = ref<Misskey.entities.Clip | null>(CTX_CLIP);
const favorited = ref(false);
const pagination = {
endpoint: 'clips/notes' as const,
@@ -64,6 +67,11 @@ const pagination = {
const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId));
watch(() => props.clipId, async () => {
+ if (CTX_CLIP && CTX_CLIP.id === props.clipId) {
+ clip.value = CTX_CLIP;
+ return;
+ }
+
clip.value = await misskeyApi('clips/show', {
clipId: props.clipId,
});
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 1e416e22d3..cae3f3ede9 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -116,7 +116,7 @@ const selectAll = () => {
if (selectedEmojis.value.length > 0) {
selectedEmojis.value = [];
} else {
- selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id);
+ selectedEmojis.value = Array.from(emojisPaginationComponent.value?.items.values(), item => item.id);
}
};
@@ -133,7 +133,7 @@ const add = async (ev: MouseEvent) => {
}, {
done: result => {
if (result.created) {
- emojisPaginationComponent.value.prepend(result.created);
+ emojisPaginationComponent.value?.prepend(result.created);
}
},
closed: () => dispose(),
@@ -146,12 +146,12 @@ const edit = (emoji) => {
}, {
done: result => {
if (result.updated) {
- emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
+ emojisPaginationComponent.value?.updateItem(result.updated.id, (oldEmoji) => ({
...oldEmoji,
...result.updated,
}));
} else if (result.deleted) {
- emojisPaginationComponent.value.removeItem(emoji.id);
+ emojisPaginationComponent.value?.removeItem(emoji.id);
}
},
closed: () => dispose(),
@@ -226,7 +226,7 @@ const setCategoryBulk = async () => {
ids: selectedEmojis.value,
category: result,
});
- emojisPaginationComponent.value.reload();
+ emojisPaginationComponent.value?.reload();
};
const setLicenseBulk = async () => {
@@ -238,43 +238,43 @@ const setLicenseBulk = async () => {
ids: selectedEmojis.value,
license: result,
});
- emojisPaginationComponent.value.reload();
+ emojisPaginationComponent.value?.reload();
};
const addTagBulk = async () => {
const { canceled, result } = await os.inputText({
title: 'Tag',
});
- if (canceled) return;
+ if (canceled || result == null) return;
await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
ids: selectedEmojis.value,
aliases: result.split(' '),
});
- emojisPaginationComponent.value.reload();
+ emojisPaginationComponent.value?.reload();
};
const removeTagBulk = async () => {
const { canceled, result } = await os.inputText({
title: 'Tag',
});
- if (canceled) return;
+ if (canceled || result == null) return;
await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
ids: selectedEmojis.value,
aliases: result.split(' '),
});
- emojisPaginationComponent.value.reload();
+ emojisPaginationComponent.value?.reload();
};
const setTagBulk = async () => {
const { canceled, result } = await os.inputText({
title: 'Tag',
});
- if (canceled) return;
+ if (canceled || result == null) return;
await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
ids: selectedEmojis.value,
aliases: result.split(' '),
});
- emojisPaginationComponent.value.reload();
+ emojisPaginationComponent.value?.reload();
};
const delBulk = async () => {
@@ -286,7 +286,7 @@ const delBulk = async () => {
await os.apiWithDialog('admin/emoji/delete-bulk', {
ids: selectedEmojis.value,
});
- emojisPaginationComponent.value.reload();
+ emojisPaginationComponent.value?.reload();
};
const headerActions = computed(() => [{
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 969aa6bbf7..3765319b25 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="windowEl"
:initialWidth="400"
:initialHeight="500"
- :canResize="false"
- @close="windowEl.close()"
- @closed="$emit('closed')"
+ :canResize="true"
+ @close="windowEl?.close()"
+ @closed="emit('closed')"
>
<template v-if="emoji" #header>:{{ emoji.name }}:</template>
<template v-else #header>New emoji</template>
@@ -95,14 +95,19 @@ import { selectFile } from '@/scripts/select-file.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
const props = defineProps<{
- emoji?: any,
+ emoji?: Misskey.entities.EmojiDetailed,
+}>();
+
+const emit = defineEmits<{
+ (ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
+ (ev: 'closed'): void
}>();
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
const name = ref<string>(props.emoji ? props.emoji.name : '');
-const category = ref<string>(props.emoji ? props.emoji.category : '');
+const category = ref<string>(props.emoji?.category ? props.emoji.category : '');
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
-const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : '');
+const license = ref<string>(props.emoji?.license ? props.emoji.license : '');
const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
@@ -115,12 +120,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
-const emit = defineEmits<{
- (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
- (ev: 'closed'): void
-}>();
-
-async function changeImage(ev) {
+async function changeImage(ev: Event) {
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
const candidate = file.value.name.replace(/\.(.+)$/, '');
if (candidate.match(/^[a-z0-9_]+$/)) {
@@ -140,7 +140,7 @@ async function addRole() {
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
}
-async function removeRole(role, ev) {
+async function removeRole(role: Misskey.entities.RoleLite, ev: Event) {
rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
}
@@ -172,7 +172,7 @@ async function done() {
},
});
- windowEl.value.close();
+ windowEl.value?.close();
} else {
const created = await os.apiWithDialog('admin/emoji/add', params);
@@ -180,11 +180,12 @@ async function done() {
created: created,
});
- windowEl.value.close();
+ windowEl.value?.close();
}
}
async function del() {
+ if (!props.emoji) return;
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: name.value }),
@@ -197,7 +198,7 @@ async function del() {
emit('done', {
deleted: true,
});
- windowEl.value.close();
+ windowEl.value?.close();
});
}
</script>
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index fcd22155b7..979d50966e 100644
--- a/packages/frontend/src/pages/emojis.emoji.vue
+++ b/packages/frontend/src/pages/emojis.emoji.vue
@@ -15,18 +15,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
+import { defineAsyncComponent } from 'vue';
+import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
+import { $i } from '@/account.js';
const props = defineProps<{
emoji: Misskey.entities.EmojiSimple;
}>();
function menu(ev) {
- os.popupMenu([{
+ const menuItems: MenuItem[] = [];
+ menuItems.push({
type: 'label',
text: ':' + props.emoji.name + ':',
}, {
@@ -48,8 +52,28 @@ function menu(ev) {
closed: () => dispose(),
});
},
- }], ev.currentTarget ?? ev.target);
+ });
+
+ if ($i?.isModerator ?? $i?.isAdmin) {
+ menuItems.push({
+ text: i18n.ts.edit,
+ icon: 'ti ti-pencil',
+ action: () => {
+ edit(props.emoji);
+ },
+ });
+ }
+
+ os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
+
+const edit = async (emoji) => {
+ const { dispose } = os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), {
+ emoji: emoji,
+ }, {
+ closed: () => dispose(),
+ });
+};
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index 8991af8086..5d819e7993 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -5,69 +5,104 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
- <template #header><MkPageHeader/></template>
+ <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
- <MkPagination ref="paginationComponent" :pagination="pagination">
- <template #empty>
- <div class="_fullinfo">
- <img :src="infoImageUrl" class="_ghost"/>
- <div>{{ i18n.ts.noFollowRequests }}</div>
- </div>
- </template>
- <template #default="{items}">
- <div class="mk-follow-requests">
- <div v-for="req in items" :key="req.id" class="user _panel">
- <MkAvatar class="avatar" :user="req.follower" indicator link preview/>
- <div class="body">
- <div class="name">
- <MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
- <p class="acct">@{{ acct(req.follower) }}</p>
- </div>
- <div class="commands">
- <MkButton class="command" rounded primary @click="accept(req.follower)"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
- <MkButton class="command" rounded danger @click="reject(req.follower)"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <div :key="tab" class="_gaps">
+ <MkPagination ref="paginationComponent" :pagination="pagination">
+ <template #empty>
+ <div class="_fullinfo">
+ <img :src="infoImageUrl" class="_ghost"/>
+ <div>{{ i18n.ts.noFollowRequests }}</div>
+ </div>
+ </template>
+ <template #default="{items}">
+ <div class="mk-follow-requests _gaps">
+ <div v-for="req in items" :key="req.id" class="user _panel">
+ <MkAvatar class="avatar" :user="displayUser(req)" indicator link preview/>
+ <div class="body">
+ <div class="name">
+ <MkA v-user-preview="displayUser(req).id" class="name" :to="userPage(displayUser(req))"><MkUserName :user="displayUser(req)"/></MkA>
+ <p class="acct">@{{ acct(displayUser(req)) }}</p>
+ </div>
+ <div v-if="tab === 'list'" class="commands">
+ <MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
+ <MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
+ </div>
+ <div v-else class="commands">
+ <MkButton class="command" rounded danger @click="cancel(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.cancel }}</MkButton>
+ </div>
+ </div>
</div>
</div>
- </div>
- </div>
- </template>
- </MkPagination>
+ </template>
+ </MkPagination>
+ </div>
+ </MkHorizontalSwipe>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
-import { shallowRef, computed } from 'vue';
-import MkPagination from '@/components/MkPagination.vue';
+import * as Misskey from 'misskey-js';
+import { shallowRef, computed, ref } from 'vue';
+import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import { userPage, acct } from '@/filters/user.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { infoImageUrl } from '@/instance.js';
+import { $i } from '@/account.js';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
-const pagination = {
- endpoint: 'following/requests/list' as const,
+const pagination = computed<Paging>(() => tab.value === 'list' ? {
+ endpoint: 'following/requests/list',
+ limit: 10,
+} : {
+ endpoint: 'following/requests/sent',
limit: 10,
-};
+});
-function accept(user) {
- misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
- paginationComponent.value.reload();
+function accept(user: Misskey.entities.UserLite) {
+ os.apiWithDialog('following/requests/accept', { userId: user.id }).then(() => {
+ paginationComponent.value?.reload();
});
}
-function reject(user) {
- misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
- paginationComponent.value.reload();
+function reject(user: Misskey.entities.UserLite) {
+ os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
+ paginationComponent.value?.reload();
});
}
+function cancel(user: Misskey.entities.UserLite) {
+ os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => {
+ paginationComponent.value?.reload();
+ });
+}
+
+function displayUser(req) {
+ return tab.value === 'list' ? req.follower : req.followee;
+}
+
const headerActions = computed(() => []);
-const headerTabs = computed(() => []);
+const headerTabs = computed(() => [
+ {
+ key: 'list',
+ title: i18n.ts._followRequest.recieved,
+ icon: 'ti ti-download',
+ }, {
+ key: 'sent',
+ title: i18n.ts._followRequest.sent,
+ icon: 'ti ti-upload',
+ },
+]);
+
+const tab = ref($i?.isLocked ? 'list' : 'sent');
definePageMetadata(() => ({
title: i18n.ts.followRequests,
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index 25e56d2b8d..3f6ae27b89 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -5,10 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
- <template #header>
- <MkPageHeader/>
- </template>
- <MKSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
+ <template #header><MkPageHeader/></template>
+ <MkSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
<div :class="$style.root">
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<div :class="$style.text">
@@ -16,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.nothing }}
</div>
</div>
- </MKSpacer>
+ </MkSpacer>
<MkSpacer v-else :contentMax="800">
<div class="_gaps_m" style="text-align: center;">
<div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div>
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 48bc568ac4..0ff1854154 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200">
+ <MkSpacer v-if="error != null" :contentMax="1200">
<div :class="$style.root">
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<p :class="$style.text">
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.nothing }}
</p>
</div>
- </MKSpacer>
+ </MkSpacer>
<MkSpacer v-else-if="list" :contentMax="700" :class="$style.main">
<div v-if="list" class="members _margin">
<div :class="$style.member_text">{{ i18n.ts.members }}</div>
@@ -50,7 +50,7 @@ const props = defineProps<{
}>();
const list = ref<Misskey.entities.UserList | null>(null);
-const error = ref();
+const error = ref<unknown | null>(null);
const users = ref<Misskey.entities.UserDetailed[]>([]);
function fetchList(): void {
diff --git a/packages/frontend/src/pages/lookup.vue b/packages/frontend/src/pages/lookup.vue
index 3233953942..6f10c69640 100644
--- a/packages/frontend/src/pages/lookup.vue
+++ b/packages/frontend/src/pages/lookup.vue
@@ -40,7 +40,7 @@ function fetch() {
return;
}
- let promise: Promise<any>;
+ let promise: Promise<unknown>;
if (uri.startsWith('https://')) {
promise = misskeyApi('ap/show', {
diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue
index ffaf739ed0..e85d2c29c1 100644
--- a/packages/frontend/src/pages/miauth.vue
+++ b/packages/frontend/src/pages/miauth.vue
@@ -4,95 +4,79 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkStickyContainer>
- <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :contentMax="800">
- <div v-if="$i">
- <div v-if="state == 'waiting'">
- <MkLoading/>
- </div>
- <div v-if="state == 'denied'">
- <p>{{ i18n.ts._auth.denied }}</p>
- </div>
- <div v-else-if="state == 'accepted'" class="accepted">
- <p v-if="callback">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
- <p v-else>{{ i18n.ts._auth.pleaseGoBack }}</p>
- </div>
- <div v-else>
- <div v-if="_permissions.length > 0">
- <p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
- <p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
- <ul>
- <li v-for="p in _permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
- </ul>
- </div>
- <div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
- <div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
- <div :class="$style.buttons">
- <MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton>
- <MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
- </div>
- </div>
+<div>
+ <MkAnimBg style="position: fixed; top: 0;"/>
+ <div :class="$style.formContainer">
+ <div :class="$style.form">
+ <MkAuthConfirm
+ ref="authRoot"
+ :name="name"
+ :icon="icon || undefined"
+ :permissions="_permissions"
+ @accept="onAccept"
+ @deny="onDeny"
+ >
+ <template #consentAdditionalInfo>
+ <div v-if="callback != null" class="_gaps_s" :class="$style.redirectRoot">
+ <div>{{ i18n.ts._auth.byClickingYouWillBeRedirectedToThisUrl }}</div>
+ <div class="_monospace" :class="$style.redirectUrl">{{ callback }}</div>
+ </div>
+ </template>
+ </MkAuthConfirm>
</div>
- <div v-else>
- <p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
- <MkSignin @login="onLogin"/>
- </div>
- </MkSpacer>
-</MkStickyContainer>
+ </div>
+</div>
</template>
<script lang="ts" setup>
-import { ref, computed } from 'vue';
-import MkSignin from '@/components/MkSignin.vue';
-import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { $i, login } from '@/account.js';
+import { computed, useTemplateRef } from 'vue';
+import * as Misskey from 'misskey-js';
+
+import MkAnimBg from '@/components/MkAnimBg.vue';
+import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
+
import { i18n } from '@/i18n.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const props = defineProps<{
session: string;
callback?: string;
- name: string;
- icon: string;
- permission: string; // コンマ区切り
+ name?: string;
+ icon?: string;
+ permission?: string; // コンマ区切り
}>();
-const _permissions = props.permission ? props.permission.split(',') : [];
+const _permissions = computed(() => {
+ return (props.permission ? props.permission.split(',').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) : []);
+});
-const state = ref<string | null>(null);
+const authRoot = useTemplateRef('authRoot');
-async function accept(): Promise<void> {
- state.value = 'waiting';
+async function onAccept(token: string) {
await misskeyApi('miauth/gen-token', {
session: props.session,
name: props.name,
iconUrl: props.icon,
- permission: _permissions,
+ permission: _permissions.value,
+ }, token).catch(() => {
+ authRoot.value?.showUI('failed');
});
- state.value = 'accepted';
- if (props.callback) {
+ if (props.callback && props.callback !== '') {
const cbUrl = new URL(props.callback);
- if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
+ if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url');
cbUrl.searchParams.set('session', props.session);
- location.href = cbUrl.href;
+ location.href = cbUrl.toString();
+ } else {
+ authRoot.value?.showUI('success');
}
}
-function deny(): void {
- state.value = 'denied';
-}
-
-function onLogin(res): void {
- login(res.i);
+function onDeny() {
+ authRoot.value?.showUI('denied');
}
-const headerActions = computed(() => []);
-
-const headerTabs = computed(() => []);
-
definePageMetadata(() => ({
title: 'MiAuth',
icon: 'ti ti-apps',
@@ -100,15 +84,38 @@ definePageMetadata(() => ({
</script>
<style lang="scss" module>
-.buttons {
- margin-top: 16px;
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
+.formContainer {
+ min-height: 100svh;
+ padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
+ box-sizing: border-box;
+ display: grid;
+ place-content: center;
+}
+
+.form {
+ position: relative;
+ z-index: 10;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-panel);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+ overflow: clip;
+ max-width: 500px;
+ width: calc(100vw - 64px);
+ height: min(65svh, calc(100svh - calc(env(safe-area-inset-bottom, 0px) + 64px)));
+ overflow-y: scroll;
+}
+
+.redirectRoot {
+ padding: 16px;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-bg);
}
-.loginMessage {
- text-align: center;
- margin: 8px 0 24px;
+.redirectUrl {
+ font-size: 90%;
+ padding: 12px;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-panel);
+ overflow-x: scroll;
}
</style>
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index ece998a7a5..acf37a9a2f 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -77,15 +77,15 @@ async function create() {
clipsCache.delete();
- pagingComponent.value.reload();
+ pagingComponent.value?.reload();
}
function onClipCreated() {
- pagingComponent.value.reload();
+ pagingComponent.value?.reload();
}
function onClipDeleted() {
- pagingComponent.value.reload();
+ pagingComponent.value?.reload();
}
const headerActions = computed(() => []);
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 804a5ae8f8..69e404bd85 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -110,7 +110,7 @@ function addUser() {
listId: list.value.id,
userId: user.id,
}).then(() => {
- paginationEl.value.reload();
+ paginationEl.value?.reload();
});
});
}
@@ -126,7 +126,7 @@ async function removeUser(item, ev) {
listId: list.value.id,
userId: item.userId,
}).then(() => {
- paginationEl.value.removeItem(item.id);
+ paginationEl.value?.removeItem(item.id);
});
},
}], ev.currentTarget ?? ev.target);
diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue
index 93a792c42f..6a2d01b6fa 100644
--- a/packages/frontend/src/pages/not-found.vue
+++ b/packages/frontend/src/pages/not-found.vue
@@ -24,7 +24,7 @@ const props = defineProps<{
}>();
if (props.showLoginPopup) {
- pleaseLogin('/');
+ pleaseLogin({ path: '/' });
}
const headerActions = computed(() => []);
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 448244204d..3e1d04bd6d 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -61,13 +61,17 @@ import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
import MkClipPreview from '@/components/MkClipPreview.vue';
import { defaultStore } from '@/store.js';
+import { pleaseLogin } from '@/scripts/please-login.js';
+import { getServerContext } from '@/server-context.js';
+
+const CTX_NOTE = getServerContext('note');
const props = defineProps<{
noteId: string;
initialTab?: string;
}>();
-const note = ref<null | Misskey.entities.Note>();
+const note = ref<null | Misskey.entities.Note>(CTX_NOTE);
const clips = ref<Misskey.entities.Clip[]>();
const showPrev = ref<'user' | 'channel' | false>(false);
const showNext = ref<'user' | 'channel' | false>(false);
@@ -115,6 +119,12 @@ function fetchNote() {
showPrev.value = false;
showNext.value = false;
note.value = null;
+
+ if (CTX_NOTE && CTX_NOTE.id === props.noteId) {
+ note.value = CTX_NOTE;
+ return;
+ }
+
misskeyApi('notes/show', {
noteId: props.noteId,
}).then(res => {
@@ -128,6 +138,11 @@ function fetchNote() {
});
}
}).catch(err => {
+ if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') {
+ pleaseLogin({
+ message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor,
+ });
+ }
error.value = err;
});
}
diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue
index 733e34eb2c..8719a769e5 100644
--- a/packages/frontend/src/pages/oauth.vue
+++ b/packages/frontend/src/pages/oauth.vue
@@ -4,40 +4,28 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkStickyContainer>
- <template #header><MkPageHeader/></template>
- <MkSpacer :contentMax="800">
- <div v-if="$i">
- <div v-if="permissions.length > 0">
- <p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
- <p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
- <ul>
- <li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
- </ul>
- </div>
- <div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
- <div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
- <form :class="$style.buttons" action="/oauth/decision" accept-charset="utf-8" method="post">
- <input name="login_token" type="hidden" :value="$i.token"/>
- <input name="transaction_id" type="hidden" :value="transactionIdMeta?.content"/>
- <MkButton inline name="cancel" value="cancel">{{ i18n.ts.cancel }}</MkButton>
- <MkButton inline primary>{{ i18n.ts.accept }}</MkButton>
- </form>
+<div>
+ <MkAnimBg style="position: fixed; top: 0;"/>
+ <div :class="$style.formContainer">
+ <div :class="$style.form">
+ <MkAuthConfirm
+ ref="authRoot"
+ :name="name"
+ :permissions="permissions"
+ :waitOnDeny="true"
+ @accept="onAccept"
+ @deny="onDeny"
+ />
</div>
- <div v-else>
- <p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
- <MkSignin @login="onLogin"/>
- </div>
- </MkSpacer>
-</MkStickyContainer>
+ </div>
+</div>
</template>
<script lang="ts" setup>
-import MkSignin from '@/components/MkSignin.vue';
-import MkButton from '@/components/MkButton.vue';
-import { $i, login } from '@/account.js';
-import { i18n } from '@/i18n.js';
+import * as Misskey from 'misskey-js';
+import MkAnimBg from '@/components/MkAnimBg.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]');
if (transactionIdMeta) {
@@ -45,10 +33,44 @@ if (transactionIdMeta) {
}
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content;
-const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ') ?? [];
+const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) ?? [];
+
+function doPost(token: string, decision: 'accept' | 'deny') {
+ const form = document.createElement('form');
+ form.action = '/oauth/decision';
+ form.method = 'post';
+ form.acceptCharset = 'utf-8';
+
+ const loginToken = document.createElement('input');
+ loginToken.type = 'hidden';
+ loginToken.name = 'login_token';
+ loginToken.value = token;
+ form.appendChild(loginToken);
+
+ const transactionId = document.createElement('input');
+ transactionId.type = 'hidden';
+ transactionId.name = 'transaction_id';
+ transactionId.value = transactionIdMeta?.content ?? '';
+ form.appendChild(transactionId);
+
+ if (decision === 'deny') {
+ const cancel = document.createElement('input');
+ cancel.type = 'hidden';
+ cancel.name = 'cancel';
+ cancel.value = 'cancel';
+ form.appendChild(cancel);
+ }
+
+ document.body.appendChild(form);
+ form.submit();
+}
+
+function onAccept(token: string) {
+ doPost(token, 'accept');
+}
-function onLogin(res): void {
- login(res.i);
+function onDeny(token: string) {
+ doPost(token, 'deny');
}
definePageMetadata(() => ({
@@ -58,15 +80,24 @@ definePageMetadata(() => ({
</script>
<style lang="scss" module>
-.buttons {
- margin-top: 16px;
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
+.formContainer {
+ min-height: 100svh;
+ padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
+ box-sizing: border-box;
+ display: grid;
+ place-content: center;
}
-.loginMessage {
- text-align: center;
- margin: 8px 0 24px;
+.form {
+ position: relative;
+ z-index: 10;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-panel);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+ overflow: clip;
+ max-width: 500px;
+ width: calc(100vw - 64px);
+ height: min(65svh, calc(100svh - calc(env(safe-area-inset-bottom, 0px) + 64px)));
+ overflow-y: scroll;
}
</style>
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
index 1cfe7a6d2d..c3ad6657b0 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
<template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template>
<template #func>
<button @click="choose()">
@@ -30,11 +30,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
- modelValue: any
+ modelValue: Misskey.entities.PageBlock & { type: 'image' };
}>();
const emit = defineEmits<{
- (ev: 'update:modelValue', value: any): void;
+ (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'image' }): void;
+ (ev: 'remove'): void;
}>();
const file = ref<Misskey.entities.DriveFile | null>(null);
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 0a28386986..36e03b4790 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
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
<section style="padding: 16px;" class="_gaps_s">
@@ -34,19 +34,24 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
- modelValue: any
+ modelValue: Misskey.entities.PageBlock & { type: 'note' };
}>();
const emit = defineEmits<{
- (ev: 'update:modelValue', value: any): void;
+ (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void;
}>();
-const id = ref<any>(props.modelValue.note);
+const id = ref(props.modelValue.note);
const note = ref<Misskey.entities.Note | null>(null);
watch(id, async () => {
if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) {
- id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop();
+ id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop() ?? null;
+ }
+
+ if (!id.value) {
+ note.value = null;
+ return;
}
emit('update:modelValue', {
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
index 0f8dc33143..3fed07f7e8 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
<template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template>
<template #func>
<button class="_button" @click="rename()">
@@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
+
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
+import * as Misskey from 'misskey-js';
import { v4 as uuid } from 'uuid';
import XContainer from '../page-editor.container.vue';
import * as os from '@/os.js';
@@ -33,14 +34,13 @@ import { getPageBlockList } from '@/pages/page-editor/common.js';
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
-const props = withDefaults(defineProps<{
- modelValue: any,
-}>(), {
- modelValue: {},
-});
+const props = defineProps<{
+ modelValue: Misskey.entities.PageBlock & { type: 'section'; },
+}>();
const emit = defineEmits<{
- (ev: 'update:modelValue', value: any): void;
+ (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void;
+ (ev: 'remove'): void;
}>();
const children = ref(deepClone(props.modelValue.children ?? []));
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
index f09f7e1acd..5795b46c00 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
<section>
@@ -15,18 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
+
import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
+import * as Misskey from 'misskey-js';
import XContainer from '../page-editor.container.vue';
import { i18n } from '@/i18n.js';
import { Autocomplete } from '@/scripts/autocomplete.js';
const props = defineProps<{
- modelValue: any
+ modelValue: Misskey.entities.PageBlock & { type: 'text' }
}>();
const emit = defineEmits<{
- (ev: 'update:modelValue', value: any): void;
+ (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void;
}>();
let autocomplete: Autocomplete;
diff --git a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
index 4967e73000..f191320180 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => $emit('update:modelValue', v)">
+<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)">
<template #item="{element}">
<div :class="$style.item">
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue
index bac1d2bb70..4cacbd0906 100644
--- a/packages/frontend/src/pages/registry.keys.vue
+++ b/packages/frontend/src/pages/registry.keys.vue
@@ -52,7 +52,7 @@ const props = defineProps<{
const scope = computed(() => props.path ? props.path.split('/') : []);
-const keys = ref<any>(null);
+const keys = ref<[string, string][]>([]);
function fetchKeys() {
misskeyApi('i/registry/keys-with-type', {
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index dfb6e3f53e..437a1a2294 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.
const props = defineProps<{
game: Misskey.entities.ReversiGameDetailed;
- connection: Misskey.ChannelConnection;
+ connection: Misskey.ChannelConnection<Misskey.Channels['reversiGame']>;
}>();
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
@@ -217,14 +217,14 @@ function onChangeReadyStates(states) {
game.value.user2Ready = states.user2;
}
-function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) {
+function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) {
props.connection.send('updateSettings', {
key: key,
value: game.value[key],
});
}
-function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) {
+function onUpdateSettings<K extends typeof Misskey.reversiUpdateKeys[number]>({ userId, key, value }: { userId: string; key: K; value: Misskey.entities.ReversiGameDetailed[K]; }) {
if (userId === $i.id) return;
if (game.value[key] === value) return;
game.value[key] = value;
diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue
index 45f8ef21ed..46e510b49b 100644
--- a/packages/frontend/src/pages/role.vue
+++ b/packages/frontend/src/pages/role.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template>
- <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200">
+ <MkSpacer v-if="error != null" :contentMax="1200">
<div :class="$style.root">
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<p :class="$style.text">
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ error }}
</p>
</div>
- </MKSpacer>
+ </MkSpacer>
<MkSpacer v-else-if="tab === 'users'" :contentMax="1200">
<div class="_gaps_s">
<div v-if="role">{{ role.description }}</div>
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkSpacer>
<MkSpacer v-else-if="tab === 'timeline'" :contentMax="700">
- <MkTimeline v-if="visible" ref="timeline" src="role" :role="props.role"/>
+ <MkTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/>
<div v-else-if="!visible" class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div>
@@ -47,23 +47,24 @@ import { instanceName } from '@@/js/config.js';
import { serverErrorImageUrl, infoImageUrl } from '@/instance.js';
const props = withDefaults(defineProps<{
- role: string;
+ roleId: string;
initialTab?: string;
}>(), {
initialTab: 'users',
});
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const tab = ref(props.initialTab);
-const role = ref<Misskey.entities.Role>();
-const error = ref();
+const role = ref<Misskey.entities.Role | null>(null);
+const error = ref<string | null>(null);
const visible = ref(false);
-watch(() => props.role, () => {
+watch(() => props.roleId, () => {
misskeyApi('roles/show', {
- roleId: props.role,
+ roleId: props.roleId,
}).then(res => {
role.value = res;
- document.title = `${role.value.name} | ${instanceName}`;
+ error.value = null;
visible.value = res.isExplorable && res.isPublic;
}).catch((err) => {
if (err.code === 'NO_SUCH_ROLE') {
@@ -71,7 +72,6 @@ watch(() => props.role, () => {
} else {
error.value = i18n.ts.somethingHappened;
}
- document.title = `${error.value} | ${instanceName}`;
});
}, { immediate: true });
@@ -79,7 +79,7 @@ const users = computed(() => ({
endpoint: 'roles/users' as const,
limit: 30,
params: {
- roleId: props.role,
+ roleId: props.roleId,
},
}));
@@ -94,7 +94,7 @@ const headerTabs = computed(() => [{
}]);
definePageMetadata(() => ({
- title: role.value ? role.value.name : i18n.ts.role,
+ title: role.value ? role.value.name : (error.value ?? i18n.ts.role),
icon: 'ti ti-badge',
}));
</script>
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 2250e1ce60..88171f7d70 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -76,7 +76,11 @@ import { claimAchievement } from '@/scripts/achievements.js';
const parser = new Parser();
let aiscript: Interpreter;
const code = ref('');
-const logs = ref<any[]>([]);
+const logs = ref<{
+ id: number;
+ text: string;
+ print: boolean;
+}[]>([]);
const root = ref<AsUiRoot>();
const components = ref<Ref<AsUiComponent>[]>([]);
const uiKey = ref(0);
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 2244047b31..18c82ffdf6 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -138,12 +138,13 @@ const token = ref<string | number | null>(null);
const backupCodes = ref<string[]>();
function cancel() {
- dialog.value.close();
+ dialog.value?.close();
}
async function tokenDone() {
+ if (token.value == null) return;
const res = await os.apiWithDialog('i/2fa/done', {
- token: token.value.toString(),
+ token: typeof token.value === 'string' ? token.value : token.value.toString(),
});
backupCodes.value = res.backupCodes;
@@ -166,7 +167,7 @@ function downloadBackupCodes() {
}
function allDone() {
- dialog.value.close();
+ dialog.value?.close();
}
</script>
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index a76b748ac1..776f59dda3 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -84,7 +84,7 @@ 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, updateAccount } from '@/account.js';
+import { signinRequired, updateAccountPartial } from '@/account.js';
import { i18n } from '@/i18n.js';
const $i = signinRequired();
@@ -123,7 +123,7 @@ async function unregisterTOTP(): Promise<void> {
password: auth.result.password,
token: auth.result.token,
}).then(res => {
- updateAccount({
+ updateAccountPartial({
twoFactorEnabled: false,
});
}).catch(error => {
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 1bbedb817e..97e960675f 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -19,13 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, ref, computed } from 'vue';
+import { ref, computed } from 'vue';
import type * as Misskey from 'misskey-js';
import FormSuspense from '@/components/form/suspense.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
-import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account.js';
+import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
@@ -74,27 +74,23 @@ async function removeAccount(account: Misskey.entities.UserDetailed) {
}
function addExistingAccount() {
- const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
- done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
- await addAccounts(res.id, res.i);
+ getAccountWithSigninDialog().then((res) => {
+ if (res != null) {
os.success();
init();
- },
- closed: () => dispose(),
+ }
});
}
function createAccount() {
- const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
- done: async (res: Misskey.entities.SignupResponse) => {
- await addAccounts(res.id, res.token);
+ getAccountWithSignupDialog().then((res) => {
+ if (res != null) {
switchAccountWithToken(res.token);
- },
- closed: () => dispose(),
+ }
});
}
-async function switchAccount(account: any) {
+async function switchAccount(account: Misskey.entities.UserDetailed) {
const fetchedAccounts = await getAccounts();
const token = fetchedAccounts.find(x => x.id === account.id)!.token;
switchAccountWithToken(token);
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 68e36ef1bb..6515503505 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
+import * as Misskey from 'misskey-js';
import FormPagination from '@/components/MkPagination.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
@@ -77,7 +78,7 @@ const pagination = {
function revoke(token) {
misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
- list.value.reload();
+ list.value?.reload();
});
}
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
index f72a0b9383..3c9914b4e2 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
@@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div>
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY }]" forceShowDecoration/>
- <i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i>
+ <i v-if="locked" :class="$style.lock" class="ti ti-lock"></i>
</div>
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
import { signinRequired } from '@/account.js';
const $i = signinRequired();
@@ -37,6 +37,8 @@ const props = defineProps<{
const emit = defineEmits<{
(ev: 'click'): void;
}>();
+
+const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
</script>
<style lang="scss" module>
@@ -67,5 +69,6 @@ const emit = defineEmits<{
position: absolute;
bottom: 12px;
right: 12px;
+ color: var(--MI_THEME-warn);
}
</style>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index 853e536ea3..40542ad5b2 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.footer" class="_buttonsCenter">
<MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton>
<MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton>
- <MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
+ <MkButton v-else :disabled="exceeded || locked" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
</div>
</div>
</MkModalWindow>
@@ -61,6 +61,7 @@ const props = defineProps<{
id: string;
url: string;
name: string;
+ roleIdsThatCanBeUsedThisDecoration: string[];
};
}>();
@@ -83,6 +84,7 @@ const emit = defineEmits<{
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0);
+const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0);
const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false);
const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0);
@@ -108,7 +110,7 @@ const decorationsForPreview = computed(() => {
});
function cancel() {
- dialog.value.close();
+ dialog.value?.close();
}
async function update() {
@@ -118,7 +120,7 @@ async function update() {
offsetX: offsetX.value,
offsetY: offsetY.value,
});
- dialog.value.close();
+ dialog.value?.close();
}
async function attach() {
@@ -128,12 +130,12 @@ async function attach() {
offsetX: offsetX.value,
offsetY: offsetY.value,
});
- dialog.value.close();
+ dialog.value?.close();
}
async function detach() {
emit('detach');
- dialog.value.close();
+ dialog.value?.close();
}
</script>
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index b189db0f8f..c38cdc4fc2 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -100,10 +100,6 @@ function reset() {
}));
}
-watch(menuDisplay, async () => {
- await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
-});
-
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
diff --git a/packages/frontend/src/pages/settings/notifications.notification-config.vue b/packages/frontend/src/pages/settings/notifications.notification-config.vue
index a36f036303..0ea415f673 100644
--- a/packages/frontend/src/pages/settings/notifications.notification-config.vue
+++ b/packages/frontend/src/pages/settings/notifications.notification-config.vue
@@ -6,13 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<MkSelect v-model="type">
- <option value="all">{{ i18n.ts.all }}</option>
- <option value="following">{{ i18n.ts.following }}</option>
- <option value="follower">{{ i18n.ts.followers }}</option>
- <option value="mutualFollow">{{ i18n.ts.mutualFollow }}</option>
- <option value="followingOrFollower">{{ i18n.ts.followingOrFollower }}</option>
- <option value="list">{{ i18n.ts.userList }}</option>
- <option value="never">{{ i18n.ts.none }}</option>
+ <option v-for="type in props.configurableTypes ?? notificationConfigTypes" :key="type" :value="type">{{ notificationConfigTypesI18nMap[type] }}</option>
</MkSelect>
<MkSelect v-if="type === 'list'" v-model="userListId">
@@ -21,31 +15,61 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect>
<div class="_buttons">
- <MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+ <MkButton inline primary :disabled="type === 'list' && userListId === null" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
</template>
+<script lang="ts">
+const notificationConfigTypes = [
+ 'all',
+ 'following',
+ 'follower',
+ 'mutualFollow',
+ 'followingOrFollower',
+ 'list',
+ 'never'
+] as const;
+
+export type NotificationConfig = {
+ type: Exclude<typeof notificationConfigTypes[number], 'list'>;
+} | {
+ type: 'list';
+ userListId: string;
+};
+</script>
+
<script lang="ts" setup>
-import { ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { ref } from 'vue';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
const props = defineProps<{
- value: any;
+ value: NotificationConfig;
userLists: Misskey.entities.UserList[];
+ configurableTypes?: NotificationConfig['type'][]; // If not specified, all types are configurable
}>();
const emit = defineEmits<{
- (ev: 'update', result: any): void;
+ (ev: 'update', result: NotificationConfig): void;
}>();
+const notificationConfigTypesI18nMap: Record<typeof notificationConfigTypes[number], string> = {
+ all: i18n.ts.all,
+ following: i18n.ts.following,
+ follower: i18n.ts.followers,
+ mutualFollow: i18n.ts.mutualFollow,
+ followingOrFollower: i18n.ts.followingOrFollower,
+ list: i18n.ts.userList,
+ never: i18n.ts.none,
+};
+
const type = ref(props.value.type);
-const userListId = ref(props.value.userListId);
+const userListId = ref(props.value.type === 'list' ? props.value.userListId : null);
function save() {
- emit('update', { type: type.value, userListId: userListId.value });
+ emit('update', type.value === 'list' ? { type: type.value, userListId: userListId.value! } : { type: type.value });
}
</script>
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index 53b3bd4936..8ffe0d6a7a 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -22,7 +22,12 @@ SPDX-License-Identifier: AGPL-3.0-only
}}
</template>
- <XNotificationConfig :userLists="userLists" :value="$i.notificationRecieveConfig[type] ?? { type: 'all' }" @update="(res) => updateReceiveConfig(type, res)"/>
+ <XNotificationConfig
+ :userLists="userLists"
+ :value="$i.notificationRecieveConfig[type] ?? { type: 'all' }"
+ :configurableTypes="onlyOnOrOffNotificationTypes.includes(type) ? ['all', 'never'] : undefined"
+ @update="(res) => updateReceiveConfig(type, res)"
+ />
</MkFolder>
</div>
</FormSection>
@@ -58,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { shallowRef, computed } from 'vue';
-import XNotificationConfig from './notifications.notification-config.vue';
+import XNotificationConfig, { type NotificationConfig } from './notifications.notification-config.vue';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
@@ -73,7 +78,9 @@ import { notificationTypes } from '@@/js/const.js';
const $i = signinRequired();
-const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[];
+const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[];
+
+const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login'] satisfies (typeof notificationTypes[number])[] as string[];
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
@@ -88,7 +95,7 @@ async function readAllNotifications() {
await os.apiWithDialog('notifications/mark-all-as-read');
}
-async function updateReceiveConfig(type, value) {
+async function updateReceiveConfig(type: typeof notificationTypes[number], value: NotificationConfig) {
await os.apiWithDialog('i/update', {
notificationRecieveConfig: {
...$i.notificationRecieveConfig,
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index d418be624e..40d9be0f60 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.noCrawleDescription }}</template>
</MkSwitch>
<MkSwitch v-model="preventAiLearning" @update:modelValue="save()">
- {{ i18n.ts.preventAiLearning }}<span class="_beta">{{ i18n.ts.beta }}</span>
+ {{ i18n.ts.preventAiLearning }}
<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template>
</MkSwitch>
<MkSwitch v-model="isExplorable" @update:modelValue="save()">
@@ -45,6 +45,93 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
<FormSection>
+ <template #label>{{ i18n.ts.lockdown }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
+
+ <div class="_gaps_m">
+ <MkSwitch :modelValue="requireSigninToViewContents" @update:modelValue="update_requireSigninToViewContents">
+ {{ i18n.ts._accountSettings.requireSigninToViewContents }}
+ <template #caption>
+ <div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div>
+ <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div>
+ <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
+ </template>
+ </MkSwitch>
+
+ <FormSlot>
+ <template #label>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBefore }}</template>
+
+ <div class="_gaps_s">
+ <MkSelect :modelValue="makeNotesFollowersOnlyBefore_type" @update:modelValue="makeNotesFollowersOnlyBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
+ <option :value="null">{{ i18n.ts.none }}</option>
+ <option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
+ <option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
+ </MkSelect>
+
+ <MkSelect v-if="makeNotesFollowersOnlyBefore_type === 'relative'" v-model="makeNotesFollowersOnlyBefore">
+ <option :value="-3600">{{ i18n.ts.oneHour }}</option>
+ <option :value="-86400">{{ i18n.ts.oneDay }}</option>
+ <option :value="-259200">{{ i18n.ts.threeDays }}</option>
+ <option :value="-604800">{{ i18n.ts.oneWeek }}</option>
+ <option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
+ <option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
+ <option :value="-31104000">{{ i18n.ts.oneYear }}</option>
+ </MkSelect>
+
+ <MkInput
+ v-if="makeNotesFollowersOnlyBefore_type === 'absolute'"
+ :modelValue="formatDateTimeString(new Date(makeNotesFollowersOnlyBefore * 1000), 'yyyy-MM-dd')"
+ type="date"
+ :manualSave="true"
+ @update:modelValue="makeNotesFollowersOnlyBefore = Math.floor(new Date($event).getTime() / 1000)"
+ >
+ </MkInput>
+ </div>
+
+ <template #caption>
+ <div>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</div>
+ <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
+ </template>
+ </FormSlot>
+
+ <FormSlot>
+ <template #label>{{ i18n.ts._accountSettings.makeNotesHiddenBefore }}</template>
+
+ <div class="_gaps_s">
+ <MkSelect :modelValue="makeNotesHiddenBefore_type" @update:modelValue="makeNotesHiddenBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
+ <option :value="null">{{ i18n.ts.none }}</option>
+ <option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
+ <option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
+ </MkSelect>
+
+ <MkSelect v-if="makeNotesHiddenBefore_type === 'relative'" v-model="makeNotesHiddenBefore">
+ <option :value="-3600">{{ i18n.ts.oneHour }}</option>
+ <option :value="-86400">{{ i18n.ts.oneDay }}</option>
+ <option :value="-259200">{{ i18n.ts.threeDays }}</option>
+ <option :value="-604800">{{ i18n.ts.oneWeek }}</option>
+ <option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
+ <option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
+ <option :value="-31104000">{{ i18n.ts.oneYear }}</option>
+ </MkSelect>
+
+ <MkInput
+ v-if="makeNotesHiddenBefore_type === 'absolute'"
+ :modelValue="formatDateTimeString(new Date(makeNotesHiddenBefore * 1000), 'yyyy-MM-dd')"
+ type="date"
+ :manualSave="true"
+ @update:modelValue="makeNotesHiddenBefore = Math.floor(new Date($event).getTime() / 1000)"
+ >
+ </MkInput>
+ </div>
+
+ <template #caption>
+ <div>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</div>
+ <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
+ </template>
+ </FormSlot>
+ </div>
+ </FormSection>
+
+ <FormSection>
<div class="_gaps_m">
<MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch>
<MkFolder v-if="!rememberNoteVisibility">
@@ -72,7 +159,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, watch } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue';
@@ -82,6 +169,10 @@ import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { signinRequired } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import FormSlot from '@/components/form/slot.vue';
+import { formatDateTimeString } from '@/scripts/format-time-string.js';
+import MkInput from '@/components/MkInput.vue';
+import * as os from '@/os.js';
const $i = signinRequired();
@@ -90,6 +181,9 @@ const autoAcceptFollowed = ref($i.autoAcceptFollowed);
const noCrawle = ref($i.noCrawle);
const preventAiLearning = ref($i.preventAiLearning);
const isExplorable = ref($i.isExplorable);
+const requireSigninToViewContents = ref($i.requireSigninToViewContents ?? false);
+const makeNotesFollowersOnlyBefore = ref($i.makeNotesFollowersOnlyBefore ?? null);
+const makeNotesHiddenBefore = ref($i.makeNotesHiddenBefore ?? null);
const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions);
const followingVisibility = ref($i.followingVisibility);
@@ -100,6 +194,43 @@ const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNote
const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
+const makeNotesFollowersOnlyBefore_type = computed(() => {
+ if (makeNotesFollowersOnlyBefore.value == null) {
+ return null;
+ } else if (makeNotesFollowersOnlyBefore.value >= 0) {
+ return 'absolute';
+ } else {
+ return 'relative';
+ }
+});
+
+const makeNotesHiddenBefore_type = computed(() => {
+ if (makeNotesHiddenBefore.value == null) {
+ return null;
+ } else if (makeNotesHiddenBefore.value >= 0) {
+ return 'absolute';
+ } else {
+ return 'relative';
+ }
+});
+
+watch([makeNotesFollowersOnlyBefore, makeNotesHiddenBefore], () => {
+ save();
+});
+
+async function update_requireSigninToViewContents(value: boolean) {
+ if (value) {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.acknowledgeNotesAndEnable,
+ });
+ if (canceled) return;
+ }
+
+ requireSigninToViewContents.value = value;
+ save();
+}
+
function save() {
misskeyApi('i/update', {
isLocked: !!isLocked.value,
@@ -107,6 +238,9 @@ function save() {
noCrawle: !!noCrawle.value,
preventAiLearning: !!preventAiLearning.value,
isExplorable: !!isExplorable.value,
+ requireSigninToViewContents: !!requireSigninToViewContents.value,
+ makeNotesFollowersOnlyBefore: makeNotesFollowersOnlyBefore.value,
+ makeNotesHiddenBefore: makeNotesHiddenBefore.value,
hideOnlineStatus: !!hideOnlineStatus.value,
publicReactions: !!publicReactions.value,
followingVisibility: followingVisibility.value,
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index 40d23e36c5..22b008fb61 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
</div>
<div :class="$style.switchBox">
- <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
+ <MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
</div>
<div :class="$style.switchBox">
diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue
index d62357caaf..727c4df2d6 100644
--- a/packages/frontend/src/pages/settings/webhook.new.vue
+++ b/packages/frontend/src/pages/settings/webhook.new.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
- <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
+ <MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
</div>
</FormSection>
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 4feba54104..044a1908ab 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -17,11 +17,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tl">
<MkTimeline
ref="tlComponent"
- :key="src + withRenotes + withReplies + onlyFiles"
+ :key="src + withRenotes + withReplies + onlyFiles + withSensitive"
:src="src.split(':')[0]"
:list="src.split(':')[1]"
:withRenotes="withRenotes"
:withReplies="withReplies"
+ :withSensitive="withSensitive"
:onlyFiles="onlyFiles"
:sound="true"
@queue="queueUpdated"
@@ -121,11 +122,6 @@ watch(src, () => {
queue.value = 0;
});
-watch(withSensitive, () => {
- // これだけはクライアント側で完結する処理なので手動でリロード
- tlComponent.value?.reloadTimeline();
-});
-
function queueUpdated(q: number): void {
queue.value = q;
}
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 00b5740639..2794db2821 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -257,7 +257,7 @@ function parallaxLoop() {
}
function parallax() {
- const banner = bannerEl.value as any;
+ const banner = bannerEl.value;
if (banner == null) return;
const top = getScrollPosition(rootEl.value);
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index a6244e2a93..d862387401 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -39,6 +39,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
+import { getServerContext } from '@/server-context.js';
const XHome = defineAsyncComponent(() => import('./home.vue'));
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
@@ -52,6 +53,8 @@ const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
+const CTX_USER = getServerContext('user');
+
const props = withDefaults(defineProps<{
acct: string;
page?: string;
@@ -61,13 +64,24 @@ const props = withDefaults(defineProps<{
const tab = ref(props.page);
-const user = ref<null | Misskey.entities.UserDetailed>(null);
+const user = ref<null | Misskey.entities.UserDetailed>(CTX_USER);
const error = ref<any>(null);
function fetchUser(): void {
if (props.acct == null) return;
+
+ const { username, host } = Misskey.acct.parse(props.acct);
+
+ if (CTX_USER && CTX_USER.username === username && CTX_USER.host === host) {
+ user.value = CTX_USER;
+ return;
+ }
+
user.value = null;
- misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => {
+ misskeyApi('users/show', {
+ username,
+ host,
+ }).then(u => {
user.value = u;
}).catch(err => {
error.value = err;
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index ac325e923f..7740fe0d39 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -241,9 +241,13 @@ export class Storage<T extends StateDef> {
* 特定のキーの、簡易的なgetter/setterを作ります
* 主にvue上で設定コントロールのmodelとして使う用
*/
- public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): {
- get: () => T[K]['default'];
- set: (value: T[K]['default']) => void;
+ public makeGetterSetter<K extends keyof T, R = T[K]['default']>(
+ key: K,
+ getter?: (v: T[K]['default']) => R,
+ setter?: (v: R) => T[K]['default'],
+ ): {
+ get: () => R;
+ set: (value: R) => void;
} {
const valueRef = ref(this.state[key]);
@@ -265,7 +269,7 @@ export class Storage<T extends StateDef> {
return valueRef.value;
}
},
- set: (value: unknown) => {
+ set: (value) => {
const val = setter ? setter(value) : value;
this.set(key, val);
valueRef.value = val;
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 75f994b865..e98e0b59b1 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -10,7 +10,7 @@ import { $i, iAmModerator } from '@/account.js';
import MkLoading from '@/pages/_loading_.vue';
import MkError from '@/pages/_error_.vue';
-export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
+export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
loader: loader,
loadingComponent: MkLoading,
errorComponent: MkError,
@@ -217,7 +217,7 @@ const routes: RouteDef[] = [{
component: page(() => import('@/pages/theme-editor.vue')),
loginRequired: true,
}, {
- path: '/roles/:role',
+ path: '/roles/:roleId',
component: page(() => import('@/pages/role.vue')),
}, {
path: '/user-tags/:tag',
diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts
index 6ee967e6f4..3c25e80d12 100644
--- a/packages/frontend/src/router/main.ts
+++ b/packages/frontend/src/router/main.ts
@@ -4,7 +4,7 @@
*/
import { EventEmitter } from 'eventemitter3';
-import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
+import { IRouter, Resolved, RouteDef, RouterEvent, RouterFlag } from '@/nirax.js';
import type { App, ShallowRef } from 'vue';
@@ -79,7 +79,7 @@ class MainRouterProxy implements IRouter {
return this.supplier().currentRoute;
}
- get navHook(): ((path: string, flag?: any) => boolean) | null {
+ get navHook(): ((path: string, flag?: RouterFlag) => boolean) | null {
return this.supplier().navHook;
}
@@ -91,11 +91,11 @@ class MainRouterProxy implements IRouter {
return this.supplier().getCurrentKey();
}
- getCurrentPath(): any {
+ getCurrentPath(): string {
return this.supplier().getCurrentPath();
}
- push(path: string, flag?: any): void {
+ push(path: string, flag?: RouterFlag): void {
this.supplier().push(path, flag);
}
diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts
index 67e896b4b9..0a37a08bf0 100644
--- a/packages/frontend/src/scripts/check-word-mute.ts
+++ b/packages/frontend/src/scripts/check-word-mute.ts
@@ -2,8 +2,9 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import * as Misskey from 'misskey-js';
-export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean {
+export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): boolean {
// 自分自身
if (me && (note.userId === me.id)) return false;
diff --git a/packages/frontend/src/scripts/device-kind.ts b/packages/frontend/src/scripts/device-kind.ts
index 7c33f8ccee..7aadb617ca 100644
--- a/packages/frontend/src/scripts/device-kind.ts
+++ b/packages/frontend/src/scripts/device-kind.ts
@@ -3,22 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { defaultStore } from '@/store.js';
-
-await defaultStore.ready;
+export type DeviceKind = 'smartphone' | 'tablet' | 'desktop';
const ua = navigator.userAgent.toLowerCase();
const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
-const isIPhone = /iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1;
-// navigator.platform may be deprecated but this check is still required
-const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
-const isIos = /ipad|iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1;
+export const DEFAULT_DEVICE_KIND: DeviceKind = (
+ isSmartphone
+ ? 'smartphone'
+ : isTablet
+ ? 'tablet'
+ : 'desktop'
+);
-export const isFullscreenNotSupported = isIPhone || isIos;
+export let deviceKind: DeviceKind = DEFAULT_DEVICE_KIND;
-export const deviceKind: 'smartphone' | 'tablet' | 'desktop' = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind
- : isSmartphone ? 'smartphone'
- : isTablet ? 'tablet'
- : 'desktop';
+export function updateDeviceKind(kind: DeviceKind | null) {
+ deviceKind = kind ?? DEFAULT_DEVICE_KIND;
+}
diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts
index 242a504c3b..1032e97ac9 100644
--- a/packages/frontend/src/scripts/form.ts
+++ b/packages/frontend/src/scripts/form.ts
@@ -15,7 +15,7 @@ type Hidden = boolean | ((v: any) => boolean);
export type FormItem = {
label?: string;
type: 'string';
- default: string | null;
+ default?: string | null;
description?: string;
required?: boolean;
hidden?: Hidden;
@@ -24,7 +24,7 @@ export type FormItem = {
} | {
label?: string;
type: 'number';
- default: number | null;
+ default?: number | null;
description?: string;
required?: boolean;
hidden?: Hidden;
@@ -32,20 +32,20 @@ export type FormItem = {
} | {
label?: string;
type: 'boolean';
- default: boolean | null;
+ default?: boolean | null;
description?: string;
hidden?: Hidden;
} | {
label?: string;
type: 'enum';
- default: string | null;
+ default?: string | null;
required?: boolean;
hidden?: Hidden;
enum: EnumItem[];
} | {
label?: string;
type: 'radio';
- default: unknown | null;
+ default?: unknown | null;
required?: boolean;
hidden?: Hidden;
options: {
@@ -55,7 +55,7 @@ export type FormItem = {
} | {
label?: string;
type: 'range';
- default: number | null;
+ default?: number | null;
description?: string;
required?: boolean;
step?: number;
@@ -66,12 +66,12 @@ export type FormItem = {
} | {
label?: string;
type: 'object';
- default: Record<string, unknown> | null;
+ default?: Record<string, unknown> | null;
hidden: Hidden;
} | {
label?: string;
type: 'array';
- default: unknown[] | null;
+ default?: unknown[] | null;
hidden: Hidden;
} | {
type: 'button';
diff --git a/packages/frontend/src/scripts/fullscreen.ts b/packages/frontend/src/scripts/fullscreen.ts
new file mode 100644
index 0000000000..7a0a018ef3
--- /dev/null
+++ b/packages/frontend/src/scripts/fullscreen.ts
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+type PartiallyPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
+
+type VideoEl = PartiallyPartial<HTMLVideoElement, 'requestFullscreen'> & {
+ webkitEnterFullscreen?(): void;
+ webkitExitFullscreen?(): void;
+};
+
+type PlayerEl = PartiallyPartial<HTMLElement, 'requestFullscreen'>;
+
+type RequestFullscreenProps = {
+ readonly videoEl: VideoEl;
+ readonly playerEl: PlayerEl;
+ readonly options?: FullscreenOptions | null;
+};
+
+type ExitFullscreenProps = {
+ readonly videoEl: VideoEl;
+};
+
+export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscreenProps) => {
+ if (playerEl.requestFullscreen != null) {
+ playerEl.requestFullscreen(options ?? undefined);
+ return;
+ }
+ if (videoEl.webkitEnterFullscreen != null) {
+ videoEl.webkitEnterFullscreen();
+ return;
+ }
+};
+
+export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (document.exitFullscreen != null) {
+ document.exitFullscreen();
+ return;
+ }
+ if (videoEl.webkitExitFullscreen != null) {
+ videoEl.webkitExitFullscreen();
+ return;
+ }
+};
diff --git a/packages/frontend/src/scripts/get-bg-color.ts b/packages/frontend/src/scripts/get-bg-color.ts
new file mode 100644
index 0000000000..ccf60b454f
--- /dev/null
+++ b/packages/frontend/src/scripts/get-bg-color.ts
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import tinycolor from 'tinycolor2';
+
+export const getBgColor = (elem?: Element | null | undefined): string | null => {
+ if (elem == null) return null;
+
+ const { backgroundColor: bg } = window.getComputedStyle(elem);
+
+ if (bg && tinycolor(bg).getAlpha() !== 0) {
+ return bg;
+ }
+
+ return getBgColor(elem.parentElement);
+};
diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts
index 1b1159fd01..e7a92e2d5c 100644
--- a/packages/frontend/src/scripts/misskey-api.ts
+++ b/packages/frontend/src/scripts/misskey-api.ts
@@ -17,7 +17,7 @@ export function misskeyApi<
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
>(
endpoint: E,
- data: P = {} as any,
+ data: P & { i?: string | null; } = {} as any,
token?: string | null | undefined,
signal?: AbortSignal,
): Promise<_ResT> {
@@ -30,8 +30,8 @@ export function misskeyApi<
const promise = new Promise<_ResT>((resolve, reject) => {
// Append a credential
- if ($i) (data as any).i = $i.token;
- if (token !== undefined) (data as any).i = token;
+ if ($i) data.i = $i.token;
+ if (token !== undefined) data.i = token;
// Send request
window.fetch(`${apiUrl}/${endpoint}`, {
diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts
index 18f05bc7f4..43dcf11936 100644
--- a/packages/frontend/src/scripts/please-login.ts
+++ b/packages/frontend/src/scripts/please-login.ts
@@ -44,17 +44,21 @@ export type OpenOnRemoteOptions = {
params: Record<string, string>;
};
-export function pleaseLogin(path?: string, openOnRemote?: OpenOnRemoteOptions) {
+export function pleaseLogin(opts: {
+ path?: string;
+ message?: string;
+ openOnRemote?: OpenOnRemoteOptions;
+} = {}) {
if ($i) return;
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
autoSet: true,
- message: openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired,
- openOnRemote,
+ message: opts.message ?? (opts.openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired),
+ openOnRemote: opts.openOnRemote,
}, {
cancelled: () => {
- if (path) {
- window.location.href = path;
+ if (opts.path) {
+ window.location.href = opts.path;
}
},
closed: () => dispose(),
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts
index 9aa38178b2..b037aa8acc 100644
--- a/packages/frontend/src/scripts/select-file.ts
+++ b/packages/frontend/src/scripts/select-file.ts
@@ -80,7 +80,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
});
}
-function select(src: any, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
+function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => {
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
@@ -107,10 +107,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Miss
});
}
-export function selectFile(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile> {
+export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
return select(src, label, false).then(files => files[0]);
}
-export function selectFiles(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
+export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
return select(src, label, true);
}
diff --git a/packages/frontend/src/scripts/shuffle.ts b/packages/frontend/src/scripts/shuffle.ts
index fed16bc71c..1f6ef1928c 100644
--- a/packages/frontend/src/scripts/shuffle.ts
+++ b/packages/frontend/src/scripts/shuffle.ts
@@ -6,8 +6,9 @@
/**
* 配列をシャッフル (破壊的)
*/
-export function shuffle<T extends any[]>(array: T): T {
- let currentIndex = array.length, randomIndex;
+export function shuffle<T extends unknown[]>(array: T): T {
+ let currentIndex = array.length;
+ let randomIndex: number;
// While there remain elements to shuffle.
while (currentIndex !== 0) {
diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts
index 22dce609c6..713573a377 100644
--- a/packages/frontend/src/scripts/upload.ts
+++ b/packages/frontend/src/scripts/upload.ts
@@ -32,13 +32,13 @@ const mimeTypeMap = {
export function uploadFile(
file: File,
- folder?: any,
+ folder?: string | Misskey.entities.DriveFolder,
name?: string,
keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
): Promise<Misskey.entities.DriveFile> {
if ($i == null) throw new Error('Not logged in');
- if (folder && typeof folder === 'object') folder = folder.id;
+ const _folder = typeof folder === 'string' ? folder : folder?.id;
if (file.size > instance.maxFileSize) {
alert({
@@ -89,11 +89,11 @@ export function uploadFile(
}
const formData = new FormData();
- formData.append('i', $i.token);
+ formData.append('i', $i!.token);
formData.append('force', 'true');
formData.append('file', resizedImage ?? file);
formData.append('name', ctx.name);
- if (folder) formData.append('folderId', folder);
+ if (_folder) formData.append('folderId', _folder);
const xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + '/drive/files/create', true);
diff --git a/packages/frontend/src/server-context.ts b/packages/frontend/src/server-context.ts
new file mode 100644
index 0000000000..aa44a10290
--- /dev/null
+++ b/packages/frontend/src/server-context.ts
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import * as Misskey from 'misskey-js';
+import { $i } from '@/account.js';
+
+const providedContextEl = document.getElementById('misskey_clientCtx');
+
+export type ServerContext = {
+ clip?: Misskey.entities.Clip;
+ note?: Misskey.entities.Note;
+ user?: Misskey.entities.UserLite;
+} | null;
+
+export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null;
+
+export function getServerContext<K extends keyof NonNullable<ServerContext>>(entity: K): Required<Pick<NonNullable<ServerContext>, K>> | null {
+ // contextは非ログイン状態の情報しかないためログイン時は利用できない
+ if ($i) return null;
+
+ return serverContext ? (serverContext[entity] ?? null) : null;
+}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index aab67e0b5c..1d981e897b 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -8,9 +8,11 @@ import * as Misskey from 'misskey-js';
import { hemisphere } from '@@/js/intl-const.js';
import lightTheme from '@@/themes/l-light.json5';
import darkTheme from '@@/themes/d-green-lime.json5';
-import { miLocalStorage } from './local-storage.js';
import type { SoundType } from '@/scripts/sound.js';
+import { DEFAULT_DEVICE_KIND, type DeviceKind } from '@/scripts/device-kind.js';
+import { miLocalStorage } from '@/local-storage.js';
import { Storage } from '@/pizzax.js';
+import type { Ast } from '@syuilo/aiscript';
interface PostFormAction {
title: string,
@@ -206,7 +208,7 @@ export const defaultStore = markRaw(new Storage('base', {
overridedDeviceKind: {
where: 'device',
- default: null as null | 'smartphone' | 'tablet' | 'desktop',
+ default: null as DeviceKind | null,
},
serverDisconnectedBehavior: {
where: 'device',
@@ -262,11 +264,11 @@ export const defaultStore = markRaw(new Storage('base', {
},
useBlurEffectForModal: {
where: 'device',
- default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない
+ default: DEFAULT_DEVICE_KIND === 'desktop',
},
useBlurEffect: {
where: 'device',
- default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない
+ default: DEFAULT_DEVICE_KIND === 'desktop',
},
showFixedPostForm: {
where: 'device',
@@ -516,7 +518,7 @@ export type Plugin = {
token: string;
src: string | null;
version: string;
- ast: any[];
+ ast: Ast.Node[];
author?: string;
description?: string;
permissions?: string[];
@@ -554,13 +556,13 @@ export class ColdDeviceStorage {
}
public static getAll(): Partial<typeof this.default> {
- return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce((acc, key) => {
+ return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce<Partial<typeof this.default>>((acc, key) => {
const value = localStorage.getItem(PREFIX + key);
if (value != null) {
acc[key] = JSON.parse(value);
}
return acc;
- }, {} as any);
+ }, {});
}
public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
@@ -605,7 +607,7 @@ export class ColdDeviceStorage {
get: () => {
return valueRef.value;
},
- set: (value: unknown) => {
+ set: (value: typeof ColdDeviceStorage.default[K]) => {
const val = value;
ColdDeviceStorage.set(key, val);
},
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 4204c5af59..48aacf10bc 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -497,20 +497,17 @@ html[data-color-scheme=dark] ._woodenFrame {
10%,
20% {
- transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
+ transform: scale3d(0.91, 0.91, 0.91) rotate3d(0, 0, 1, -2deg);
}
30%,
- 50%,
- 70%,
- 90% {
- transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
+ 70% {
+ transform: scale3d(1.09, 1.09, 1.09) rotate3d(0, 0, 1, 2deg);
}
- 40%,
- 60%,
- 80% {
- transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
+ 50%,
+ 90% {
+ transform: scale3d(1.09, 1.09, 1.09) rotate3d(0, 0, 1, -2deg);
}
to {
diff --git a/packages/frontend/src/types/post-form.ts b/packages/frontend/src/types/post-form.ts
new file mode 100644
index 0000000000..5bb04a95a0
--- /dev/null
+++ b/packages/frontend/src/types/post-form.ts
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+
+export interface PostFormProps {
+ reply?: Misskey.entities.Note;
+ renote?: Misskey.entities.Note;
+ channel?: Misskey.entities.Channel; // TODO
+ mention?: Misskey.entities.User;
+ specified?: Misskey.entities.UserDetailed;
+ initialText?: string;
+ initialCw?: string;
+ initialVisibility?: (typeof Misskey.noteVisibilities)[number];
+ initialFiles?: Misskey.entities.DriveFile[];
+ initialLocalOnly?: boolean;
+ initialVisibleUsers?: Misskey.entities.UserDetailed[];
+ initialNote?: Misskey.entities.Note;
+ instant?: boolean;
+};
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 8ae11efa2c..8fc76741e3 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -56,6 +56,21 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</div>
</div>
+ <button v-if="!forceIconOnly" class="_button" :class="$style.toggleButton" @click="toggleIconOnly">
+ <!--
+ <svg viewBox="0 0 16 48" :class="$style.toggleButtonShape">
+ <g transform="matrix(0.333333,0,0,0.222222,0.000895785,13.3333)">
+ <path d="M23.935,-24C37.223,-24 47.995,-7.842 47.995,12.09C47.995,34.077 47.995,62.07 47.995,84.034C47.995,93.573 45.469,102.721 40.972,109.466C36.475,116.211 30.377,120 24.018,120L23.997,120C10.743,120 -0.003,136.118 -0.003,156C-0.003,156 -0.003,156 -0.003,156L-0.003,-60L-0.003,-59.901C-0.003,-50.379 2.519,-41.248 7.007,-34.515C11.496,-27.782 17.584,-24 23.931,-24C23.932,-24 23.934,-24 23.935,-24Z" style="fill:var(--MI_THEME-navBg);"/>
+ </g>
+ </svg>
+ -->
+ <svg viewBox="0 0 16 64" :class="$style.toggleButtonShape">
+ <g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)">
+ <path d="M47.488,7.995C47.79,10.11 47.943,12.266 47.943,14.429C47.997,26.989 47.997,84 47.997,84C47.997,84 44.018,118.246 23.997,133.5C-0.374,152.07 -0.003,192 -0.003,192L-0.003,-96C-0.003,-96 0.151,-56.216 23.997,-37.5C40.861,-24.265 46.043,-1.243 47.488,7.995Z" style="fill:var(--MI_THEME-navBg);"/>
+ </g>
+ </svg>
+ <i :class="'ti ' + `ti-chevron-${ iconOnly ? 'right' : 'left' }`" style="font-size: 12px; margin-left: -8px;"></i>
+ </button>
</div>
</template>
@@ -80,9 +95,11 @@ const otherMenuItemIndicated = computed(() => {
return false;
});
-const calcViewState = () => {
- iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
-};
+const forceIconOnly = window.innerWidth <= 1279;
+
+function calcViewState() {
+ iconOnly.value = forceIconOnly || (defaultStore.state.menuDisplay === 'sideIcon');
+}
calcViewState();
@@ -92,6 +109,10 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
calcViewState();
});
+function toggleIconOnly() {
+ defaultStore.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon');
+}
+
function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
withExtraOperation: true,
@@ -133,6 +154,38 @@ function more(ev: MouseEvent) {
contain: strict;
display: flex;
flex-direction: column;
+ direction: rtl; // スクロールバーを左に表示したいため
+}
+
+.top {
+ direction: ltr;
+}
+
+.middle {
+ direction: ltr;
+}
+
+.bottom {
+ direction: ltr;
+}
+
+.toggleButton {
+ position: fixed;
+ bottom: 20px;
+ left: var(--nav-width);
+ z-index: 1001;
+ width: 16px;
+ height: 64px;
+ box-sizing: border-box;
+}
+
+.toggleButtonShape {
+ position: absolute;
+ z-index: -1;
+ top: 0;
+ left: 0;
+ width: 16px;
+ height: 64px;
}
.root:not(.iconOnly) {
@@ -363,6 +416,10 @@ function more(ev: MouseEvent) {
position: relative;
font-size: 0.9em;
}
+
+ .toggleButton {
+ left: var(--nav-width);
+ }
}
.root.iconOnly {
@@ -563,5 +620,9 @@ function more(ev: MouseEvent) {
font-size: 10px;
}
}
+
+ .toggleButton {
+ left: var(--nav-icon-only-width);
+ }
}
</style>
diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue
index 550fc39b00..da8fa8bb21 100644
--- a/packages/frontend/src/ui/_common_/statusbar-rss.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue
@@ -48,7 +48,7 @@ const fetching = ref(true);
const key = ref(0);
const tick = () => {
- window.fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
+ window.fetch(`/api/fetch-rss?url=${encodeURIComponent(props.url)}`, {}).then(res => {
res.json().then((feed: Misskey.entities.FetchRssResponse) => {
if (props.shuffle) {
shuffle(feed.items);
diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts
index eb587554b9..3186982349 100644
--- a/packages/frontend/src/ui/deck/deck-store.ts
+++ b/packages/frontend/src/ui/deck/deck-store.ts
@@ -49,6 +49,7 @@ export type Column = {
tl?: BasicTimelineType;
withRenotes?: boolean;
withReplies?: boolean;
+ withSensitive?: boolean;
onlyFiles?: boolean;
soundSetting: SoundStore;
};
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index 01da92f731..74c4fb504b 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:src="column.tl"
:withRenotes="withRenotes"
:withReplies="withReplies"
+ :withSensitive="withSensitive"
:onlyFiles="onlyFiles"
@note="onNote"
/>
@@ -54,6 +55,7 @@ const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
const withRenotes = ref(props.column.withRenotes ?? true);
const withReplies = ref(props.column.withReplies ?? false);
+const withSensitive = ref(props.column.withSensitive ?? true);
const onlyFiles = ref(props.column.onlyFiles ?? false);
watch(withRenotes, v => {
@@ -68,6 +70,12 @@ watch(withReplies, v => {
});
});
+watch(withSensitive, v => {
+ updateColumn(props.column.id, {
+ withSensitive: v,
+ });
+});
+
watch(onlyFiles, v => {
updateColumn(props.column.id, {
onlyFiles: v,
@@ -144,6 +152,10 @@ const menu = computed<MenuItem[]>(() => {
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
disabled: hasWithReplies(props.column.tl) ? withReplies : false,
+ }, {
+ type: 'switch',
+ text: i18n.ts.withSensitive,
+ ref: withSensitive,
});
return menuItems;
diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index 34be8c5e57..40e2d8fbc7 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -68,10 +68,10 @@ const onDriveFileCreated = (file) => {
}
};
-const thumbnail = (image: any): string => {
+const thumbnail = (image: Misskey.entities.DriveFile): string => {
return defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(image.url)
- : image.thumbnailUrl;
+ : image.thumbnailUrl ?? image.url;
};
misskeyApi('drive/stream', {
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 3e43687709..92dc6d148e 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -70,7 +70,7 @@ const items = computed(() => rawItems.value.slice(0, widgetProps.maxEntries));
const fetching = ref(true);
const fetchEndpoint = computed(() => {
const url = new URL('/api/fetch-rss', base);
- url.searchParams.set('url', widgetProps.url);
+ url.searchParams.set('url', encodeURIComponent(widgetProps.url));
return url;
});
const intervalClear = ref<(() => void) | undefined>();
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index 4f594b720f..6957878572 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -99,7 +99,7 @@ const items = computed(() => {
const fetching = ref(true);
const fetchEndpoint = computed(() => {
const url = new URL('/api/fetch-rss', base);
- url.searchParams.set('url', widgetProps.url);
+ url.searchParams.set('url', encodeURIComponent(widgetProps.url));
return url;
});
const intervalClear = ref<(() => void) | undefined>();