diff options
Diffstat (limited to 'packages/frontend/src')
282 files changed, 4185 insertions, 6205 deletions
diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts index 875353f8a4..13a97e433c 100644 --- a/packages/frontend/src/_boot_.ts +++ b/packages/frontend/src/_boot_.ts @@ -6,6 +6,8 @@ // https://vitejs.dev/config/build-options.html#build-modulepreload import 'vite/modulepreload-polyfill'; +import '@tabler/icons-webfont/dist/tabler-icons.scss'; + import '@/style.scss'; import { mainBoot } from '@/boot/main-boot.js'; import { subBoot } from '@/boot/sub-boot.js'; diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts index 09495dece4..1601f247d7 100644 --- a/packages/frontend/src/_dev_boot_.ts +++ b/packages/frontend/src/_dev_boot_.ts @@ -3,11 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -// devモードで起動される際(index.htmlを使うとき)はrouterが暴発してしまってうまく読み込めない。 -// よって、devモードとして起動されるときはビルド時に組み込む形としておく。 -// (pnpm start時はpugファイルの中で静的リソースとして読み込むようになっており、この問題は起こっていない) -import '@phosphor-icons/web/bold'; - await main(); import('@/_boot_.js'); diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 4fdd51c33b..e3416f2c29 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -8,9 +8,9 @@ import * as Misskey from 'misskey-js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; -import { MenuButton } from '@/types/menu.js'; +import type { MenuItem, MenuButton } from '@/types/menu.js'; import { del, get, set } from '@/scripts/idb-proxy.js'; -import { apiUrl } from '@/config.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'; @@ -289,14 +289,26 @@ export async function openAccountMenu(opts: { }); })); + const menuItems: MenuItem[] = []; + if (opts.withExtraOperation) { - popupMenu([...[{ - type: 'link' as const, + menuItems.push({ + type: 'link', text: i18n.ts.profile, - to: `/@${ $i.username }`, + to: `/@${$i.username}`, avatar: $i, - }, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { - type: 'parent' as const, + }, { + type: 'divider', + }); + + if (opts.includeCurrentAccount) { + menuItems.push(createItem($i)); + } + + menuItems.push(...accountItemPromises); + + menuItems.push({ + type: 'parent', icon: 'ti ti-plus', text: i18n.ts.addAccount, children: [{ @@ -307,7 +319,7 @@ export async function openAccountMenu(opts: { action: () => { createAccount(); }, }], }, { - type: 'link' as const, + type: 'link', icon: 'ti ti-users', text: i18n.ts.manageAccounts, to: '/settings/accounts', @@ -316,14 +328,18 @@ export async function openAccountMenu(opts: { icon: 'ph-power ph-bold ph-lg', text: i18n.ts.logout, action: () => { signout(); }, - }]], ev.currentTarget ?? ev.target, { - align: 'left', }); } else { - popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, { - align: 'left', - }); + if (opts.includeCurrentAccount) { + menuItems.push(createItem($i)); + } + + menuItems.push(...accountItemPromises); } + + popupMenu(menuItems, ev.currentTarget ?? ev.target, { + align: 'left', + }); } if (_DEV_) { diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 94040c6413..487eefe60a 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -5,10 +5,10 @@ import { computed, watch, version as vueVersion, App } from 'vue'; import { compareVersions } from 'compare-versions'; +import { version, lang, updateLocale, locale } from '@@/js/config.js'; import widgets from '@/widgets/index.js'; import directives from '@/directives/index.js'; import components from '@/components/index.js'; -import { version, lang, updateLocale, locale } from '@/config.js'; import { applyTheme } from '@/scripts/theme.js'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; import { updateI18n } from '@/i18n.js'; @@ -22,7 +22,8 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; -import { setupRouter } from '@/router/definition.js'; +import { setupRouter } from '@/router/main.js'; +import { createMainRouter } from '@/router/definition.js'; export async function common(createVue: () => App<Element>) { console.info(`Sharkey v${version}`); @@ -145,10 +146,9 @@ export async function common(createVue: () => App<Element>) { // NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため) watch(defaultStore.reactiveState.darkMode, (darkMode) => { applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); - document.documentElement.dataset.colorMode = darkMode ? 'dark' : 'light'; }, { immediate: miLocalStorage.getItem('theme') == null }); - document.documentElement.dataset.colorMode = defaultStore.state.darkMode ? 'dark' : 'light'; + document.documentElement.dataset.colorScheme = defaultStore.state.darkMode ? 'dark' : 'light'; const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); @@ -243,7 +243,7 @@ export async function common(createVue: () => App<Element>) { const app = createVue(); - setupRouter(app); + setupRouter(app, createMainRouter); if (_DEV_) { app.config.performance = true; diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 5ff998fac4..395d7d9ad1 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -6,7 +6,7 @@ import { createApp, defineAsyncComponent, markRaw } from 'vue'; import { common } from './common.js'; import type * as Misskey from 'misskey-js'; -import { ui } from '@/config.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'; @@ -23,6 +23,7 @@ import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mainRouter } from '@/router/main.js'; import { setFavIconDot } from '@/scripts/favicon-dot.js'; import { type Keymap, makeHotkey } from '@/scripts/hotkey.js'; +import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( @@ -61,6 +62,18 @@ export async function mainBoot() { } }); + stream.on('emojiAdded', emojiData => { + addCustomEmoji(emojiData.emoji); + }); + + stream.on('emojiUpdated', emojiData => { + updateCustomEmojis(emojiData.emojis); + }); + + stream.on('emojiDeleted', emojiData => { + removeCustomEmojis(emojiData.emojis); + }); + for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { import('@/plugin.js').then(async ({ install }) => { // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue index 6c0774b634..796524fce9 100644 --- a/packages/frontend/src/components/MkAccountMoved.vue +++ b/packages/frontend/src/components/MkAccountMoved.vue @@ -16,7 +16,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkMention from './MkMention.vue'; import { i18n } from '@/i18n.js'; -import { host as localHost } from '@/config.js'; +import { host as localHost } from '@@/js/config.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; const user = ref<Misskey.entities.UserLite>(); diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index 7e150f7dd5..e2af4f034e 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/> </template> </MkFolder> - <div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align, backgroundColor: c.bgColor, color: c.fgColor, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }"> + <div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="containerStyle"> <template v-for="child in c.children" :key="child"> <MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/> </template> @@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { Ref, ref } from 'vue'; +import { Ref, ref, computed } from 'vue'; import * as os from '@/os.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -97,6 +97,29 @@ function g(id) { } as AsUiRoot; } +const containerStyle = computed(() => { + if (c.type !== 'container') return undefined; + + // width, color, styleのうち一つでも指定があれば、枠線がちゃんと表示されるようにwidthとstyleのデフォルト値を設定 + // radiusは単に角を丸める用途もあるため除外 + const isBordered = c.borderWidth ?? c.borderColor ?? c.borderStyle; + + const border = isBordered ? { + borderWidth: c.borderWidth ?? '1px', + borderColor: c.borderColor ?? 'var(--divider)', + borderStyle: c.borderStyle ?? 'solid', + } : undefined; + + return { + textAlign: c.align, + backgroundColor: c.bgColor, + color: c.fgColor, + padding: c.padding ? `${c.padding}px` : 0, + borderRadius: (c.borderRadius ?? (c.rounded ? 8 : 0)) + 'px', + ...border, + }; +}); + const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false); function onSwitchUpdate(v) { diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index b04a1c92e7..de5207f350 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -46,17 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import sanitizeHtml from 'sanitize-html'; +import { emojilist, getEmojiName } from '@@/js/emojilist.js'; import contains from '@/scripts/contains.js'; -import { char2twemojiFilePath, char2fluentEmojiFilePath, char2tossfaceFilePath } from '@/scripts/emoji-base.js'; +import { char2twemojiFilePath, char2fluentEmojiFilePath, char2tossfaceFilePath } from '@@/js/emoji-base.js'; import { acct } from '@/filters/user.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; -import { emojilist, getEmojiName } from '@/scripts/emojilist.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; -import { MFM_TAGS, MFM_PARAMS } from '@/const.js'; +import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js'; import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js'; const lib = emojilist.filter(x => x.category !== 'flags'); diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index f968fc5861..e30f74460d 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -171,11 +171,11 @@ function onMousedown(evt: MouseEvent): void { background: var(--accent); &:not(:disabled):hover { - background: var(--X8); + background: hsl(from var(--accent) h s calc(l + 5)); } &:not(:disabled):active { - background: var(--X8); + background: hsl(from var(--accent) h s calc(l + 5)); } } @@ -220,15 +220,16 @@ function onMousedown(evt: MouseEvent): void { background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } &:not(:disabled):active { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } } &.danger { + font-weight: bold; color: #ff2a2a; &.primary { @@ -246,7 +247,7 @@ function onMousedown(evt: MouseEvent): void { } &:disabled { - opacity: 0.7; + opacity: 0.5; } &:focus-visible { diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue index a50416befc..2f7ec34d44 100644 --- a/packages/frontend/src/components/MkChannelPreview.vue +++ b/packages/frontend/src/components/MkChannelPreview.vue @@ -117,7 +117,7 @@ const bannerStyle = computed(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); + background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); } > .name { diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index 4b24562249..57d325b11a 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -13,29 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> -<script lang="ts" setup> -/* eslint-disable id-denylist -- - Chart.js has a `data` attribute in most chart definitions, which triggers the - id-denylist violation when setting it. This is causing about 60+ lint issues. - As this is part of Chart.js's API it makes sense to disable the check here. -*/ -import { onMounted, ref, shallowRef, watch } from 'vue'; -import { Chart } from 'chart.js'; -import * as Misskey from 'misskey-js'; -import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; -import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; -import { chartVLine } from '@/scripts/chart-vline.js'; -import { alpha } from '@/scripts/color.js'; -import date from '@/filters/date.js'; -import bytes from '@/filters/bytes.js'; -import { initChart } from '@/scripts/init-chart.js'; -import { chartLegend } from '@/scripts/chart-legend.js'; -import MkChartLegend from '@/components/MkChartLegend.vue'; - -initChart(); - -type ChartSrc = +<script lang="ts"> +export type ChartSrc = | 'federation' | 'ap-request' | 'users' @@ -62,7 +41,30 @@ type ChartSrc = | 'per-user-pv' | 'per-user-following' | 'per-user-followers' - | 'per-user-drive' + | 'per-user-drive'; +</script> + +<script lang="ts" setup> +/* eslint-disable id-denylist -- + Chart.js has a `data` attribute in most chart definitions, which triggers the + id-denylist violation when setting it. This is causing about 60+ lint issues. + As this is part of Chart.js's API it makes sense to disable the check here. +*/ +import { onMounted, ref, shallowRef, watch } from 'vue'; +import { Chart } from 'chart.js'; +import * as Misskey from 'misskey-js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; +import { defaultStore } from '@/store.js'; +import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; +import { chartVLine } from '@/scripts/chart-vline.js'; +import { alpha } from '@/scripts/color.js'; +import date from '@/filters/date.js'; +import bytes from '@/filters/bytes.js'; +import { initChart } from '@/scripts/init-chart.js'; +import { chartLegend } from '@/scripts/chart-legend.js'; +import MkChartLegend from '@/components/MkChartLegend.vue'; + +initChart(); const props = withDefaults(defineProps<{ src: ChartSrc; diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 00506fb735..9a0a9fba05 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, onMounted, onUnmounted, ref } from 'vue'; import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; import * as os from '@/os.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import * as game from '@/scripts/clicker-game.js'; import number from '@/filters/number.js'; import { claimAchievement } from '@/scripts/achievements.js'; diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index 2475e3dc89..05cde89dd9 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.codeBlockRoot"> - <button :class="$style.codeBlockCopyButton" class="_button" @click="copy"> + <button v-if="copyButton" :class="$style.codeBlockCopyButton" class="_button" @click="copy"> <i class="ti ti-copy"></i> </button> <Suspense> @@ -32,12 +32,17 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ code: string; + forceShow?: boolean; + copyButton?: boolean; lang?: string; -}>(); +}>(), { + copyButton: true, + forceShow: false, +}); -const show = ref(!defaultStore.state.dataSaver.code); +const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code); const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue')); diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index d1fe5dbdcf..5f71e289b8 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -216,7 +216,7 @@ onUnmounted(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); + background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index 8ea8fa6cf3..f51fefa0c0 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue'; import MkMenu from './MkMenu.vue'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import contains from '@/scripts/contains.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 54f6f39c9d..2e1e92cbdf 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -39,7 +39,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue'; import * as os from '@/os.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; -import { apiUrl } from '@/config.js'; +import { apiUrl } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 9976cd00c9..98bf5191f7 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -43,9 +43,9 @@ export default defineComponent({ setup(props, { slots, expose }) { const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫 - function getDateText(time: string) { - const date = new Date(time).getDate(); - const month = new Date(time).getMonth() + 1; + function getDateText(dateInstance: Date) { + const date = dateInstance.getDate(); + const month = dateInstance.getMonth() + 1; return i18n.tsx.monthAndDay({ month: month.toString(), day: date.toString(), @@ -62,9 +62,16 @@ export default defineComponent({ })[0]; if (el.key == null && item.id) el.key = item.id; + const date = new Date(item.createdAt); + const nextDate = props.items[i + 1] ? new Date(props.items[i + 1].createdAt) : null; + if ( i !== props.items.length - 1 && - new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate() + nextDate != null && ( + date.getFullYear() !== nextDate.getFullYear() || + date.getMonth() !== nextDate.getMonth() || + date.getDate() !== nextDate.getDate() + ) ) { const separator = h('div', { class: $style['separator'], @@ -78,12 +85,12 @@ export default defineComponent({ h('i', { class: `ti ti-chevron-up ${$style['date-1-icon']}`, }), - getDateText(item.createdAt), + getDateText(date), ]), h('span', { class: $style['date-2'], }, [ - getDateText(props.items[i + 1].createdAt), + getDateText(nextDate), h('i', { class: `ti ti-chevron-down ${$style['date-2-icon']}`, }), diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue index 930f0f54cc..1dfdebf0d4 100644 --- a/packages/frontend/src/components/MkDonation.vue +++ b/packages/frontend/src/components/MkDonation.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import MkButton from '@/components/MkButton.vue'; import MkLink from '@/components/MkLink.vue'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { miLocalStorage } from '@/local-storage.js'; diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 3990d0b861..44788a6ffb 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -42,7 +42,7 @@ import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 2d1c7c95f0..a471457b44 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -634,7 +634,9 @@ function fetchMoreFiles() { } function getMenu() { - const menu: MenuItem[] = [{ + const menu: MenuItem[] = []; + + menu.push({ type: 'switch', text: i18n.ts.keepOriginalUploading, ref: keepOriginal, @@ -652,19 +654,25 @@ function getMenu() { }, { type: 'divider' }, { text: folder.value ? folder.value.name : i18n.ts.drive, type: 'label', - }, folder.value ? { - text: i18n.ts.renameFolder, - icon: 'ti ti-forms', - action: () => { if (folder.value) renameFolder(folder.value); }, - } : undefined, folder.value ? { - text: i18n.ts.deleteFolder, - icon: 'ti ti-trash', - action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }, - } : undefined, { + }); + + if (folder.value) { + menu.push({ + text: i18n.ts.renameFolder, + icon: 'ti ti-forms', + action: () => { if (folder.value) renameFolder(folder.value); }, + }, { + text: i18n.ts.deleteFolder, + icon: 'ti ti-trash', + action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }, + }); + } + + menu.push({ text: i18n.ts.createFolder, icon: 'ti ti-folder-plus', action: () => { createFolder(); }, - }]; + }); return menu; } diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue index 9a6d272113..543cf24022 100644 --- a/packages/frontend/src/components/MkDriveFileThumbnail.vue +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div ref="thumbnail" :class="$style.root"> +<div + ref="thumbnail" + :class="[ + $style.root, + { [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive }, + ]" +> <ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/> <i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i> <i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i> @@ -27,6 +33,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; const props = defineProps<{ file: Misskey.entities.DriveFile; fit: 'cover' | 'contain'; + highlightWhenSensitive?: boolean; }>(); const is = computed(() => { @@ -67,6 +74,18 @@ const isThumbnailAvailable = computed(() => { overflow: clip; } +.sensitiveHighlight::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + border-radius: inherit; + box-shadow: inset 0 0 0 4px var(--warn); +} + .iconSub { position: absolute; width: 30%; diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue new file mode 100644 index 0000000000..c060c3a659 --- /dev/null +++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue @@ -0,0 +1,414 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialogEl" + :width="1000" + :height="600" + :scroll="false" + :withOkButton="false" + @close="cancel()" + @closed="$emit('closed')" +> + <template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template> + + <div :class="$style.embedCodeGenRoot"> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" + > + <div v-if="phase === 'input'" key="input" :class="$style.embedCodeGenInputRoot"> + <div + :class="$style.embedCodeGenPreviewRoot" + > + <MkLoading v-if="iframeLoading" :class="$style.embedCodeGenPreviewSpinner"/> + <div :class="$style.embedCodeGenPreviewWrapper"> + <div class="_acrylic" :class="$style.embedCodeGenPreviewTitle">{{ i18n.ts.preview }}</div> + <div ref="resizerRootEl" :class="$style.embedCodeGenPreviewResizerRoot" inert> + <div + :class="$style.embedCodeGenPreviewResizer" + :style="{ transform: iframeStyle }" + > + <iframe + ref="iframeEl" + :src="embedPreviewUrl" + :class="$style.embedCodeGenPreviewIframe" + :style="{ height: `${iframeHeight}px` }" + @load="iframeOnLoad" + ></iframe> + </div> + </div> + </div> + </div> + <div :class="$style.embedCodeGenSettings" class="_gaps"> + <MkInput v-if="isEmbedWithScrollbar" v-model="maxHeight" type="number" :min="0"> + <template #label>{{ i18n.ts._embedCodeGen.maxHeight }}</template> + <template #suffix>px</template> + <template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template> + </MkInput> + <MkSelect v-model="colorMode"> + <template #label>{{ i18n.ts.theme }}</template> + <option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option> + <option value="light">{{ i18n.ts.light }}</option> + <option value="dark">{{ i18n.ts.dark }}</option> + </MkSelect> + <MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch> + <MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch> + <MkSwitch v-model="border">{{ i18n.ts._embedCodeGen.border }}</MkSwitch> + <MkInfo v-if="isEmbedWithScrollbar && (!maxHeight || maxHeight <= 0)" warn>{{ i18n.ts._embedCodeGen.maxHeightWarn }}</MkInfo> + <MkInfo v-if="typeof maxHeight === 'number' && (maxHeight <= 0 || maxHeight > 700)">{{ i18n.ts._embedCodeGen.previewIsNotActual }}</MkInfo> + <div class="_buttons"> + <MkButton :disabled="iframeLoading" @click="applyToPreview">{{ i18n.ts._embedCodeGen.applyToPreview }}</MkButton> + <MkButton :disabled="iframeLoading" primary @click="generate">{{ i18n.ts._embedCodeGen.generateCode }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + </div> + <div v-else-if="phase === 'result'" key="result" :class="$style.embedCodeGenResultRoot"> + <div :class="$style.embedCodeGenResultWrapper" class="_gaps"> + <div class="_gaps_s"> + <div :class="$style.embedCodeGenResultHeadingIcon"><i class="ti ti-check"></i></div> + <div :class="$style.embedCodeGenResultHeading">{{ i18n.ts._embedCodeGen.codeGenerated }}</div> + <div :class="$style.embedCodeGenResultDescription">{{ i18n.ts._embedCodeGen.codeGeneratedDescription }}</div> + </div> + <div class="_gaps_s"> + <MkCode :code="result" lang="html" :forceShow="true" :copyButton="false" :class="$style.embedCodeGenResultCode"/> + <MkButton :class="$style.embedCodeGenResultButtons" rounded primary @click="doCopy"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> + </div> + <MkButton :class="$style.embedCodeGenResultButtons" rounded transparent @click="close">{{ i18n.ts.close }}</MkButton> + </div> + </div> + </Transition> + </div> +</MkModalWindow> +</template> + +<script setup lang="ts"> +import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue'; +import { url } from '@@/js/config.js'; +import { embedRouteWithScrollbar } from '@@/js/embed-page.js'; +import type { EmbeddableEntity, EmbedParams } from '@@/js/embed-page.js'; +import MkModalWindow from '@/components/MkModalWindow.vue'; + +import MkInput from '@/components/MkInput.vue'; +import MkSelect from '@/components/MkSelect.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkButton from '@/components/MkButton.vue'; + +import MkCode from '@/components/MkCode.vue'; +import MkInfo from '@/components/MkInfo.vue'; + +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js'; + +const emit = defineEmits<{ + (ev: 'ok'): void; + (ev: 'cancel'): void; + (ev: 'closed'): void; +}>(); + +const props = defineProps<{ + entity: EmbeddableEntity; + id: string; + params?: EmbedParams; +}>(); + +//#region Modalの制御 +const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); + +function cancel() { + emit('cancel'); + dialogEl.value?.close(); +} + +function close() { + dialogEl.value?.close(); +} + +const phase = ref<'input' | 'result'>('input'); +//#endregion + +//#region 埋め込みURL生成・カスタマイズ + +// 本URL生成用params +const paramsForUrl = computed<EmbedParams>(() => ({ + header: header.value, + maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined, + colorMode: colorMode.value === 'auto' ? undefined : colorMode.value, + rounded: rounded.value, + border: border.value, +})); + +// プレビュー用params(手動で更新を掛けるのでref) +const paramsForPreview = ref<EmbedParams>(props.params ?? {}); + +const embedPreviewUrl = computed(() => { + const paramClass = new URLSearchParams(normalizeEmbedParams(paramsForPreview.value)); + if (paramClass.has('maxHeight')) { + const maxHeight = parseInt(paramClass.get('maxHeight')!); + paramClass.set('maxHeight', maxHeight === 0 ? '500' : Math.min(maxHeight, 700).toString()); // プレビューであまりにも縮小されると見づらいため、700pxまでに制限 + } + return `${url}/embed/${props.entity}/${props.id}${paramClass.toString() ? '?' + paramClass.toString() : ''}`; +}); + +const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity)); +const header = ref(props.params?.header ?? true); +const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500); + +const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto'); +const rounded = ref(props.params?.rounded ?? true); +const border = ref(props.params?.border ?? true); + +function applyToPreview() { + const currentPreviewUrl = embedPreviewUrl.value; + + paramsForPreview.value = { + header: header.value, + maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined, + colorMode: colorMode.value === 'auto' ? undefined : colorMode.value, + rounded: rounded.value, + border: border.value, + }; + + nextTick(() => { + if (currentPreviewUrl === embedPreviewUrl.value) { + // URLが変わらなくてもリロード + iframeEl.value?.contentWindow?.location.reload(); + } + }); +} + +const result = ref(''); + +function generate() { + result.value = getEmbedCode(`/embed/${props.entity}/${props.id}`, paramsForUrl.value); + phase.value = 'result'; +} + +function doCopy() { + copyToClipboard(result.value); + os.success(); +} +//#endregion + +//#region プレビューのリサイズ +const resizerRootEl = shallowRef<HTMLDivElement>(); +const iframeLoading = ref(true); +const iframeEl = shallowRef<HTMLIFrameElement>(); +const iframeHeight = ref(0); +const iframeScale = ref(1); +const iframeStyle = computed(() => { + return `translate(-50%, -50%) scale(${iframeScale.value})`; +}); +const resizeObserver = new ResizeObserver(() => { + calcScale(); +}); + +function iframeOnLoad() { + iframeEl.value?.contentWindow?.addEventListener('beforeunload', () => { + iframeLoading.value = true; + nextTick(() => { + iframeHeight.value = 0; + iframeScale.value = 1; + }); + }); +} + +function windowEventHandler(event: MessageEvent) { + if (event.source !== iframeEl.value?.contentWindow) { + return; + } + if (event.data.type === 'misskey:embed:ready') { + iframeEl.value!.contentWindow?.postMessage({ + type: 'misskey:embedParent:registerIframeId', + payload: { + iframeId: 'embedCodeGen', // 同じタイミングで複数のembed iframeがある際の区別用なのでここではなんでもいい + }, + }); + } + if (event.data.type === 'misskey:embed:changeHeight') { + iframeHeight.value = event.data.payload.height; + nextTick(() => { + calcScale(); + iframeLoading.value = false; // 初回の高さ変更まで待つ + }); + } +} + +function calcScale() { + if (!resizerRootEl.value) return; + const previewWidth = resizerRootEl.value.clientWidth - 40; // 左右の余白 20pxずつ + const previewHeight = resizerRootEl.value.clientHeight - 40; // 上下の余白 20pxずつ + const iframeWidth = 500; + const scale = Math.min(previewWidth / iframeWidth, previewHeight / iframeHeight.value, 1); // 拡大はしないので1を上限に + iframeScale.value = scale; +} + +onMounted(() => { + window.addEventListener('message', windowEventHandler); + if (!resizerRootEl.value) return; + resizeObserver.observe(resizerRootEl.value); +}); + +function reset() { + window.removeEventListener('message', windowEventHandler); + resizeObserver.disconnect(); + + // プレビューのリセット + iframeHeight.value = 0; + iframeScale.value = 1; + iframeLoading.value = true; + result.value = ''; + phase.value = 'input'; +} + +onDeactivated(() => { + reset(); +}); + +onUnmounted(() => { + reset(); +}); +//#endregion +</script> + +<style module> +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_x_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_x_leaveTo { + opacity: 0; + transform: translateX(-50px); +} + +.embedCodeGenRoot { + container-type: inline-size; + height: 100%; +} + +.embedCodeGenInputRoot { + height: 100%; + display: grid; + grid-template-columns: 1fr 400px; +} + +.embedCodeGenPreviewRoot { + position: relative; + background-color: var(--bg); + background-size: auto auto; + background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--panel) 6px, var(--panel) 12px); + cursor: not-allowed; +} + +.embedCodeGenPreviewWrapper { + display: flex; + flex-direction: column; + height: 100%; + pointer-events: none; + user-select: none; + -webkit-user-drag: none; +} + +.embedCodeGenPreviewTitle { + position: absolute; + z-index: 100; + top: 8px; + left: 8px; + padding: 6px 10px; + border-radius: 6px; + font-size: 85%; +} + +.embedCodeGenPreviewSpinner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + pointer-events: none; + user-select: none; + -webkit-user-drag: none; +} + +.embedCodeGenPreviewResizerRoot { + position: relative; + flex: 1 0; +} + +.embedCodeGenPreviewResizer { + position: absolute; + top: 50%; + left: 50%; +} + +.embedCodeGenPreviewIframe { + display: block; + border: none; + width: 500px; + color-scheme: light dark; +} + +.embedCodeGenSettings { + padding: 24px; + overflow-y: scroll; +} + +.embedCodeGenResultRoot { + box-sizing: border-box; + padding: 24px; + height: 100%; + max-width: 700px; + margin: 0 auto; + display: flex; + align-items: center; +} + +.embedCodeGenResultHeading { + text-align: center; + font-size: 1.2em; +} + +.embedCodeGenResultHeadingIcon { + margin: 0 auto; + background-color: var(--accentedBg); + color: var(--accent); + text-align: center; + height: 64px; + width: 64px; + font-size: 24px; + line-height: 64px; + border-radius: 50%; +} + +.embedCodeGenResultDescription { + text-align: center; + white-space: pre-wrap; +} + +.embedCodeGenResultWrapper, +.embedCodeGenResultCode { + width: 100%; +} + +.embedCodeGenResultButtons { + margin: 0 auto; +} + +@container (max-width: 800px) { + .embedCodeGenInputRoot { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } +} +</style> diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 008613c27e..151843b18c 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed, Ref } from 'vue'; -import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js'; +import { CustomEmojiFolderTree, getEmojiName } from '@@/js/emojilist.js'; import { i18n } from '@/i18n.js'; import { customEmojis } from '@/custom-emojis.js'; import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue'; diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 297d5f899e..7ef6efa939 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -117,7 +117,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, shallowRef, computed, watch, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; -import XSection from '@/components/MkEmojiPicker.section.vue'; import { emojilist, emojiCharByCategory, @@ -126,7 +125,8 @@ import { getEmojiName, CustomEmojiFolderTree, getUnicodeEmoji, -} from '@/scripts/emojilist.js'; +} from '@@/js/emojilist.js'; +import XSection from '@/components/MkEmojiPicker.section.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import * as os from '@/os.js'; import { isTouchUsing } from '@/scripts/touch.js'; @@ -611,6 +611,7 @@ defineExpose({ width: auto; height: auto; min-width: 0; + padding: 0; &:disabled { cursor: not-allowed; @@ -717,7 +718,7 @@ defineExpose({ > .item { position: relative; - padding: 0; + padding: 0 3px; width: var(--eachSize); height: var(--eachSize); contain: strict; diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 0ec0679393..18e80d8445 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="modal" v-slot="{ type, maxHeight }" :zPriority="'middle'" - :preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'" + :preferType="defaultStore.state.emojiPickerStyle" :hasInteractionWithOtherFocusTrappedEls="true" :transparentBg="true" :manualShowing="manualShowing" diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue index bab844c27f..7af68a32ba 100644 --- a/packages/frontend/src/components/MkFileListForAdmin.vue +++ b/packages/frontend/src/components/MkFileListForAdmin.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only class="file _button" > <div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div> - <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> + <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain" :highlightWhenSensitive="true"/> <div v-if="viewMode === 'list'" class="body"> <div> <small style="opacity: 0.7;">{{ file.name }}</small> diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 229cd59056..392963fdb9 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -41,6 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :marginMin="14" :marginMax="22"> <slot></slot> </MkSpacer> + <div v-if="$slots.footer" :class="$style.footer"> + <slot name="footer"></slot> + </div> </div> </KeepAlive> </Transition> @@ -136,7 +139,7 @@ onMounted(() => { width: 100%; box-sizing: border-box; padding: 9px 12px 9px 12px; - background: var(--buttonBg); + background: var(--folderHeaderBg); -webkit-backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px)); border-radius: var(--radius-sm); @@ -144,7 +147,7 @@ onMounted(() => { &:hover { text-decoration: none; - background: var(--buttonHoverBg); + background: var(--folderHeaderHoverBg); } &:focus-within { @@ -153,7 +156,7 @@ onMounted(() => { &.active { color: var(--accent); - background: var(--buttonHoverBg); + background: var(--folderHeaderHoverBg); } &.opened { @@ -224,4 +227,18 @@ onMounted(() => { background: var(--bg); } } + +.footer { + position: sticky !important; + z-index: 1; + bottom: var(--stickyBottom, 0px); + left: 0; + padding: 12px; + background: var(--acrylicBg); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); + background-size: auto auto; + background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--panel) 5px, var(--panel) 10px); + border-radius: 0 0 6px 6px; +} </style> diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index 3fdf673eb3..52497a2994 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/> </template> <template v-else-if="isFollowing"> - <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.youFollowing }}</span><i class="ti ti-minus"></i> </template> <template v-else-if="!isFollowing && user.isLocked"> <span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i> @@ -43,7 +43,7 @@ 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 '@/config.js'; +import { host } from '@@/js/config.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue new file mode 100644 index 0000000000..1e88d59d8e --- /dev/null +++ b/packages/frontend/src/components/MkFormFooter.vue @@ -0,0 +1,49 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <div :class="$style.text">{{ i18n.tsx.thereAreNChanges({ n: form.modifiedCount.value }) }}</div> + <div style="margin-left: auto;" class="_buttons"> + <MkButton danger rounded @click="form.discard"><i class="ti ti-x"></i> {{ i18n.ts.discard }}</MkButton> + <MkButton primary rounded @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + </div> +</div> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import MkButton from './MkButton.vue'; +import { i18n } from '@/i18n.js'; + +const props = defineProps<{ + form: { + modifiedCount: { + value: number; + }; + discard: () => void; + save: () => void; + }; +}>(); +</script> + +<style lang="scss" module> +.root { + display: flex; + align-items: center; +} + +.text { + color: var(--warn); + font-size: 90%; + animation: modified-blink 2s infinite; +} + +@keyframes modified-blink { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} +</style> diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index 8d301f16bd..c04d0864fb 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> import DrawBlurhash from '@/workers/draw-blurhash?worker'; import TestWebGL2 from '@/workers/test-webgl2?worker'; -import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js'; -import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js'; +import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js'; +import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js'; const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => { // テスト環境で Web Worker インスタンスは作成できない diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 06389b4013..42e1146e27 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; import { debounce } from 'throttle-debounce'; import MkButton from '@/components/MkButton.vue'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js'; diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index 094d2f177f..46d42248d3 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed } from 'vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import { instance as Instance } from '@/instance.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue index de51a98789..4aee64f78e 100644 --- a/packages/frontend/src/components/MkInviteCode.vue +++ b/packages/frontend/src/components/MkInviteCode.vue @@ -11,8 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span> <span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span> </template> + <template #footer> + <div class="_buttons"> + <MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> + <MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </template> - <div class="_gaps_s" :class="$style.root"> + <div :class="$style.root"> <div :class="$style.items"> <div> <div :class="$style.label">{{ i18n.ts.invitationCode }}</div> @@ -49,10 +55,6 @@ SPDX-License-Identifier: AGPL-3.0-only <div><MkTime :time="invite.createdAt" mode="absolute"/></div> </div> </div> - <div :class="$style.buttons"> - <MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> - <MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> - </div> </div> </MkFolder> </template> @@ -121,9 +123,4 @@ function copyInviteCode() { width: var(--height); height: var(--height); } - -.buttons { - display: flex; - gap: 8px; -} </style> diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index b04edd1150..263cd95eb1 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, ref } from 'vue'; -import { url as local } from '@/config.js'; +import { url as local } from '@@/js/config.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import * as os from '@/os.js'; import { isEnabledUrlPreview } from '@/instance.js'; diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index 93affd930f..2d7cde1af2 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -175,9 +175,7 @@ async function show() { const menuShowing = ref(false); function showMenu(ev: MouseEvent) { - let menu: MenuItem[] = []; - - menu = [ + const menu: MenuItem[] = [ // TODO: 再生キューに追加 { type: 'switch', @@ -225,7 +223,7 @@ function showMenu(ev: MouseEvent) { menu.push({ type: 'divider', }, { - type: 'link' as const, + type: 'link', text: i18n.ts._fileViewer.title, icon: 'ti ti-info-circle', to: `/my/drive/file/${props.audio.id}`, diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index cac419d42b..02c054956c 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick"> +<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" @click="onclick"> <component :is="disableImageLink ? 'div' : 'a'" v-bind="disableImageLink ? { @@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { watch, ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; +import type { MenuItem } from '@/types/menu.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import bytes from '@/filters/bytes.js'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; @@ -75,7 +76,6 @@ const props = withDefaults(defineProps<{ }); const hide = ref(true); -const darkMode = ref<boolean>(defaultStore.state.darkMode); const url = computed(() => (props.raw || defaultStore.state.loadRawImages) ? props.image.url @@ -112,27 +112,39 @@ watch(() => props.image, () => { }); function showMenu(ev: MouseEvent) { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ text: i18n.ts.hide, icon: 'ti ti-eye-off', action: () => { hide.value = true; }, - }, ...(iAmModerator ? [{ - text: i18n.ts.markAsSensitive, - icon: 'ti ti-eye-exclamation', - danger: true, - action: () => { - os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); - }, - }] : []), ...($i?.id === props.image.userId ? [{ - type: 'divider' as const, - }, { - type: 'link' as const, - text: i18n.ts._fileViewer.title, - icon: 'ti ti-info-circle', - to: `/my/drive/file/${props.image.id}`, - }] : [])], ev.currentTarget ?? ev.target); + }); + + if (iAmModerator) { + menuItems.push({ + text: i18n.ts.markAsSensitive, + icon: 'ti ti-eye-exclamation', + danger: true, + action: () => { + os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); + }, + }); + } + + if ($i?.id === props.image.userId) { + menuItems.push({ + type: 'divider', + }, { + type: 'link', + text: i18n.ts._fileViewer.title, + icon: 'ti ti-info-circle', + to: `/my/drive/file/${props.image.id}`, + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } </script> @@ -197,10 +209,19 @@ function showMenu(ev: MouseEvent) { position: relative; //box-shadow: 0 0 0 1px var(--divider) inset; background: var(--bg); - background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); background-size: 16px 16px; } +html[data-color-scheme=dark] .visible { + --c: rgb(255 255 255 / 2%); + background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); +} + +html[data-color-scheme=light] .visible { + --c: rgb(0 0 0 / 2%); + background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); +} + .menu { display: block; position: absolute; diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 4bc2b9fba7..39fa6ff012 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -39,7 +39,7 @@ import XImage from '@/components/MkMediaImage.vue'; import XVideo from '@/components/MkMediaVideo.vue'; import XModPlayer from '@/components/SkModPlayer.vue'; import * as os from '@/os.js'; -import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@/const.js'; +import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@@/js/const.js'; import { defaultStore } from '@/store.js'; import { focusParent } from '@/scripts/focus.js'; diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 1c3c9a312b..0502bdd401 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -195,9 +195,7 @@ async function show() { const menuShowing = ref(false); function showMenu(ev: MouseEvent) { - let menu: MenuItem[] = []; - - menu = [ + const menu: MenuItem[] = [ // TODO: 再生キューに追加 { type: 'switch', @@ -250,7 +248,7 @@ function showMenu(ev: MouseEvent) { menu.push({ type: 'divider', }, { - type: 'link' as const, + type: 'link', text: i18n.ts._fileViewer.title, icon: 'ti ti-info-circle', to: `/my/drive/file/${props.video.id}`, diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index 80a1b68459..de2048b6f2 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { toUnicode } from 'punycode'; import { computed } from 'vue'; import tinycolor from 'tinycolor2'; -import { host as localHost } from '@/config.js'; +import { host as localHost } from '@@/js/config.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue index 235790556c..086573ba6d 100644 --- a/packages/frontend/src/components/MkMenu.child.vue +++ b/packages/frontend/src/components/MkMenu.child.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue'; import MkMenu from './MkMenu.vue'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ items: MenuItem[]; diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 0537f4f988..fe6df7090c 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -4,17 +4,22 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div role="menu" @focusin.passive.stop="() => {}"> +<div + role="menu" + :class="{ + [$style.root]: true, + [$style.center]: align === 'center', + [$style.big]: big, + [$style.asDrawer]: asDrawer, + }" + @focusin.passive.stop="() => {}" +> <div ref="itemsEl" v-hotkey="keymap" tabindex="0" class="_popup _shadow" - :class="{ - [$style.root]: true, - [$style.center]: align === 'center', - [$style.asDrawer]: asDrawer, - }" + :class="$style.menu" :style="{ width: (width && !asDrawer) ? `${width}px` : '', maxHeight: maxHeight ? `min(${maxHeight}px, calc(100dvh - 32px))` : 'calc(100dvh - 32px)', @@ -200,6 +205,8 @@ const emit = defineEmits<{ (ev: 'hide'): void; }>(); +const big = isTouchUsing; + const isNestingMenu = inject<boolean>('isNestingMenu', false); const itemsEl = shallowRef<HTMLElement>(); @@ -297,6 +304,8 @@ async function showRadioOptions(item: MenuRadio, ev: Event) { } async function showChildren(item: MenuParent, ev: Event) { + ev.stopPropagation(); + const children: MenuItem[] = await (async () => { if (childrenCache.has(item)) { return childrenCache.get(item)!; @@ -418,48 +427,67 @@ onBeforeUnmount(() => { <style lang="scss" module> .root { - padding: 8px 0; - box-sizing: border-box; - max-width: 100vw; - min-width: 200px; - overflow: auto; - overscroll-behavior: contain; - - &:focus-visible { - outline: none; + &.center { + > .menu { + > .item { + text-align: center; + } + } } - &.center { - > .item { - text-align: center; + &.big:not(.asDrawer) { + > .menu { + > .item { + padding: 6px 20px; + font-size: 1em; + line-height: 24px; + } } } &.asDrawer { - padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; - width: 100%; - border-radius: var(--radius-lg); - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; + max-width: 600px; + margin: auto; - > .item { - font-size: 1em; - padding: 12px 24px; + > .menu { + padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; + width: 100%; + border-radius: var(--radius-lg); + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; - &::before { - width: calc(100% - 24px); - border-radius: var(--radius); + > .item { + font-size: 1em; + padding: 12px 24px; + + &::before { + width: calc(100% - 24px); + border-radius: var(--radius); + } + + > .icon { + margin-right: 14px; + width: 24px; + } } - > .icon { - margin-right: 14px; - width: 24px; + > .divider { + margin: 12px 0; } } + } +} - > .divider { - margin: 12px 0; - } +.menu { + padding: 8px 0; + box-sizing: border-box; + max-width: 100vw; + min-width: 200px; + overflow: auto; + overscroll-behavior: contain; + + &:focus-visible { + outline: none; } } diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue index f2f2bf47a8..1b6f6cef31 100644 --- a/packages/frontend/src/components/MkMiniChart.vue +++ b/packages/frontend/src/components/MkMiniChart.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { watch, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import tinycolor from 'tinycolor2'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; const props = defineProps<{ src: number[]; diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index f8032f9b43..c766a33823 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -106,7 +106,7 @@ const zIndex = os.claimZIndex(props.zPriority); const useSendAnime = ref(false); const type = computed<ModalTypes>(() => { if (props.preferType === 'auto') { - if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') { + if ((defaultStore.state.menuStyle === 'drawer') || (defaultStore.state.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) { return 'drawer'; } else { return props.src != null ? 'popup' : 'dialog'; diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index c3c7812036..f26959888b 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -94,12 +94,12 @@ defineExpose({ --root-margin: 24px; + --headerHeight: 46px; + --headerHeightNarrow: 42px; + @media (max-width: 500px) { --root-margin: 16px; } - - --headerHeight: 46px; - --headerHeightNarrow: 42px; } .header { diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index edae1e91b2..dc50dc96ad 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -62,7 +62,15 @@ SPDX-License-Identifier: AGPL-3.0-only <div style="container-type: inline-size;"> <bdi> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :isBlock="true" :author="appearNote.user" :nyaize="'respect'"/> + <Mfm + v-if="appearNote.cw != ''" + :text="appearNote.cw" + :author="appearNote.user" + :nyaize="'respect'" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + :isBlock="true" + /> <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/> </p> <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> @@ -148,7 +156,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> @@ -192,6 +200,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue'; import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; +import { isLink } from '@@/js/is-link.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; @@ -224,13 +233,13 @@ import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; -import { shouldCollapsed } from '@/scripts/collapsed.js'; import { useRouter } from '@/router/supplier.js'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; -import { host } from '@/config.js'; +import { shouldCollapsed } from '@@/js/collapsed.js'; +import { host } from '@@/js/config.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { type Keymap } from '@/scripts/hotkey.js'; import { focusPrev, focusNext } from '@/scripts/focus.js'; @@ -744,16 +753,6 @@ function onContextmenu(ev: MouseEvent): void { return; } - const isLink = (el: HTMLElement): boolean => { - if (el.tagName === 'A') return true; - // 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。 - if (el.tagName === 'AUDIO') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - return false; - }; - if (ev.target && isLink(ev.target as HTMLElement)) return; if (window.getSelection()?.toString() !== '') return; @@ -884,7 +883,7 @@ function emitUpdReaction(emoji: string, delta: number) { // 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる // 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?) //content-visibility: auto; - //contain-intrinsic-size: 0 128px; + //contain-intrinsic-size: 0 128px; &:focus-visible { outline: none; @@ -1128,7 +1127,7 @@ function emitUpdReaction(emoji: string, delta: number) { z-index: 2; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), var(--X15)); + //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); &:hover > .collapsedLabel { background: var(--panelHighlight); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 123e94c3e0..f503d6cf66 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -69,7 +69,15 @@ SPDX-License-Identifier: AGPL-3.0-only </header> <div :class="$style.noteContent"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :isBlock="true" :author="appearNote.user" :nyaize="'respect'"/> + <Mfm + v-if="appearNote.cw != ''" + :text="appearNote.cw" + :author="appearNote.user" + :nyaize="'respect'" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + :isBlock="true" + /> <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/> </p> <div v-show="appearNote.cw == null || showContent"> @@ -149,7 +157,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> @@ -227,6 +235,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, inject, onMounted, provide, ref, shallowRef, watch } from 'vue'; import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; +import { isLink } from '@@/js/is-link.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; @@ -250,9 +259,10 @@ 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 '@/config.js'; +import { host } from '@@/js/config.js'; import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; + import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; @@ -703,14 +713,6 @@ function toggleReact() { } function onContextmenu(ev: MouseEvent): void { - const isLink = (el: HTMLElement): boolean => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - return false; - }; - if (ev.target && isLink(ev.target as HTMLElement)) return; if (window.getSelection()?.toString() !== '') return; diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index fd5da0d687..cd6fdf576c 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -5,14 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <header :class="$style.root"> - <div v-if="mock" :class="$style.name"> - <MkUserName :user="note.user"/> - </div> - <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> - <MkUserName :user="note.user"/> - </MkA> - <div v-if="note.user.isBot" :class="$style.isBot">bot</div> - <div :class="$style.username"><MkAcct :user="note.user"/></div> + <component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.5" style="min-width: 0;"> + <div style="display: flex; white-space: nowrap; align-items: baseline;"> + <div v-if="mock" :class="$style.name"> + <MkUserName :user="note.user"/> + </div> + <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <MkUserName :user="note.user"/> + </MkA> + <div v-if="note.user.isBot" :class="$style.isBot">bot</div> + <div :class="$style.username"><MkAcct :user="note.user"/></div> + </div> + </component> <div v-if="note.user.badgeRoles" :class="$style.badgeRoles"> <img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/> </div> @@ -43,6 +47,7 @@ import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; import { popupMenu } from '@/os.js'; +import { defaultStore } from '@/store.js'; const props = defineProps<{ note: Misskey.entities.Note; diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 9948676198..7bec9bdc65 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -13,7 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/> <MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/> - <img v-else-if="'icon' in notification" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/> + <MkAvatar v-else-if="notification.type === 'exportCompleted'" :class="$style.icon" :user="$i" link preview/> + <img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/> <div :class="[$style.subIcon, { [$style.t_follow]: notification.type === 'follow', @@ -25,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_quote]: notification.type === 'quote', [$style.t_pollEnded]: notification.type === 'pollEnded', [$style.t_achievementEarned]: notification.type === 'achievementEarned', + [$style.t_exportCompleted]: notification.type === 'exportCompleted', [$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null, [$style.t_pollEnded]: notification.type === 'edited', }]" @@ -38,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> + <i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i> <template v-else-if="notification.type === 'roleAssigned'"> <img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/> <i v-else class="ti ti-badges"></i> @@ -49,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only :withTooltip="true" :reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')" :noStyle="true" - style="width: 100%; height: 100%;" + style="width: 100%; height: 100% !important; object-fit: contain;" /> </div> </div> @@ -60,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> + <span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span> <MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span> <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span> @@ -102,10 +106,20 @@ SPDX-License-Identifier: AGPL-3.0-only <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> {{ i18n.ts._achievements._types['_' + notification.achievement].title }} </MkA> + <MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`"> + {{ i18n.ts.showFile }} + </MkA> <template v-else-if="notification.type === 'follow'"> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span> </template> - <span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> + <template v-else-if="notification.type === 'followRequestAccepted'"> + <div :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</div> + <div v-if="notification.message" :class="$style.text" style="opacity: 0.6; font-style: oblique;"> + <i class="ti ti-quote" :class="$style.quote"></i> + <span>{{ notification.message }}</span> + <i class="ti ti-quote" :class="$style.quote"></i> + </div> + </template> <template v-else-if="notification.type === 'receiveFollowRequest'"> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span> <div v-if="full && !followRequestDone" :class="$style.followRequestCommands"> @@ -126,7 +140,7 @@ SPDX-License-Identifier: AGPL-3.0-only :withTooltip="true" :reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')" :noStyle="true" - style="width: 100%; height: 100%;" + style="width: 100%; height: 100% !important; object-fit: contain;" /> </div> </div> @@ -171,6 +185,20 @@ const props = withDefaults(defineProps<{ full: false, }); +type ExportCompletedNotification = Misskey.entities.Notification & { type: 'exportCompleted' }; + +const exportEntityName = { + antenna: i18n.ts.antennas, + blocking: i18n.ts.blockedUsers, + clip: i18n.ts.clips, + customEmoji: i18n.ts.customEmojis, + favorite: i18n.ts.favorites, + following: i18n.ts.following, + muting: i18n.ts.mutedUsers, + note: i18n.ts.notes, + userList: i18n.ts.lists, +} as const satisfies Record<ExportCompletedNotification['exportedEntity'], string>; + const followRequestDone = ref(false); const acceptFollowRequest = () => { @@ -200,6 +228,14 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) overflow-wrap: break-word; display: flex; contain: content; + + --eventFollow: #36aed2; + --eventRenote: #36d298; + --eventReply: #007aff; + --eventReactionHeart: var(--love); + --eventReaction: #e99a0b; + --eventAchievement: #cb9a11; + --eventOther: #88a6b7; } .head { @@ -308,6 +344,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) pointer-events: none; } +.t_exportCompleted { + padding: 3px; + background: var(--eventOther); + pointer-events: none; +} + .t_roleAssigned { padding: 3px; background: var(--eventOther); diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue index 71b38d99ed..47a9c79e45 100644 --- a/packages/frontend/src/components/MkNotificationSelectWindow.vue +++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue @@ -35,7 +35,7 @@ import MkSwitch from './MkSwitch.vue'; import MkInfo from './MkInfo.vue'; import MkButton from './MkButton.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; import { i18n } from '@/i18n.js'; type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>> diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 2dd6c21ef6..a395734add 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -30,7 +30,7 @@ import XNotification from '@/components/MkNotification.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; import { infoImageUrl } from '@/instance.js'; import { defaultStore } from '@/store.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue index a0bc0c628e..94cbaf5c91 100644 --- a/packages/frontend/src/components/MkOmit.vue +++ b/packages/frontend/src/components/MkOmit.vue @@ -62,7 +62,7 @@ onUnmounted(() => { left: 0; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), var(--X15)); + //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 8f6109ca04..f67a1e5b63 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -34,13 +34,13 @@ 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 '@/config.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 '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; import { useRouterFactory } from '@/router/supplier.js'; import { mainRouter } from '@/router/main.js'; import MkUserName from './global/MkUserName.vue'; diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 0164883099..f37cb10f6d 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -45,10 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch, type Ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; +import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js'; -import { useDocumentVisibility } from '@/scripts/use-document-visibility.js'; import { defaultStore } from '@/store.js'; import { MisskeyEntity } from '@/types/date-separated-list.js'; import { i18n } from '@/i18n.js'; @@ -126,8 +126,6 @@ const items = ref<MisskeyEntityMap>(new Map()); */ const queue = ref<MisskeyEntityMap>(new Map()); -const offset = ref(0); - /** * 初期化中かどうか(trueならMkLoadingで全て隠す) */ @@ -180,7 +178,9 @@ watch([backed, contentEl], () => { if (!backed.value) { if (!contentEl.value) return; - scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE); + scrollRemove.value = props.pagination.reversed + ? onScrollBottom(contentEl.value, executeQueue, TOLERANCE) + : onScrollTop(contentEl.value, (topVisible) => { if (topVisible) executeQueue(); }, TOLERANCE); } else { if (scrollRemove.value) scrollRemove.value(); scrollRemove.value = null; @@ -230,7 +230,6 @@ async function init(): Promise<void> { more.value = true; } - offset.value = res.length; error.value = false; fetching.value = false; @@ -254,7 +253,7 @@ const fetchMore = async (): Promise<void> => { ...params, limit: SECOND_FETCH_LIMIT, ...(offsetMode ? { - offset: offset.value, + offset: items.value.size, } : { untilId: Array.from(items.value.keys()).at(-1), }), @@ -304,7 +303,6 @@ const fetchMore = async (): Promise<void> => { moreFetching.value = false; } } - offset.value += res.length; }, err => { moreFetching.value = false; }); @@ -319,7 +317,7 @@ const fetchMoreAhead = async (): Promise<void> => { ...params, limit: SECOND_FETCH_LIMIT, ...(offsetMode ? { - offset: offset.value, + offset: items.value.size, } : { sinceId: Array.from(items.value.keys()).at(-1), }), @@ -331,7 +329,6 @@ const fetchMoreAhead = async (): Promise<void> => { items.value = concatMapWithArray(items.value, res); more.value = true; } - offset.value += res.length; moreFetching.value = false; }, err => { moreFetching.value = false; diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 393ac4efba..5f23aa4b8c 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -31,14 +31,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref } from 'vue'; import * as Misskey from 'misskey-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 '@/config.js'; -import { useInterval } from '@/scripts/use-interval.js'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; +import { host } from '@@/js/config.js'; +import { useInterval } from '@@/js/use-interval.js'; const props = defineProps<{ noteId: string; @@ -85,9 +85,10 @@ if (props.poll.expiresAt) { } const vote = async (id) => { + if (props.readOnly || closed.value || isVoted.value) return; + pleaseLogin(undefined, pleaseLoginContext.value); - if (props.readOnly || closed.value || isVoted.value) return; if (!props.poll.multiple) { const { canceled } = await os.confirm({ type: 'question', diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue index ff29b66193..d14873008b 100644 --- a/packages/frontend/src/components/MkPopupMenu.vue +++ b/packages/frontend/src/components/MkPopupMenu.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, shallowRef } from 'vue'; import MkModal from './MkModal.vue'; import MkMenu from './MkMenu.vue'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; defineProps<{ items: MenuItem[]; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 8dd08cc1c4..f538340920 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -106,11 +106,11 @@ import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; import { toASCII } from 'punycode/'; +import { host, url } from '@@/js/config.js'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkNotePreview from '@/components/MkNotePreview.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue'; -import { host, url } from '@/config.js'; import { erase, unique } from '@/scripts/array.js'; import { extractMentions } from '@/scripts/extract-mentions.js'; import { formatTimeString } from '@/scripts/format-time-string.js'; @@ -247,7 +247,7 @@ const submitText = computed((): string => { }); const textLength = computed((): number => { - return (text.value + imeText.value).trim().length + (cw.value?.trim().length ?? 0); + return (text.value + imeText.value).length + (cw.value?.length ?? 0); }); const maxTextLength = computed((): number => { @@ -1183,13 +1183,13 @@ defineExpose({ &:not(:disabled):hover { > .inner { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } } &:not(:disabled):active { > .inner { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } } } @@ -1256,6 +1256,15 @@ defineExpose({ min-height: 75px; max-height: 150px; overflow: auto; + background-size: auto auto; +} + +html[data-color-scheme=dark] .preview { + background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, #0004 5px, #0004 10px); +} + +html[data-color-scheme=light] .preview { + background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, #00000005 5px, #00000005 10px); } .targetNote { diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 7d24828e5b..f90fcfef33 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -33,6 +33,7 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import type { MenuItem } from '@/types/menu.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -70,7 +71,7 @@ async function detachAndDeleteMedia(file: Misskey.entities.DriveFile) { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('driveFileDeleteConfirm', { name: file.name }), + text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }), }); if (canceled) return; @@ -143,7 +144,10 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar if (menuShowing) return; const isImage = file.type.startsWith('image/'); - os.popupMenu([{ + + const menuItems: MenuItem[] = []; + + menuItems.push({ text: i18n.ts.renameFile, icon: 'ti ti-forms', action: () => { rename(file); }, @@ -155,11 +159,17 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar text: i18n.ts.describeFile, icon: 'ti ti-text-caption', action: () => { describe(file); }, - }, ...isImage ? [{ - text: i18n.ts.cropImage, - icon: 'ti ti-crop', - action: () : void => { crop(file); }, - }] : [], { + }); + + if (isImage) { + menuItems.push({ + text: i18n.ts.cropImage, + icon: 'ti ti-crop', + action: () : void => { crop(file); }, + }); + } + + menuItems.push({ type: 'divider', }, { text: i18n.ts.attachCancel, @@ -170,7 +180,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar icon: 'ti ti-trash', danger: true, action: () => { detachAndDeleteMedia(file); }, - }], ev.currentTarget ?? ev.target).then(() => menuShowing = false); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => menuShowing = false); menuShowing = true; } </script> diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue index 649dee2fdb..6efd99d14b 100644 --- a/packages/frontend/src/components/MkPreview.vue +++ b/packages/frontend/src/components/MkPreview.vue @@ -42,7 +42,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkRadio from '@/components/MkRadio.vue'; import * as os from '@/os.js'; -import * as config from '@/config.js'; +import * as config from '@@/js/config.js'; import { $i } from '@/account.js'; const text = ref(''); diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index e0d0b561be..4fb4c6fe56 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, ref, shallowRef } from 'vue'; import { i18n } from '@/i18n.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; import { isHorizontalSwipeSwiping } from '@/scripts/touch.js'; const SCROLL_STOP = 10; diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index 244fcdceae..22c187c357 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -5,7 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="timctyfi" :class="{ disabled, easing }"> - <div class="label"><slot name="label"></slot></div> + <div class="label"> + <slot name="label"></slot> + </div> <div v-adaptive-border class="body"> <div ref="containerEl" class="container"> <div class="track"> @@ -14,15 +16,25 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="steps && showTicks" class="ticks"> <div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div> </div> - <div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div> + <div + ref="thumbEl" + class="thumb" + :style="{ left: thumbPosition + 'px' }" + @mouseenter.passive="onMouseenter" + @mousedown="onMousedown" + @touchstart="onMousedown" + ></div> </div> </div> - <div class="caption"><slot name="caption"></slot></div> + <div class="caption"> + <slot name="caption"></slot> + </div> </div> </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch, shallowRef } from 'vue'; +import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'; +import { isTouchUsing } from '@/scripts/touch.js'; import * as os from '@/os.js'; const props = withDefaults(defineProps<{ @@ -101,12 +113,36 @@ const steps = computed(() => { } }); +const tooltipForDragShowing = ref(false); +const tooltipForHoverShowing = ref(false); + +function onMouseenter() { + if (isTouchUsing) return; + + tooltipForHoverShowing.value = true; + + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { + showing: computed(() => tooltipForHoverShowing.value && !tooltipForDragShowing.value), + text: computed(() => { + return props.textConverter(finalValue.value); + }), + targetElement: thumbEl, + }, { + closed: () => dispose(), + }); + + thumbEl.value!.addEventListener('mouseleave', () => { + tooltipForHoverShowing.value = false; + }, { once: true, passive: true }); +} + function onMousedown(ev: MouseEvent | TouchEvent) { ev.preventDefault(); - const tooltipShowing = ref(true); + tooltipForDragShowing.value = true; + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { - showing: tooltipShowing, + showing: tooltipForDragShowing, text: computed(() => { return props.textConverter(finalValue.value); }), @@ -137,7 +173,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { const onMouseup = () => { document.head.removeChild(style); - tooltipShowing.value = false; + tooltipForDragShowing.value = false; window.removeEventListener('mousemove', onDrag); window.removeEventListener('touchmove', onDrag); window.removeEventListener('mouseup', onMouseup); @@ -261,12 +297,12 @@ function onMousedown(ev: MouseEvent | TouchEvent) { > .container { > .track { > .highlight { - transition: width 0.2s cubic-bezier(0,0,0,1); + transition: width 0.2s cubic-bezier(0, 0, 0, 1); } } > .thumb { - transition: left 0.2s cubic-bezier(0,0,0,1); + transition: left 0.2s cubic-bezier(0, 0, 0, 1); } } } diff --git a/packages/frontend/src/components/MkReactionTooltip.vue b/packages/frontend/src/components/MkReactionTooltip.vue index 15409a216a..77ca841ad0 100644 --- a/packages/frontend/src/components/MkReactionTooltip.vue +++ b/packages/frontend/src/components/MkReactionTooltip.vue @@ -36,6 +36,7 @@ const emit = defineEmits<{ .icon { display: block; width: 60px; + max-height: 60px; font-size: 60px; // unicodeな絵文字についてはwidthが効かないため margin: 0 auto; object-fit: contain; diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index 60118fadd2..8038ec7429 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -23,9 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; +import { getEmojiName } from '@@/js/emojilist.js'; import MkTooltip from './MkTooltip.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; -import { getEmojiName } from '@/scripts/emojilist.js'; defineProps<{ showing: boolean; @@ -63,6 +63,7 @@ function getReactionName(reaction: string): string { .reactionIcon { display: block; width: 60px; + max-height: 60px; font-size: 60px; // unicodeな絵文字についてはwidthが効かないため object-fit: contain; margin: 0 auto; diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 6506035f8f..957ee0e76b 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, inject, onMounted, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; +import { getUnicodeEmoji } from '@@/js/emojilist.js'; import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue'; import XDetails from '@/components/MkReactionsViewer.details.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; @@ -34,7 +35,6 @@ import { i18n } from '@/i18n.js'; import * as sound from '@/scripts/sound.js'; import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; import { customEmojisMap } from '@/custom-emojis.js'; -import { getUnicodeEmoji } from '@/scripts/emojilist.js'; const props = defineProps<{ reaction: string; diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 8254ac83cf..150a5c6d54 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -44,9 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only 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 '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ modelValue: string | null; diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 9813774da3..58a666de6f 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix>@</template> <template #suffix>@{{ host }}</template> </MkInput> - <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password> + <MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password> <template #prefix><i class="ti ti-lock"></i></template> <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> </MkInput> @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> <div v-if="user && user.securityKeys" class="twofa-group tap-group"> <p>{{ i18n.ts.useSecurityKey }}</p> - <MkButton v-if="!queryingKey" @click="queryKey"> + <MkButton v-if="!queryingKey" @click="query2FaKey"> {{ i18n.ts.retry }} </MkButton> </div> @@ -45,10 +45,6 @@ SPDX-License-Identifier: AGPL-3.0-only <p :class="$style.orMsg">{{ i18n.ts.or }}</p> </div> <div class="twofa-group totp-group _gaps"> - <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required> - <template #label>{{ i18n.ts.password }}</template> - <template #prefix><i class="ti ti-lock"></i></template> - </MkInput> <MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'"> <template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template> <template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template> @@ -57,6 +53,16 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> </div> </div> + <div v-if="!totpLogin && usePasswordLessLogin" :class="$style.orHr"> + <p :class="$style.orMsg">{{ i18n.ts.or }}</p> + </div> + <div v-if="!totpLogin && usePasswordLessLogin" class="twofa-group tap-group"> + <MkButton v-if="!queryingKey" type="submit" :disabled="signing" style="margin: auto auto;" rounded large primary @click="onPasskeyLogin"> + <i class="ti ti-device-usb" style="font-size: medium;"></i> + {{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }} + </MkButton> + <p v-if="queryingKey">{{ i18n.ts.useSecurityKey }}</p> + </div> </div> </form> </template> @@ -66,21 +72,24 @@ import { defineAsyncComponent, ref } from 'vue'; import { toUnicode } from 'punycode/'; import * as Misskey from 'misskey-js'; import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; +import { SigninWithPasskeyResponse } from 'misskey-js/entities.js'; +import { query, extractDomain } from '@@/js/url.js'; +import { host as configHost } from '@@/js/config.js'; +import MkDivider from './MkDivider.vue'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { host as configHost } from '@/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { query, extractDomain } from '@/scripts/url.js'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; import { showSystemAccountDialog } from '@/scripts/show-system-account-dialog.js'; const signing = ref(false); const user = ref<Misskey.entities.UserDetailed | null>(null); +const usePasswordLessLogin = ref<Misskey.entities.UserDetailed['usePasswordLessLogin']>(true); const username = ref(''); const password = ref(''); const token = ref(''); @@ -89,6 +98,7 @@ const totpLogin = ref(false); const isBackupCode = ref(false); const queryingKey = ref(false); let credentialRequest: CredentialRequestOptions | null = null; +const passkey_context = ref(''); const emit = defineEmits<{ (ev: 'login', v: any): void; @@ -111,8 +121,10 @@ function onUsernameChange(): void { username: username.value, }).then(userResponse => { user.value = userResponse; + usePasswordLessLogin.value = userResponse.usePasswordLessLogin; }, () => { user.value = null; + usePasswordLessLogin.value = true; }); } @@ -122,7 +134,7 @@ function onLogin(res: any): Promise<void> | void { } } -async function queryKey(): Promise<void> { +async function query2FaKey(): Promise<void> { if (credentialRequest == null) return; queryingKey.value = true; await webAuthnRequest(credentialRequest) @@ -151,6 +163,47 @@ async function queryKey(): Promise<void> { }); } +function onPasskeyLogin(): void { + signing.value = true; + if (webAuthnSupported()) { + misskeyApi('signin-with-passkey', {}) + .then((res: SigninWithPasskeyResponse) => { + totpLogin.value = false; + signing.value = false; + queryingKey.value = true; + passkey_context.value = res.context ?? ''; + credentialRequest = parseRequestOptionsFromJSON({ + publicKey: res.option, + }); + }) + .then(() => queryPasskey()) + .catch(loginFailed); + } +} + +async function queryPasskey(): Promise<void> { + if (credentialRequest == null) return; + queryingKey.value = true; + console.log('Waiting passkey auth...'); + await webAuthnRequest(credentialRequest) + .catch((err) => { + console.warn('Passkey Auth fail!: ', err); + queryingKey.value = false; + return Promise.reject(null); + }).then(credential => { + credentialRequest = null; + queryingKey.value = false; + signing.value = true; + return misskeyApi('signin-with-passkey', { + credential: credential.toJSON(), + context: passkey_context.value, + }); + }).then((res: SigninWithPasskeyResponse) => { + emit('login', res.signinResponse); + return onLogin(res.signinResponse); + }); +} + function onSubmit(): void { signing.value = true; if (!totpLogin.value && user.value && user.value.twoFactorEnabled) { @@ -165,7 +218,7 @@ function onSubmit(): void { publicKey: res, }); }) - .then(() => queryKey()) + .then(() => query2FaKey()) .catch(loginFailed); } else { totpLogin.value = true; @@ -217,6 +270,30 @@ function loginFailed(err: any): void { }); break; } + case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.unknownWebAuthnKey, + }); + break; + } + case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.passkeyVerificationFailed, + }); + break; + } + case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled, + }); + break; + } default: { console.error(err); os.alert({ diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index 524c62b4d3..d48780e9de 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkModalWindow ref="dialog" :width="400" - :height="430" + :height="450" @close="onClose" @closed="emit('closed')" > diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index e673b6d530..5a5c712a48 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -88,7 +88,7 @@ import * as Misskey from 'misskey-js'; import MkButton from './MkButton.vue'; import MkInput from './MkInput.vue'; import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; -import * as config from '@/config.js'; +import * as config from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue index cd884f0b19..7743a89242 100644 --- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue +++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import MkButton from '@/components/MkButton.vue'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { miLocalStorage } from '@/local-storage.js'; diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 8386a783fc..6bd00fcc2a 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -46,7 +46,7 @@ import MkMediaList from '@/components/MkMediaList.vue'; import MkPoll from '@/components/MkPoll.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { shouldCollapsed } from '@/scripts/collapsed.js'; +import { shouldCollapsed } from '@@/js/collapsed.js'; import { defaultStore } from '@/store.js'; import { useRouter } from '@/router/supplier.js'; import * as os from '@/os.js'; @@ -110,7 +110,7 @@ watch(() => props.expandAllCws, (expandAllCws) => { left: 0; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), var(--X15)); + // background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 041ae88109..430e3c7958 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -100,14 +100,14 @@ defineProps<{ &.grid { > .group { + margin-left: 0; + margin-right: 0; + & + .group { padding-top: 0; border-top: none; } - margin-left: 0; - margin-right: 0; - > .title { font-size: 1em; opacity: 0.7; diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts index 69b8edd85a..19e4eea733 100644 --- a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts +++ b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts @@ -4,9 +4,10 @@ */ import { defineAsyncComponent } from 'vue'; +import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; -export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved'; +export type SystemWebhookEventType = Misskey.entities.SystemWebhook['on'][number]; export type MkSystemWebhookEditorProps = { mode: 'create' | 'edit'; diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue index f5c7a3160b..ec3b1c90ca 100644 --- a/packages/frontend/src/components/MkSystemWebhookEditor.vue +++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue @@ -35,16 +35,31 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts._webhookSettings.trigger }}</template> - <div class="_gaps_s"> - <MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport"> - <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template> - </MkSwitch> - <MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved"> - <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template> - </MkSwitch> - <MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated"> - <template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template> - </MkSwitch> + <div class="_gaps"> + <div class="_gaps_s"> + <div :class="$style.switchBox"> + <MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport"> + <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template> + </MkSwitch> + <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReport)" @click="test('abuseReport')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved"> + <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template> + </MkSwitch> + <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReportResolved)" @click="test('abuseReportResolved')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated"> + <template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template> + </MkSwitch> + <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.userCreated)" @click="test('userCreated')"><i class="ti ti-send"></i></MkButton> + </div> + </div> + + <div v-show="mode === 'edit'" :class="$style.description"> + {{ i18n.ts._webhookSettings.testRemarks }} + </div> </div> </MkFolder> @@ -66,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> import { computed, onMounted, ref, shallowRef, toRefs } from 'vue'; +import * as Misskey from 'misskey-js'; import MkInput from '@/components/MkInput.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import { @@ -180,6 +196,21 @@ async function loadingScope<T>(fn: () => Promise<T>): Promise<T> { } } +async function test(type: Misskey.entities.SystemWebhook['on'][number]): Promise<void> { + if (!id.value) { + return Promise.resolve(); + } + + await os.apiWithDialog('admin/system-webhook/test', { + webhookId: id.value, + type, + override: { + secret: secret.value, + url: url.value, + }, + }); +} + onMounted(async () => { await loadingScope(async () => { switch (mode.value) { @@ -235,4 +266,29 @@ onMounted(async () => { -webkit-backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px)); } + +.switchBox { + display: flex; + align-items: center; + justify-content: start; + + .testButton { + $buttonSize: 28px; + padding: 0; + width: $buttonSize; + min-width: $buttonSize; + max-width: $buttonSize; + height: $buttonSize; + margin-left: auto; + line-height: normal; + font-size: 90%; + border-radius: 9999px; + } +} + +.description { + font-size: 0.85em; + padding: 8px 0 0 0; + color: var(--fgTransparentWeak); +} </style> diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue index 9adc8d466c..1f5a2b9381 100644 --- a/packages/frontend/src/components/MkTutorialDialog.vue +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -158,7 +158,7 @@ import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue'; import MkAnimBg from '@/components/MkAnimBg.vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { claimAchievement } from '@/scripts/achievements.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index 4fb0749931..91f5b86c2d 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -19,7 +19,7 @@ import { onMounted, shallowRef } from 'vue'; import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import MkSparkle from '@/components/MkSparkle.vue'; -import { version } from '@/config.js'; +import { version } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { confetti } from '@/scripts/confetti.js'; diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index a51b878580..04f5314463 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -85,12 +85,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue'; import type { summaly } from '@misskey-dev/summaly'; -import { url as local } from '@/config.js'; +import { url as local } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { deviceKind } from '@/scripts/device-kind.js'; import MkButton from '@/components/MkButton.vue'; -import { versatileLang } from '@/scripts/intl-const.js'; +import { versatileLang } from '@@/js/intl-const.js'; import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index 7d210a4385..a5b48c8ce2 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -70,7 +70,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { host as currentHost, hostname } from '@/config.js'; +import { host as currentHost, hostname } from '@@/js/config.js'; const emit = defineEmits<{ (ev: 'ok', selected: Misskey.entities.UserDetailed): void; diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index 514350c930..1fb1eda039 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -137,7 +137,7 @@ import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue'; import MkAnimBg from '@/components/MkAnimBg.vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index ff2e27aaf8..874eff6c79 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -62,7 +62,7 @@ import XSignupDialog from '@/components/MkSignupDialog.vue'; import MkButton from '@/components/MkButton.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index 7aed652811..99840bf8d7 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -57,6 +57,7 @@ import MkButton from '@/components/MkButton.vue'; import { widgets as widgetDefs } from '@/widgets/index.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; +import { isLink } from '@@/js/is-link.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -98,13 +99,6 @@ const updateWidget = (id, data) => { function onContextmenu(widget: Widget, ev: MouseEvent) { const element = ev.target as HTMLElement | null; - const isLink = (el: HTMLElement): boolean => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - return false; - }; if (element && isLink(element)) return; if (element && (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(element.tagName) || element.attributes['contenteditable'])) return; if (window.getSelection()?.toString() !== '') return; diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 303e49de00..08906a1205 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue'; import contains from '@/scripts/contains.js'; import * as os from '@/os.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; @@ -508,10 +508,6 @@ defineExpose({ .header { --height: 39px; - &.mini { - --height: 32px; - } - display: flex; position: relative; z-index: 1; @@ -524,6 +520,10 @@ defineExpose({ //border-bottom: solid 1px var(--divider); font-size: 90%; font-weight: bold; + + &.mini { + --height: 32px; + } } .headerButton { diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue index e3711b3463..1122976436 100644 --- a/packages/frontend/src/components/MkYouTubePlayer.vue +++ b/packages/frontend/src/components/MkYouTubePlayer.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import MkWindow from '@/components/MkWindow.vue'; -import { versatileLang } from '@/scripts/intl-const.js'; +import { versatileLang } from '@@/js/intl-const.js'; import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue index 164606e1f9..f5546edf1e 100644 --- a/packages/frontend/src/components/form/link.vue +++ b/packages/frontend/src/components/form/link.vue @@ -60,18 +60,18 @@ const props = defineProps<{ width: 100%; box-sizing: border-box; padding: 10px 14px; - background: var(--buttonBg); + background: var(--folderHeaderBg); border-radius: var(--radius-sm); font-size: 0.9em; &:hover { text-decoration: none; - background: var(--buttonHoverBg); + background: var(--folderHeaderHoverBg); } &.active { color: var(--accent); - background: var(--buttonHoverBg); + background: var(--folderHeaderHoverBg); } } diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index e0303dbb27..23f049ebb4 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -17,7 +17,7 @@ export type MkABehavior = 'window' | 'browser' | null; import { computed, inject, shallowRef } from 'vue'; import * as os from '@/os.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { useRouter } from '@/router/supplier.js'; diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue index bbcb070803..9a1ac3aca2 100644 --- a/packages/frontend/src/components/global/MkAcct.vue +++ b/packages/frontend/src/components/global/MkAcct.vue @@ -4,11 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :minScale="2 / 3"> - <span>@{{ user.username }}</span> - <span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span> -</MkCondensedLine> -<span v-else> +<span> <span>@{{ user.username }}</span> <span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span> </span> @@ -17,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; import { toUnicode } from 'punycode/'; -import { host as hostRaw } from '@/config.js'; +import { host as hostRaw } from '@@/js/config.js'; import { defaultStore } from '@/store.js'; defineProps<{ diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index bee1d9ca4c..1238e6ef23 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed } from 'vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { url as local, host } from '@/config.js'; +import { url as local, host } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 4a01d3f32d..1f78b068a2 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="showDecoration"> <img v-for="decoration in decorations ?? user.avatarDecorations" - :class="[$style.decoration]" + :class="[$style.decoration, { [$style.decorationBlink]: decoration.blink }]" :src="getDecorationUrl(decoration)" :style="{ rotate: getDecorationAngle(decoration), @@ -43,10 +43,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { watch, ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; +import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js'; import MkImgWithBlurhash from '../MkImgWithBlurhash.vue'; import MkA from './MkA.vue'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; -import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js'; import { acct, userPage } from '@/filters/user.js'; import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; import { defaultStore } from '@/store.js'; @@ -61,7 +61,7 @@ const props = withDefaults(defineProps<{ link?: boolean; preview?: boolean; indicator?: boolean; - decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[]; + decorations?: (Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'> & { blink?: boolean; })[]; forceShowDecoration?: boolean; }>(), { target: null, @@ -336,4 +336,17 @@ watch(() => props.user.avatarBlurhash, () => { width: 200%; pointer-events: none; } + +.decorationBlink { + animation: blink 1s infinite; +} + +@keyframes blink { + 0%, 100% { + filter: brightness(2); + } + 50% { + filter: brightness(1); + } +} </style> diff --git a/packages/frontend/src/components/global/MkCondensedLine.vue b/packages/frontend/src/components/global/MkCondensedLine.vue index 7c4957d77f..473d444c16 100644 --- a/packages/frontend/src/components/global/MkCondensedLine.vue +++ b/packages/frontend/src/components/global/MkCondensedLine.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <span :class="$style.container"> - <span ref="content" :class="$style.content"> + <span ref="content" :class="$style.content" :style="{ maxWidth: `${100 / minScale}%` }"> <slot/> </span> </span> diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 4cacc7b292..fbc716016c 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -35,6 +35,7 @@ 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'; const props = defineProps<{ name: string; @@ -86,7 +87,10 @@ const errored = ref(url.value == null); function onClick(ev: MouseEvent) { if (props.menu) { ev.stopPropagation(); - os.popupMenu([{ + + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'label', text: `:${props.name}:`, }, { @@ -96,14 +100,20 @@ function onClick(ev: MouseEvent) { copyToClipboard(`:${props.name}:`); os.success(); }, - }, ...(props.menuReaction && react ? [{ - text: i18n.ts.doReaction, - icon: 'ph-smiley ph-bold ph-lg', - action: () => { - react(`:${props.name}:`); - sound.playMisskeySfx('reaction'); - }, - }] : []), { + }); + + if (props.menuReaction && react) { + menuItems.push({ + text: i18n.ts.doReaction, + icon: 'ph-smiley ph-bold ph-lg', + action: () => { + react(`:${props.name}:`); + sound.playMisskeySfx('reaction'); + }, + }); + } + + menuItems.push({ text: i18n.ts.info, icon: 'ti ti-info-circle', action: async () => { @@ -115,7 +125,9 @@ function onClick(ev: MouseEvent) { closed: () => dispose(), }); }, - }], ev.currentTarget ?? ev.target); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } } </script> diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index c485305376..bd9b1d665a 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -10,13 +10,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, inject } from 'vue'; -import { char2fluentEmojiFilePath, char2twemojiFilePath, char2tossfaceFilePath } from '@/scripts/emoji-base.js'; +import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js'; +import { char2fluentEmojiFilePath, char2twemojiFilePath, char2tossfaceFilePath } from '@@/js/emoji-base.js'; import { defaultStore } from '@/store.js'; -import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js'; import * as os from '@/os.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as sound from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ emoji: string; @@ -40,7 +41,10 @@ function computeTitle(event: PointerEvent): void { function onClick(ev: MouseEvent) { if (props.menu) { ev.stopPropagation(); - os.popupMenu([{ + + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'label', text: props.emoji, }, { @@ -50,14 +54,20 @@ function onClick(ev: MouseEvent) { copyToClipboard(props.emoji); os.success(); }, - }, ...(props.menuReaction && react ? [{ - text: i18n.ts.doReaction, - icon: 'ph-smiley ph-bold ph-lg', - action: () => { - react(props.emoji); - sound.playMisskeySfx('reaction'); - }, - }] : [])], ev.currentTarget ?? ev.target); + }); + + if (props.menuReaction && react) { + menuItems.push({ + text: i18n.ts.doReaction, + icon: 'ph-smiley ph-bold ph-lg', + action: () => { + react(props.emoji); + sound.playMisskeySfx('reaction'); + }, + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } } </script> diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMfm.stories.impl.ts index 730351f795..1daf7a29cb 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts +++ b/packages/frontend/src/components/global/MkMfm.stories.impl.ts @@ -2,16 +2,15 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ + import { StoryObj } from '@storybook/vue3'; import { expect, within } from '@storybook/test'; -import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.js'; +import MkMfm from './MkMfm.js'; export const Default = { render(args) { return { components: { - MkMisskeyFlavoredMarkdown, + MkMfm, }, setup() { return { @@ -25,7 +24,7 @@ export const Default = { }; }, }, - template: '<MkMisskeyFlavoredMarkdown v-bind="props" />', + template: '<MkMfm v-bind="props" />', }; }, async play({ canvasElement, args }) { @@ -54,25 +53,25 @@ export const Default = { parameters: { layout: 'centered', }, -} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; +} satisfies StoryObj<typeof MkMfm>; export const Plain = { ...Default, args: { ...Default.args, plain: true, }, -} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; +} satisfies StoryObj<typeof MkMfm>; export const Nowrap = { ...Default, args: { ...Default.args, nowrap: true, }, -} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; +} satisfies StoryObj<typeof MkMfm>; export const IsNotNote = { ...Default, args: { ...Default.args, isNote: false, }, -} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; +} satisfies StoryObj<typeof MkMfm>; diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMfm.ts index 5046f17357..9bf9f4a872 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMfm.ts @@ -18,10 +18,15 @@ import MkCodeInline from '@/components/MkCodeInline.vue'; import MkGoogle from '@/components/MkGoogle.vue'; import MkSparkle from '@/components/MkSparkle.vue'; import MkA, { MkABehavior } from '@/components/global/MkA.vue'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { defaultStore } from '@/store.js'; -import { nyaize as doNyaize } from '@/scripts/nyaize.js'; -import { safeParseFloat } from '@/scripts/safe-parse.js'; + +function safeParseFloat(str: unknown): number | null { + if (typeof str !== 'string' || str === '') return null; + const num = parseFloat(str); + if (isNaN(num)) return null; + return num; +} const QUOTE_STYLE = ` display: block; @@ -92,7 +97,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven case 'text': { let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); if (!disableNyaize && shouldNyaize) { - text = doNyaize(text); + text = Misskey.nyaize(text); } if (!props.plain) { @@ -340,14 +345,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven const child = token.children[0]; let text = child.type === 'text' ? child.props.text : ''; if (!disableNyaize && shouldNyaize) { - text = doNyaize(text); + text = Misskey.nyaize(text); } return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]); } else { const rt = token.children.at(-1)!; let text = rt.type === 'text' ? rt.props.text : ''; if (!disableNyaize && shouldNyaize) { - text = doNyaize(text); + text = Misskey.nyaize(text); } return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); } @@ -459,7 +464,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'emojiCode': { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (props.author?.host == null) { return [h(MkCustomEmoji, { key: Math.random(), diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 95ac102013..bb20f54fa4 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue'; import tinycolor from 'tinycolor2'; import XTabs, { Tab } from './MkPageHeader.tabs.vue'; -import { scrollToTop } from '@/scripts/scroll.js'; +import { scrollToTop } from '@@/js/scroll.js'; import { globalEvents } from '@/events.js'; import { injectReactiveMetadata } from '@/scripts/page-metadata.js'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue index b12dc8cb31..72993991ce 100644 --- a/packages/frontend/src/components/global/MkStickyContainer.vue +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="bodyEl" :data-sticky-container-header-height="headerHeight" :data-sticky-container-footer-height="footerHeight" + style="position: relative; z-index: 0;" > <slot></slot> </div> @@ -24,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue'; -import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const.js'; +import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js'; const rootEl = shallowRef<HTMLElement>(); const headerEl = shallowRef<HTMLElement>(); @@ -83,14 +84,14 @@ onMounted(() => { if (headerEl.value != null) { headerEl.value.style.position = 'sticky'; headerEl.value.style.top = 'var(--stickyTop, 0)'; - headerEl.value.style.zIndex = '1000'; + headerEl.value.style.zIndex = '1'; observer.observe(headerEl.value); } if (footerEl.value != null) { footerEl.value.style.position = 'sticky'; footerEl.value.style.bottom = 'var(--stickyBottom, 0)'; - footerEl.value.style.zIndex = '1000'; + footerEl.value.style.zIndex = '1'; observer.observe(footerEl.value); } }); diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts index ffd4a849a2..ccf7f200b5 100644 --- a/packages/frontend/src/components/global/MkTime.stories.impl.ts +++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts @@ -8,7 +8,7 @@ import { expect } from '@storybook/test'; import { StoryObj } from '@storybook/vue3'; import MkTime from './MkTime.vue'; import { i18n } from '@/i18n.js'; -import { dateTimeFormat } from '@/scripts/intl-const.js'; +import { dateTimeFormat } from '@@/js/intl-const.js'; const now = new Date('2023-04-01T00:00:00.000Z'); const future = new Date('2024-04-01T00:00:00.000Z'); const oneHourAgo = new Date(now.getTime() - 3600000); diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 027b226f3f..50bec990a1 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only import isChromatic from 'chromatic/isChromatic'; import { onMounted, onUnmounted, ref, computed } from 'vue'; import { i18n } from '@/i18n.js'; -import { dateTimeFormat } from '@/scripts/intl-const.js'; +import { dateTimeFormat } from '@@/js/intl-const.js'; const props = withDefaults(defineProps<{ time: Date | string | number | null; diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 1dec8ad28c..8cca47c1db 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -29,14 +29,21 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, ref } from 'vue'; import { toUnicode as decodePunycode } from 'punycode/'; -import { url as local } from '@/config.js'; +import { url as local } from '@@/js/config.js'; import * as os from '@/os.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; -import { safeURIDecode } from '@/scripts/safe-uri-decode.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { MkABehavior } from '@/components/global/MkA.vue'; import { warningExternalWebsite } from '@/scripts/warning-external-website.js'; +function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} + const props = withDefaults(defineProps<{ url: string; rel?: string; diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 44d8d59941..b36625ed1b 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -5,7 +5,7 @@ import { App } from 'vue'; -import Mfm from './global/MkMisskeyFlavoredMarkdown.js'; +import Mfm from './global/MkMfm.js'; import MkA from './global/MkA.vue'; import MkAcct from './global/MkAcct.vue'; import MkAvatar from './global/MkAvatar.vue'; diff --git a/packages/frontend/src/config.ts b/packages/frontend/src/config.ts deleted file mode 100644 index e3922a0cd5..0000000000 --- a/packages/frontend/src/config.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { miLocalStorage } from '@/local-storage.js'; - -const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href); -const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content; - -export const host = address.host; -export const hostname = address.hostname; -export const url = address.origin; -export const apiUrl = location.origin + '/api'; -export const wsOrigin = location.origin; -export const lang = miLocalStorage.getItem('lang') ?? 'en-US'; -export const langs = _LANGS_; -const preParseLocale = miLocalStorage.getItem('locale'); -export let locale = preParseLocale ? JSON.parse(preParseLocale) : null; -export const version = _VERSION_; -export const instanceName = siteName === 'Sharkey' || siteName == null ? host : siteName; -export const ui = miLocalStorage.getItem('ui'); -export const debug = miLocalStorage.getItem('debug') === 'true'; - -export function updateLocale(newLocale): void { - locale = newLocale; -} diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts deleted file mode 100644 index 058db9b981..0000000000 --- a/packages/frontend/src/const.ts +++ /dev/null @@ -1,194 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// ブラウザで直接表示することを許可するファイルの種類のリスト -// ここに含まれないものは application/octet-stream としてレスポンスされる -// SVGはXSSを生むので許可しない -export const FILE_TYPE_BROWSERSAFE = [ - // Images - 'image/png', - 'image/gif', - 'image/jpeg', - 'image/webp', - 'image/avif', - 'image/apng', - 'image/bmp', - 'image/tiff', - 'image/x-icon', - 'image/jxl', - - // OggS - 'audio/opus', - 'video/ogg', - 'audio/ogg', - 'application/ogg', - - // ISO/IEC base media file format - 'video/quicktime', - 'video/mp4', - 'audio/mp4', - 'video/x-m4v', - 'audio/x-m4a', - 'video/3gpp', - 'video/3gpp2', - - 'video/mpeg', - 'audio/mpeg', - - 'video/webm', - 'audio/webm', - - 'audio/aac', - - // see https://github.com/misskey-dev/misskey/pull/10686 - 'audio/flac', - 'audio/wav', - // backward compatibility - 'audio/x-flac', - 'audio/vnd.wave', -]; - -export const FILE_TYPE_TRACKER_MODULES = [ - 'audio/mod', - 'audio/x-mod', - 'audio/s3m', - 'audio/x-s3m', - 'audio/xm', - 'audio/x-xm', - 'audio/it', - 'audio/x-it', -]; - -export const FILE_EXT_TRACKER_MODULES = [ - 'mod', - 's3m', - 'xm', - 'it', - 'mptm', - 'stm', - 'nst', - 'm15', - 'stk', - 'wow', - 'ult', - '669', - 'mtm', - 'med', - 'far', - 'mdl', - 'ams', - 'dsm', - 'amf', - 'okt', - 'dmf', - 'ptm', - 'psm', - 'mt2', - 'dbm', - 'digi', - 'imf', - 'j2b', - 'gdm', - 'umx', - 'plm', - 'mo3', - 'xpk', - 'ppm', - 'mmcmp', -]; - -/* -https://github.com/sindresorhus/file-type/blob/main/supported.js -https://github.com/sindresorhus/file-type/blob/main/core.js -https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers -*/ - -export const notificationTypes = [ - 'note', - 'follow', - 'mention', - 'reply', - 'renote', - 'quote', - 'reaction', - 'pollEnded', - 'receiveFollowRequest', - 'followRequestAccepted', - 'roleAssigned', - 'achievementEarned', - 'app', - 'edited' -] as const; -export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; - -export const ROLE_POLICIES = [ - 'gtlAvailable', - 'ltlAvailable', - 'btlAvailable', - 'canPublicNote', - 'canImportNotes', - 'mentionLimit', - 'canInvite', - 'inviteLimit', - 'inviteLimitCycle', - 'inviteExpirationTime', - 'canManageCustomEmojis', - 'canManageAvatarDecorations', - 'canSearchNotes', - 'canUseTranslator', - 'canHideAds', - 'driveCapacityMb', - 'alwaysMarkNsfw', - 'canUpdateBioMedia', - 'pinLimit', - 'antennaLimit', - 'wordMuteLimit', - 'webhookLimit', - 'clipLimit', - 'noteEachClipsLimit', - 'userListLimit', - 'userEachUserListsLimit', - 'rateLimitFactor', - 'avatarDecorationLimit', -] as const; - -// なんか動かない -//export const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP'); -//export const CURRENT_STICKY_BOTTOM = Symbol('CURRENT_STICKY_BOTTOM'); -export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP'; -export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM'; - -export const DEFAULT_SERVER_ERROR_IMAGE_URL = '/status/error.png'; -export const DEFAULT_NOT_FOUND_IMAGE_URL = '/status/missingpage.webp'; -export const DEFAULT_INFO_IMAGE_URL = '/status/nothinghere.png'; - -export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime', 'crop', 'fade', 'followmouse']; -export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = { - tada: ['speed=', 'delay='], - jelly: ['speed=', 'delay='], - twitch: ['speed=', 'delay='], - shake: ['speed=', 'delay='], - spin: ['speed=', 'delay=', 'left', 'alternate', 'x', 'y'], - jump: ['speed=', 'delay='], - bounce: ['speed=', 'delay='], - flip: ['h', 'v'], - x2: [], - x3: [], - x4: [], - scale: ['x=', 'y='], - position: ['x=', 'y='], - fg: ['color='], - bg: ['color='], - border: ['width=', 'style=', 'color=', 'radius=', 'noclip'], - font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'], - blur: [], - rainbow: ['speed=', 'delay='], - rotate: ['deg='], - ruby: [], - unixtime: [], - fade: ['speed=', 'delay=', 'loop=', 'out'], - crop: ['top=', 'bottom=', 'left=', 'right='], - followmouse: ['x', 'y', 'rotateByVelocity', 'speed='], -}; diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts index 9da3582e1a..0d03282cee 100644 --- a/packages/frontend/src/custom-emojis.ts +++ b/packages/frontend/src/custom-emojis.ts @@ -6,7 +6,6 @@ import { shallowRef, computed, markRaw, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useStream } from '@/stream.js'; import { get, set } from '@/scripts/idb-proxy.js'; const storageCache = await get('emojis'); @@ -29,23 +28,20 @@ watch(customEmojis, emojis => { } }, { immediate: true }); -// TODO: ここら辺副作用なのでいい感じにする -const stream = useStream(); - -stream.on('emojiAdded', emojiData => { - customEmojis.value = [emojiData.emoji, ...customEmojis.value]; +export function addCustomEmoji(emoji: Misskey.entities.EmojiSimple) { + customEmojis.value = [emoji, ...customEmojis.value]; set('emojis', customEmojis.value); -}); +} -stream.on('emojiUpdated', emojiData => { - customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.EmojiSimple ?? item); +export function updateCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) { + customEmojis.value = customEmojis.value.map(item => emojis.find(search => search.name === item.name) ?? item); set('emojis', customEmojis.value); -}); +} -stream.on('emojiDeleted', emojiData => { - customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name)); +export function removeCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) { + customEmojis.value = customEmojis.value.filter(item => !emojis.some(search => search.name === item.name)); set('emojis', customEmojis.value); -}); +} export async function fetchCustomEmojis(force = false) { const now = Date.now(); diff --git a/packages/frontend/src/directives/follow-append.ts b/packages/frontend/src/directives/follow-append.ts index f200f242ed..615dd99fa8 100644 --- a/packages/frontend/src/directives/follow-append.ts +++ b/packages/frontend/src/directives/follow-append.ts @@ -4,7 +4,7 @@ */ import { Directive } from 'vue'; -import { getScrollContainer, getScrollPosition } from '@/scripts/scroll.js'; +import { getScrollContainer, getScrollPosition } from '@@/js/scroll.js'; export default { mounted(src, binding, vn) { diff --git a/packages/frontend/src/emojilist.json b/packages/frontend/src/emojilist.json deleted file mode 100644 index 75d5c34d71..0000000000 --- a/packages/frontend/src/emojilist.json +++ /dev/null @@ -1,1805 +0,0 @@ -[ - ["😀", "grinning", 0], - ["😬", "grimacing", 0], - ["😁", "grin", 0], - ["😂", "joy", 0], - ["🤣", "rofl", 0], - ["🥳", "partying", 0], - ["😃", "smiley", 0], - ["😄", "smile", 0], - ["😅", "sweat_smile", 0], - ["🥲", "smiling_face_with_tear", 0], - ["😆", "laughing", 0], - ["😇", "innocent", 0], - ["😉", "wink", 0], - ["😊", "blush", 0], - ["🙂", "slightly_smiling_face", 0], - ["🙃", "upside_down_face", 0], - ["☺️", "relaxed", 0], - ["😋", "yum", 0], - ["😌", "relieved", 0], - ["😍", "heart_eyes", 0], - ["🥰", "smiling_face_with_three_hearts", 0], - ["😘", "kissing_heart", 0], - ["😗", "kissing", 0], - ["😙", "kissing_smiling_eyes", 0], - ["😚", "kissing_closed_eyes", 0], - ["😜", "stuck_out_tongue_winking_eye", 0], - ["🤪", "zany", 0], - ["🤨", "raised_eyebrow", 0], - ["🧐", "monocle", 0], - ["😝", "stuck_out_tongue_closed_eyes", 0], - ["😛", "stuck_out_tongue", 0], - ["🤑", "money_mouth_face", 0], - ["🤓", "nerd_face", 0], - ["🥸", "disguised_face", 0], - ["😎", "sunglasses", 0], - ["🤩", "star_struck", 0], - ["🤡", "clown_face", 0], - ["🤠", "cowboy_hat_face", 0], - ["🤗", "hugs", 0], - ["😏", "smirk", 0], - ["😶", "no_mouth", 0], - ["😐", "neutral_face", 0], - ["😑", "expressionless", 0], - ["😒", "unamused", 0], - ["🙄", "roll_eyes", 0], - ["🤔", "thinking", 0], - ["🤥", "lying_face", 0], - ["🤭", "hand_over_mouth", 0], - ["🤫", "shushing", 0], - ["🤬", "symbols_over_mouth", 0], - ["🤯", "exploding_head", 0], - ["😳", "flushed", 0], - ["😞", "disappointed", 0], - ["😟", "worried", 0], - ["😠", "angry", 0], - ["😡", "rage", 0], - ["😔", "pensive", 0], - ["😕", "confused", 0], - ["🙁", "slightly_frowning_face", 0], - ["☹", "frowning_face", 0], - ["😣", "persevere", 0], - ["😖", "confounded", 0], - ["😫", "tired_face", 0], - ["😩", "weary", 0], - ["🥺", "pleading", 0], - ["😤", "triumph", 0], - ["😮", "open_mouth", 0], - ["😱", "scream", 0], - ["😨", "fearful", 0], - ["😰", "cold_sweat", 0], - ["😯", "hushed", 0], - ["😦", "frowning", 0], - ["😧", "anguished", 0], - ["😢", "cry", 0], - ["😥", "disappointed_relieved", 0], - ["🤤", "drooling_face", 0], - ["😪", "sleepy", 0], - ["😓", "sweat", 0], - ["🥵", "hot", 0], - ["🥶", "cold", 0], - ["😭", "sob", 0], - ["😵", "dizzy_face", 0], - ["😲", "astonished", 0], - ["🤐", "zipper_mouth_face", 0], - ["🤢", "nauseated_face", 0], - ["🤧", "sneezing_face", 0], - ["🤮", "vomiting", 0], - ["😷", "mask", 0], - ["🤒", "face_with_thermometer", 0], - ["🤕", "face_with_head_bandage", 0], - ["🥴", "woozy", 0], - ["🥱", "yawning", 0], - ["😴", "sleeping", 0], - ["💤", "zzz", 0], - ["😶🌫️", "face_in_clouds", 0], - ["😮💨", "face_exhaling", 0], - ["😵💫", "face_with_spiral_eyes", 0], - ["🫠", "melting_face", 0], - ["🫢", "face_with_open_eyes_and_hand_over_mouth", 0], - ["🫣", "face_with_peeking_eye", 0], - ["🫡", "saluting_face", 0], - ["🫥", "dotted_line_face", 0], - ["🫤", "face_with_diagonal_mouth", 0], - ["🥹", "face_holding_back_tears", 0], - ["🫨", "shaking_face", 0], - ["💩", "poop", 0], - ["😈", "smiling_imp", 0], - ["👿", "imp", 0], - ["👹", "japanese_ogre", 0], - ["👺", "japanese_goblin", 0], - ["💀", "skull", 0], - ["👻", "ghost", 0], - ["👽", "alien", 0], - ["🤖", "robot", 0], - ["😺", "smiley_cat", 0], - ["😸", "smile_cat", 0], - ["😹", "joy_cat", 0], - ["😻", "heart_eyes_cat", 0], - ["😼", "smirk_cat", 0], - ["😽", "kissing_cat", 0], - ["🙀", "scream_cat", 0], - ["😿", "crying_cat_face", 0], - ["😾", "pouting_cat", 0], - ["🤲", "palms_up", 1], - ["🙌", "raised_hands", 1], - ["👏", "clap", 1], - ["👋", "wave", 1], - ["🤙", "call_me_hand", 1], - ["👍", "+1", 1], - ["👎", "-1", 1], - ["👊", "facepunch", 1], - ["✊", "fist", 1], - ["🤛", "fist_left", 1], - ["🤜", "fist_right", 1], - ["🫷", "leftwards_pushing_hand", 1], - ["🫸", "rightwards_pushing_hand", 1], - ["✌", "v", 1], - ["👌", "ok_hand", 1], - ["✋", "raised_hand", 1], - ["🤚", "raised_back_of_hand", 1], - ["👐", "open_hands", 1], - ["💪", "muscle", 1], - ["🦾", "mechanical_arm", 1], - ["🙏", "pray", 1], - ["🦶", "foot", 1], - ["🦵", "leg", 1], - ["🦿", "mechanical_leg", 1], - ["🤝", "handshake", 1], - ["☝", "point_up", 1], - ["👆", "point_up_2", 1], - ["👇", "point_down", 1], - ["👈", "point_left", 1], - ["👉", "point_right", 1], - ["🖕", "fu", 1], - ["🖐", "raised_hand_with_fingers_splayed", 1], - ["🤟", "love_you", 1], - ["🤘", "metal", 1], - ["🤞", "crossed_fingers", 1], - ["🖖", "vulcan_salute", 1], - ["✍", "writing_hand", 1], - ["🫰", "hand_with_index_finger_and_thumb_crossed", 1], - ["🫱", "rightwards_hand", 1], - ["🫲", "leftwards_hand", 1], - ["🫳", "palm_down_hand", 1], - ["🫴", "palm_up_hand", 1], - ["🫵", "index_pointing_at_the_viewer", 1], - ["🫶", "heart_hands", 1], - ["🤏", "pinching_hand", 1], - ["🤌", "pinched_fingers", 1], - ["🤳", "selfie", 1], - ["💅", "nail_care", 1], - ["👄", "lips", 1], - ["🫦", "biting_lip", 1], - ["🦷", "tooth", 1], - ["👅", "tongue", 1], - ["👂", "ear", 1], - ["🦻", "ear_with_hearing_aid", 1], - ["👃", "nose", 1], - ["👁", "eye", 1], - ["👀", "eyes", 1], - ["🧠", "brain", 1], - ["🫀", "anatomical_heart", 1], - ["🫁", "lungs", 1], - ["👤", "bust_in_silhouette", 1], - ["👥", "busts_in_silhouette", 1], - ["🗣", "speaking_head", 1], - ["👶", "baby", 1], - ["🧒", "child", 1], - ["👦", "boy", 1], - ["👧", "girl", 1], - ["🧑", "adult", 1], - ["👨", "man", 1], - ["👩", "woman", 1], - ["🧑🦱", "curly_hair", 1], - ["👩🦱", "curly_hair_woman", 1], - ["👨🦱", "curly_hair_man", 1], - ["🧑🦰", "red_hair", 1], - ["👩🦰", "red_hair_woman", 1], - ["👨🦰", "red_hair_man", 1], - ["👱♀️", "blonde_woman", 1], - ["👱", "blonde_man", 1], - ["🧑🦳", "white_hair", 1], - ["👩🦳", "white_hair_woman", 1], - ["👨🦳", "white_hair_man", 1], - ["🧑🦲", "bald", 1], - ["👩🦲", "bald_woman", 1], - ["👨🦲", "bald_man", 1], - ["🧔", "bearded_person", 1], - ["🧓", "older_adult", 1], - ["👴", "older_man", 1], - ["👵", "older_woman", 1], - ["👲", "man_with_gua_pi_mao", 1], - ["🧕", "woman_with_headscarf", 1], - ["👳♀️", "woman_with_turban", 1], - ["👳", "man_with_turban", 1], - ["👮♀️", "policewoman", 1], - ["👮", "policeman", 1], - ["👷♀️", "construction_worker_woman", 1], - ["👷", "construction_worker_man", 1], - ["💂♀️", "guardswoman", 1], - ["💂", "guardsman", 1], - ["🕵️♀️", "female_detective", 1], - ["🕵", "male_detective", 1], - ["🧑⚕️", "health_worker", 1], - ["👩⚕️", "woman_health_worker", 1], - ["👨⚕️", "man_health_worker", 1], - ["🧑🌾", "farmer", 1], - ["👩🌾", "woman_farmer", 1], - ["👨🌾", "man_farmer", 1], - ["🧑🍳", "cook", 1], - ["👩🍳", "woman_cook", 1], - ["👨🍳", "man_cook", 1], - ["🧑🎓", "student", 1], - ["👩🎓", "woman_student", 1], - ["👨🎓", "man_student", 1], - ["🧑🎤", "singer", 1], - ["👩🎤", "woman_singer", 1], - ["👨🎤", "man_singer", 1], - ["🧑🏫", "teacher", 1], - ["👩🏫", "woman_teacher", 1], - ["👨🏫", "man_teacher", 1], - ["🧑🏭", "factory_worker", 1], - ["👩🏭", "woman_factory_worker", 1], - ["👨🏭", "man_factory_worker", 1], - ["🧑💻", "technologist", 1], - ["👩💻", "woman_technologist", 1], - ["👨💻", "man_technologist", 1], - ["🧑💼", "office_worker", 1], - ["👩💼", "woman_office_worker", 1], - ["👨💼", "man_office_worker", 1], - ["🧑🔧", "mechanic", 1], - ["👩🔧", "woman_mechanic", 1], - ["👨🔧", "man_mechanic", 1], - ["🧑🔬", "scientist", 1], - ["👩🔬", "woman_scientist", 1], - ["👨🔬", "man_scientist", 1], - ["🧑🎨", "artist", 1], - ["👩🎨", "woman_artist", 1], - ["👨🎨", "man_artist", 1], - ["🧑🚒", "firefighter", 1], - ["👩🚒", "woman_firefighter", 1], - ["👨🚒", "man_firefighter", 1], - ["🧑✈️", "pilot", 1], - ["👩✈️", "woman_pilot", 1], - ["👨✈️", "man_pilot", 1], - ["🧑🚀", "astronaut", 1], - ["👩🚀", "woman_astronaut", 1], - ["👨🚀", "man_astronaut", 1], - ["🧑⚖️", "judge", 1], - ["👩⚖️", "woman_judge", 1], - ["👨⚖️", "man_judge", 1], - ["🦸♀️", "woman_superhero", 1], - ["🦸♂️", "man_superhero", 1], - ["🦹♀️", "woman_supervillain", 1], - ["🦹♂️", "man_supervillain", 1], - ["🤶", "mrs_claus", 1], - ["🧑🎄", "mx_claus", 1], - ["🎅", "santa", 1], - ["🥷", "ninja", 1], - ["🧙♀️", "sorceress", 1], - ["🧙♂️", "wizard", 1], - ["🧝♀️", "woman_elf", 1], - ["🧝♂️", "man_elf", 1], - ["🧛♀️", "woman_vampire", 1], - ["🧛♂️", "man_vampire", 1], - ["🧟♀️", "woman_zombie", 1], - ["🧟♂️", "man_zombie", 1], - ["🧞♀️", "woman_genie", 1], - ["🧞♂️", "man_genie", 1], - ["🧜♀️", "mermaid", 1], - ["🧜♂️", "merman", 1], - ["🧚♀️", "woman_fairy", 1], - ["🧚♂️", "man_fairy", 1], - ["👼", "angel", 1], - ["🧌", "troll", 1], - ["🤰", "pregnant_woman", 1], - ["🫃", "pregnant_man", 1], - ["🫄", "pregnant_person", 1], - ["🫅", "person_with_crown", 1], - ["🤱", "breastfeeding", 1], - ["👩🍼", "woman_feeding_baby", 1], - ["👨🍼", "man_feeding_baby", 1], - ["🧑🍼", "person_feeding_baby", 1], - ["👸", "princess", 1], - ["🤴", "prince", 1], - ["👰", "person_with_veil", 1], - ["👰", "bride_with_veil", 1], - ["🤵", "person_in_tuxedo", 1], - ["🤵", "man_in_tuxedo", 1], - ["🏃♀️", "running_woman", 1], - ["🏃", "running_man", 1], - ["🚶♀️", "walking_woman", 1], - ["🚶", "walking_man", 1], - ["💃", "dancer", 1], - ["🕺", "man_dancing", 1], - ["👯", "dancing_women", 1], - ["👯♂️", "dancing_men", 1], - ["👫", "couple", 1], - ["🧑🤝🧑", "people_holding_hands", 1], - ["👬", "two_men_holding_hands", 1], - ["👭", "two_women_holding_hands", 1], - ["🫂", "people_hugging", 1], - ["🙇♀️", "bowing_woman", 1], - ["🙇", "bowing_man", 1], - ["🤦♂️", "man_facepalming", 1], - ["🤦♀️", "woman_facepalming", 1], - ["🤷", "woman_shrugging", 1], - ["🤷♂️", "man_shrugging", 1], - ["💁", "tipping_hand_woman", 1], - ["💁♂️", "tipping_hand_man", 1], - ["🙅", "no_good_woman", 1], - ["🙅♂️", "no_good_man", 1], - ["🙆", "ok_woman", 1], - ["🙆♂️", "ok_man", 1], - ["🙋", "raising_hand_woman", 1], - ["🙋♂️", "raising_hand_man", 1], - ["🙎", "pouting_woman", 1], - ["🙎♂️", "pouting_man", 1], - ["🙍", "frowning_woman", 1], - ["🙍♂️", "frowning_man", 1], - ["💇", "haircut_woman", 1], - ["💇♂️", "haircut_man", 1], - ["💆", "massage_woman", 1], - ["💆♂️", "massage_man", 1], - ["🧖♀️", "woman_in_steamy_room", 1], - ["🧖♂️", "man_in_steamy_room", 1], - ["🧏♀️", "woman_deaf", 1], - ["🧏♂️", "man_deaf", 1], - ["🧍♀️", "woman_standing", 1], - ["🧍♂️", "man_standing", 1], - ["🧎♀️", "woman_kneeling", 1], - ["🧎♂️", "man_kneeling", 1], - ["🧑🦯", "person_with_probing_cane", 1], - ["👩🦯", "woman_with_probing_cane", 1], - ["👨🦯", "man_with_probing_cane", 1], - ["🧑🦼", "person_in_motorized_wheelchair", 1], - ["👩🦼", "woman_in_motorized_wheelchair", 1], - ["👨🦼", "man_in_motorized_wheelchair", 1], - ["🧑🦽", "person_in_manual_wheelchair", 1], - ["👩🦽", "woman_in_manual_wheelchair", 1], - ["👨🦽", "man_in_manual_wheelchair", 1], - ["💑", "couple_with_heart_woman_man", 1], - ["👩❤️👩", "couple_with_heart_woman_woman", 1], - ["👨❤️👨", "couple_with_heart_man_man", 1], - ["💏", "couplekiss_man_woman", 1], - ["👩❤️💋👩", "couplekiss_woman_woman", 1], - ["👨❤️💋👨", "couplekiss_man_man", 1], - ["👪", "family_man_woman_boy", 1], - ["👨👩👧", "family_man_woman_girl", 1], - ["👨👩👧👦", "family_man_woman_girl_boy", 1], - ["👨👩👦👦", "family_man_woman_boy_boy", 1], - ["👨👩👧👧", "family_man_woman_girl_girl", 1], - ["👩👩👦", "family_woman_woman_boy", 1], - ["👩👩👧", "family_woman_woman_girl", 1], - ["👩👩👧👦", "family_woman_woman_girl_boy", 1], - ["👩👩👦👦", "family_woman_woman_boy_boy", 1], - ["👩👩👧👧", "family_woman_woman_girl_girl", 1], - ["👨👨👦", "family_man_man_boy", 1], - ["👨👨👧", "family_man_man_girl", 1], - ["👨👨👧👦", "family_man_man_girl_boy", 1], - ["👨👨👦👦", "family_man_man_boy_boy", 1], - ["👨👨👧👧", "family_man_man_girl_girl", 1], - ["👩👦", "family_woman_boy", 1], - ["👩👧", "family_woman_girl", 1], - ["👩👧👦", "family_woman_girl_boy", 1], - ["👩👦👦", "family_woman_boy_boy", 1], - ["👩👧👧", "family_woman_girl_girl", 1], - ["👨👦", "family_man_boy", 1], - ["👨👧", "family_man_girl", 1], - ["👨👧👦", "family_man_girl_boy", 1], - ["👨👦👦", "family_man_boy_boy", 1], - ["👨👧👧", "family_man_girl_girl", 1], - ["🧶", "yarn", 1], - ["🧵", "thread", 1], - ["🧥", "coat", 1], - ["🥼", "labcoat", 1], - ["👚", "womans_clothes", 1], - ["👕", "tshirt", 1], - ["👖", "jeans", 1], - ["👔", "necktie", 1], - ["👗", "dress", 1], - ["👙", "bikini", 1], - ["🩱", "one_piece_swimsuit", 1], - ["👘", "kimono", 1], - ["🥻", "sari", 1], - ["🩲", "briefs", 1], - ["🩳", "shorts", 1], - ["💄", "lipstick", 1], - ["💋", "kiss", 1], - ["👣", "footprints", 1], - ["🥿", "flat_shoe", 1], - ["👠", "high_heel", 1], - ["👡", "sandal", 1], - ["👢", "boot", 1], - ["👞", "mans_shoe", 1], - ["👟", "athletic_shoe", 1], - ["🩴", "thong_sandal", 1], - ["🩰", "ballet_shoes", 1], - ["🧦", "socks", 1], - ["🧤", "gloves", 1], - ["🧣", "scarf", 1], - ["👒", "womans_hat", 1], - ["🎩", "tophat", 1], - ["🧢", "billed_hat", 1], - ["⛑", "rescue_worker_helmet", 1], - ["🪖", "military_helmet", 1], - ["🎓", "mortar_board", 1], - ["👑", "crown", 1], - ["🎒", "school_satchel", 1], - ["🧳", "luggage", 1], - ["👝", "pouch", 1], - ["👛", "purse", 1], - ["👜", "handbag", 1], - ["💼", "briefcase", 1], - ["👓", "eyeglasses", 1], - ["🕶", "dark_sunglasses", 1], - ["🥽", "goggles", 1], - ["💍", "ring", 1], - ["🌂", "closed_umbrella", 1], - ["🐶", "dog", 2], - ["🐱", "cat", 2], - ["🐈⬛", "black_cat", 2], - ["🐭", "mouse", 2], - ["🐹", "hamster", 2], - ["🐰", "rabbit", 2], - ["🦊", "fox_face", 2], - ["🐻", "bear", 2], - ["🐼", "panda_face", 2], - ["🐨", "koala", 2], - ["🐯", "tiger", 2], - ["🦁", "lion", 2], - ["🐮", "cow", 2], - ["🐷", "pig", 2], - ["🐽", "pig_nose", 2], - ["🐸", "frog", 2], - ["🦑", "squid", 2], - ["🐙", "octopus", 2], - ["🪼", "jellyfish", 2], - ["🦐", "shrimp", 2], - ["🐵", "monkey_face", 2], - ["🦍", "gorilla", 2], - ["🙈", "see_no_evil", 2], - ["🙉", "hear_no_evil", 2], - ["🙊", "speak_no_evil", 2], - ["🐒", "monkey", 2], - ["🐔", "chicken", 2], - ["🐧", "penguin", 2], - ["🐦", "bird", 2], - ["🐤", "baby_chick", 2], - ["🐣", "hatching_chick", 2], - ["🐥", "hatched_chick", 2], - ["🪿", "goose", 2], - ["🦆", "duck", 2], - ["🐦⬛", "black_bird", 2], - ["🦅", "eagle", 2], - ["🦉", "owl", 2], - ["🦇", "bat", 2], - ["🐺", "wolf", 2], - ["🐗", "boar", 2], - ["🐴", "horse", 2], - ["🦄", "unicorn", 2], - ["🫎", "moose", 2], - ["🐝", "honeybee", 2], - ["🐛", "bug", 2], - ["🦋", "butterfly", 2], - ["🐌", "snail", 2], - ["🐞", "lady_beetle", 2], - ["🐜", "ant", 2], - ["🦗", "grasshopper", 2], - ["🕷", "spider", 2], - ["🪲", "beetle", 2], - ["🪳", "cockroach", 2], - ["🪰", "fly", 2], - ["🪱", "worm", 2], - ["🦂", "scorpion", 2], - ["🦀", "crab", 2], - ["🐍", "snake", 2], - ["🦎", "lizard", 2], - ["🦖", "t-rex", 2], - ["🦕", "sauropod", 2], - ["🐢", "turtle", 2], - ["🐠", "tropical_fish", 2], - ["🐟", "fish", 2], - ["🐡", "blowfish", 2], - ["🐬", "dolphin", 2], - ["🦈", "shark", 2], - ["🐳", "whale", 2], - ["🐋", "whale2", 2], - ["🐊", "crocodile", 2], - ["🐆", "leopard", 2], - ["🦓", "zebra", 2], - ["🐅", "tiger2", 2], - ["🐃", "water_buffalo", 2], - ["🐂", "ox", 2], - ["🐄", "cow2", 2], - ["🦌", "deer", 2], - ["🐪", "dromedary_camel", 2], - ["🐫", "camel", 2], - ["🦒", "giraffe", 2], - ["🐘", "elephant", 2], - ["🦏", "rhinoceros", 2], - ["🐐", "goat", 2], - ["🐏", "ram", 2], - ["🐑", "sheep", 2], - ["🫏", "donkey", 2], - ["🐎", "racehorse", 2], - ["🐖", "pig2", 2], - ["🐀", "rat", 2], - ["🐁", "mouse2", 2], - ["🐓", "rooster", 2], - ["🦃", "turkey", 2], - ["🕊", "dove", 2], - ["🐕", "dog2", 2], - ["🐩", "poodle", 2], - ["🐈", "cat2", 2], - ["🐇", "rabbit2", 2], - ["🐿", "chipmunk", 2], - ["🦔", "hedgehog", 2], - ["🦝", "raccoon", 2], - ["🦙", "llama", 2], - ["🦛", "hippopotamus", 2], - ["🦘", "kangaroo", 2], - ["🦡", "badger", 2], - ["🦢", "swan", 2], - ["🦚", "peacock", 2], - ["🦜", "parrot", 2], - ["🦞", "lobster", 2], - ["🦠", "microbe", 2], - ["🦟", "mosquito", 2], - ["🦬", "bison", 2], - ["🦣", "mammoth", 2], - ["🦫", "beaver", 2], - ["🐻❄️", "polar_bear", 2], - ["🦤", "dodo", 2], - ["🪶", "feather", 2], - ["🪽", "wing", 2], - ["🦭", "seal", 2], - ["🐾", "paw_prints", 2], - ["🐉", "dragon", 2], - ["🐲", "dragon_face", 2], - ["🦧", "orangutan", 2], - ["🦮", "guide_dog", 2], - ["🐕🦺", "service_dog", 2], - ["🦥", "sloth", 2], - ["🦦", "otter", 2], - ["🦨", "skunk", 2], - ["🦩", "flamingo", 2], - ["🌵", "cactus", 2], - ["🎄", "christmas_tree", 2], - ["🌲", "evergreen_tree", 2], - ["🌳", "deciduous_tree", 2], - ["🌴", "palm_tree", 2], - ["🌱", "seedling", 2], - ["🌿", "herb", 2], - ["☘", "shamrock", 2], - ["🍀", "four_leaf_clover", 2], - ["🎍", "bamboo", 2], - ["🎋", "tanabata_tree", 2], - ["🍃", "leaves", 2], - ["🍂", "fallen_leaf", 2], - ["🍁", "maple_leaf", 2], - ["🌾", "ear_of_rice", 2], - ["🌺", "hibiscus", 2], - ["🌻", "sunflower", 2], - ["🌹", "rose", 2], - ["🥀", "wilted_flower", 2], - ["🪻", "hyacinth", 2], - ["🌷", "tulip", 2], - ["🌼", "blossom", 2], - ["🌸", "cherry_blossom", 2], - ["💐", "bouquet", 2], - ["🍄", "mushroom", 2], - ["🪴", "potted_plant", 2], - ["🌰", "chestnut", 2], - ["🎃", "jack_o_lantern", 2], - ["🐚", "shell", 2], - ["🕸", "spider_web", 2], - ["🌎", "earth_americas", 2], - ["🌍", "earth_africa", 2], - ["🌏", "earth_asia", 2], - ["🪐", "ringed_planet", 2], - ["🌕", "full_moon", 2], - ["🌖", "waning_gibbous_moon", 2], - ["🌗", "last_quarter_moon", 2], - ["🌘", "waning_crescent_moon", 2], - ["🌑", "new_moon", 2], - ["🌒", "waxing_crescent_moon", 2], - ["🌓", "first_quarter_moon", 2], - ["🌔", "waxing_gibbous_moon", 2], - ["🌚", "new_moon_with_face", 2], - ["🌝", "full_moon_with_face", 2], - ["🌛", "first_quarter_moon_with_face", 2], - ["🌜", "last_quarter_moon_with_face", 2], - ["🌞", "sun_with_face", 2], - ["🌙", "crescent_moon", 2], - ["⭐", "star", 2], - ["🌟", "star2", 2], - ["💫", "dizzy", 2], - ["✨", "sparkles", 2], - ["☄", "comet", 2], - ["☀️", "sunny", 2], - ["🌤", "sun_behind_small_cloud", 2], - ["⛅", "partly_sunny", 2], - ["🌥", "sun_behind_large_cloud", 2], - ["🌦", "sun_behind_rain_cloud", 2], - ["☁️", "cloud", 2], - ["🌧", "cloud_with_rain", 2], - ["⛈", "cloud_with_lightning_and_rain", 2], - ["🌩", "cloud_with_lightning", 2], - ["⚡", "zap", 2], - ["🔥", "fire", 2], - ["💥", "boom", 2], - ["❄️", "snowflake", 2], - ["🌨", "cloud_with_snow", 2], - ["⛄", "snowman", 2], - ["☃", "snowman_with_snow", 2], - ["🌬", "wind_face", 2], - ["💨", "dash", 2], - ["🌪", "tornado", 2], - ["🌫", "fog", 2], - ["☂", "open_umbrella", 2], - ["☔", "umbrella", 2], - ["💧", "droplet", 2], - ["💦", "sweat_drops", 2], - ["🌊", "ocean", 2], - ["🪷", "lotus", 2], - ["🪸", "coral", 2], - ["🪹", "empty_nest", 2], - ["🪺", "nest_with_eggs", 2], - ["🍏", "green_apple", 3], - ["🍎", "apple", 3], - ["🍐", "pear", 3], - ["🍊", "tangerine", 3], - ["🍋", "lemon", 3], - ["🍌", "banana", 3], - ["🍉", "watermelon", 3], - ["🍇", "grapes", 3], - ["🍓", "strawberry", 3], - ["🍈", "melon", 3], - ["🍒", "cherries", 3], - ["🍑", "peach", 3], - ["🍍", "pineapple", 3], - ["🥥", "coconut", 3], - ["🥝", "kiwi_fruit", 3], - ["🥭", "mango", 3], - ["🥑", "avocado", 3], - ["🫛", "pea_pod", 3], - ["🥦", "broccoli", 3], - ["🍅", "tomato", 3], - ["🍆", "eggplant", 3], - ["🥒", "cucumber", 3], - ["🫐", "blueberries", 3], - ["🫒", "olive", 3], - ["🫑", "bell_pepper", 3], - ["🥕", "carrot", 3], - ["🌶", "hot_pepper", 3], - ["🥔", "potato", 3], - ["🌽", "corn", 3], - ["🥬", "leafy_greens", 3], - ["🍠", "sweet_potato", 3], - ["🫚", "ginger_root", 3], - ["🥜", "peanuts", 3], - ["🧄", "garlic", 3], - ["🧅", "onion", 3], - ["🍯", "honey_pot", 3], - ["🥐", "croissant", 3], - ["🍞", "bread", 3], - ["🥖", "baguette_bread", 3], - ["🥯", "bagel", 3], - ["🥨", "pretzel", 3], - ["🧀", "cheese", 3], - ["🥚", "egg", 3], - ["🥓", "bacon", 3], - ["🥩", "steak", 3], - ["🥞", "pancakes", 3], - ["🍗", "poultry_leg", 3], - ["🍖", "meat_on_bone", 3], - ["🦴", "bone", 3], - ["🍤", "fried_shrimp", 3], - ["🍳", "fried_egg", 3], - ["🍔", "hamburger", 3], - ["🍟", "fries", 3], - ["🥙", "stuffed_flatbread", 3], - ["🌭", "hotdog", 3], - ["🍕", "pizza", 3], - ["🥪", "sandwich", 3], - ["🥫", "canned_food", 3], - ["🍝", "spaghetti", 3], - ["🌮", "taco", 3], - ["🌯", "burrito", 3], - ["🥗", "green_salad", 3], - ["🥘", "shallow_pan_of_food", 3], - ["🍜", "ramen", 3], - ["🍲", "stew", 3], - ["🍥", "fish_cake", 3], - ["🥠", "fortune_cookie", 3], - ["🍣", "sushi", 3], - ["🍱", "bento", 3], - ["🍛", "curry", 3], - ["🍙", "rice_ball", 3], - ["🍚", "rice", 3], - ["🍘", "rice_cracker", 3], - ["🍢", "oden", 3], - ["🍡", "dango", 3], - ["🍧", "shaved_ice", 3], - ["🍨", "ice_cream", 3], - ["🍦", "icecream", 3], - ["🥧", "pie", 3], - ["🍰", "cake", 3], - ["🧁", "cupcake", 3], - ["🥮", "moon_cake", 3], - ["🎂", "birthday", 3], - ["🍮", "custard", 3], - ["🍬", "candy", 3], - ["🍭", "lollipop", 3], - ["🍫", "chocolate_bar", 3], - ["🍿", "popcorn", 3], - ["🥟", "dumpling", 3], - ["🍩", "doughnut", 3], - ["🍪", "cookie", 3], - ["🧇", "waffle", 3], - ["🧆", "falafel", 3], - ["🧈", "butter", 3], - ["🦪", "oyster", 3], - ["🫓", "flatbread", 3], - ["🫔", "tamale", 3], - ["🫕", "fondue", 3], - ["🥛", "milk_glass", 3], - ["🍺", "beer", 3], - ["🍻", "beers", 3], - ["🥂", "clinking_glasses", 3], - ["🍷", "wine_glass", 3], - ["🥃", "tumbler_glass", 3], - ["🍸", "cocktail", 3], - ["🍹", "tropical_drink", 3], - ["🍾", "champagne", 3], - ["🍶", "sake", 3], - ["🍵", "tea", 3], - ["🥤", "cup_with_straw", 3], - ["☕", "coffee", 3], - ["🫖", "teapot", 3], - ["🧋", "bubble_tea", 3], - ["🍼", "baby_bottle", 3], - ["🧃", "beverage_box", 3], - ["🧉", "mate", 3], - ["🧊", "ice_cube", 3], - ["🧂", "salt", 3], - ["🥄", "spoon", 3], - ["🍴", "fork_and_knife", 3], - ["🍽", "plate_with_cutlery", 3], - ["🥣", "bowl_with_spoon", 3], - ["🥡", "takeout_box", 3], - ["🥢", "chopsticks", 3], - ["🫗", "pouring_liquid", 3], - ["🫘", "beans", 3], - ["🫙", "jar", 3], - ["⚽", "soccer", 4], - ["🏀", "basketball", 4], - ["🏈", "football", 4], - ["⚾", "baseball", 4], - ["🥎", "softball", 4], - ["🎾", "tennis", 4], - ["🏐", "volleyball", 4], - ["🏉", "rugby_football", 4], - ["🥏", "flying_disc", 4], - ["🎱", "8ball", 4], - ["⛳", "golf", 4], - ["🏌️♀️", "golfing_woman", 4], - ["🏌", "golfing_man", 4], - ["🏓", "ping_pong", 4], - ["🏸", "badminton", 4], - ["🥅", "goal_net", 4], - ["🏒", "ice_hockey", 4], - ["🏑", "field_hockey", 4], - ["🥍", "lacrosse", 4], - ["🏏", "cricket", 4], - ["🎿", "ski", 4], - ["⛷", "skier", 4], - ["🏂", "snowboarder", 4], - ["🤺", "person_fencing", 4], - ["🤼♀️", "women_wrestling", 4], - ["🤼♂️", "men_wrestling", 4], - ["🤸♀️", "woman_cartwheeling", 4], - ["🤸♂️", "man_cartwheeling", 4], - ["🤾♀️", "woman_playing_handball", 4], - ["🤾♂️", "man_playing_handball", 4], - ["⛸", "ice_skate", 4], - ["🥌", "curling_stone", 4], - ["🛹", "skateboard", 4], - ["🛷", "sled", 4], - ["🏹", "bow_and_arrow", 4], - ["🎣", "fishing_pole_and_fish", 4], - ["🥊", "boxing_glove", 4], - ["🥋", "martial_arts_uniform", 4], - ["🚣♀️", "rowing_woman", 4], - ["🚣", "rowing_man", 4], - ["🧗♀️", "climbing_woman", 4], - ["🧗♂️", "climbing_man", 4], - ["🏊♀️", "swimming_woman", 4], - ["🏊", "swimming_man", 4], - ["🤽♀️", "woman_playing_water_polo", 4], - ["🤽♂️", "man_playing_water_polo", 4], - ["🧘♀️", "woman_in_lotus_position", 4], - ["🧘♂️", "man_in_lotus_position", 4], - ["🏄♀️", "surfing_woman", 4], - ["🏄", "surfing_man", 4], - ["🛀", "bath", 4], - ["⛹️♀️", "basketball_woman", 4], - ["⛹", "basketball_man", 4], - ["🏋️♀️", "weight_lifting_woman", 4], - ["🏋", "weight_lifting_man", 4], - ["🚴♀️", "biking_woman", 4], - ["🚴", "biking_man", 4], - ["🚵♀️", "mountain_biking_woman", 4], - ["🚵", "mountain_biking_man", 4], - ["🏇", "horse_racing", 4], - ["🤿", "diving_mask", 4], - ["🪀", "yo_yo", 4], - ["🪁", "kite", 4], - ["🦺", "safety_vest", 4], - ["🪡", "sewing_needle", 4], - ["🪢", "knot", 4], - ["🕴", "business_suit_levitating", 4], - ["🏆", "trophy", 4], - ["🎽", "running_shirt_with_sash", 4], - ["🏅", "medal_sports", 4], - ["🎖", "medal_military", 4], - ["🥇", "1st_place_medal", 4], - ["🥈", "2nd_place_medal", 4], - ["🥉", "3rd_place_medal", 4], - ["🎗", "reminder_ribbon", 4], - ["🏵", "rosette", 4], - ["🎫", "ticket", 4], - ["🎟", "tickets", 4], - ["🎭", "performing_arts", 4], - ["🎨", "art", 4], - ["🎪", "circus_tent", 4], - ["🤹♀️", "woman_juggling", 4], - ["🤹♂️", "man_juggling", 4], - ["🎤", "microphone", 4], - ["🎧", "headphones", 4], - ["🎼", "musical_score", 4], - ["🎹", "musical_keyboard", 4], - ["🪇", "maracas", 4], - ["🥁", "drum", 4], - ["🎷", "saxophone", 4], - ["🎺", "trumpet", 4], - ["🪈", "flute", 4], - ["🎸", "guitar", 4], - ["🎻", "violin", 4], - ["🪕", "banjo", 4], - ["🪗", "accordion", 4], - ["🪘", "long_drum", 4], - ["🎬", "clapper", 4], - ["🎮", "video_game", 4], - ["👾", "space_invader", 4], - ["🎯", "dart", 4], - ["🎲", "game_die", 4], - ["♟️", "chess_pawn", 4], - ["🎰", "slot_machine", 4], - ["🧩", "jigsaw", 4], - ["🎳", "bowling", 4], - ["🪄", "magic_wand", 4], - ["🪅", "pinata", 4], - ["🪆", "nesting_dolls", 4], - ["🪬", "hamsa", 4], - ["🪩", "mirror_ball", 4], - ["🚗", "red_car", 5], - ["🚕", "taxi", 5], - ["🚙", "blue_car", 5], - ["🚌", "bus", 5], - ["🚎", "trolleybus", 5], - ["🏎", "racing_car", 5], - ["🚓", "police_car", 5], - ["🚑", "ambulance", 5], - ["🚒", "fire_engine", 5], - ["🚐", "minibus", 5], - ["🚚", "truck", 5], - ["🚛", "articulated_lorry", 5], - ["🚜", "tractor", 5], - ["🛴", "kick_scooter", 5], - ["🏍", "motorcycle", 5], - ["🚲", "bike", 5], - ["🛵", "motor_scooter", 5], - ["🦽", "manual_wheelchair", 5], - ["🦼", "motorized_wheelchair", 5], - ["🛺", "auto_rickshaw", 5], - ["🪂", "parachute", 5], - ["🚨", "rotating_light", 5], - ["🚔", "oncoming_police_car", 5], - ["🚍", "oncoming_bus", 5], - ["🚘", "oncoming_automobile", 5], - ["🚖", "oncoming_taxi", 5], - ["🚡", "aerial_tramway", 5], - ["🚠", "mountain_cableway", 5], - ["🚟", "suspension_railway", 5], - ["🚃", "railway_car", 5], - ["🚋", "train", 5], - ["🚝", "monorail", 5], - ["🚄", "bullettrain_side", 5], - ["🚅", "bullettrain_front", 5], - ["🚈", "light_rail", 5], - ["🚞", "mountain_railway", 5], - ["🚂", "steam_locomotive", 5], - ["🚆", "train2", 5], - ["🚇", "metro", 5], - ["🚊", "tram", 5], - ["🚉", "station", 5], - ["🛸", "flying_saucer", 5], - ["🚁", "helicopter", 5], - ["🛩", "small_airplane", 5], - ["✈️", "airplane", 5], - ["🛫", "flight_departure", 5], - ["🛬", "flight_arrival", 5], - ["⛵", "sailboat", 5], - ["🛥", "motor_boat", 5], - ["🚤", "speedboat", 5], - ["⛴", "ferry", 5], - ["🛳", "passenger_ship", 5], - ["🚀", "rocket", 5], - ["🛰", "artificial_satellite", 5], - ["🛻", "pickup_truck", 5], - ["🛼", "roller_skate", 5], - ["💺", "seat", 5], - ["🛶", "canoe", 5], - ["⚓", "anchor", 5], - ["🚧", "construction", 5], - ["⛽", "fuelpump", 5], - ["🚏", "busstop", 5], - ["🚦", "vertical_traffic_light", 5], - ["🚥", "traffic_light", 5], - ["🏁", "checkered_flag", 5], - ["🚢", "ship", 5], - ["🎡", "ferris_wheel", 5], - ["🎢", "roller_coaster", 5], - ["🎠", "carousel_horse", 5], - ["🏗", "building_construction", 5], - ["🌁", "foggy", 5], - ["🏭", "factory", 5], - ["⛲", "fountain", 5], - ["🎑", "rice_scene", 5], - ["⛰", "mountain", 5], - ["🏔", "mountain_snow", 5], - ["🗻", "mount_fuji", 5], - ["🌋", "volcano", 5], - ["🗾", "japan", 5], - ["🏕", "camping", 5], - ["⛺", "tent", 5], - ["🏞", "national_park", 5], - ["🛣", "motorway", 5], - ["🛤", "railway_track", 5], - ["🌅", "sunrise", 5], - ["🌄", "sunrise_over_mountains", 5], - ["🏜", "desert", 5], - ["🏖", "beach_umbrella", 5], - ["🏝", "desert_island", 5], - ["🌇", "city_sunrise", 5], - ["🌆", "city_sunset", 5], - ["🏙", "cityscape", 5], - ["🌃", "night_with_stars", 5], - ["🌉", "bridge_at_night", 5], - ["🌌", "milky_way", 5], - ["🌠", "stars", 5], - ["🎇", "sparkler", 5], - ["🎆", "fireworks", 5], - ["🌈", "rainbow", 5], - ["🏘", "houses", 5], - ["🏰", "european_castle", 5], - ["🏯", "japanese_castle", 5], - ["🗼", "tokyo_tower", 5], - ["", "shibuya_109", 5], - ["🏟", "stadium", 5], - ["🗽", "statue_of_liberty", 5], - ["🏠", "house", 5], - ["🏡", "house_with_garden", 5], - ["🏚", "derelict_house", 5], - ["🏢", "office", 5], - ["🏬", "department_store", 5], - ["🏣", "post_office", 5], - ["🏤", "european_post_office", 5], - ["🏥", "hospital", 5], - ["🏦", "bank", 5], - ["🏨", "hotel", 5], - ["🏪", "convenience_store", 5], - ["🏫", "school", 5], - ["🏩", "love_hotel", 5], - ["💒", "wedding", 5], - ["🏛", "classical_building", 5], - ["⛪", "church", 5], - ["🕌", "mosque", 5], - ["🕍", "synagogue", 5], - ["🕋", "kaaba", 5], - ["⛩", "shinto_shrine", 5], - ["🛕", "hindu_temple", 5], - ["🪨", "rock", 5], - ["🪵", "wood", 5], - ["🛖", "hut", 5], - ["🛝", "playground_slide", 5], - ["🛞", "wheel", 5], - ["🛟", "ring_buoy", 5], - ["⌚", "watch", 6], - ["📱", "iphone", 6], - ["📲", "calling", 6], - ["💻", "computer", 6], - ["⌨", "keyboard", 6], - ["🖥", "desktop_computer", 6], - ["🖨", "printer", 6], - ["🖱", "computer_mouse", 6], - ["🖲", "trackball", 6], - ["🕹", "joystick", 6], - ["🗜", "clamp", 6], - ["💽", "minidisc", 6], - ["💾", "floppy_disk", 6], - ["💿", "cd", 6], - ["📀", "dvd", 6], - ["📼", "vhs", 6], - ["📷", "camera", 6], - ["📸", "camera_flash", 6], - ["📹", "video_camera", 6], - ["🎥", "movie_camera", 6], - ["📽", "film_projector", 6], - ["🎞", "film_strip", 6], - ["📞", "telephone_receiver", 6], - ["☎️", "phone", 6], - ["📟", "pager", 6], - ["📠", "fax", 6], - ["📺", "tv", 6], - ["📻", "radio", 6], - ["🎙", "studio_microphone", 6], - ["🎚", "level_slider", 6], - ["🎛", "control_knobs", 6], - ["🧭", "compass", 6], - ["⏱", "stopwatch", 6], - ["⏲", "timer_clock", 6], - ["⏰", "alarm_clock", 6], - ["🕰", "mantelpiece_clock", 6], - ["⏳", "hourglass_flowing_sand", 6], - ["⌛", "hourglass", 6], - ["📡", "satellite", 6], - ["🔋", "battery", 6], - ["🪫", "low_battery", 6], - ["🔌", "electric_plug", 6], - ["💡", "bulb", 6], - ["🔦", "flashlight", 6], - ["🕯", "candle", 6], - ["🧯", "fire_extinguisher", 6], - ["🗑", "wastebasket", 6], - ["🛢", "oil_drum", 6], - ["💸", "money_with_wings", 6], - ["💵", "dollar", 6], - ["💴", "yen", 6], - ["💶", "euro", 6], - ["💷", "pound", 6], - ["💰", "moneybag", 6], - ["🪙", "coin", 6], - ["💳", "credit_card", 6], - ["🪪", "identification_card", 6], - ["💎", "gem", 6], - ["⚖", "balance_scale", 6], - ["🧰", "toolbox", 6], - ["🔧", "wrench", 6], - ["🔨", "hammer", 6], - ["⚒", "hammer_and_pick", 6], - ["🛠", "hammer_and_wrench", 6], - ["⛏", "pick", 6], - ["🪓", "axe", 6], - ["🦯", "probing_cane", 6], - ["🔩", "nut_and_bolt", 6], - ["⚙", "gear", 6], - ["🪃", "boomerang", 6], - ["🪚", "carpentry_saw", 6], - ["🪛", "screwdriver", 6], - ["🪝", "hook", 6], - ["🪜", "ladder", 6], - ["🧱", "brick", 6], - ["⛓", "chains", 6], - ["🧲", "magnet", 6], - ["🔫", "gun", 6], - ["💣", "bomb", 6], - ["🧨", "firecracker", 6], - ["🔪", "hocho", 6], - ["🗡", "dagger", 6], - ["⚔", "crossed_swords", 6], - ["🛡", "shield", 6], - ["🚬", "smoking", 6], - ["☠", "skull_and_crossbones", 6], - ["⚰", "coffin", 6], - ["⚱", "funeral_urn", 6], - ["🏺", "amphora", 6], - ["🔮", "crystal_ball", 6], - ["📿", "prayer_beads", 6], - ["🧿", "nazar_amulet", 6], - ["💈", "barber", 6], - ["⚗", "alembic", 6], - ["🔭", "telescope", 6], - ["🔬", "microscope", 6], - ["🕳", "hole", 6], - ["💊", "pill", 6], - ["💉", "syringe", 6], - ["🩸", "drop_of_blood", 6], - ["🩹", "adhesive_bandage", 6], - ["🩺", "stethoscope", 6], - ["🪒", "razor", 6], - ["🪮", "hair_pick", 6], - ["🩻", "xray", 6], - ["🩼", "crutch", 6], - ["🧬", "dna", 6], - ["🧫", "petri_dish", 6], - ["🧪", "test_tube", 6], - ["🌡", "thermometer", 6], - ["🧹", "broom", 6], - ["🧺", "basket", 6], - ["🧻", "toilet_paper", 6], - ["🏷", "label", 6], - ["🔖", "bookmark", 6], - ["🚽", "toilet", 6], - ["🚿", "shower", 6], - ["🛁", "bathtub", 6], - ["🧼", "soap", 6], - ["🧽", "sponge", 6], - ["🧴", "lotion_bottle", 6], - ["🔑", "key", 6], - ["🗝", "old_key", 6], - ["🛋", "couch_and_lamp", 6], - ["🪔", "diya_Lamp", 6], - ["🛌", "sleeping_bed", 6], - ["🛏", "bed", 6], - ["🚪", "door", 6], - ["🪑", "chair", 6], - ["🛎", "bellhop_bell", 6], - ["🧸", "teddy_bear", 6], - ["🖼", "framed_picture", 6], - ["🗺", "world_map", 6], - ["🛗", "elevator", 6], - ["🪞", "mirror", 6], - ["🪟", "window", 6], - ["🪠", "plunger", 6], - ["🪤", "mouse_trap", 6], - ["🪣", "bucket", 6], - ["🪥", "toothbrush", 6], - ["🫧", "bubbles", 6], - ["⛱", "parasol_on_ground", 6], - ["🗿", "moyai", 6], - ["🛍", "shopping", 6], - ["🛒", "shopping_cart", 6], - ["🎈", "balloon", 6], - ["🎏", "flags", 6], - ["🎀", "ribbon", 6], - ["🎁", "gift", 6], - ["🎊", "confetti_ball", 6], - ["🎉", "tada", 6], - ["🎎", "dolls", 6], - ["🪭", "folding_hand_fan", 6], - ["🎐", "wind_chime", 6], - ["🎌", "crossed_flags", 6], - ["🏮", "izakaya_lantern", 6], - ["🧧", "red_envelope", 6], - ["✉️", "email", 6], - ["📩", "envelope_with_arrow", 6], - ["📨", "incoming_envelope", 6], - ["📧", "e-mail", 6], - ["💌", "love_letter", 6], - ["📮", "postbox", 6], - ["📪", "mailbox_closed", 6], - ["📫", "mailbox", 6], - ["📬", "mailbox_with_mail", 6], - ["📭", "mailbox_with_no_mail", 6], - ["📦", "package", 6], - ["📯", "postal_horn", 6], - ["📥", "inbox_tray", 6], - ["📤", "outbox_tray", 6], - ["📜", "scroll", 6], - ["📃", "page_with_curl", 6], - ["📑", "bookmark_tabs", 6], - ["🧾", "receipt", 6], - ["📊", "bar_chart", 6], - ["📈", "chart_with_upwards_trend", 6], - ["📉", "chart_with_downwards_trend", 6], - ["📄", "page_facing_up", 6], - ["📅", "date", 6], - ["📆", "calendar", 6], - ["🗓", "spiral_calendar", 6], - ["📇", "card_index", 6], - ["🗃", "card_file_box", 6], - ["🗳", "ballot_box", 6], - ["🗄", "file_cabinet", 6], - ["📋", "clipboard", 6], - ["🗒", "spiral_notepad", 6], - ["📁", "file_folder", 6], - ["📂", "open_file_folder", 6], - ["🗂", "card_index_dividers", 6], - ["🗞", "newspaper_roll", 6], - ["📰", "newspaper", 6], - ["📓", "notebook", 6], - ["📕", "closed_book", 6], - ["📗", "green_book", 6], - ["📘", "blue_book", 6], - ["📙", "orange_book", 6], - ["📔", "notebook_with_decorative_cover", 6], - ["📒", "ledger", 6], - ["📚", "books", 6], - ["📖", "open_book", 6], - ["🧷", "safety_pin", 6], - ["🔗", "link", 6], - ["📎", "paperclip", 6], - ["🖇", "paperclips", 6], - ["✂️", "scissors", 6], - ["📐", "triangular_ruler", 6], - ["📏", "straight_ruler", 6], - ["🧮", "abacus", 6], - ["📌", "pushpin", 6], - ["📍", "round_pushpin", 6], - ["🚩", "triangular_flag_on_post", 6], - ["🏳", "white_flag", 6], - ["🏴", "black_flag", 6], - ["🏳️🌈", "rainbow_flag", 6], - ["🏳️⚧️", "transgender_flag", 6], - ["🔐", "closed_lock_with_key", 6], - ["🔒", "lock", 6], - ["🔓", "unlock", 6], - ["🔏", "lock_with_ink_pen", 6], - ["🖊", "pen", 6], - ["🖋", "fountain_pen", 6], - ["✒️", "black_nib", 6], - ["📝", "memo", 6], - ["✏️", "pencil2", 6], - ["🖍", "crayon", 6], - ["🖌", "paintbrush", 6], - ["🔍", "mag", 6], - ["🔎", "mag_right", 6], - ["🪦", "headstone", 6], - ["🪧", "placard", 6], - ["💯", "100", 7], - ["🔢", "1234", 7], - ["🩷", "pink_heart", 7], - ["❤️", "heart", 7], - ["🧡", "orange_heart", 7], - ["💛", "yellow_heart", 7], - ["💚", "green_heart", 7], - ["🩵", "light_blue_heart", 7], - ["💙", "blue_heart", 7], - ["💜", "purple_heart", 7], - ["🤎", "brown_heart", 7], - ["🖤", "black_heart", 7], - ["🩶", "grey_heart", 7], - ["🤍", "white_heart", 7], - ["💔", "broken_heart", 7], - ["❣", "heavy_heart_exclamation", 7], - ["💕", "two_hearts", 7], - ["💞", "revolving_hearts", 7], - ["💓", "heartbeat", 7], - ["💗", "heartpulse", 7], - ["💖", "sparkling_heart", 7], - ["💘", "cupid", 7], - ["💝", "gift_heart", 7], - ["💟", "heart_decoration", 7], - ["❤️🔥", "heart_on_fire", 7], - ["❤️🩹", "mending_heart", 7], - ["☮", "peace_symbol", 7], - ["✝", "latin_cross", 7], - ["☪", "star_and_crescent", 7], - ["🕉", "om", 7], - ["☸", "wheel_of_dharma", 7], - ["🪯", "khanda", 7], - ["✡", "star_of_david", 7], - ["🔯", "six_pointed_star", 7], - ["🕎", "menorah", 7], - ["☯", "yin_yang", 7], - ["☦", "orthodox_cross", 7], - ["🛐", "place_of_worship", 7], - ["⛎", "ophiuchus", 7], - ["♈", "aries", 7], - ["♉", "taurus", 7], - ["♊", "gemini", 7], - ["♋", "cancer", 7], - ["♌", "leo", 7], - ["♍", "virgo", 7], - ["♎", "libra", 7], - ["♏", "scorpius", 7], - ["♐", "sagittarius", 7], - ["♑", "capricorn", 7], - ["♒", "aquarius", 7], - ["♓", "pisces", 7], - ["🆔", "id", 7], - ["⚛", "atom_symbol", 7], - ["⚧️", "transgender_symbol", 7], - ["🈳", "u7a7a", 7], - ["🈹", "u5272", 7], - ["☢", "radioactive", 7], - ["☣", "biohazard", 7], - ["📴", "mobile_phone_off", 7], - ["📳", "vibration_mode", 7], - ["🈶", "u6709", 7], - ["🈚", "u7121", 7], - ["🈸", "u7533", 7], - ["🈺", "u55b6", 7], - ["🈷️", "u6708", 7], - ["✴️", "eight_pointed_black_star", 7], - ["🆚", "vs", 7], - ["🉑", "accept", 7], - ["💮", "white_flower", 7], - ["🉐", "ideograph_advantage", 7], - ["㊙️", "secret", 7], - ["㊗️", "congratulations", 7], - ["🈴", "u5408", 7], - ["🈵", "u6e80", 7], - ["🈲", "u7981", 7], - ["🅰️", "a", 7], - ["🅱️", "b", 7], - ["🆎", "ab", 7], - ["🆑", "cl", 7], - ["🅾️", "o2", 7], - ["🆘", "sos", 7], - ["⛔", "no_entry", 7], - ["📛", "name_badge", 7], - ["🚫", "no_entry_sign", 7], - ["❌", "x", 7], - ["⭕", "o", 7], - ["🛑", "stop_sign", 7], - ["💢", "anger", 7], - ["♨️", "hotsprings", 7], - ["🚷", "no_pedestrians", 7], - ["🚯", "do_not_litter", 7], - ["🚳", "no_bicycles", 7], - ["🚱", "non-potable_water", 7], - ["🔞", "underage", 7], - ["📵", "no_mobile_phones", 7], - ["❗", "exclamation", 7], - ["❕", "grey_exclamation", 7], - ["❓", "question", 7], - ["❔", "grey_question", 7], - ["‼️", "bangbang", 7], - ["⁉️", "interrobang", 7], - ["🔅", "low_brightness", 7], - ["🔆", "high_brightness", 7], - ["🔱", "trident", 7], - ["⚜", "fleur_de_lis", 7], - ["〽️", "part_alternation_mark", 7], - ["⚠️", "warning", 7], - ["🚸", "children_crossing", 7], - ["🔰", "beginner", 7], - ["♻️", "recycle", 7], - ["🈯", "u6307", 7], - ["💹", "chart", 7], - ["❇️", "sparkle", 7], - ["✳️", "eight_spoked_asterisk", 7], - ["❎", "negative_squared_cross_mark", 7], - ["✅", "white_check_mark", 7], - ["💠", "diamond_shape_with_a_dot_inside", 7], - ["🌀", "cyclone", 7], - ["➿", "loop", 7], - ["🌐", "globe_with_meridians", 7], - ["Ⓜ️", "m", 7], - ["🏧", "atm", 7], - ["🈂️", "sa", 7], - ["🛂", "passport_control", 7], - ["🛃", "customs", 7], - ["🛄", "baggage_claim", 7], - ["🛅", "left_luggage", 7], - ["🛜", "wireless", 7], - ["♿", "wheelchair", 7], - ["🚭", "no_smoking", 7], - ["🚾", "wc", 7], - ["🅿️", "parking", 7], - ["🚰", "potable_water", 7], - ["🚹", "mens", 7], - ["🚺", "womens", 7], - ["🚼", "baby_symbol", 7], - ["🚻", "restroom", 7], - ["🚮", "put_litter_in_its_place", 7], - ["🎦", "cinema", 7], - ["📶", "signal_strength", 7], - ["🈁", "koko", 7], - ["🆖", "ng", 7], - ["🆗", "ok", 7], - ["🆙", "up", 7], - ["🆒", "cool", 7], - ["🆕", "new", 7], - ["🆓", "free", 7], - ["0️⃣", "zero", 7], - ["1️⃣", "one", 7], - ["2️⃣", "two", 7], - ["3️⃣", "three", 7], - ["4️⃣", "four", 7], - ["5️⃣", "five", 7], - ["6️⃣", "six", 7], - ["7️⃣", "seven", 7], - ["8️⃣", "eight", 7], - ["9️⃣", "nine", 7], - ["🔟", "keycap_ten", 7], - ["*⃣", "asterisk", 7], - ["⏏️", "eject_button", 7], - ["▶️", "arrow_forward", 7], - ["⏸", "pause_button", 7], - ["⏭", "next_track_button", 7], - ["⏹", "stop_button", 7], - ["⏺", "record_button", 7], - ["⏯", "play_or_pause_button", 7], - ["⏮", "previous_track_button", 7], - ["⏩", "fast_forward", 7], - ["⏪", "rewind", 7], - ["🔀", "twisted_rightwards_arrows", 7], - ["🔁", "repeat", 7], - ["🔂", "repeat_one", 7], - ["◀️", "arrow_backward", 7], - ["🔼", "arrow_up_small", 7], - ["🔽", "arrow_down_small", 7], - ["⏫", "arrow_double_up", 7], - ["⏬", "arrow_double_down", 7], - ["➡️", "arrow_right", 7], - ["⬅️", "arrow_left", 7], - ["⬆️", "arrow_up", 7], - ["⬇️", "arrow_down", 7], - ["↗️", "arrow_upper_right", 7], - ["↘️", "arrow_lower_right", 7], - ["↙️", "arrow_lower_left", 7], - ["↖️", "arrow_upper_left", 7], - ["↕️", "arrow_up_down", 7], - ["↔️", "left_right_arrow", 7], - ["🔄", "arrows_counterclockwise", 7], - ["↪️", "arrow_right_hook", 7], - ["↩️", "leftwards_arrow_with_hook", 7], - ["⤴️", "arrow_heading_up", 7], - ["⤵️", "arrow_heading_down", 7], - ["#️⃣", "hash", 7], - ["ℹ️", "information_source", 7], - ["🔤", "abc", 7], - ["🔡", "abcd", 7], - ["🔠", "capital_abcd", 7], - ["🔣", "symbols", 7], - ["🎵", "musical_note", 7], - ["🎶", "notes", 7], - ["〰️", "wavy_dash", 7], - ["➰", "curly_loop", 7], - ["✔️", "heavy_check_mark", 7], - ["🔃", "arrows_clockwise", 7], - ["➕", "heavy_plus_sign", 7], - ["➖", "heavy_minus_sign", 7], - ["➗", "heavy_division_sign", 7], - ["✖️", "heavy_multiplication_x", 7], - ["🟰", "heavy_equals_sign", 7], - ["♾", "infinity", 7], - ["💲", "heavy_dollar_sign", 7], - ["💱", "currency_exchange", 7], - ["©️", "copyright", 7], - ["®️", "registered", 7], - ["™️", "tm", 7], - ["🔚", "end", 7], - ["🔙", "back", 7], - ["🔛", "on", 7], - ["🔝", "top", 7], - ["🔜", "soon", 7], - ["☑️", "ballot_box_with_check", 7], - ["🔘", "radio_button", 7], - ["⚫", "black_circle", 7], - ["⚪", "white_circle", 7], - ["🔴", "red_circle", 7], - ["🟠", "orange_circle", 7], - ["🟡", "yellow_circle", 7], - ["🟢", "green_circle", 7], - ["🔵", "large_blue_circle", 7], - ["🟣", "purple_circle", 7], - ["🟤", "brown_circle", 7], - ["🔸", "small_orange_diamond", 7], - ["🔹", "small_blue_diamond", 7], - ["🔶", "large_orange_diamond", 7], - ["🔷", "large_blue_diamond", 7], - ["🔺", "small_red_triangle", 7], - ["▪️", "black_small_square", 7], - ["▫️", "white_small_square", 7], - ["⬛", "black_large_square", 7], - ["⬜", "white_large_square", 7], - ["🟥", "red_square", 7], - ["🟧", "orange_square", 7], - ["🟨", "yellow_square", 7], - ["🟩", "green_square", 7], - ["🟦", "blue_square", 7], - ["🟪", "purple_square", 7], - ["🟫", "brown_square", 7], - ["🔻", "small_red_triangle_down", 7], - ["◼️", "black_medium_square", 7], - ["◻️", "white_medium_square", 7], - ["◾", "black_medium_small_square", 7], - ["◽", "white_medium_small_square", 7], - ["🔲", "black_square_button", 7], - ["🔳", "white_square_button", 7], - ["🔈", "speaker", 7], - ["🔉", "sound", 7], - ["🔊", "loud_sound", 7], - ["🔇", "mute", 7], - ["📣", "mega", 7], - ["📢", "loudspeaker", 7], - ["🔔", "bell", 7], - ["🔕", "no_bell", 7], - ["🃏", "black_joker", 7], - ["🀄", "mahjong", 7], - ["♠️", "spades", 7], - ["♣️", "clubs", 7], - ["♥️", "hearts", 7], - ["♦️", "diamonds", 7], - ["🎴", "flower_playing_cards", 7], - ["💭", "thought_balloon", 7], - ["🗯", "right_anger_bubble", 7], - ["💬", "speech_balloon", 7], - ["🗨", "left_speech_bubble", 7], - ["🕐", "clock1", 7], - ["🕑", "clock2", 7], - ["🕒", "clock3", 7], - ["🕓", "clock4", 7], - ["🕔", "clock5", 7], - ["🕕", "clock6", 7], - ["🕖", "clock7", 7], - ["🕗", "clock8", 7], - ["🕘", "clock9", 7], - ["🕙", "clock10", 7], - ["🕚", "clock11", 7], - ["🕛", "clock12", 7], - ["🕜", "clock130", 7], - ["🕝", "clock230", 7], - ["🕞", "clock330", 7], - ["🕟", "clock430", 7], - ["🕠", "clock530", 7], - ["🕡", "clock630", 7], - ["🕢", "clock730", 7], - ["🕣", "clock830", 7], - ["🕤", "clock930", 7], - ["🕥", "clock1030", 7], - ["🕦", "clock1130", 7], - ["🕧", "clock1230", 7], - ["🇦🇫", "afghanistan", 8], - ["🇦🇽", "aland_islands", 8], - ["🇦🇱", "albania", 8], - ["🇩🇿", "algeria", 8], - ["🇦🇸", "american_samoa", 8], - ["🇦🇩", "andorra", 8], - ["🇦🇴", "angola", 8], - ["🇦🇮", "anguilla", 8], - ["🇦🇶", "antarctica", 8], - ["🇦🇬", "antigua_barbuda", 8], - ["🇦🇷", "argentina", 8], - ["🇦🇲", "armenia", 8], - ["🇦🇼", "aruba", 8], - ["🇦🇨", "ascension_island", 8], - ["🇦🇺", "australia", 8], - ["🇦🇹", "austria", 8], - ["🇦🇿", "azerbaijan", 8], - ["🇧🇸", "bahamas", 8], - ["🇧🇭", "bahrain", 8], - ["🇧🇩", "bangladesh", 8], - ["🇧🇧", "barbados", 8], - ["🇧🇾", "belarus", 8], - ["🇧🇪", "belgium", 8], - ["🇧🇿", "belize", 8], - ["🇧🇯", "benin", 8], - ["🇧🇲", "bermuda", 8], - ["🇧🇹", "bhutan", 8], - ["🇧🇴", "bolivia", 8], - ["🇧🇶", "caribbean_netherlands", 8], - ["🇧🇦", "bosnia_herzegovina", 8], - ["🇧🇼", "botswana", 8], - ["🇧🇷", "brazil", 8], - ["🇮🇴", "british_indian_ocean_territory", 8], - ["🇻🇬", "british_virgin_islands", 8], - ["🇧🇳", "brunei", 8], - ["🇧🇬", "bulgaria", 8], - ["🇧🇫", "burkina_faso", 8], - ["🇧🇮", "burundi", 8], - ["🇨🇻", "cape_verde", 8], - ["🇰🇭", "cambodia", 8], - ["🇨🇲", "cameroon", 8], - ["🇨🇦", "canada", 8], - ["🇮🇨", "canary_islands", 8], - ["🇰🇾", "cayman_islands", 8], - ["🇨🇫", "central_african_republic", 8], - ["🇹🇩", "chad", 8], - ["🇨🇱", "chile", 8], - ["🇨🇳", "cn", 8], - ["🇨🇽", "christmas_island", 8], - ["🇨🇨", "cocos_islands", 8], - ["🇨🇴", "colombia", 8], - ["🇰🇲", "comoros", 8], - ["🇨🇬", "congo_brazzaville", 8], - ["🇨🇩", "congo_kinshasa", 8], - ["🇨🇰", "cook_islands", 8], - ["🇨🇷", "costa_rica", 8], - ["🇭🇷", "croatia", 8], - ["🇨🇺", "cuba", 8], - ["🇨🇼", "curacao", 8], - ["🇨🇾", "cyprus", 8], - ["🇨🇿", "czech_republic", 8], - ["🇩🇰", "denmark", 8], - ["🇩🇯", "djibouti", 8], - ["🇩🇲", "dominica", 8], - ["🇩🇴", "dominican_republic", 8], - ["🇪🇨", "ecuador", 8], - ["🇪🇬", "egypt", 8], - ["🇸🇻", "el_salvador", 8], - ["🇬🇶", "equatorial_guinea", 8], - ["🇪🇷", "eritrea", 8], - ["🇪🇪", "estonia", 8], - ["🇪🇹", "ethiopia", 8], - ["🇪🇺", "eu", 8], - ["🇫🇰", "falkland_islands", 8], - ["🇫🇴", "faroe_islands", 8], - ["🇫🇯", "fiji", 8], - ["🇫🇮", "finland", 8], - ["🇫🇷", "fr", 8], - ["🇬🇫", "french_guiana", 8], - ["🇵🇫", "french_polynesia", 8], - ["🇹🇫", "french_southern_territories", 8], - ["🇬🇦", "gabon", 8], - ["🇬🇲", "gambia", 8], - ["🇬🇪", "georgia", 8], - ["🇩🇪", "de", 8], - ["🇬🇭", "ghana", 8], - ["🇬🇮", "gibraltar", 8], - ["🇬🇷", "greece", 8], - ["🇬🇱", "greenland", 8], - ["🇬🇩", "grenada", 8], - ["🇬🇵", "guadeloupe", 8], - ["🇬🇺", "guam", 8], - ["🇬🇹", "guatemala", 8], - ["🇬🇬", "guernsey", 8], - ["🇬🇳", "guinea", 8], - ["🇬🇼", "guinea_bissau", 8], - ["🇬🇾", "guyana", 8], - ["🇭🇹", "haiti", 8], - ["🇭🇳", "honduras", 8], - ["🇭🇰", "hong_kong", 8], - ["🇭🇺", "hungary", 8], - ["🇮🇸", "iceland", 8], - ["🇮🇳", "india", 8], - ["🇮🇩", "indonesia", 8], - ["🇮🇷", "iran", 8], - ["🇮🇶", "iraq", 8], - ["🇮🇪", "ireland", 8], - ["🇮🇲", "isle_of_man", 8], - ["🇮🇱", "israel", 8], - ["🇮🇹", "it", 8], - ["🇨🇮", "cote_divoire", 8], - ["🇯🇲", "jamaica", 8], - ["🇯🇵", "jp", 8], - ["🇯🇪", "jersey", 8], - ["🇯🇴", "jordan", 8], - ["🇰🇿", "kazakhstan", 8], - ["🇰🇪", "kenya", 8], - ["🇰🇮", "kiribati", 8], - ["🇽🇰", "kosovo", 8], - ["🇰🇼", "kuwait", 8], - ["🇰🇬", "kyrgyzstan", 8], - ["🇱🇦", "laos", 8], - ["🇱🇻", "latvia", 8], - ["🇱🇧", "lebanon", 8], - ["🇱🇸", "lesotho", 8], - ["🇱🇷", "liberia", 8], - ["🇱🇾", "libya", 8], - ["🇱🇮", "liechtenstein", 8], - ["🇱🇹", "lithuania", 8], - ["🇱🇺", "luxembourg", 8], - ["🇲🇴", "macau", 8], - ["🇲🇰", "macedonia", 8], - ["🇲🇬", "madagascar", 8], - ["🇲🇼", "malawi", 8], - ["🇲🇾", "malaysia", 8], - ["🇲🇻", "maldives", 8], - ["🇲🇱", "mali", 8], - ["🇲🇹", "malta", 8], - ["🇲🇭", "marshall_islands", 8], - ["🇲🇶", "martinique", 8], - ["🇲🇷", "mauritania", 8], - ["🇲🇺", "mauritius", 8], - ["🇾🇹", "mayotte", 8], - ["🇲🇽", "mexico", 8], - ["🇫🇲", "micronesia", 8], - ["🇲🇩", "moldova", 8], - ["🇲🇨", "monaco", 8], - ["🇲🇳", "mongolia", 8], - ["🇲🇪", "montenegro", 8], - ["🇲🇸", "montserrat", 8], - ["🇲🇦", "morocco", 8], - ["🇲🇿", "mozambique", 8], - ["🇲🇲", "myanmar", 8], - ["🇳🇦", "namibia", 8], - ["🇳🇷", "nauru", 8], - ["🇳🇵", "nepal", 8], - ["🇳🇱", "netherlands", 8], - ["🇳🇨", "new_caledonia", 8], - ["🇳🇿", "new_zealand", 8], - ["🇳🇮", "nicaragua", 8], - ["🇳🇪", "niger", 8], - ["🇳🇬", "nigeria", 8], - ["🇳🇺", "niue", 8], - ["🇳🇫", "norfolk_island", 8], - ["🇲🇵", "northern_mariana_islands", 8], - ["🇰🇵", "north_korea", 8], - ["🇳🇴", "norway", 8], - ["🇴🇲", "oman", 8], - ["🇵🇰", "pakistan", 8], - ["🇵🇼", "palau", 8], - ["🇵🇸", "palestinian_territories", 8], - ["🇵🇦", "panama", 8], - ["🇵🇬", "papua_new_guinea", 8], - ["🇵🇾", "paraguay", 8], - ["🇵🇪", "peru", 8], - ["🇵🇭", "philippines", 8], - ["🇵🇳", "pitcairn_islands", 8], - ["🇵🇱", "poland", 8], - ["🇵🇹", "portugal", 8], - ["🇵🇷", "puerto_rico", 8], - ["🇶🇦", "qatar", 8], - ["🇷🇪", "reunion", 8], - ["🇷🇴", "romania", 8], - ["🇷🇺", "ru", 8], - ["🇷🇼", "rwanda", 8], - ["🇧🇱", "st_barthelemy", 8], - ["🇸🇭", "st_helena", 8], - ["🇰🇳", "st_kitts_nevis", 8], - ["🇱🇨", "st_lucia", 8], - ["🇵🇲", "st_pierre_miquelon", 8], - ["🇻🇨", "st_vincent_grenadines", 8], - ["🇼🇸", "samoa", 8], - ["🇸🇲", "san_marino", 8], - ["🇸🇹", "sao_tome_principe", 8], - ["🇸🇦", "saudi_arabia", 8], - ["🇸🇳", "senegal", 8], - ["🇷🇸", "serbia", 8], - ["🇸🇨", "seychelles", 8], - ["🇸🇱", "sierra_leone", 8], - ["🇸🇬", "singapore", 8], - ["🇸🇽", "sint_maarten", 8], - ["🇸🇰", "slovakia", 8], - ["🇸🇮", "slovenia", 8], - ["🇸🇧", "solomon_islands", 8], - ["🇸🇴", "somalia", 8], - ["🇿🇦", "south_africa", 8], - ["🇬🇸", "south_georgia_south_sandwich_islands", 8], - ["🇰🇷", "kr", 8], - ["🇸🇸", "south_sudan", 8], - ["🇪🇸", "es", 8], - ["🇱🇰", "sri_lanka", 8], - ["🇸🇩", "sudan", 8], - ["🇸🇷", "suriname", 8], - ["🇸🇿", "swaziland", 8], - ["🇸🇪", "sweden", 8], - ["🇨🇭", "switzerland", 8], - ["🇸🇾", "syria", 8], - ["🇹🇼", "taiwan", 8], - ["🇹🇯", "tajikistan", 8], - ["🇹🇿", "tanzania", 8], - ["🇹🇭", "thailand", 8], - ["🇹🇱", "timor_leste", 8], - ["🇹🇬", "togo", 8], - ["🇹🇰", "tokelau", 8], - ["🇹🇴", "tonga", 8], - ["🇹🇹", "trinidad_tobago", 8], - ["🇹🇦", "tristan_da_cunha", 8], - ["🇹🇳", "tunisia", 8], - ["🇹🇷", "tr", 8], - ["🇹🇲", "turkmenistan", 8], - ["🇹🇨", "turks_caicos_islands", 8], - ["🇹🇻", "tuvalu", 8], - ["🇺🇬", "uganda", 8], - ["🇺🇦", "ukraine", 8], - ["🇦🇪", "united_arab_emirates", 8], - ["🇬🇧", "uk", 8], - ["🏴", "england", 8], - ["🏴", "scotland", 8], - ["🏴", "wales", 8], - ["🇺🇸", "us", 8], - ["🇻🇮", "us_virgin_islands", 8], - ["🇺🇾", "uruguay", 8], - ["🇺🇿", "uzbekistan", 8], - ["🇻🇺", "vanuatu", 8], - ["🇻🇦", "vatican_city", 8], - ["🇻🇪", "venezuela", 8], - ["🇻🇳", "vietnam", 8], - ["🇼🇫", "wallis_futuna", 8], - ["🇪🇭", "western_sahara", 8], - ["🇾🇪", "yemen", 8], - ["🇿🇲", "zambia", 8], - ["🇿🇼", "zimbabwe", 8], - ["🇺🇳", "united_nations", 8], - ["🏴☠️", "pirate_flag", 8] -] diff --git a/packages/frontend/src/filters/date.ts b/packages/frontend/src/filters/date.ts index 2ffe93e868..d13d1a5e42 100644 --- a/packages/frontend/src/filters/date.ts +++ b/packages/frontend/src/filters/date.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { dateTimeFormat } from '@/scripts/intl-const.js'; +import { dateTimeFormat } from '@@/js/intl-const.js'; export default (d: Date | number | undefined) => dateTimeFormat.format(d); export const dateString = (d: string) => dateTimeFormat.format(new Date(d)); diff --git a/packages/frontend/src/filters/number.ts b/packages/frontend/src/filters/number.ts index 2e7cc60ff4..10fb64deb4 100644 --- a/packages/frontend/src/filters/number.ts +++ b/packages/frontend/src/filters/number.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { numberFormat } from '@/scripts/intl-const.js'; +import { numberFormat } from '@@/js/intl-const.js'; export default n => n == null ? 'N/A' : numberFormat.format(n); diff --git a/packages/frontend/src/filters/user.ts b/packages/frontend/src/filters/user.ts index a87766764d..d9bc316764 100644 --- a/packages/frontend/src/filters/user.ts +++ b/packages/frontend/src/filters/user.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; export const acct = (user: Misskey.Acct) => { return Misskey.acct.toString(user); diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts index 10d6adbcd0..6ad503b089 100644 --- a/packages/frontend/src/i18n.ts +++ b/packages/frontend/src/i18n.ts @@ -4,11 +4,11 @@ */ import { markRaw } from 'vue'; +import { I18n } from '@@/js/i18n.js'; import type { Locale } from '../../../locales/index.js'; -import { locale } from '@/config.js'; -import { I18n } from '@/scripts/i18n.js'; +import { locale } from '@@/js/config.js'; -export const i18n = markRaw(new I18n<Locale>(locale)); +export const i18n = markRaw(new I18n<Locale>(locale, _DEV_)); export function updateI18n(newLocale: Locale) { i18n.locale = newLocale; diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts index 6847321d6c..71cb42b30c 100644 --- a/packages/frontend/src/instance.ts +++ b/packages/frontend/src/instance.ts @@ -7,7 +7,7 @@ import { computed, reactive } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { miLocalStorage } from '@/local-storage.js'; -import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const.js'; +import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@@/js/const.js'; // TODO: 他のタブと永続化されたstateを同期 diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 2b2f59edb3..89c0a4b849 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -type Keys = +export type Keys = 'v' | 'lastVersion' | 'instance' | @@ -39,12 +39,22 @@ type Keys = `aiscript:${string}` | 'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~) 'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~); - `channelLastReadedAt:${string}` + `channelLastReadedAt:${string}` | + `idbfallback::${string}` + +// セッション毎に廃棄されるLocalStorage代替(セーフモードなどで使用できそう) +//const safeSessionStorage = new Map<Keys, string>(); export const miLocalStorage = { - getItem: (key: Keys): string | null => window.localStorage.getItem(key), - setItem: (key: Keys, value: string): void => window.localStorage.setItem(key, value), - removeItem: (key: Keys): void => window.localStorage.removeItem(key), + getItem: (key: Keys): string | null => { + return window.localStorage.getItem(key); + }, + setItem: (key: Keys, value: string): void => { + window.localStorage.setItem(key, value); + }, + removeItem: (key: Keys): void => { + window.localStorage.removeItem(key); + }, getItemAsJson: (key: Keys): any | undefined => { const item = miLocalStorage.getItem(key); if (item === null) { @@ -52,5 +62,7 @@ export const miLocalStorage = { } return JSON.parse(item); }, - setItemAsJson: (key: Keys, value: any): void => window.localStorage.setItem(key, JSON.stringify(value)), + setItemAsJson: (key: Keys, value: any): void => { + miLocalStorage.setItem(key, JSON.stringify(value)); + }, }; diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index b6385b5ad2..b92fdb17b9 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -12,7 +12,7 @@ import { openInstanceMenu } from '@/ui/_common_/common.js'; import { lookup } from '@/scripts/lookup.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { ui } from '@/config.js'; +import { ui } from '@@/js/config.js'; import { unisonReload } from '@/scripts/unison-reload.js'; export const navbarItemDef = reactive({ @@ -131,7 +131,7 @@ export const navbarItemDef = reactive({ ui: { title: i18n.ts.switchUi, icon: 'ti ti-devices', - action: (ev) => { + action: (ev: MouseEvent) => { os.popupMenu([{ text: i18n.ts.default, active: ui === 'default' || ui === null, diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts index 6a8ea09ed6..25f853453a 100644 --- a/packages/frontend/src/nirax.ts +++ b/packages/frontend/src/nirax.ts @@ -7,7 +7,14 @@ import { Component, onMounted, shallowRef, ShallowRef } from 'vue'; import { EventEmitter } from 'eventemitter3'; -import { safeURIDecode } from '@/scripts/safe-uri-decode.js'; + +function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} interface RouteDefBase { path: string; diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index bd96a0655e..e73aa77a3c 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -22,7 +22,7 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue'; import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue'; import MkPopupMenu from '@/components/MkPopupMenu.vue'; import MkContextMenu from '@/components/MkContextMenu.vue'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue index dbc113a766..83f596b870 100644 --- a/packages/frontend/src/pages/_error_.vue +++ b/packages/frontend/src/pages/_error_.vue @@ -29,7 +29,7 @@ import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkLink from '@/components/MkLink.vue'; -import { version } from '@/config.js'; +import { version } from '@@/js/config.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/pages/about-sharkey.vue b/packages/frontend/src/pages/about-sharkey.vue index 938786949c..7ebae62a7b 100644 --- a/packages/frontend/src/pages/about-sharkey.vue +++ b/packages/frontend/src/pages/about-sharkey.vue @@ -185,7 +185,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { nextTick, onBeforeUnmount, ref, shallowRef, computed } from 'vue'; -import { version } from '@/config.js'; +import { version } from '@@/js/config.js'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue index 72acd9bfca..ca070c65a4 100644 --- a/packages/frontend/src/pages/about.overview.vue +++ b/packages/frontend/src/pages/about.overview.vue @@ -148,7 +148,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import sanitizeHtml from '@/scripts/sanitize-html.js'; -import { host, version } from '@/config.js'; +import { host, version } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import number from '@/filters/number.js'; diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index d8311186ab..60f6be51d4 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -44,6 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </div> + <div v-else-if="tab === 'notes' && info" class="_gaps_m"> + <XNotes :fileId="fileId"/> + </div> <div v-else-if="tab === 'ip' && info" class="_gaps_m"> <MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo> <MkKeyValue v-if="info.requestIp" class="_monospace" :copy="info.requestIp" oneline> @@ -67,7 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; +import { computed, defineAsyncComponent, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; @@ -88,6 +91,7 @@ const tab = ref('overview'); const file = ref<Misskey.entities.DriveFile | null>(null); const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null); const isSensitive = ref<boolean>(false); +const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue')); const props = defineProps<{ fileId: string, @@ -131,6 +135,10 @@ const headerTabs = computed(() => [{ title: i18n.ts.overview, icon: 'ti ti-info-circle', }, iAmModerator ? { + key: 'notes', + title: i18n.ts._fileViewer.attachedNotes, + icon: 'ti ti-pencil', +} : null, iAmModerator ? { key: 'ip', title: 'IP', icon: 'ti ti-password', diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 16a2a7524b..24894fff72 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -208,7 +208,7 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue'; import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { acct } from '@/filters/user.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue index 61dc4f8549..2a71e3efab 100644 --- a/packages/frontend/src/pages/admin/_header_.vue +++ b/packages/frontend/src/pages/admin/_header_.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, onMounted, onUnmounted, ref, shallowRef, watch, nextTick } from 'vue'; import tinycolor from 'tinycolor2'; import { popupMenu } from '@/os.js'; -import { scrollToTop } from '@/scripts/scroll.js'; +import { scrollToTop } from '@@/js/scroll.js'; import MkButton from '@/components/MkButton.vue'; import { globalEvents } from '@/events.js'; import { injectReactiveMetadata } from '@/scripts/page-metadata.js'; diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index bd442ccc69..6c8901b10b 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -65,18 +65,18 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTextarea v-model="ad.memo"> <template #label>{{ i18n.ts.memo }}</template> </MkTextarea> - <div class="buttons"> - <MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"> + <div class="_buttons"> + <MkButton inline primary style="margin-right: 12px;" @click="save(ad)"> <i class="ti ti-device-floppy" ></i> {{ i18n.ts.save }} </MkButton> - <MkButton class="button" inline danger @click="remove(ad)"> + <MkButton inline danger @click="remove(ad)"> <i class="ti ti-trash"></i> {{ i18n.ts.remove }} </MkButton> </div> </div> - <MkButton class="button" @click="more()"> + <MkButton @click="more()"> <i class="ti ti-reload"></i>{{ i18n.ts.more }} </MkButton> </div> diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index b9e09c8d03..fd37311b21 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -29,8 +29,16 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> </template> <template #caption>{{ announcement.text }}</template> + <template #footer> + <div class="_buttons"> + <MkButton rounded primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="announcement.id != null && announcement.isActive" rounded @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton> + <MkButton v-if="announcement.id != null && !announcement.isActive" rounded @click="unarchive(announcement)"><i class="ti ti-restore"></i> {{ i18n.ts.unarchive }}</MkButton> + <MkButton v-if="announcement.id != null" rounded danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </template> - <div class="_gaps_m"> + <div class="_gaps"> <MkInput v-model="announcement.title"> <template #label>{{ i18n.ts.title }}</template> </MkInput> @@ -64,16 +72,10 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.needConfirmationToRead }} </MkSwitch> <p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p> - <div class="buttons _buttons"> - <MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-if="announcement.id != null && announcement.isActive" class="button" inline @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton> - <MkButton v-if="announcement.id != null && !announcement.isActive" class="button" inline @click="unarchive(announcement)"><i class="ti ti-restore"></i> {{ i18n.ts.unarchive }}</MkButton> - <MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> - </div> </div> </MkFolder> <MkLoading v-if="loadingMore"/> - <MkButton class="button" @click="more()"> + <MkButton @click="more()"> <i class="ti ti-reload"></i>{{ i18n.ts.more }} </MkButton> </template> @@ -170,7 +172,7 @@ function more() { loadingMore.value = true; misskeyApi('admin/announcements/list', { status: announcementsStatus.value, - untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id + untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id, }).then(announcementResponse => { announcements.value = announcements.value.concat(announcementResponse); loadingMore.value = false; diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index 73c5e1919f..b34592cd6a 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -4,145 +4,143 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div> - <FormSuspense :p="init"> - <div class="_gaps_m"> - <MkRadios v-model="provider"> - <option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option> - <option value="hcaptcha">hCaptcha</option> - <option value="mcaptcha">mCaptcha</option> - <option value="recaptcha">reCAPTCHA</option> - <option value="turnstile">Turnstile</option> - </MkRadios> +<MkFolder> + <template #icon><i class="ti ti-shield"></i></template> + <template #label>{{ i18n.ts.botProtection }}</template> + <template v-if="botProtectionForm.savedState.provider === 'hcaptcha'" #suffix>hCaptcha</template> + <template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template> + <template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template> + <template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template> + <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> + <template v-if="botProtectionForm.modified.value" #footer> + <MkFormFooter :form="botProtectionForm"/> + </template> - <template v-if="provider === 'hcaptcha'"> - <MkInput v-model="hcaptchaSiteKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template> - </MkInput> - <MkInput v-model="hcaptchaSecretKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template> - </MkInput> - <FormSlot> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/> - </FormSlot> - </template> - <template v-else-if="provider === 'mcaptcha'"> - <MkInput v-model="mcaptchaSiteKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template> - </MkInput> - <MkInput v-model="mcaptchaSecretKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template> - </MkInput> - <MkInput v-model="mcaptchaInstanceUrl"> - <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template> - </MkInput> - <FormSlot v-if="mcaptchaSiteKey && mcaptchaInstanceUrl"> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="mcaptcha" :sitekey="mcaptchaSiteKey" :instanceUrl="mcaptchaInstanceUrl"/> - </FormSlot> - </template> - <template v-else-if="provider === 'recaptcha'"> - <MkInput v-model="recaptchaSiteKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.recaptchaSiteKey }}</template> - </MkInput> - <MkInput v-model="recaptchaSecretKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.recaptchaSecretKey }}</template> - </MkInput> - <FormSlot v-if="recaptchaSiteKey"> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/> - </FormSlot> - </template> - <template v-else-if="provider === 'turnstile'"> - <MkInput v-model="turnstileSiteKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.turnstileSiteKey }}</template> - </MkInput> - <MkInput v-model="turnstileSecretKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.turnstileSecretKey }}</template> - </MkInput> - <FormSlot> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/> - </FormSlot> - </template> + <div class="_gaps_m"> + <MkRadios v-model="botProtectionForm.state.provider"> + <option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option> + <option value="hcaptcha">hCaptcha</option> + <option value="mcaptcha">mCaptcha</option> + <option value="recaptcha">reCAPTCHA</option> + <option value="turnstile">Turnstile</option> + </MkRadios> - <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - </div> - </FormSuspense> -</div> + <template v-if="botProtectionForm.state.provider === 'hcaptcha'"> + <MkInput v-model="botProtectionForm.state.hcaptchaSiteKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template> + </MkInput> + <MkInput v-model="botProtectionForm.state.hcaptchaSecretKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template> + </MkInput> + <FormSlot> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="hcaptcha" :sitekey="botProtectionForm.state.hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/> + </FormSlot> + </template> + <template v-else-if="botProtectionForm.state.provider === 'mcaptcha'"> + <MkInput v-model="botProtectionForm.state.mcaptchaSiteKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template> + </MkInput> + <MkInput v-model="botProtectionForm.state.mcaptchaSecretKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template> + </MkInput> + <MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template> + </MkInput> + <FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl"> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="mcaptcha" :sitekey="botProtectionForm.state.mcaptchaSiteKey" :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"/> + </FormSlot> + </template> + <template v-else-if="botProtectionForm.state.provider === 'recaptcha'"> + <MkInput v-model="botProtectionForm.state.recaptchaSiteKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.recaptchaSiteKey }}</template> + </MkInput> + <MkInput v-model="botProtectionForm.state.recaptchaSecretKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.recaptchaSecretKey }}</template> + </MkInput> + <FormSlot v-if="botProtectionForm.state.recaptchaSiteKey"> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="recaptcha" :sitekey="botProtectionForm.state.recaptchaSiteKey"/> + </FormSlot> + </template> + <template v-else-if="botProtectionForm.state.provider === 'turnstile'"> + <MkInput v-model="botProtectionForm.state.turnstileSiteKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.turnstileSiteKey }}</template> + </MkInput> + <MkInput v-model="botProtectionForm.state.turnstileSecretKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.turnstileSecretKey }}</template> + </MkInput> + <FormSlot> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="turnstile" :sitekey="botProtectionForm.state.turnstileSiteKey || '1x00000000000000000000AA'"/> + </FormSlot> + </template> + </div> +</MkFolder> </template> <script lang="ts" setup> import { defineAsyncComponent, ref } from 'vue'; -import type { CaptchaProvider } from '@/components/MkCaptcha.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkInput from '@/components/MkInput.vue'; -import MkButton from '@/components/MkButton.vue'; -import FormSuspense from '@/components/form/suspense.vue'; import FormSlot from '@/components/form/slot.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; +import { useForm } from '@/scripts/use-form.js'; +import MkFormFooter from '@/components/MkFormFooter.vue'; +import MkFolder from '@/components/MkFolder.vue'; const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue')); -const provider = ref<CaptchaProvider | null>(null); -const hcaptchaSiteKey = ref<string | null>(null); -const hcaptchaSecretKey = ref<string | null>(null); -const mcaptchaSiteKey = ref<string | null>(null); -const mcaptchaSecretKey = ref<string | null>(null); -const mcaptchaInstanceUrl = ref<string | null>(null); -const recaptchaSiteKey = ref<string | null>(null); -const recaptchaSecretKey = ref<string | null>(null); -const turnstileSiteKey = ref<string | null>(null); -const turnstileSecretKey = ref<string | null>(null); +const meta = await misskeyApi('admin/meta'); -async function init() { - const meta = await misskeyApi('admin/meta'); - hcaptchaSiteKey.value = meta.hcaptchaSiteKey; - hcaptchaSecretKey.value = meta.hcaptchaSecretKey; - mcaptchaSiteKey.value = meta.mcaptchaSiteKey; - mcaptchaSecretKey.value = meta.mcaptchaSecretKey; - mcaptchaInstanceUrl.value = meta.mcaptchaInstanceUrl; - recaptchaSiteKey.value = meta.recaptchaSiteKey; - recaptchaSecretKey.value = meta.recaptchaSecretKey; - turnstileSiteKey.value = meta.turnstileSiteKey; - turnstileSecretKey.value = meta.turnstileSecretKey; - - provider.value = meta.enableHcaptcha ? 'hcaptcha' : - meta.enableRecaptcha ? 'recaptcha' : - meta.enableTurnstile ? 'turnstile' : - meta.enableMcaptcha ? 'mcaptcha' : null; -} - -function save() { - os.apiWithDialog('admin/update-meta', { - enableHcaptcha: provider.value === 'hcaptcha', - hcaptchaSiteKey: hcaptchaSiteKey.value, - hcaptchaSecretKey: hcaptchaSecretKey.value, - enableMcaptcha: provider.value === 'mcaptcha', - mcaptchaSiteKey: mcaptchaSiteKey.value, - mcaptchaSecretKey: mcaptchaSecretKey.value, - mcaptchaInstanceUrl: mcaptchaInstanceUrl.value, - enableRecaptcha: provider.value === 'recaptcha', - recaptchaSiteKey: recaptchaSiteKey.value, - recaptchaSecretKey: recaptchaSecretKey.value, - enableTurnstile: provider.value === 'turnstile', - turnstileSiteKey: turnstileSiteKey.value, - turnstileSecretKey: turnstileSecretKey.value, - }).then(() => { - fetchInstance(true); +const botProtectionForm = useForm({ + provider: meta.enableHcaptcha + ? 'hcaptcha' + : meta.enableRecaptcha + ? 'recaptcha' + : meta.enableTurnstile + ? 'turnstile' + : meta.enableMcaptcha + ? 'mcaptcha' + : null, + hcaptchaSiteKey: meta.hcaptchaSiteKey, + hcaptchaSecretKey: meta.hcaptchaSecretKey, + mcaptchaSiteKey: meta.mcaptchaSiteKey, + mcaptchaSecretKey: meta.mcaptchaSecretKey, + mcaptchaInstanceUrl: meta.mcaptchaInstanceUrl, + recaptchaSiteKey: meta.recaptchaSiteKey, + recaptchaSecretKey: meta.recaptchaSecretKey, + turnstileSiteKey: meta.turnstileSiteKey, + turnstileSecretKey: meta.turnstileSecretKey, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableHcaptcha: state.provider === 'hcaptcha', + hcaptchaSiteKey: state.hcaptchaSiteKey, + hcaptchaSecretKey: state.hcaptchaSecretKey, + enableMcaptcha: state.provider === 'mcaptcha', + mcaptchaSiteKey: state.mcaptchaSiteKey, + mcaptchaSecretKey: state.mcaptchaSecretKey, + mcaptchaInstanceUrl: state.mcaptchaInstanceUrl, + enableRecaptcha: state.provider === 'recaptcha', + recaptchaSiteKey: state.recaptchaSiteKey, + recaptchaSecretKey: state.recaptchaSecretKey, + enableTurnstile: state.provider === 'turnstile', + turnstileSiteKey: state.turnstileSiteKey, + turnstileSecretKey: state.turnstileSecretKey, }); -} + fetchInstance(true); +}); </script> diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index bab93514c3..d3d52002fe 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -134,7 +134,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import MkColorInput from '@/components/MkColorInput.vue'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; const iconUrl = ref<string | null>(null); const sidebarLogoUrl = ref<string | null>(null); diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index e4308e6030..50e2c2dd51 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> - <FormSection> + <MkFolder> <template #label>DeepL Translation</template> <div class="_gaps_m"> @@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="deeplIsPro"> <template #label>Pro account</template> </MkSwitch> + <MkSwitch v-model="deeplFreeMode"> <template #label>{{ i18n.ts.deeplFreeMode }}</template> </MkSwitch> @@ -27,17 +28,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>DeepLX-JS URL</template> <template #caption>{{ i18n.ts.deeplFreeModeDescription }}</template> </MkInput> + + <MkButton primary @click="save_deepl">Save</MkButton> </div> - </FormSection> + </MkFolder> </FormSuspense> </MkSpacer> - <template #footer> - <div :class="$style.footer"> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> - </MkSpacer> - </div> - </template> </MkStickyContainer> </template> @@ -48,12 +44,12 @@ import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormSuspense from '@/components/form/suspense.vue'; -import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkFolder from '@/components/MkFolder.vue'; const deeplAuthKey = ref<string>(''); const deeplIsPro = ref<boolean>(false); @@ -68,7 +64,7 @@ async function init() { deeplFreeInstance.value = meta.deeplFreeInstance; } -function save() { +function save_deepl() { os.apiWithDialog('admin/update-meta', { deeplAuthKey: deeplAuthKey.value, deeplIsPro: deeplIsPro.value, @@ -88,10 +84,3 @@ definePageMetadata(() => ({ icon: 'ph-arrow-square-out ph-bold ph-lg', })); </script> - -<style lang="scss" module> -.footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); -} -</style> diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index f547bedacb..1045b1e2b1 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -215,16 +215,6 @@ const menuDef = computed(() => [{ to: '/admin/relays', active: currentPage.value?.route.name === 'relays', }, { - icon: 'ti ti-ban', - text: i18n.ts.instanceBlocking, - to: '/admin/instance-block', - active: currentPage.value?.route.name === 'instance-block', - }, { - icon: 'ti ti-ghost', - text: i18n.ts.proxyAccount, - to: '/admin/proxy-account', - active: currentPage.value?.route.name === 'proxy-account', - }, { icon: 'ph-arrow-square-out ph-bold ph-lg', text: i18n.ts.externalServices, to: '/admin/external-services', @@ -235,10 +225,10 @@ const menuDef = computed(() => [{ to: '/admin/system-webhook', active: currentPage.value?.route.name === 'system-webhook', }, { - icon: 'ti ti-adjustments', - text: i18n.ts.other, - to: '/admin/other-settings', - active: currentPage.value?.route.name === 'other-settings', + icon: 'ti ti-bolt', + text: i18n.ts.performance, + to: '/admin/performance', + active: currentPage.value?.route.name === 'performance', }], }, { title: i18n.ts.info, diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue deleted file mode 100644 index e090616b26..0000000000 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ /dev/null @@ -1,84 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<MkStickyContainer> - <template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> - <FormSuspense :p="init"> - <template v-if="tab === 'block'"> - <MkTextarea v-model="blockedHosts"> - <span>{{ i18n.ts.blockedInstances }}</span> - <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> - </MkTextarea> - </template> - <template v-else-if="tab === 'silence'"> - <MkTextarea v-model="silencedHosts" class="_formBlock"> - <span>{{ i18n.ts.silencedInstances }}</span> - <template #caption>{{ i18n.ts.silencedInstancesDescription }}</template> - </MkTextarea> - <MkTextarea v-model="mediaSilencedHosts" class="_formBlock"> - <span>{{ i18n.ts.mediaSilencedInstances }}</span> - <template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template> - </MkTextarea> - </template> - <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - </FormSuspense> - </MkSpacer> -</MkStickyContainer> -</template> - -<script lang="ts" setup> -import { ref, computed } from 'vue'; -import XHeader from './_header_.vue'; -import MkButton from '@/components/MkButton.vue'; -import MkTextarea from '@/components/MkTextarea.vue'; -import FormSuspense from '@/components/form/suspense.vue'; -import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { fetchInstance } from '@/instance.js'; -import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; - -const blockedHosts = ref<string>(''); -const silencedHosts = ref<string>(''); -const mediaSilencedHosts = ref<string>(''); -const tab = ref('block'); - -async function init() { - const meta = await misskeyApi('admin/meta'); - blockedHosts.value = meta.blockedHosts.join('\n'); - silencedHosts.value = meta.silencedHosts.join('\n'); - mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n'); -} - -function save() { - os.apiWithDialog('admin/update-meta', { - blockedHosts: blockedHosts.value.split('\n') || [], - silencedHosts: silencedHosts.value.split('\n') || [], - mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [], - - }).then(() => { - fetchInstance(true); - }); -} - -const headerActions = computed(() => []); - -const headerTabs = computed(() => [{ - key: 'block', - title: i18n.ts.block, - icon: 'ti ti-ban', -}, { - key: 'silence', - title: i18n.ts.silence, - icon: 'ti ti-eye-off', -}]); - -definePageMetadata(() => ({ - title: i18n.ts.instanceBlocking, - icon: 'ti ti-ban', -})); -</script> diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 0a5b06a969..bbcf2a6f77 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -10,76 +10,130 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <div class="_gaps_m"> - <MkSwitch v-model="enableRegistration"> + <MkSwitch v-model="enableRegistration" @change="onChange_enableRegistration"> <template #label>{{ i18n.ts.enableRegistration }}</template> </MkSwitch> - <MkSwitch v-model="emailRequiredForSignup"> + <MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup"> <template #label>{{ i18n.ts.emailRequiredForSignup }}</template> </MkSwitch> - <MkSwitch v-model="approvalRequiredForSignup"> + <MkSwitch v-model="approvalRequiredForSignup" @change="onChange_approvalRequiredForSignup"> <template #label>{{ i18n.ts.approvalRequiredForSignup }}</template> </MkSwitch> <FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink> - <MkInput v-model="tosUrl" type="url"> - <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts.tosUrl }}</template> - </MkInput> - - <MkInput v-model="privacyPolicyUrl" type="url"> - <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts.privacyPolicyUrl }}</template> - </MkInput> - - <MkTextarea v-if="bubbleTimelineEnabled" v-model="bubbleTimeline"> + <MkFolder v-if="bubbleTimelineEnabled"> + <template #icon><i class="ph-drop ph-bold ph-lg"></i></template> <template #label>Bubble timeline</template> - <template #caption>Choose which instances should be displayed in the bubble.</template> - </MkTextarea> - <MkInput v-model="inquiryUrl" type="url"> + <div class="_gaps"> + <MkTextarea v-model="bubbleTimeline"> + <template #caption>Choose which instances should be displayed in the bubble.</template> + </MkTextarea> + <MkButton primary @click="save_bubbleTimeline">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts._serverSettings.inquiryUrl }}</template> - <template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template> - </MkInput> + <template #label>{{ i18n.ts.trustedLinkUrlPatterns }}</template> - <MkTextarea v-model="preservedUsernames"> + <div class="_gaps"> + <MkTextarea v-model="trustedLinkUrlPatterns"> + <template #caption>{{ i18n.ts.trustedLinkUrlPatternsDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_trustedLinkUrlPatterns">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-lock-star"></i></template> <template #label>{{ i18n.ts.preservedUsernames }}</template> - <template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template> - </MkTextarea> - <MkTextarea v-model="trustedLinkUrlPatterns"> - <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts.trustedLinkUrlPatterns }}</template> - <template #caption>{{ i18n.ts.trustedLinkUrlPatternsDescription }}</template> - </MkTextarea> + <div class="_gaps"> + <MkTextarea v-model="preservedUsernames"> + <template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_preservedUsernames">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> - <MkTextarea v-model="sensitiveWords"> + <MkFolder> + <template #icon><i class="ti ti-message-exclamation"></i></template> <template #label>{{ i18n.ts.sensitiveWords }}</template> - <template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template> - </MkTextarea> - <MkTextarea v-model="prohibitedWords"> + <div class="_gaps"> + <MkTextarea v-model="sensitiveWords"> + <template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template> + </MkTextarea> + <MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-message-x"></i></template> <template #label>{{ i18n.ts.prohibitedWords }}</template> - <template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template> - </MkTextarea> - <MkTextarea v-model="hiddenTags"> + <div class="_gaps"> + <MkTextarea v-model="prohibitedWords"> + <template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template> + </MkTextarea> + <MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-eye-off"></i></template> <template #label>{{ i18n.ts.hiddenTags }}</template> - <template #caption>{{ i18n.ts.hiddenTagsDescription }}</template> - </MkTextarea> + + <div class="_gaps"> + <MkTextarea v-model="hiddenTags"> + <template #caption>{{ i18n.ts.hiddenTagsDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_hiddenTags">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-eye-off"></i></template> + <template #label>{{ i18n.ts.silencedInstances }}</template> + + <div class="_gaps"> + <MkTextarea v-model="silencedHosts"> + <template #caption>{{ i18n.ts.silencedInstancesDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_silencedHosts">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-eye-off"></i></template> + <template #label>{{ i18n.ts.mediaSilencedInstances }}</template> + + <div class="_gaps"> + <MkTextarea v-model="mediaSilencedHosts"> + <template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_mediaSilencedHosts">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-ban"></i></template> + <template #label>{{ i18n.ts.blockedInstances }}</template> + + <div class="_gaps"> + <MkTextarea v-model="blockedHosts"> + <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_blockedHosts">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> </div> </FormSuspense> </MkSpacer> - <template #footer> - <div :class="$style.footer"> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> - </MkSpacer> - </div> - </template> </MkStickyContainer> </div> </template> @@ -98,6 +152,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import FormLink from '@/components/form/link.vue'; +import MkFolder from '@/components/MkFolder.vue'; const enableRegistration = ref<boolean>(false); const emailRequiredForSignup = ref<boolean>(false); @@ -108,10 +163,10 @@ const prohibitedWords = ref<string>(''); const hiddenTags = ref<string>(''); const preservedUsernames = ref<string>(''); const bubbleTimeline = ref<string>(''); -const tosUrl = ref<string | null>(null); -const privacyPolicyUrl = ref<string | null>(null); -const inquiryUrl = ref<string | null>(null); const trustedLinkUrlPatterns = ref<string>(''); +const blockedHosts = ref<string>(''); +const silencedHosts = ref<string>(''); +const mediaSilencedHosts = ref<string>(''); async function init() { const meta = await misskeyApi('admin/meta'); @@ -122,28 +177,105 @@ async function init() { prohibitedWords.value = meta.prohibitedWords.join('\n'); hiddenTags.value = meta.hiddenTags.join('\n'); preservedUsernames.value = meta.preservedUsernames.join('\n'); - tosUrl.value = meta.tosUrl; - privacyPolicyUrl.value = meta.privacyPolicyUrl; bubbleTimeline.value = meta.bubbleInstances.join('\n'); bubbleTimelineEnabled.value = meta.policies.btlAvailable; - inquiryUrl.value = meta.inquiryUrl; trustedLinkUrlPatterns.value = meta.trustedLinkUrlPatterns.join('\n'); + blockedHosts.value = meta.blockedHosts.join('\n'); + silencedHosts.value = meta.silencedHosts.join('\n'); + mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n'); +} + +function onChange_enableRegistration(value: boolean) { + os.apiWithDialog('admin/update-meta', { + disableRegistration: !value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_emailRequiredForSignup(value: boolean) { + os.apiWithDialog('admin/update-meta', { + emailRequiredForSignup: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_approvalRequiredForSignup(value: boolean) { + os.apiWithDialog('admin/update-meta', { + approvalRequiredForSignup: value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_bubbleTimeline() { + os.apiWithDialog('admin/update-meta', { + bubbleInstances: bubbleTimeline.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_trustedLinkUrlPatterns() { + os.apiWithDialog('admin/update-meta', { + trustedLinkUrlPatterns: trustedLinkUrlPatterns.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_preservedUsernames() { + os.apiWithDialog('admin/update-meta', { + preservedUsernames: preservedUsernames.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); } -function save() { +function save_sensitiveWords() { os.apiWithDialog('admin/update-meta', { - disableRegistration: !enableRegistration.value, - emailRequiredForSignup: emailRequiredForSignup.value, - approvalRequiredForSignup: approvalRequiredForSignup.value, - tosUrl: tosUrl.value, - privacyPolicyUrl: privacyPolicyUrl.value, - inquiryUrl: inquiryUrl.value, sensitiveWords: sensitiveWords.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_prohibitedWords() { + os.apiWithDialog('admin/update-meta', { prohibitedWords: prohibitedWords.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_hiddenTags() { + os.apiWithDialog('admin/update-meta', { hiddenTags: hiddenTags.value.split('\n'), - preservedUsernames: preservedUsernames.value.split('\n'), - bubbleInstances: bubbleTimeline.value.split('\n'), - trustedLinkUrlPatterns: trustedLinkUrlPatterns.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_blockedHosts() { + os.apiWithDialog('admin/update-meta', { + blockedHosts: blockedHosts.value.split('\n') || [], + }).then(() => { + fetchInstance(true); + }); +} + +function save_silencedHosts() { + os.apiWithDialog('admin/update-meta', { + silencedHosts: silencedHosts.value.split('\n') || [], + }).then(() => { + fetchInstance(true); + }); +} + +function save_mediaSilencedHosts() { + os.apiWithDialog('admin/update-meta', { + mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [], }).then(() => { fetchInstance(true); }); @@ -156,10 +288,3 @@ definePageMetadata(() => ({ icon: 'ti ti-shield', })); </script> - -<style lang="scss" module> -.footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); -} -</style> diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue deleted file mode 100644 index a92034f2d7..0000000000 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ /dev/null @@ -1,113 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<MkStickyContainer> - <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> - <FormSuspense :p="init"> - <div class="_gaps"> - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableServerMachineStats"> - <template #label>{{ i18n.ts.enableServerMachineStats }}</template> - <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> - </MkSwitch> - </div> - - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableAchievements"> - <template #label>{{ i18n.ts.enableAchievements }}</template> - <template #caption>{{ i18n.ts.turnOffAchievements}}</template> - </MkSwitch> - </div> - - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableBotTrending"> - <template #label>{{ i18n.ts.enableBotTrending }}</template> - <template #caption>{{ i18n.ts.turnOffBotTrending }}</template> - </MkSwitch> - </div> - - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableIdenticonGeneration"> - <template #label>{{ i18n.ts.enableIdenticonGeneration }}</template> - <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> - </MkSwitch> - </div> - - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableChartsForRemoteUser"> - <template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template> - <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> - </MkSwitch> - </div> - - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableChartsForFederatedInstances"> - <template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template> - <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> - </MkSwitch> - </div> - </div> - </FormSuspense> - </MkSpacer> -</MkStickyContainer> -</template> - -<script lang="ts" setup> -import { ref, computed } from 'vue'; -import XHeader from './_header_.vue'; -import FormSuspense from '@/components/form/suspense.vue'; -import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { fetchInstance } from '@/instance.js'; -import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; -import MkSwitch from '@/components/MkSwitch.vue'; - -const enableServerMachineStats = ref<boolean>(false); -const enableAchievements = ref<boolean>(false); -const enableBotTrending = ref<boolean>(false); -const enableIdenticonGeneration = ref<boolean>(false); -const enableChartsForRemoteUser = ref<boolean>(false); -const enableChartsForFederatedInstances = ref<boolean>(false); - -async function init() { - const meta = await misskeyApi('admin/meta'); - enableServerMachineStats.value = meta.enableServerMachineStats; - enableAchievements.value = meta.enableAchievements; - enableBotTrending.value = meta.enableBotTrending; - enableIdenticonGeneration.value = meta.enableIdenticonGeneration; - enableChartsForRemoteUser.value = meta.enableChartsForRemoteUser; - enableChartsForFederatedInstances.value = meta.enableChartsForFederatedInstances; -} - -function save() { - os.apiWithDialog('admin/update-meta', { - enableServerMachineStats: enableServerMachineStats.value, - enableAchievements: enableAchievements.value, - enableBotTrending: enableBotTrending.value, - enableIdenticonGeneration: enableIdenticonGeneration.value, - enableChartsForRemoteUser: enableChartsForRemoteUser.value, - enableChartsForFederatedInstances: enableChartsForFederatedInstances.value, - }).then(() => { - fetchInstance(true); - }); -} - -const headerActions = computed(() => [{ - asFullButton: true, - icon: 'ti ti-check', - text: i18n.ts.save, - handler: save, -}]); - -const headerTabs = computed(() => []); - -definePageMetadata(() => ({ - title: i18n.ts.other, - icon: 'ti ti-adjustments', -})); -</script> diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts b/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts new file mode 100644 index 0000000000..584cd3e4d9 --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { StoryObj } from '@storybook/vue3'; +import { http, HttpResponse } from 'msw'; +import { action } from '@storybook/addon-actions'; +import { commonHandlers } from '../../../.storybook/mocks.js'; +import overview_ap_requests from './overview.ap-requests.vue'; +export const Default = { + render(args) { + return { + components: { + overview_ap_requests, + }, + setup() { + return { + args, + }; + }, + template: '<overview_ap_requests />', + }; + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/charts/ap-request', async ({ request }) => { + action('POST /api/charts/ap-request')(await request.json()); + return HttpResponse.json({ + deliverFailed: [0, 0, 0, 2, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1, 0, 0, 0, 3, 1, 1, 2, 0, 0], + deliverSucceeded: [0, 1, 51, 34, 136, 189, 51, 17, 17, 34, 1, 17, 18, 51, 34, 68, 287, 0, 17, 33, 32, 96, 96, 0, 49, 64, 0, 32, 0, 32, 81, 48, 65, 1, 16, 50, 90, 148, 33, 43, 72, 127, 17, 138, 78, 91, 78, 91, 13, 52], + inboxReceived: [507, 1173, 1096, 871, 958, 937, 908, 1026, 956, 909, 807, 1002, 832, 995, 1039, 1047, 1109, 930, 711, 835, 764, 679, 835, 958, 634, 654, 691, 895, 811, 676, 1044, 1389, 1318, 863, 887, 952, 1011, 1061, 592, 900, 611, 595, 604, 562, 607, 621, 854, 666, 1197, 644], + }); + }), + ], + }, + }, +} satisfies StoryObj<typeof overview_ap_requests>; diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue index d4c83f21b6..4bbb9210af 100644 --- a/packages/frontend/src/pages/admin/overview.ap-requests.vue +++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; +import isChromatic from 'chromatic'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -41,7 +42,7 @@ const { handler: externalTooltipHandler } = useChartTooltip(); const { handler: externalTooltipHandler2 } = useChartTooltip(); onMounted(async () => { - const now = new Date(); + const now = isChromatic() ? new Date('2024-08-31T10:00:00Z') : new Date(); const getDate = (ago: number) => { const y = now.getFullYear(); @@ -51,14 +52,14 @@ onMounted(async () => { return new Date(y, m, d - ago); }; - const format = (arr) => { + const format = (arr: number[]) => { return arr.map((v, i) => ({ x: getDate(i).getTime(), y: v, })); }; - const formatMinus = (arr) => { + const formatMinus = (arr: number[]) => { return arr.map((v, i) => ({ x: getDate(i).getTime(), y: -v, @@ -78,7 +79,6 @@ onMounted(async () => { type: 'line', data: { datasets: [{ - stack: 'a', parsing: false, label: 'Out: Succ', data: format(raw.deliverSucceeded).slice().reverse(), @@ -92,7 +92,6 @@ onMounted(async () => { fill: true, clip: 8, }, { - stack: 'a', parsing: false, label: 'Out: Fail', data: formatMinus(raw.deliverFailed).slice().reverse(), @@ -137,7 +136,6 @@ onMounted(async () => { min: getDate(chartLimit).getTime(), }, y: { - stacked: true, position: 'left', suggestedMax: 10, grid: { @@ -171,6 +169,9 @@ onMounted(async () => { duration: 0, }, external: externalTooltipHandler, + callbacks: { + label: context => `${context.dataset.label}: ${Math.abs(context.parsed.y)}`, + }, }, gradient, }, diff --git a/packages/frontend/src/pages/admin/overview.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue index a09db2a6d5..292e2e1dbc 100644 --- a/packages/frontend/src/pages/admin/overview.instances.vue +++ b/packages/frontend/src/pages/admin/overview.instances.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue index c7478f252a..fb190f5325 100644 --- a/packages/frontend/src/pages/admin/overview.queue.vue +++ b/packages/frontend/src/pages/admin/overview.queue.vue @@ -36,7 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue'; +import * as Misskey from 'misskey-js'; import XChart from './overview.queue.chart.vue'; +import type { ApQueueDomain } from '@/pages/admin/queue.vue'; import number from '@/filters/number.js'; import { useStream } from '@/stream.js'; @@ -52,10 +54,10 @@ const chartDelayed = shallowRef<InstanceType<typeof XChart>>(); const chartWaiting = shallowRef<InstanceType<typeof XChart>>(); const props = defineProps<{ - domain: string; + domain: ApQueueDomain; }>(); -const onStats = (stats) => { +function onStats(stats: Misskey.entities.QueueStats) { activeSincePrevTick.value = stats[props.domain].activeSincePrevTick; active.value = stats[props.domain].active; delayed.value = stats[props.domain].delayed; @@ -65,13 +67,13 @@ const onStats = (stats) => { chartActive.value.pushData(stats[props.domain].active); chartDelayed.value.pushData(stats[props.domain].delayed); chartWaiting.value.pushData(stats[props.domain].waiting); -}; +} -const onStatsLog = (statsLog) => { - const dataProcess = []; - const dataActive = []; - const dataDelayed = []; - const dataWaiting = []; +function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) { + const dataProcess: Misskey.entities.QueueStats[ApQueueDomain]['activeSincePrevTick'][] = []; + const dataActive: Misskey.entities.QueueStats[ApQueueDomain]['active'][] = []; + const dataDelayed: Misskey.entities.QueueStats[ApQueueDomain]['delayed'][] = []; + const dataWaiting: Misskey.entities.QueueStats[ApQueueDomain]['waiting'][] = []; for (const stats of [...statsLog].reverse()) { dataProcess.push(stats[props.domain].activeSincePrevTick); @@ -84,7 +86,7 @@ const onStatsLog = (statsLog) => { chartActive.value.setData(dataActive); chartDelayed.value.setData(dataDelayed); chartWaiting.value.setData(dataWaiting); -}; +} onMounted(() => { connection.on('stats', onStats); diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue index 408be88d47..8c9d7a8197 100644 --- a/packages/frontend/src/pages/admin/overview.users.vue +++ b/packages/frontend/src/pages/admin/overview.users.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import { defaultStore } from '@/store.js'; @@ -47,14 +47,14 @@ useInterval(fetch, 1000 * 60, { .root { &:global { > .users { - .chart-move { - transition: transform 1s ease; - } - display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-gap: 12px; + .chart-move { + transition: transform 1s ease; + } + > .user:hover { text-decoration: none; } diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue new file mode 100644 index 0000000000..7e0a932f82 --- /dev/null +++ b/packages/frontend/src/pages/admin/performance.vue @@ -0,0 +1,193 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> + <div class="_gaps"> + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableServerMachineStats" @change="onChange_enableServerMachineStats"> + <template #label>{{ i18n.ts.enableServerMachineStats }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableIdenticonGeneration" @change="onChange_enableIdenticonGeneration"> + <template #label>{{ i18n.ts.enableIdenticonGeneration }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableChartsForRemoteUser" @change="onChange_enableChartsForRemoteUser"> + <template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances"> + <template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-bolt"></i></template> + <template #label>Misskey® Fan-out Timeline Technology™ (FTT)</template> + <template v-if="fttForm.savedState.enableFanoutTimeline" #suffix>Enabled</template> + <template v-else #suffix>Disabled</template> + <template v-if="fttForm.modified.value" #footer> + <MkFormFooter :form="fttForm"/> + </template> + + <div class="_gaps"> + <MkSwitch v-model="fttForm.state.enableFanoutTimeline"> + <template #label>{{ i18n.ts.enable }}<span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption> + <div>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</div> + <div><MkLink target="_blank" url="https://misskey-hub.net/docs/for-admin/features/ftt/">{{ i18n.ts.details }}</MkLink></div> + </template> + </MkSwitch> + + <template v-if="fttForm.state.enableFanoutTimeline"> + <MkSwitch v-model="fttForm.state.enableFanoutTimelineDbFallback"> + <template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}<span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template> + </MkSwitch> + + <MkInput v-model="fttForm.state.perLocalUserUserTimelineCacheMax" type="number"> + <template #label>perLocalUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkInput> + + <MkInput v-model="fttForm.state.perRemoteUserUserTimelineCacheMax" type="number"> + <template #label>perRemoteUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkInput> + + <MkInput v-model="fttForm.state.perUserHomeTimelineCacheMax" type="number"> + <template #label>perUserHomeTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkInput> + + <MkInput v-model="fttForm.state.perUserListTimelineCacheMax" type="number"> + <template #label>perUserListTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkInput> + </template> + </div> + </MkFolder> + + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-bolt"></i></template> + <template #label>Misskey® Reactions Boost Technology™ (RBT)<span class="_beta">{{ i18n.ts.beta }}</span></template> + <template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template> + <template v-else #suffix>Disabled</template> + <template v-if="rbtForm.modified.value" #footer> + <MkFormFooter :form="rbtForm"/> + </template> + + <div class="_gaps_m"> + <MkSwitch v-model="rbtForm.state.enableReactionsBuffering"> + <template #label>{{ i18n.ts.enable }}<span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</template> + </MkSwitch> + </div> + </MkFolder> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { ref, computed } from 'vue'; +import XHeader from './_header_.vue'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { fetchInstance } from '@/instance.js'; +import { i18n } from '@/i18n.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkLink from '@/components/MkLink.vue'; +import { useForm } from '@/scripts/use-form.js'; +import MkFormFooter from '@/components/MkFormFooter.vue'; + +const meta = await misskeyApi('admin/meta'); + +const enableServerMachineStats = ref(meta.enableServerMachineStats); +const enableIdenticonGeneration = ref(meta.enableIdenticonGeneration); +const enableChartsForRemoteUser = ref(meta.enableChartsForRemoteUser); +const enableChartsForFederatedInstances = ref(meta.enableChartsForFederatedInstances); + +function onChange_enableServerMachineStats(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableServerMachineStats: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableIdenticonGeneration(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableIdenticonGeneration: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableChartsForRemoteUser(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableChartsForRemoteUser: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableChartsForFederatedInstances(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableChartsForFederatedInstances: value, + }).then(() => { + fetchInstance(true); + }); +} + +const fttForm = useForm({ + enableFanoutTimeline: meta.enableFanoutTimeline, + enableFanoutTimelineDbFallback: meta.enableFanoutTimelineDbFallback, + perLocalUserUserTimelineCacheMax: meta.perLocalUserUserTimelineCacheMax, + perRemoteUserUserTimelineCacheMax: meta.perRemoteUserUserTimelineCacheMax, + perUserHomeTimelineCacheMax: meta.perUserHomeTimelineCacheMax, + perUserListTimelineCacheMax: meta.perUserListTimelineCacheMax, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableFanoutTimeline: state.enableFanoutTimeline, + enableFanoutTimelineDbFallback: state.enableFanoutTimelineDbFallback, + perLocalUserUserTimelineCacheMax: state.perLocalUserUserTimelineCacheMax, + perRemoteUserUserTimelineCacheMax: state.perRemoteUserUserTimelineCacheMax, + perUserHomeTimelineCacheMax: state.perUserHomeTimelineCacheMax, + perUserListTimelineCacheMax: state.perUserListTimelineCacheMax, + }); + fetchInstance(true); +}); + +const rbtForm = useForm({ + enableReactionsBuffering: meta.enableReactionsBuffering, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableReactionsBuffering: state.enableReactionsBuffering, + }); + fetchInstance(true); +}); + +const headerActions = computed(() => []); + +const headerTabs = computed(() => []); + +definePageMetadata(() => ({ + title: i18n.ts.other, + icon: 'ti ti-adjustments', +})); +</script> diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue deleted file mode 100644 index 81db9f1da9..0000000000 --- a/packages/frontend/src/pages/admin/proxy-account.vue +++ /dev/null @@ -1,71 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> - <FormSuspense :p="init"> - <MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo> - <MkKeyValue> - <template #key>{{ i18n.ts.proxyAccount }}</template> - <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template> - </MkKeyValue> - - <MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton> - </FormSuspense> - </MkSpacer> -</MkStickyContainer> -</template> - -<script lang="ts" setup> -import { ref, computed } from 'vue'; -import * as Misskey from 'misskey-js'; -import MkKeyValue from '@/components/MkKeyValue.vue'; -import MkButton from '@/components/MkButton.vue'; -import MkInfo from '@/components/MkInfo.vue'; -import FormSuspense from '@/components/form/suspense.vue'; -import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { fetchInstance } from '@/instance.js'; -import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; - -const proxyAccount = ref<Misskey.entities.UserDetailed | null>(null); -const proxyAccountId = ref<string | null>(null); - -async function init() { - const meta = await misskeyApi('admin/meta'); - proxyAccountId.value = meta.proxyAccountId; - if (proxyAccountId.value) { - proxyAccount.value = await misskeyApi('users/show', { userId: proxyAccountId.value }); - } -} - -function chooseProxyAccount() { - os.selectUser({ localOnly: true }).then(user => { - proxyAccount.value = user; - proxyAccountId.value = user.id; - save(); - }); -} - -function save() { - os.apiWithDialog('admin/update-meta', { - proxyAccountId: proxyAccountId.value, - }).then(() => { - fetchInstance(true); - }); -} - -const headerActions = computed(() => []); - -const headerTabs = computed(() => []); - -definePageMetadata(() => ({ - title: i18n.ts.proxyAccount, - icon: 'ti ti-ghost', -})); -</script> diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue index 8d3fe35320..960a263a86 100644 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -49,7 +49,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue'; +import * as Misskey from 'misskey-js'; import XChart from './queue.chart.chart.vue'; +import type { ApQueueDomain } from '@/pages/admin/queue.vue'; import number from '@/filters/number.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; @@ -62,17 +64,17 @@ const activeSincePrevTick = ref(0); const active = ref(0); const delayed = ref(0); const waiting = ref(0); -const jobs = ref<(string | number)[][]>([]); +const jobs = ref<Misskey.Endpoints[`admin/queue/${ApQueueDomain}-delayed`]['res']>([]); const chartProcess = shallowRef<InstanceType<typeof XChart>>(); const chartActive = shallowRef<InstanceType<typeof XChart>>(); const chartDelayed = shallowRef<InstanceType<typeof XChart>>(); const chartWaiting = shallowRef<InstanceType<typeof XChart>>(); const props = defineProps<{ - domain: string; + domain: ApQueueDomain; }>(); -const onStats = (stats) => { +function onStats(stats: Misskey.entities.QueueStats) { activeSincePrevTick.value = stats[props.domain].activeSincePrevTick; active.value = stats[props.domain].active; delayed.value = stats[props.domain].delayed; @@ -82,13 +84,13 @@ const onStats = (stats) => { chartActive.value.pushData(stats[props.domain].active); chartDelayed.value.pushData(stats[props.domain].delayed); chartWaiting.value.pushData(stats[props.domain].waiting); -}; +} -const onStatsLog = (statsLog) => { - const dataProcess = []; - const dataActive = []; - const dataDelayed = []; - const dataWaiting = []; +function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) { + const dataProcess: Misskey.entities.QueueStats[ApQueueDomain]['activeSincePrevTick'][] = []; + const dataActive: Misskey.entities.QueueStats[ApQueueDomain]['active'][] = []; + const dataDelayed: Misskey.entities.QueueStats[ApQueueDomain]['delayed'][] = []; + const dataWaiting: Misskey.entities.QueueStats[ApQueueDomain]['waiting'][] = []; for (const stats of [...statsLog].reverse()) { dataProcess.push(stats[props.domain].activeSincePrevTick); @@ -101,14 +103,12 @@ const onStatsLog = (statsLog) => { chartActive.value.setData(dataActive); chartDelayed.value.setData(dataDelayed); chartWaiting.value.setData(dataWaiting); -}; +} onMounted(() => { - if (props.domain === 'inbox' || props.domain === 'deliver') { - misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => { - jobs.value = result; - }); - } + misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => { + jobs.value = result; + }); connection.on('stats', onStats); connection.on('statsLog', onStatsLog); diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue index 8d77d927d7..512039242e 100644 --- a/packages/frontend/src/pages/admin/queue.vue +++ b/packages/frontend/src/pages/admin/queue.vue @@ -16,16 +16,18 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, type Ref } from 'vue'; import XQueue from './queue.chart.vue'; import XHeader from './_header_.vue'; import * as os from '@/os.js'; -import * as config from '@/config.js'; +import * as config from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; -const tab = ref('deliver'); +export type ApQueueDomain = 'deliver' | 'inbox'; + +const tab: Ref<ApQueueDomain> = ref('deliver'); function clear() { os.confirm({ diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 8200244cd7..1763db2323 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -630,6 +630,106 @@ SPDX-License-Identifier: AGPL-3.0-only </MkRange> </div> </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportAntennas, 'canImportAntennas'])"> + <template #label>{{ i18n.ts._role._options.canImportAntennas }}</template> + <template #suffix> + <span v-if="role.policies.canImportAntennas.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canImportAntennas.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportAntennas)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canImportAntennas.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canImportAntennas.value" :disabled="role.policies.canImportAntennas.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canImportAntennas.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportBlocking, 'canImportBlocking'])"> + <template #label>{{ i18n.ts._role._options.canImportBlocking }}</template> + <template #suffix> + <span v-if="role.policies.canImportBlocking.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canImportBlocking.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportBlocking)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canImportBlocking.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canImportBlocking.value" :disabled="role.policies.canImportBlocking.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canImportBlocking.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportFollowing, 'canImportFollowing'])"> + <template #label>{{ i18n.ts._role._options.canImportFollowing }}</template> + <template #suffix> + <span v-if="role.policies.canImportFollowing.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canImportFollowing.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportFollowing)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canImportFollowing.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canImportFollowing.value" :disabled="role.policies.canImportFollowing.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canImportFollowing.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportMuting, 'canImportMuting'])"> + <template #label>{{ i18n.ts._role._options.canImportMuting }}</template> + <template #suffix> + <span v-if="role.policies.canImportMuting.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canImportMuting.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportMuting)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canImportMuting.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canImportMuting.value" :disabled="role.policies.canImportMuting.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canImportMuting.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportUserLists, 'canImportUserLists'])"> + <template #label>{{ i18n.ts._role._options.canImportUserLists }}</template> + <template #suffix> + <span v-if="role.policies.canImportUserLists.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canImportUserLists.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportUserLists)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canImportUserLists.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canImportUserLists.value" :disabled="role.policies.canImportUserLists.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canImportUserLists.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> </div> </FormSlot> </div> @@ -648,7 +748,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkRange from '@/components/MkRange.vue'; import FormSlot from '@/components/form/slot.vue'; import { i18n } from '@/i18n.js'; -import { ROLE_POLICIES } from '@/const.js'; +import { ROLE_POLICIES } from '@@/js/const.js'; import { instance } from '@/instance.js'; import { deepClone } from '@/scripts/clone.js'; diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 0a8bd0e898..00a25446ab 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -11,6 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <MkFolder> <template #label>{{ i18n.ts._role.baseRole }}</template> + <template #footer> + <MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton> + </template> <div class="_gaps_s"> <MkInput v-model="baseRoleQ" type="search"> <template #prefix><i class="ti ti-search"></i></template> @@ -233,7 +236,45 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> </MkFolder> - <MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportAntennas, 'canImportAntennas'])"> + <template #label>{{ i18n.ts._role._options.canImportAntennas }}</template> + <template #suffix>{{ policies.canImportAntennas ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canImportAntennas"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportBlocking, 'canImportBlocking'])"> + <template #label>{{ i18n.ts._role._options.canImportBlocking }}</template> + <template #suffix>{{ policies.canImportBlocking ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canImportBlocking"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportFollowing, 'canImportFollowing'])"> + <template #label>{{ i18n.ts._role._options.canImportFollowing }}</template> + <template #suffix>{{ policies.canImportFollowing ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canImportFollowing"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportMuting, 'canImportMuting'])"> + <template #label>{{ i18n.ts._role._options.canImportMuting }}</template> + <template #suffix>{{ policies.canImportMuting ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canImportMuting"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportUserLists, 'canImportUserList'])"> + <template #label>{{ i18n.ts._role._options.canImportUserLists }}</template> + <template #suffix>{{ policies.canImportUserLists ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canImportUserLists"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> </div> </MkFolder> <MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton> @@ -259,6 +300,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, reactive, ref } from 'vue'; +import { ROLE_POLICIES } from '@@/js/const.js'; import XHeader from './_header_.vue'; import MkInput from '@/components/MkInput.vue'; import MkFolder from '@/components/MkFolder.vue'; @@ -273,7 +315,6 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { instance, fetchInstance } from '@/instance.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import { ROLE_POLICIES } from '@/const.js'; import { useRouter } from '@/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index 2e80622d7a..4358821092 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -7,76 +7,71 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> - <FormSuspense :p="init"> - <div class="_gaps_m"> - <MkFolder> - <template #icon><i class="ti ti-shield"></i></template> - <template #label>{{ i18n.ts.botProtection }}</template> - <template v-if="enableHcaptcha" #suffix>hCaptcha</template> - <template v-else-if="enableMcaptcha" #suffix>mCaptcha</template> - <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> - <template v-else-if="enableTurnstile" #suffix>Turnstile</template> - <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> + <div class="_gaps_m"> + <XBotProtection/> - <XBotProtection/> - </MkFolder> + <MkFolder> + <template #label>Active Email Validation</template> + <template v-if="emailValidationForm.savedState.enableActiveEmailValidation" #suffix>Enabled</template> + <template v-else #suffix>Disabled</template> + <template v-if="emailValidationForm.modified.value" #footer> + <MkFormFooter :form="emailValidationForm"/> + </template> - <MkFolder> - <template #label>Active Email Validation</template> - <template v-if="enableActiveEmailValidation" #suffix>Enabled</template> - <template v-else #suffix>Disabled</template> + <div class="_gaps_m"> + <span>{{ i18n.ts.activeEmailValidationDescription }}</span> + <MkSwitch v-model="emailValidationForm.state.enableActiveEmailValidation"> + <template #label>Enable</template> + </MkSwitch> + <MkSwitch v-model="emailValidationForm.state.enableVerifymailApi"> + <template #label>Use Verifymail.io API</template> + </MkSwitch> + <MkInput v-model="emailValidationForm.state.verifymailAuthKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>Verifymail.io API Auth Key</template> + </MkInput> + <MkSwitch v-model="emailValidationForm.state.enableTruemailApi"> + <template #label>Use TrueMail API</template> + </MkSwitch> + <MkInput v-model="emailValidationForm.state.truemailInstance"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>TrueMail API Instance</template> + </MkInput> + <MkInput v-model="emailValidationForm.state.truemailAuthKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>TrueMail API Auth Key</template> + </MkInput> + </div> + </MkFolder> - <div class="_gaps_m"> - <span>{{ i18n.ts.activeEmailValidationDescription }}</span> - <MkSwitch v-model="enableActiveEmailValidation"> - <template #label>Enable</template> - </MkSwitch> - <MkSwitch v-model="enableVerifymailApi"> - <template #label>Use Verifymail.io API</template> - </MkSwitch> - <MkInput v-model="verifymailAuthKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>Verifymail.io API Auth Key</template> - </MkInput> - <MkSwitch v-model="enableTruemailApi"> - <template #label>Use TrueMail API</template> - </MkSwitch> - <MkInput v-model="truemailInstance"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>TrueMail API Instance</template> - </MkInput> - <MkInput v-model="truemailAuthKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>TrueMail API Auth Key</template> - </MkInput> - <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - </div> - </MkFolder> + <MkFolder> + <template #label>Banned Email Domains</template> + <template v-if="bannedEmailDomainsForm.modified.value" #footer> + <MkFormFooter :form="bannedEmailDomainsForm"/> + </template> - <MkFolder> - <template #label>Banned Email Domains</template> + <div class="_gaps_m"> + <MkTextarea v-model="bannedEmailDomainsForm.state.bannedEmailDomains"> + <template #label>Banned Email Domains List</template> + </MkTextarea> + </div> + </MkFolder> - <div class="_gaps_m"> - <MkTextarea v-model="bannedEmailDomains"> - <template #label>Banned Email Domains List</template> - </MkTextarea> - <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - </div> - </MkFolder> + <MkFolder> + <template #label>Log IP address</template> + <template v-if="ipLoggingForm.savedState.enableIpLogging" #suffix>Enabled</template> + <template v-else #suffix>Disabled</template> + <template v-if="ipLoggingForm.modified.value" #footer> + <MkFormFooter :form="ipLoggingForm"/> + </template> - <MkFolder> - <template #label>Log IP address</template> - <template v-if="enableIpLogging" #suffix>Enabled</template> - <template v-else #suffix>Disabled</template> - - <div class="_gaps_m"> - <MkSwitch v-model="enableIpLogging" @update:modelValue="save"> - <template #label>Enable</template> - </MkSwitch> - </div> - </MkFolder> - </div> - </FormSuspense> + <div class="_gaps_m"> + <MkSwitch v-model="ipLoggingForm.state.enableIpLogging"> + <template #label>Enable</template> + </MkSwitch> + </div> + </MkFolder> + </div> </MkSpacer> </MkStickyContainer> </template> @@ -88,60 +83,55 @@ import XHeader from './_header_.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkSwitch from '@/components/MkSwitch.vue'; -import FormSuspense from '@/components/form/suspense.vue'; import MkRange from '@/components/MkRange.vue'; import MkInput from '@/components/MkInput.vue'; -import MkButton from '@/components/MkButton.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useForm } from '@/scripts/use-form.js'; +import MkFormFooter from '@/components/MkFormFooter.vue'; + +const meta = await misskeyApi('admin/meta'); -const enableHcaptcha = ref<boolean>(false); -const enableMcaptcha = ref<boolean>(false); -const enableRecaptcha = ref<boolean>(false); -const enableTurnstile = ref<boolean>(false); -const enableIpLogging = ref<boolean>(false); -const enableActiveEmailValidation = ref<boolean>(false); -const enableVerifymailApi = ref<boolean>(false); -const verifymailAuthKey = ref<string | null>(null); -const enableTruemailApi = ref<boolean>(false); -const truemailInstance = ref<string | null>(null); -const truemailAuthKey = ref<string | null>(null); -const bannedEmailDomains = ref<string>(''); +const ipLoggingForm = useForm({ + enableIpLogging: meta.enableIpLogging, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableIpLogging: state.enableIpLogging, + }); + fetchInstance(true); +}); -async function init() { - const meta = await misskeyApi('admin/meta'); - enableHcaptcha.value = meta.enableHcaptcha; - enableMcaptcha.value = meta.enableMcaptcha; - enableRecaptcha.value = meta.enableRecaptcha; - enableTurnstile.value = meta.enableTurnstile; - enableIpLogging.value = meta.enableIpLogging; - enableActiveEmailValidation.value = meta.enableActiveEmailValidation; - enableVerifymailApi.value = meta.enableVerifymailApi; - verifymailAuthKey.value = meta.verifymailAuthKey; - enableTruemailApi.value = meta.enableTruemailApi; - truemailInstance.value = meta.truemailInstance; - truemailAuthKey.value = meta.truemailAuthKey; - bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || ''; -} +const emailValidationForm = useForm({ + enableActiveEmailValidation: meta.enableActiveEmailValidation, + enableVerifymailApi: meta.enableVerifymailApi, + verifymailAuthKey: meta.verifymailAuthKey, + enableTruemailApi: meta.enableTruemailApi, + truemailInstance: meta.truemailInstance, + truemailAuthKey: meta.truemailAuthKey, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableActiveEmailValidation: state.enableActiveEmailValidation, + enableVerifymailApi: state.enableVerifymailApi, + verifymailAuthKey: state.verifymailAuthKey, + enableTruemailApi: state.enableTruemailApi, + truemailInstance: state.truemailInstance, + truemailAuthKey: state.truemailAuthKey, + }); + fetchInstance(true); +}); -function save() { - os.apiWithDialog('admin/update-meta', { - enableIpLogging: enableIpLogging.value, - enableActiveEmailValidation: enableActiveEmailValidation.value, - enableVerifymailApi: enableVerifymailApi.value, - verifymailAuthKey: verifymailAuthKey.value, - enableTruemailApi: enableTruemailApi.value, - truemailInstance: truemailInstance.value, - truemailAuthKey: truemailAuthKey.value, - bannedEmailDomains: bannedEmailDomains.value.split('\n'), - }).then(() => { - fetchInstance(true); +const bannedEmailDomainsForm = useForm({ + bannedEmailDomains: meta.bannedEmailDomains?.join('\n') || '', +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + bannedEmailDomains: state.bannedEmailDomains.split('\n'), }); -} + fetchInstance(true); +}); const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 90ac259c53..bbc1d308cf 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -8,178 +8,215 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer> <template #header><XHeader :tabs="headerTabs"/></template> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> - <FormSuspense :p="init"> - <div class="_gaps_m"> - <MkInput v-model="name"> - <template #label>{{ i18n.ts.instanceName }}</template> - </MkInput> + <div class="_gaps_m"> + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-info-circle"></i></template> + <template #label>{{ i18n.ts.info }}</template> + <template v-if="infoForm.modified.value" #footer> + <MkFormFooter :form="infoForm"/> + </template> - <MkInput v-model="shortName"> - <template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})</template> - <template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template> - </MkInput> - - <MkTextarea v-model="description"> - <template #label>{{ i18n.ts.instanceDescription }}</template> - </MkTextarea> - - <FormSplit :minWidth="300"> - <MkInput v-model="maintainerName"> - <template #label>{{ i18n.ts.maintainerName }}</template> + <div class="_gaps"> + <MkInput v-model="infoForm.state.name"> + <template #label>{{ i18n.ts.instanceName }}<span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template> </MkInput> - <MkInput v-model="maintainerEmail" type="email"> - <template #prefix><i class="ti ti-mail"></i></template> - <template #label>{{ i18n.ts.maintainerEmail }}</template> + <MkInput v-model="infoForm.state.shortName"> + <template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template> </MkInput> - </FormSplit> - <MkInput v-model="repositoryUrl" type="url"> - <template #label>{{ i18n.ts.repositoryUrl }}</template> - <template #prefix><i class="ti ti-link"></i></template> - <template #caption>{{ i18n.ts.repositoryUrlDescription }}</template> - </MkInput> + <MkTextarea v-model="infoForm.state.description"> + <template #label>{{ i18n.ts.instanceDescription }}<span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkTextarea> - <MkInfo v-if="!instance.providesTarball && !repositoryUrl" warn> - {{ i18n.ts.repositoryUrlOrTarballRequired }} - </MkInfo> + <FormSplit :minWidth="300"> + <MkInput v-model="infoForm.state.maintainerName"> + <template #label>{{ i18n.ts.maintainerName }}<span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkInput> - <MkInput v-model="impressumUrl" type="url"> - <template #label>{{ i18n.ts.impressumUrl }}</template> - <template #prefix><i class="ti ti-link"></i></template> - <template #caption>{{ i18n.ts.impressumDescription }}</template> - </MkInput> + <MkInput v-model="infoForm.state.maintainerEmail" type="email"> + <template #label>{{ i18n.ts.maintainerEmail }}<span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #prefix><i class="ti ti-mail"></i></template> + </MkInput> + </FormSplit> - <MkInput v-model="donationUrl" type="url"> - <template #label>{{ i18n.ts.donationUrl }}</template> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> - </MkInput> + <MkInput v-model="infoForm.state.tosUrl" type="url"> + <template #label>{{ i18n.ts.tosUrl }}<span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #prefix><i class="ti ti-link"></i></template> + </MkInput> - <MkTextarea v-model="pinnedUsers"> - <template #label>{{ i18n.ts.pinnedUsers }}</template> - <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> - </MkTextarea> + <MkInput v-model="infoForm.state.privacyPolicyUrl" type="url"> + <template #label>{{ i18n.ts.privacyPolicyUrl }}<span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #prefix><i class="ti ti-link"></i></template> + </MkInput> - <FormSection> - <template #label>{{ i18n.ts.files }}</template> + <MkInput v-model="infoForm.state.inquiryUrl" type="url"> + <template #label>{{ i18n.ts._serverSettings.inquiryUrl }}<span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template> + <template #prefix><i class="ti ti-link"></i></template> + </MkInput> - <div class="_gaps_m"> - <MkSwitch v-model="cacheRemoteFiles"> - <template #label>{{ i18n.ts.cacheRemoteFiles }}</template> - <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template> - </MkSwitch> + <MkInput v-model="infoForm.state.repositoryUrl" type="url"> + <template #label>{{ i18n.ts.repositoryUrl }}<span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.repositoryUrlDescription }}</template> + <template #prefix><i class="ti ti-link"></i></template> + </MkInput> - <template v-if="cacheRemoteFiles"> - <MkSwitch v-model="cacheRemoteSensitiveFiles"> - <template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}</template> - <template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template> - </MkSwitch> - </template> - </div> - </FormSection> + <MkInfo v-if="!instance.providesTarball && !infoForm.state.repositoryUrl" warn> + {{ i18n.ts.repositoryUrlOrTarballRequired }} + </MkInfo> - <FormSection> - <template #label>ServiceWorker</template> + <MkInput v-model="infoForm.state.impressumUrl" type="url"> + <template #label>{{ i18n.ts.impressumUrl }}<span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.impressumDescription }}</template> + <template #prefix><i class="ti ti-link"></i></template> + </MkInput> - <div class="_gaps_m"> - <MkSwitch v-model="enableServiceWorker"> - <template #label>{{ i18n.ts.enableServiceworker }}</template> - <template #caption>{{ i18n.ts.serviceworkerInfo }}</template> - </MkSwitch> + <MkInput v-model="infoForm.state.donationUrl" type="url"> + <template #label>{{ i18n.ts.donationUrl }}</template> + <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + </MkInput> + </div> + </MkFolder> - <template v-if="enableServiceWorker"> - <MkInput v-model="swPublicKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>Public key</template> - </MkInput> + <MkFolder> + <template #icon><i class="ti ti-user-star"></i></template> + <template #label>{{ i18n.ts.pinnedUsers }}</template> + <template v-if="pinnedUsersForm.modified.value" #footer> + <MkFormFooter :form="pinnedUsersForm"/> + </template> - <MkInput v-model="swPrivateKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>Private key</template> - </MkInput> - </template> - </div> - </FormSection> + <MkTextarea v-model="pinnedUsersForm.state.pinnedUsers"> + <template #label>{{ i18n.ts.pinnedUsers }}<span v-if="pinnedUsersForm.modifiedStates.pinnedUsers" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> + </MkTextarea> + </MkFolder> - <FormSection> - <template #label>Misskey® Fan-out Timeline Technology™ (FTT)</template> + <MkFolder> + <template #icon><i class="ti ti-cloud"></i></template> + <template #label>{{ i18n.ts.files }}</template> + <template v-if="filesForm.modified.value" #footer> + <MkFormFooter :form="filesForm"/> + </template> - <div class="_gaps_m"> - <MkSwitch v-model="enableFanoutTimeline"> - <template #label>{{ i18n.ts.enable }}</template> - <template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</template> - </MkSwitch> + <div class="_gaps"> + <MkSwitch v-model="filesForm.state.cacheRemoteFiles"> + <template #label>{{ i18n.ts.cacheRemoteFiles }}<span v-if="filesForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template> + </MkSwitch> - <MkSwitch v-model="enableFanoutTimelineDbFallback"> - <template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}</template> - <template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template> + <template v-if="filesForm.state.cacheRemoteFiles"> + <MkSwitch v-model="filesForm.state.cacheRemoteSensitiveFiles"> + <template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}<span v-if="filesForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template> </MkSwitch> + </template> + </div> + </MkFolder> - <MkInput v-model="perLocalUserUserTimelineCacheMax" type="number"> - <template #label>perLocalUserUserTimelineCacheMax</template> - </MkInput> + <MkFolder> + <template #icon><i class="ti ti-world-cog"></i></template> + <template #label>ServiceWorker</template> + <template v-if="serviceWorkerForm.modified.value" #footer> + <MkFormFooter :form="serviceWorkerForm"/> + </template> - <MkInput v-model="perRemoteUserUserTimelineCacheMax" type="number"> - <template #label>perRemoteUserUserTimelineCacheMax</template> - </MkInput> + <div class="_gaps"> + <MkSwitch v-model="serviceWorkerForm.state.enableServiceWorker"> + <template #label>{{ i18n.ts.enableServiceworker }}<span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.serviceworkerInfo }}</template> + </MkSwitch> - <MkInput v-model="perUserHomeTimelineCacheMax" type="number"> - <template #label>perUserHomeTimelineCacheMax</template> + <template v-if="serviceWorkerForm.state.enableServiceWorker"> + <MkInput v-model="serviceWorkerForm.state.swPublicKey"> + <template #label>Public key<span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #prefix><i class="ti ti-key"></i></template> </MkInput> - <MkInput v-model="perUserListTimelineCacheMax" type="number"> - <template #label>perUserListTimelineCacheMax</template> + <MkInput v-model="serviceWorkerForm.state.swPrivateKey"> + <template #label>Private key<span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #prefix><i class="ti ti-key"></i></template> </MkInput> - </div> - </FormSection> + </template> + </div> + </MkFolder> - <FormSection> - <template #label>{{ i18n.ts._ad.adsSettings }}</template> + <MkFolder> + <template #icon><i class="ph-faders ph-bold ph-lg ti-fw"></i></template> + <template #label>{{ i18n.ts.otherSettings }}</template> + <template v-if="otherForm.modified.value" #footer> + <MkFormFooter :form="otherForm"/> + </template> - <div class="_gaps_m"> - <div class="_gaps_s"> - <MkInput v-model="notesPerOneAd" :min="0" type="number"> - <template #label>{{ i18n.ts._ad.notesPerOneAd }}</template> - <template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template> - </MkInput> - <MkInfo v-if="notesPerOneAd > 0 && notesPerOneAd < 20" :warn="true"> - {{ i18n.ts._ad.adsTooClose }} - </MkInfo> - </div> + <div class="_gaps"> + <MkSwitch v-model="otherForm.state.enableAchievements"> + <template #label>{{ i18n.ts.enableAchievements }}<span v-if="otherForm.modifiedStates.enableAchievements" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.turnOffAchievements}}</template> + </MkSwitch> + + <MkSwitch v-model="otherForm.state.enableBotTrending"> + <template #label>{{ i18n.ts.enableBotTrending }}<span v-if="otherForm.modifiedStates.enableBotTrending" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.turnOffBotTrending }}</template> + </MkSwitch> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-ad"></i></template> + <template #label>{{ i18n.ts._ad.adsSettings }}</template> + <template v-if="adForm.modified.value" #footer> + <MkFormFooter :form="adForm"/> + </template> + + <div class="_gaps"> + <div class="_gaps_s"> + <MkInput v-model="adForm.state.notesPerOneAd" :min="0" type="number"> + <template #label>{{ i18n.ts._ad.notesPerOneAd }}<span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template> + </MkInput> + <MkInfo v-if="adForm.state.notesPerOneAd > 0 && adForm.state.notesPerOneAd < 20" :warn="true"> + {{ i18n.ts._ad.adsTooClose }} + </MkInfo> </div> - </FormSection> + </div> + </MkFolder> - <FormSection> - <template #label>{{ i18n.ts._urlPreviewSetting.title }}</template> + <MkFolder> + <template #icon><i class="ti ti-world-search"></i></template> + <template #label>{{ i18n.ts._urlPreviewSetting.title }}</template> + <template v-if="urlPreviewForm.modified.value" #footer> + <MkFormFooter :form="urlPreviewForm"/> + </template> - <div class="_gaps_m"> - <MkSwitch v-model="urlPreviewEnabled"> - <template #label>{{ i18n.ts._urlPreviewSetting.enable }}</template> - </MkSwitch> + <div class="_gaps"> + <MkSwitch v-model="urlPreviewForm.state.urlPreviewEnabled"> + <template #label>{{ i18n.ts._urlPreviewSetting.enable }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkSwitch> - <MkSwitch v-model="urlPreviewRequireContentLength"> - <template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}</template> + <template v-if="urlPreviewForm.state.urlPreviewEnabled"> + <MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength"> + <template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template> <template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template> </MkSwitch> - <MkInput v-model="urlPreviewMaximumContentLength" type="number"> - <template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}</template> + <MkInput v-model="urlPreviewForm.state.urlPreviewMaximumContentLength" type="number"> + <template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template> <template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template> </MkInput> - <MkInput v-model="urlPreviewTimeout" type="number"> - <template #label>{{ i18n.ts._urlPreviewSetting.timeout }}</template> + <MkInput v-model="urlPreviewForm.state.urlPreviewTimeout" type="number"> + <template #label>{{ i18n.ts._urlPreviewSetting.timeout }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template> <template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template> </MkInput> - <MkInput v-model="urlPreviewUserAgent" type="text"> - <template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}</template> + <MkInput v-model="urlPreviewForm.state.urlPreviewUserAgent" type="text"> + <template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template> <template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template> </MkInput> <div> - <MkInput v-model="urlPreviewSummaryProxyUrl" type="text"> - <template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}</template> + <MkInput v-model="urlPreviewForm.state.urlPreviewSummaryProxyUrl" type="text"> + <template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template> <template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template> </MkInput> @@ -193,18 +230,51 @@ SPDX-License-Identifier: AGPL-3.0-only </ul> </div> </div> - </div> - </FormSection> - </div> - </FormSuspense> - </MkSpacer> - <template #footer> - <div :class="$style.footer"> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> - </MkSpacer> + </template> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-planet"></i></template> + <template #label>{{ i18n.ts.federation }}</template> + <template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template> + <template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template> + <template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template> + <template v-if="federationForm.modified.value" #footer> + <MkFormFooter :form="federationForm"/> + </template> + + <div class="_gaps"> + <MkRadios v-model="federationForm.state.federation"> + <template #label>{{ i18n.ts.behavior }}<span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template> + <option value="all">{{ i18n.ts.all }}</option> + <option value="specified">{{ i18n.ts.specifyHost }}</option> + <option value="none">{{ i18n.ts.none }}</option> + </MkRadios> + + <MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts"> + <template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template> + </MkTextarea> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-ghost"></i></template> + <template #label>{{ i18n.ts.proxyAccount }}</template> + + <div class="_gaps"> + <MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo> + <MkKeyValue> + <template #key>{{ i18n.ts.proxyAccount }}</template> + <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template> + </MkKeyValue> + + <MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton> + </div> + </MkFolder> </div> - </template> + </MkSpacer> </MkStickyContainer> </div> </template> @@ -216,9 +286,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkInfo from '@/components/MkInfo.vue'; -import FormSection from '@/components/form/section.vue'; import FormSplit from '@/components/form/split.vue'; -import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance, instance } from '@/instance.js'; @@ -226,99 +294,136 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; -import MkSelect from '@/components/MkSelect.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; +import { useForm } from '@/scripts/use-form.js'; +import MkFormFooter from '@/components/MkFormFooter.vue'; +import MkRadios from '@/components/MkRadios.vue'; -const name = ref<string | null>(null); -const shortName = ref<string | null>(null); -const description = ref<string | null>(null); -const maintainerName = ref<string | null>(null); -const maintainerEmail = ref<string | null>(null); -const repositoryUrl = ref<string | null>(null); -const impressumUrl = ref<string | null>(null); -const donationUrl = ref<string | null>(null); -const pinnedUsers = ref<string>(''); -const cacheRemoteFiles = ref<boolean>(false); -const cacheRemoteSensitiveFiles = ref<boolean>(false); -const enableServiceWorker = ref<boolean>(false); -const swPublicKey = ref<string | null>(null); -const swPrivateKey = ref<string | null>(null); -const enableFanoutTimeline = ref<boolean>(false); -const enableFanoutTimelineDbFallback = ref<boolean>(false); -const perLocalUserUserTimelineCacheMax = ref<number>(0); -const perRemoteUserUserTimelineCacheMax = ref<number>(0); -const perUserHomeTimelineCacheMax = ref<number>(0); -const perUserListTimelineCacheMax = ref<number>(0); -const notesPerOneAd = ref<number>(0); -const urlPreviewEnabled = ref<boolean>(true); -const urlPreviewTimeout = ref<number>(10000); -const urlPreviewMaximumContentLength = ref<number>(1024 * 1024 * 10); -const urlPreviewRequireContentLength = ref<boolean>(true); -const urlPreviewUserAgent = ref<string | null>(null); -const urlPreviewSummaryProxyUrl = ref<string | null>(null); +const meta = await misskeyApi('admin/meta'); -async function init(): Promise<void> { - const meta = await misskeyApi('admin/meta'); - name.value = meta.name; - shortName.value = meta.shortName; - description.value = meta.description; - maintainerName.value = meta.maintainerName; - maintainerEmail.value = meta.maintainerEmail; - repositoryUrl.value = meta.repositoryUrl; - impressumUrl.value = meta.impressumUrl; - donationUrl.value = meta.donationUrl; - pinnedUsers.value = meta.pinnedUsers.join('\n'); - cacheRemoteFiles.value = meta.cacheRemoteFiles; - cacheRemoteSensitiveFiles.value = meta.cacheRemoteSensitiveFiles; - enableServiceWorker.value = meta.enableServiceWorker; - swPublicKey.value = meta.swPublickey; - swPrivateKey.value = meta.swPrivateKey; - enableFanoutTimeline.value = meta.enableFanoutTimeline; - enableFanoutTimelineDbFallback.value = meta.enableFanoutTimelineDbFallback; - perLocalUserUserTimelineCacheMax.value = meta.perLocalUserUserTimelineCacheMax; - perRemoteUserUserTimelineCacheMax.value = meta.perRemoteUserUserTimelineCacheMax; - perUserHomeTimelineCacheMax.value = meta.perUserHomeTimelineCacheMax; - perUserListTimelineCacheMax.value = meta.perUserListTimelineCacheMax; - notesPerOneAd.value = meta.notesPerOneAd; - urlPreviewEnabled.value = meta.urlPreviewEnabled; - urlPreviewTimeout.value = meta.urlPreviewTimeout; - urlPreviewMaximumContentLength.value = meta.urlPreviewMaximumContentLength; - urlPreviewRequireContentLength.value = meta.urlPreviewRequireContentLength; - urlPreviewUserAgent.value = meta.urlPreviewUserAgent; - urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl; -} +const proxyAccount = ref(meta.proxyAccountId ? await misskeyApi('users/show', { userId: meta.proxyAccountId }) : null); + +const infoForm = useForm({ + name: meta.name ?? '', + shortName: meta.shortName ?? '', + description: meta.description ?? '', + maintainerName: meta.maintainerName ?? '', + maintainerEmail: meta.maintainerEmail ?? '', + tosUrl: meta.tosUrl ?? '', + privacyPolicyUrl: meta.privacyPolicyUrl ?? '', + inquiryUrl: meta.inquiryUrl ?? '', + repositoryUrl: meta.repositoryUrl ?? '', + impressumUrl: meta.impressumUrl ?? '', + donationUrl: meta.donationUrl ?? '', +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + name: state.name, + shortName: state.shortName === '' ? null : state.shortName, + description: state.description, + maintainerName: state.maintainerName, + maintainerEmail: state.maintainerEmail, + tosUrl: state.tosUrl, + privacyPolicyUrl: state.privacyPolicyUrl, + inquiryUrl: state.inquiryUrl, + repositoryUrl: state.repositoryUrl, + impressumUrl: state.impressumUrl, + donationUrl: state.donationUrl, + }); + fetchInstance(true); +}); + +const pinnedUsersForm = useForm({ + pinnedUsers: meta.pinnedUsers.join('\n'), +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + pinnedUsers: state.pinnedUsers.split('\n'), + }); + fetchInstance(true); +}); + +const filesForm = useForm({ + cacheRemoteFiles: meta.cacheRemoteFiles, + cacheRemoteSensitiveFiles: meta.cacheRemoteSensitiveFiles, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + cacheRemoteFiles: state.cacheRemoteFiles, + cacheRemoteSensitiveFiles: state.cacheRemoteSensitiveFiles, + }); + fetchInstance(true); +}); + +const serviceWorkerForm = useForm({ + enableServiceWorker: meta.enableServiceWorker, + swPublicKey: meta.swPublickey ?? '', + swPrivateKey: meta.swPrivateKey ?? '', +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableServiceWorker: state.enableServiceWorker, + swPublicKey: state.swPublicKey, + swPrivateKey: state.swPrivateKey, + }); + fetchInstance(true); +}); + +const otherForm = useForm({ + enableAchievements: meta.enableAchievements, + enableBotTrending: meta.enableBotTrending, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableAchievements: state.enableAchievements, + enableBotTrending: state.enableBotTrending, + }); + fetchInstance(true); +}); + +const adForm = useForm({ + notesPerOneAd: meta.notesPerOneAd, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + notesPerOneAd: state.notesPerOneAd, + }); + fetchInstance(true); +}); -async function save() { +const urlPreviewForm = useForm({ + urlPreviewEnabled: meta.urlPreviewEnabled, + urlPreviewTimeout: meta.urlPreviewTimeout, + urlPreviewMaximumContentLength: meta.urlPreviewMaximumContentLength, + urlPreviewRequireContentLength: meta.urlPreviewRequireContentLength, + urlPreviewUserAgent: meta.urlPreviewUserAgent ?? '', + urlPreviewSummaryProxyUrl: meta.urlPreviewSummaryProxyUrl ?? '', +}, async (state) => { await os.apiWithDialog('admin/update-meta', { - name: name.value, - shortName: shortName.value === '' ? null : shortName.value, - description: description.value, - maintainerName: maintainerName.value, - maintainerEmail: maintainerEmail.value, - repositoryUrl: repositoryUrl.value, - impressumUrl: impressumUrl.value, - donationUrl: donationUrl.value, - pinnedUsers: pinnedUsers.value.split('\n'), - cacheRemoteFiles: cacheRemoteFiles.value, - cacheRemoteSensitiveFiles: cacheRemoteSensitiveFiles.value, - enableServiceWorker: enableServiceWorker.value, - swPublicKey: swPublicKey.value, - swPrivateKey: swPrivateKey.value, - enableFanoutTimeline: enableFanoutTimeline.value, - enableFanoutTimelineDbFallback: enableFanoutTimelineDbFallback.value, - perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value, - perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value, - perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value, - perUserListTimelineCacheMax: perUserListTimelineCacheMax.value, - notesPerOneAd: notesPerOneAd.value, - urlPreviewEnabled: urlPreviewEnabled.value, - urlPreviewTimeout: urlPreviewTimeout.value, - urlPreviewMaximumContentLength: urlPreviewMaximumContentLength.value, - urlPreviewRequireContentLength: urlPreviewRequireContentLength.value, - urlPreviewUserAgent: urlPreviewUserAgent.value, - urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value, + urlPreviewEnabled: state.urlPreviewEnabled, + urlPreviewTimeout: state.urlPreviewTimeout, + urlPreviewMaximumContentLength: state.urlPreviewMaximumContentLength, + urlPreviewRequireContentLength: state.urlPreviewRequireContentLength, + urlPreviewUserAgent: state.urlPreviewUserAgent, + urlPreviewSummaryProxyUrl: state.urlPreviewSummaryProxyUrl, }); + fetchInstance(true); +}); +const federationForm = useForm({ + federation: meta.federation, + federationHosts: meta.federationHosts.join('\n'), +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + federation: state.federation, + federationHosts: state.federationHosts.split('\n'), + }); fetchInstance(true); +}); + +function chooseProxyAccount() { + os.selectUser({ localOnly: true }).then(user => { + proxyAccount.value = user; + os.apiWithDialog('admin/update-meta', { + proxyAccountId: user.id, + }).then(() => { + fetchInstance(true); + }); + }); } const headerTabs = computed(() => []); @@ -330,11 +435,6 @@ definePageMetadata(() => ({ </script> <style lang="scss" module> -.footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); -} - .subCaption { font-size: 0.85em; color: var(--fgTransparentWeak); diff --git a/packages/frontend/src/pages/admin/system-webhook.item.vue b/packages/frontend/src/pages/admin/system-webhook.item.vue index 0c07122af3..4e767fba16 100644 --- a/packages/frontend/src/pages/admin/system-webhook.item.vue +++ b/packages/frontend/src/pages/admin/system-webhook.item.vue @@ -4,8 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="$style.main"> - <span :class="$style.icon"> +<MkFolder> + <template #label>{{ entity.name || entity.url }}</template> + <template #icon> <i v-if="!entity.isActive" class="ti ti-player-pause"/> <i v-else-if="entity.latestStatus === null" class="ti ti-circle"/> <i @@ -14,23 +15,38 @@ SPDX-License-Identifier: AGPL-3.0-only :style="{ color: 'var(--success)' }" /> <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"/> - </span> - <span :class="$style.text">{{ entity.name || entity.url }}</span> - <span :class="$style.suffix"> + </template> + <template #suffix> <MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/> - <button :class="$style.suffixButton" @click="onEditClick"> - <i class="ti ti-settings"></i> - </button> - <button :class="$style.suffixButton" @click="onDeleteClick"> - <i class="ti ti-trash"></i> - </button> - </span> -</div> + <span v-else>-</span> + </template> + <template #footer> + <div class="_buttons"> + <MkButton @click="onEditClick"> + <i class="ti ti-settings"></i> {{ i18n.ts.edit }} + </MkButton> + <MkButton danger @click="onDeleteClick"> + <i class="ti ti-trash"></i> {{ i18n.ts.delete }} + </MkButton> + </div> + </template> + + <div class="_gaps"> + <MkKeyValue> + <template #key>latestStatus</template> + <template #value>{{ entity.latestStatus ?? '-' }}</template> + </MkKeyValue> + </div> +</MkFolder> </template> <script lang="ts" setup> import { entities } from 'misskey-js'; import { toRefs } from 'vue'; +import MkFolder from '@/components/MkFolder.vue'; +import { i18n } from '@/i18n.js'; +import MkButton from '@/components/MkButton.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; const emit = defineEmits<{ (ev: 'edit', value: entities.SystemWebhook): void; @@ -54,64 +70,10 @@ function onDeleteClick() { </script> <style module lang="scss"> -.main { - display: flex; - align-items: center; - width: 100%; - box-sizing: border-box; - padding: 10px 14px; - background: var(--buttonBg); - border: none; - border-radius: 6px; - font-size: 0.9em; - - &:hover { - text-decoration: none; - background: var(--buttonHoverBg); - } - - &.active { - color: var(--accent); - background: var(--buttonHoverBg); - } -} - .icon { margin-right: 0.75em; flex-shrink: 0; text-align: center; color: var(--fgTransparentWeak); } - -.text { - flex-shrink: 1; - white-space: normal; - padding-right: 12px; - text-align: center; -} - -.suffix { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gaps: 4px; - margin-left: auto; - margin-right: -8px; - opacity: 0.7; - white-space: nowrap; -} - -.suffixButton { - background: transparent; - border: none; - border-radius: 9999px; - margin-top: -8px; - margin-bottom: -8px; - padding: 8px; - - &:hover { - background: var(--buttonBg); - } -} </style> diff --git a/packages/frontend/src/pages/admin/system-webhook.vue b/packages/frontend/src/pages/admin/system-webhook.vue index 7a40eec944..c59abda24a 100644 --- a/packages/frontend/src/pages/admin/system-webhook.vue +++ b/packages/frontend/src/pages/admin/system-webhook.vue @@ -11,8 +11,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="900"> <div class="_gaps_m"> - <MkButton :class="$style.linkButton" full @click="onCreateWebhookClicked"> - {{ i18n.ts._webhookSettings.createWebhook }} + <MkButton primary @click="onCreateWebhookClicked"> + <i class="ti ti-plus"></i> {{ i18n.ts._webhookSettings.createWebhook }} </MkButton> <FormSection> @@ -89,8 +89,5 @@ definePageMetadata(() => ({ </script> <style module lang="scss"> -.linkButton { - text-align: left; - padding: 10px 18px; -} + </style> diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index 6a6e266d05..e57e212b60 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkTimeline from '@/components/MkTimeline.vue'; -import { scroll } from '@/scripts/scroll.js'; +import { scroll } from '@@/js/scroll.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue index ad9ec3c4ee..b377314856 100644 --- a/packages/frontend/src/pages/avatar-decorations.vue +++ b/packages/frontend/src/pages/avatar-decorations.vue @@ -12,19 +12,31 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ avatarDecoration.name }}</template> <template #caption>{{ avatarDecoration.description }}</template> - <div class="_gaps_m"> - <MkInput v-model="avatarDecoration.name"> - <template #label>{{ i18n.ts.name }}</template> - </MkInput> - <MkTextarea v-model="avatarDecoration.description"> - <template #label>{{ i18n.ts.description }}</template> - </MkTextarea> - <MkInput v-model="avatarDecoration.url"> - <template #label>{{ i18n.ts.imageUrl }}</template> - </MkInput> - <div class="buttons _buttons"> - <MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + <div :class="$style.editorRoot"> + <div :class="$style.editorWrapper"> + <div :class="$style.preview"> + <div :class="[$style.previewItem, $style.light]"> + <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/> + </div> + <div :class="[$style.previewItem, $style.dark]"> + <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/> + </div> + </div> + <div class="_gaps_m"> + <MkInput v-model="avatarDecoration.name"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkTextarea v-model="avatarDecoration.description"> + <template #label>{{ i18n.ts.description }}</template> + </MkTextarea> + <MkInput v-model="avatarDecoration.url"> + <template #label>{{ i18n.ts.imageUrl }}</template> + </MkInput> + <div class="_buttons"> + <MkButton inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="avatarDecoration.id != null" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </div> </div> </div> </MkFolder> @@ -39,6 +51,7 @@ import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; +import { signinRequired } from '@/account.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; @@ -47,6 +60,8 @@ import MkFolder from '@/components/MkFolder.vue'; const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]); +const $i = signinRequired(); + function add() { avatarDecorations.value.unshift({ _id: Math.random().toString(36), @@ -99,3 +114,55 @@ definePageMetadata(() => ({ icon: 'ti ti-sparkles', })); </script> + +<style lang="scss" module> +.editorRoot { + container: editor / inline-size; +} + +.editorWrapper { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto auto; + gap: var(--margin); +} + +.preview { + display: grid; + place-items: center; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + gap: var(--margin); +} + +.previewItem { + width: 100%; + height: 100%; + min-height: 160px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius); + + &.light { + background: #eee; + } + + &.dark { + background: #222; + } +} + +@container editor (min-width: 600px) { + .editorWrapper { + grid-template-columns: 200px 1fr; + grid-template-rows: 1fr; + gap: calc(var(--margin) * 2); + } + + .preview { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } +} +</style> diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index e922599642..790e16e471 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -82,7 +82,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { deviceKind } from '@/scripts/device-kind.js'; import MkNotes from '@/components/MkNotes.vue'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { favoritedChannelsCache } from '@/cache.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -341,7 +341,7 @@ definePageMetadata(() => ({ left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); + background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); } .bannerStatus { diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 506d906683..52b852ad17 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -39,11 +39,13 @@ import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import { clipsCache } from '@/cache.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ clipId: string, @@ -127,21 +129,41 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ clipsCache.delete(); }, }, ...(clip.value.isPublic ? [{ - icon: 'ti ti-link', - text: i18n.ts.copyUrl, - handler: async (): Promise<void> => { - copyToClipboard(`${url}/clips/${clip.value.id}`); - os.success(); - }, -}] : []), ...(clip.value.isPublic && isSupportShare() ? [{ icon: 'ti ti-share', text: i18n.ts.share, - handler: async (): Promise<void> => { - navigator.share({ - title: clip.value.name, - text: clip.value.description, - url: `${url}/clips/${clip.value.id}`, + handler: (ev: MouseEvent): void => { + const menuItems: MenuItem[] = []; + + menuItems.push({ + icon: 'ti ti-link', + text: i18n.ts.copyUrl, + action: () => { + copyToClipboard(`${url}/clips/${clip.value!.id}`); + os.success(); + }, + }, { + icon: 'ti ti-code', + text: i18n.ts.genEmbedCode, + action: () => { + genEmbedCode('clips', clip.value!.id); + }, }); + + if (isSupportShare()) { + menuItems.push({ + icon: 'ti ti-share', + text: i18n.ts.share, + action: async () => { + navigator.share({ + title: clip.value!.name, + text: clip.value!.description ?? '', + url: `${url}/clips/${clip.value!.id}`, + }); + }, + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); }, }] : []), { icon: 'ti ti-trash', diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index 39e61aa218..36e95cec2d 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -99,12 +99,12 @@ const file = ref<Misskey.entities.DriveFile>(); const folderHierarchy = computed(() => { if (!file.value) return [i18n.ts.drive]; const folderNames = [i18n.ts.drive]; - + function get(folder: Misskey.entities.DriveFolder) { if (folder.parent) get(folder.parent); folderNames.push(folder.name); } - + if (file.value.folder) get(file.value.folder); return folderNames; }); diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue index 0f0b7e1ea8..4db952eac2 100644 --- a/packages/frontend/src/pages/drop-and-fusion.game.vue +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -205,8 +205,8 @@ import { claimAchievement } from '@/scripts/achievements.js'; import { defaultStore } from '@/store.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; -import { apiUrl } from '@/config.js'; +import { useInterval } from '@@/js/use-interval.js'; +import { apiUrl } from '@@/js/config.js'; import { $i } from '@/account.js'; import * as sound from '@/scripts/sound.js'; import MkRange from '@/components/MkRange.vue'; diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index d282ed4810..fd6fadd0b3 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -11,6 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-model="title"> <template #label>{{ i18n.ts._play.title }}</template> </MkInput> + <MkSelect v-model="visibility"> + <template #label>{{ i18n.ts.visibility }}</template> + <template #caption>{{ i18n.ts._play.visibilityDescription }}</template> + <option :key="'public'" :value="'public'">{{ i18n.ts.public }}</option> + <option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option> + </MkSelect> <MkTextarea v-model="summary" :mfmAutocomplete="true" :mfmPreview="true"> <template #label>{{ i18n.ts._play.summary }}</template> </MkTextarea> @@ -18,19 +24,19 @@ SPDX-License-Identifier: AGPL-3.0-only <MkCodeEditor v-model="script" lang="is"> <template #label>{{ i18n.ts._play.script }}</template> </MkCodeEditor> - <MkSelect v-model="visibility"> - <template #label>{{ i18n.ts.visibility }}</template> - <template #caption>{{ i18n.ts._play.visibilityDescription }}</template> - <option :key="'public'" :value="'public'">{{ i18n.ts.public }}</option> - <option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option> - </MkSelect> - <div class="_buttons"> - <MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> - <MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton> - <MkButton v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> - </div> </div> </MkSpacer> + <template #footer> + <div :class="$style.footer"> + <MkSpacer> + <div class="_buttons"> + <MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton> + <MkButton v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </MkSpacer> + </div> + </template> </MkStickyContainer> </template> @@ -459,3 +465,10 @@ definePageMetadata(() => ({ title: flash.value ? `${i18n.ts._play.edit}: ${flash.value.title}` : i18n.ts._play.new, })); </script> +<style lang="scss" module> +.footer { + backdrop-filter: var(--blur, blur(15px)); + background: var(--acrylicBg); + border-top: solid .5px var(--divider); +} +</style> diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index 1b277c936a..1229fcfd4e 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -68,7 +68,7 @@ import { Interpreter, Parser, values } from '@syuilo/aiscript'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkAsUi from '@/components/MkAsUi.vue'; @@ -80,7 +80,7 @@ import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { MenuItem } from '@/types/menu'; +import type { MenuItem } from '@/types/menu.js'; import { pleaseLogin } from '@/scripts/please-login.js'; const props = defineProps<{ @@ -104,18 +104,23 @@ function fetchFlash() { function share(ev: MouseEvent) { if (!flash.value) return; - os.popupMenu([ - { - text: i18n.ts.shareWithNote, - icon: 'ph-repeat ph-bold ph-lg ti-fw', - action: shareWithNote, - }, - ...(isSupportShare() ? [{ + const menuItems: MenuItem[] = []; + + menuItems.push({ + text: i18n.ts.shareWithNote, + icon: 'ph-repeat ph-bold ph-lg ti-fw', + action: shareWithNote, + }); + + if (isSupportShare()) { + menuItems.push({ text: i18n.ts.share, icon: 'ti ti-share', action: shareWithNavigator, - }] : []), - ], ev.currentTarget ?? ev.target); + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } function copyLink() { diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index 913758ba7e..6ed119c0c4 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -72,7 +72,7 @@ import MkContainer from '@/components/MkContainer.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { defaultStore } from '@/store.js'; @@ -80,7 +80,7 @@ import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { useRouter } from '@/router/supplier.js'; -import { MenuItem } from '@/types/menu'; +import type { MenuItem } from '@/types/menu.js'; const router = useRouter(); @@ -171,35 +171,35 @@ function reportAbuse() { function showMenu(ev: MouseEvent) { if (!post.value) return; - const menu: MenuItem[] = [ - ...($i && $i.id !== post.value.userId ? [ - { - icon: 'ti ti-exclamation-circle', - text: i18n.ts.reportAbuse, - action: reportAbuse, - }, - ...($i.isModerator || $i.isAdmin ? [ - { - type: 'divider' as const, - }, - { - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: () => os.confirm({ - type: 'warning', - text: i18n.ts.deleteConfirm, - }).then(({ canceled }) => { - if (canceled || !post.value) return; + const menuItems: MenuItem[] = []; - os.apiWithDialog('gallery/posts/delete', { postId: post.value.id }); - }), - }, - ] : []), - ] : []), - ]; + if ($i && $i.id !== post.value.userId) { + menuItems.push({ + icon: 'ti ti-exclamation-circle', + text: i18n.ts.reportAbuse, + action: reportAbuse, + }); - os.popupMenu(menu, ev.currentTarget ?? ev.target); + if ($i.isModerator || $i.isAdmin) { + menuItems.push({ + type: 'divider', + }, { + icon: 'ti ti-trash', + text: i18n.ts.delete, + danger: true, + action: () => os.confirm({ + type: 'warning', + text: i18n.ts.deleteConfirm, + }).then(({ canceled }) => { + if (canceled || !post.value) return; + + os.apiWithDialog('gallery/posts/delete', { postId: post.value.id }); + }), + }); + } + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } watch(() => props.postId, fetchPost, { immediate: true }); diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 3dc2b2878c..8f6261b9d1 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -173,7 +173,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed, watch } from 'vue'; import * as Misskey from 'misskey-js'; -import MkChart from '@/components/MkChart.vue'; +import MkChart, { type ChartSrc } from '@/components/MkChart.vue'; import MkObjectView from '@/components/MkObjectView.vue'; import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/MkLink.vue'; @@ -189,7 +189,7 @@ import { iAmModerator, iAmAdmin } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; -import MkPagination from '@/components/MkPagination.vue'; +import MkPagination, { type Paging } from '@/components/MkPagination.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; import { dateString } from '@/filters/date.js'; @@ -202,7 +202,7 @@ const props = defineProps<{ const tab = ref('overview'); -const chartSrc = ref('instance-requests'); +const chartSrc = ref<ChartSrc>('instance-requests'); const meta = ref<Misskey.entities.AdminMetaResponse | null>(null); const instance = ref<Misskey.entities.FederationInstance | null>(null); const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none'); @@ -230,7 +230,7 @@ const isBaseSilenced = computed(() => meta.value && baseDomains.value.some(d => const isBaseMediaSilenced = computed(() => meta.value && baseDomains.value.some(d => meta.value?.mediaSilencedHosts.includes(d))); const usersPagination = { - endpoint: iAmModerator ? 'admin/show-users' : 'users' as const, + endpoint: iAmModerator ? 'admin/show-users' : 'users', limit: 10, params: { sort: '+updatedAt', @@ -238,7 +238,7 @@ const usersPagination = { hostname: props.host, }, offsetMode: true, -}; +} satisfies Paging; const followingPagination = { endpoint: 'federation/following' as const, @@ -260,9 +260,12 @@ const followersPagination = { offsetMode: false, }; -watch(moderationNote, async () => { - await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value }); -}); +if (iAmModerator) { + watch(moderationNote, async () => { + if (instance.value == null) return; + await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value }); + }); +} async function fetch(): Promise<void> { if (iAmAdmin) { @@ -282,6 +285,7 @@ async function fetch(): Promise<void> { } async function toggleBlock(): Promise<void> { + if (!iAmAdmin) return; if (!meta.value) throw new Error('No meta?'); if (!instance.value) throw new Error('No instance?'); const { host } = instance.value; @@ -291,6 +295,7 @@ async function toggleBlock(): Promise<void> { } async function toggleSilenced(): Promise<void> { + if (!iAmAdmin) return; if (!meta.value) throw new Error('No meta?'); if (!instance.value) throw new Error('No instance?'); const { host } = instance.value; @@ -301,6 +306,7 @@ async function toggleSilenced(): Promise<void> { } async function toggleMediaSilenced(): Promise<void> { + if (!iAmAdmin) return; if (!meta.value) throw new Error('No meta?'); if (!instance.value) throw new Error('No instance?'); const { host } = instance.value; @@ -311,6 +317,7 @@ async function toggleMediaSilenced(): Promise<void> { } async function stopDelivery(): Promise<void> { + if (!iAmModerator) return; if (!instance.value) throw new Error('No instance?'); suspensionState.value = 'manuallySuspended'; await misskeyApi('admin/federation/update-instance', { @@ -320,6 +327,7 @@ async function stopDelivery(): Promise<void> { } async function resumeDelivery(): Promise<void> { + if (!iAmModerator) return; if (!instance.value) throw new Error('No instance?'); suspensionState.value = 'none'; await misskeyApi('admin/federation/update-instance', { @@ -345,6 +353,7 @@ async function toggleRejectReports(): Promise<void> { } function refreshMetadata(): void { + if (!iAmModerator) return; if (!instance.value) throw new Error('No instance?'); misskeyApi('admin/federation/refresh-remote-instance-metadata', { host: instance.value.host, diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index a2ceb222fe..5f195693cc 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -134,12 +134,14 @@ async function removeUser(item, ev) { async function showMembershipMenu(item, ev) { const withRepliesRef = ref(item.withReplies); + os.popupMenu([{ type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, icon: 'ti ti-messages', ref: withRepliesRef, }], ev.currentTarget ?? ev.target); + watch(withRepliesRef, withReplies => { misskeyApi('users/lists/update-membership', { listId: list.value!.id, diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 28f5838296..bd93fc8369 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -30,7 +30,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; const tab = ref('all'); const includeTypes = ref<string[] | null>(null); diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index 6bc3c0908a..ac9f3e7401 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -69,6 +69,7 @@ import MkButton from '@/components/MkButton.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; +import { url } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { selectFile } from '@/scripts/select-file.js'; diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index fc8627a772..89dd6e10d2 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -104,7 +104,7 @@ import XPage from '@/components/page/page.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import MkMediaImage from '@/components/MkMediaImage.vue'; import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; @@ -121,7 +121,7 @@ import { instance } from '@/instance.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { useRouter } from '@/router/supplier.js'; -import { MenuItem } from '@/types/menu'; +import type { MenuItem } from '@/types/menu.js'; const router = useRouter(); @@ -165,18 +165,23 @@ function fetchPage() { function share(ev: MouseEvent) { if (!page.value) return; - os.popupMenu([ - { - text: i18n.ts.shareWithNote, - icon: 'ti ti-pencil', - action: shareWithNote, - }, - ...(isSupportShare() ? [{ + const menuItems: MenuItem[] = []; + + menuItems.push({ + text: i18n.ts.shareWithNote, + icon: 'ti ti-pencil', + action: shareWithNote, + }); + + if (isSupportShare()) { + menuItems.push({ text: i18n.ts.share, icon: 'ti ti-share', action: shareWithNavigator, - }] : []), - ], ev.currentTarget ?? ev.target); + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } function copyLink() { @@ -256,51 +261,59 @@ function reportAbuse() { function showMenu(ev: MouseEvent) { if (!page.value) return; - const menu: MenuItem[] = [ - ...($i && $i.id === page.value.userId ? [ - { - icon: 'ti ti-code', - text: i18n.ts._pages.viewSource, - action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`), - }, - ...($i.pinnedPageId === page.value.id ? [{ + const menuItems: MenuItem[] = []; + + if ($i && $i.id === page.value.userId) { + menuItems.push({ + icon: 'ti ti-pencil', + text: i18n.ts.editThisPage, + action: () => router.push(`/pages/edit/${page.value.id}`), + }); + + if ($i.pinnedPageId === page.value.id) { + menuItems.push({ icon: 'ti ti-pinned-off', text: i18n.ts.unpin, action: () => pin(false), - }] : [{ + }); + } else { + menuItems.push({ icon: 'ti ti-pin', text: i18n.ts.pin, action: () => pin(true), - }]), - ] : []), - ...($i && $i.id !== page.value.userId ? [ - { - icon: 'ti ti-exclamation-circle', - text: i18n.ts.reportAbuse, - action: reportAbuse, - }, - ...($i.isModerator || $i.isAdmin ? [ - { - type: 'divider' as const, - }, - { - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: () => os.confirm({ - type: 'warning', - text: i18n.ts.deleteConfirm, - }).then(({ canceled }) => { - if (canceled || !page.value) return; + }); + } + } else if ($i && $i.id !== page.value.userId) { + menuItems.push({ + icon: 'ti ti-code', + text: i18n.ts._pages.viewSource, + action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`), + }, { + icon: 'ti ti-exclamation-circle', + text: i18n.ts.reportAbuse, + action: reportAbuse, + }); - os.apiWithDialog('pages/delete', { pageId: page.value.id }); - }), - }, - ] : []), - ] : []), - ]; + if ($i.isModerator || $i.isAdmin) { + menuItems.push({ + type: 'divider', + }, { + icon: 'ti ti-trash', + text: i18n.ts.delete, + danger: true, + action: () => os.confirm({ + type: 'warning', + text: i18n.ts.deleteConfirm, + }).then(({ canceled }) => { + if (canceled || !page.value) return; - os.popupMenu(menu, ev.currentTarget ?? ev.target); + os.apiWithDialog('pages/delete', { pageId: page.value.id }); + }), + }); + } + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } watch(() => path.value, fetchPage, { immediate: true }); @@ -433,13 +446,12 @@ definePageMetadata(() => ({ .pageBannerTitleUser { --height: 32px; flex-shrink: 0; + line-height: var(--height); .avatar { height: var(--height); width: var(--height); } - - line-height: var(--height); } .pageBannerTitleSubActions { diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 7d9cefa5c9..54e66f6e16 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -149,9 +149,9 @@ import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import { deepClone } from '@/scripts/clone.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { signinRequired } from '@/account.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { userPage } from '@/filters/user.js'; diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue index 31c0003130..08bb3cb76c 100644 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -121,7 +121,7 @@ import MkRadios from '@/components/MkRadios.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { useRouter } from '@/router/supplier.js'; const $i = signinRequired(); diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue index 97a793753d..10ea3717ab 100644 --- a/packages/frontend/src/pages/reversi/game.vue +++ b/packages/frontend/src/pages/reversi/game.vue @@ -20,9 +20,9 @@ import { useStream } from '@/stream.js'; import { signinRequired } from '@/account.js'; import { useRouter } from '@/router/supplier.js'; import * as os from '@/os.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; const $i = signinRequired(); diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index 51a03e4418..d823861b4a 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -117,7 +117,7 @@ import { $i } from '@/account.js'; import MkPagination from '@/components/MkPagination.vue'; import { useRouter } from '@/router/supplier.js'; import * as os from '@/os.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index aee1fd488a..9ee3fd7f01 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -43,7 +43,7 @@ import MkUserList from '@/components/MkUserList.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import MkTimeline from '@/components/MkTimeline.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import { serverErrorImageUrl, infoImageUrl } from '@/instance.js'; const props = withDefaults(defineProps<{ diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index 9aaa8ff9c6..155d8b82d7 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -30,6 +30,24 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkContainer> + <MkContainer :foldable="true" :expanded="false"> + <template #header>{{ i18n.ts.uiInspector }}</template> + <div :class="$style.uiInspector"> + <div v-for="c in components" :key="c.value.id" :class="{ [$style.uiInspectorUnShown]: !showns.has(c.value.id) }"> + <div :class="$style.uiInspectorType">{{ c.value.type }}</div> + <div :class="$style.uiInspectorId">{{ c.value.id }}</div> + <button :class="$style.uiInspectorPropsToggle" @click="() => uiInspectorOpenedComponents.set(c, !uiInspectorOpenedComponents.get(c))"> + <i v-if="uiInspectorOpenedComponents.get(c)" class="ti ti-chevron-up icon"></i> + <i v-else class="ti ti-chevron-down icon"></i> + </button> + <div v-if="uiInspectorOpenedComponents.get(c)"> + <MkTextarea :modelValue="stringifyUiProps(c.value)" code readonly></MkTextarea> + </div> + </div> + <div :class="$style.uiInspectorDescription">{{ i18n.ts.uiInspectorDescription }}</div> + </div> + </MkContainer> + <div class=""> {{ i18n.ts.scratchpadDescription }} </div> @@ -43,6 +61,7 @@ import { onDeactivated, onUnmounted, Ref, ref, watch, computed } from 'vue'; import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import MkContainer from '@/components/MkContainer.vue'; import MkButton from '@/components/MkButton.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue'; import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; import * as os from '@/os.js'; @@ -61,6 +80,7 @@ const logs = ref<any[]>([]); const root = ref<AsUiRoot>(); const components = ref<Ref<AsUiComponent>[]>([]); const uiKey = ref(0); +const uiInspectorOpenedComponents = ref(new Map<string, boolean>); const saved = miLocalStorage.getItem('scratchpad'); if (saved) { @@ -71,6 +91,14 @@ watch(code, () => { miLocalStorage.setItem('scratchpad', code.value); }); +function stringifyUiProps(uiProps) { + return JSON.stringify( + { ...uiProps, type: undefined, id: undefined }, + (k, v) => typeof v === 'function' ? '<function>' : v, + 2 + ); +} + async function run() { if (aiscript) aiscript.abort(); root.value = undefined; @@ -152,6 +180,20 @@ const headerActions = computed(() => []); const headerTabs = computed(() => []); +const showns = computed(() => { + const result = new Set<string>(); + (function addChildrenToResult(c: AsUiComponent) { + result.add(c.id); + if (c.children) { + const childComponents = components.value.filter(v => c.children.includes(v.value.id)); + for (const child of childComponents) { + addChildrenToResult(child.value); + } + } + })(root.value); + return result; +}); + definePageMetadata(() => ({ title: i18n.ts.scratchpad, icon: 'ti ti-terminal-2', @@ -192,4 +234,39 @@ definePageMetadata(() => ({ } } } + +.uiInspector { + display: grid; + gap: 8px; + padding: 16px; +} + +.uiInspectorUnShown { + color: var(--fgTransparent); +} + +.uiInspectorType { + display: inline-block; + border: hidden; + border-radius: 10px; + background-color: var(--panelHighlight); + padding: 2px 8px; + font-size: 12px; +} + +.uiInspectorId { + display: inline-block; + padding-left: 8px; +} + +.uiInspectorDescription { + display: block; + font-size: 12px; + padding-top: 16px; +} + +.uiInspectorPropsToggle { + background: none; + border: none; +} </style> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue index 400b365ca6..4ec4610279 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue @@ -103,6 +103,7 @@ const decorationsForPreview = computed(() => { offsetX: offsetX.value, offsetY: offsetY.value, showBelow: showBelow.value, + blink: true, }; const decorations = [...$i.avatarDecorations]; if (props.usingIndex != null) { diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue index 1df723c850..bb919c6c53 100644 --- a/packages/frontend/src/pages/settings/emoji-picker.vue +++ b/packages/frontend/src/pages/settings/emoji-picker.vue @@ -124,10 +124,13 @@ SPDX-License-Identifier: AGPL-3.0-only <option :value="4">{{ i18n.ts.large }}+</option> </MkRadios> - <MkSwitch v-model="emojiPickerUseDrawerForMobile"> - {{ i18n.ts.useDrawerReactionPickerForMobile }} + <MkSelect v-model="emojiPickerStyle"> + <template #label>{{ i18n.ts.style }}</template> <template #caption>{{ i18n.ts.needReloadToApply }}</template> - </MkSwitch> + <option value="auto">{{ i18n.ts.auto }}</option> + <option value="popup">{{ i18n.ts.popup }}</option> + <option value="drawer">{{ i18n.ts.drawer }}</option> + </MkSelect> </div> </FormSection> </div> @@ -140,7 +143,7 @@ import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; import FromSlot from '@/components/form/slot.vue'; -import MkSwitch from '@/components/MkSwitch.vue'; +import MkSelect from '@/components/MkSelect.vue'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; @@ -159,7 +162,7 @@ const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmoji const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale')); const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth')); const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight')); -const emojiPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('emojiPickerUseDrawerForMobile')); +const emojiPickerStyle = computed(defaultStore.makeGetterSetter('emojiPickerStyle')); const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev); const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev); diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 2858822b79..9b6cbc51ca 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -24,13 +24,6 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </MkSelect> - <MkRadios v-model="hemisphere"> - <template #label>{{ i18n.ts.hemisphere }}</template> - <option value="N">{{ i18n.ts._hemisphere.N }}</option> - <option value="S">{{ i18n.ts._hemisphere.S }}</option> - <template #caption>{{ i18n.ts._hemisphere.caption }}</template> - </MkRadios> - <MkRadios v-model="overridedDeviceKind"> <template #label>{{ i18n.ts.overridedDeviceKind }}</template> <option :value="null">{{ i18n.ts.auto }}</option> @@ -194,12 +187,19 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch> <MkSwitch v-model="showAvatarDecorations">{{ i18n.ts.showAvatarDecorations }}</MkSwitch> <MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch> - <MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch> <MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch> <MkSwitch v-model="oneko">{{ i18n.ts.oneko }}</MkSwitch> <MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch> <MkSwitch v-model="useNativeUIForVideoAudioPlayer">{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</MkSwitch> </div> + + <MkSelect v-model="menuStyle"> + <template #label>{{ i18n.ts.menuStyle }}</template> + <option value="auto">{{ i18n.ts.auto }}</option> + <option value="popup">{{ i18n.ts.popup }}</option> + <option value="drawer">{{ i18n.ts.drawer }}</option> + </MkSelect> + <div> <MkRadios v-model="emojiStyle"> <template #label>{{ i18n.ts.emojiStyle }}</template> @@ -314,6 +314,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.other }}</template> <div class="_gaps"> + <MkRadios v-model="hemisphere"> + <template #label>{{ i18n.ts.hemisphere }}</template> + <option value="N">{{ i18n.ts._hemisphere.N }}</option> + <option value="S">{{ i18n.ts._hemisphere.S }}</option> + <template #caption>{{ i18n.ts._hemisphere.caption }}</template> + </MkRadios> <MkFolder> <template #label>{{ i18n.ts.additionalEmojiDictionary }}</template> <div class="_buttons"> @@ -333,6 +339,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, reactive, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; +import { langs } from '@@/js/config.js'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkRadios from '@/components/MkRadios.vue'; @@ -344,12 +351,11 @@ import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/MkLink.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { langs } from '@/config.js'; import { searchEngineMap } from '@/scripts/search-engine-map.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; +import { reloadAsk } from '@/scripts/reload-ask.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; @@ -364,16 +370,6 @@ const cornerRadius = ref(miLocalStorage.getItem('cornerRadius')); const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null); const dataSaver = ref(defaultStore.state.dataSaver); -async function reloadAsk() { - const { canceled } = await os.confirm({ - type: 'info', - text: i18n.ts.reloadToApplySetting, - }); - if (canceled) return; - - unisonReload(); -} - const hemisphere = computed(defaultStore.makeGetterSetter('hemisphere')); const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind')); const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior')); @@ -403,7 +399,7 @@ const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm')); const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount')); const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction')); const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle')); -const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer')); +const menuStyle = computed(defaultStore.makeGetterSetter('menuStyle')); const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages')); const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds')); const oneko = computed(defaultStore.makeGetterSetter('oneko')); @@ -506,7 +502,7 @@ watch([ confirmWhenRevealingSensitiveMedia, contextMenu, ], async () => { - await reloadAsk(); + await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); }); const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const; diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index 9c02b604c0..e000c608fe 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </div> </MkFolder> - <MkFolder v-if="$i && !$i.movedTo"> + <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportFollowing"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkSwitch v-model="withReplies"> @@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> - <MkFolder v-if="$i && !$i.movedTo"> + <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportUserLists"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> @@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> - <MkFolder v-if="$i && !$i.movedTo"> + <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportMuting"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> @@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> - <MkFolder v-if="$i && !$i.movedTo"> + <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportBlocking"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> @@ -123,7 +123,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportAntennas()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> - <MkFolder v-if="$i && !$i.movedTo"> + <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportAntennas"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importAntennas($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index 7f8460e316..a0e6cad9c8 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -54,7 +54,7 @@ import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { defaultStore } from '@/store.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; +import { reloadAsk } from '@/scripts/reload-ask.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -67,16 +67,6 @@ const items = ref(defaultStore.state.menu.map(x => ({ const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); -async function reloadAsk() { - const { canceled } = await os.confirm({ - type: 'info', - text: i18n.ts.reloadToApplySetting, - }); - if (canceled) return; - - unisonReload(); -} - async function addItem() { const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k)); const { canceled, result: item } = await os.select({ @@ -100,7 +90,7 @@ function removeItem(index: number) { async function save() { defaultStore.set('menu', items.value.map(x => x.type)); - await reloadAsk(); + await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); } function reset() { @@ -111,7 +101,7 @@ function reset() { } watch(menuDisplay, async () => { - await reloadAsk(); + await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); }); const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index a861f6ee0d..491102fece 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -69,12 +69,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; const $i = signinRequired(); -const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted']; -const notificationTypesWithoutSender = ['achievementEarned']; +const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[]; +const notificationTypesWithoutSender = ['achievementEarned'] as const satisfies (typeof notificationTypes[number])[]; const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>(); const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer); diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 86bbae431d..d8b3105423 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -62,8 +62,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.experimentalFeatures }}</template> <div class="_gaps_m"> - <MkSwitch v-model="enableCondensedLineForAcct"> - <template #label>Enable condensed line for acct</template> + <MkSwitch v-model="enableCondensedLine"> + <template #label>Enable condensed line</template> </MkSwitch> </div> </MkFolder> @@ -109,13 +109,13 @@ import { defaultStore } from '@/store.js'; import { signout, signinRequired } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; +import { reloadAsk } from '@/scripts/reload-ask.js'; import FormSection from '@/components/form/section.vue'; const $i = signinRequired(); const reportError = computed(defaultStore.makeGetterSetter('reportError')); -const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct')); +const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine')); const devMode = computed(defaultStore.makeGetterSetter('devMode')); const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); @@ -143,16 +143,6 @@ async function deleteAccount() { await signout(); } -async function reloadAsk() { - const { canceled } = await os.confirm({ - type: 'info', - text: i18n.ts.reloadToApplySetting, - }); - if (canceled) return; - - unisonReload(); -} - async function updateRepliesAll(withReplies: boolean) { const { canceled } = await os.confirm({ type: 'warning', @@ -178,9 +168,9 @@ const exportData = () => { }; watch([ - enableCondensedLineForAcct, + enableCondensedLineForAcct, ], async () => { - await reloadAsk(); + await reloadAsk(); }); const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index b0a79ddc39..8036ef5555 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> <div :class="$style.buttons"> - <MkButton inline primary @click="saveNew">{{ ts._preferencesBackups.saveNew }}</MkButton> - <MkButton inline @click="loadFile">{{ ts._preferencesBackups.loadFile }}</MkButton> + <MkButton inline primary @click="saveNew">{{ i18n.ts._preferencesBackups.saveNew }}</MkButton> + <MkButton inline @click="loadFile">{{ i18n.ts._preferencesBackups.loadFile }}</MkButton> </div> <FormSection> - <template #label>{{ ts._preferencesBackups.list }}</template> + <template #label>{{ i18n.ts._preferencesBackups.list }}</template> <template v-if="profiles && Object.keys(profiles).length > 0"> <div class="_gaps_s"> <div @@ -23,13 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only @contextmenu.prevent.stop="$event => menu($event, id)" > <div :class="$style.profileName">{{ profile.name }}</div> - <div :class="$style.profileTime">{{ t('_preferencesBackups.createdAt', { date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div> - <div v-if="profile.updatedAt" :class="$style.profileTime">{{ t('_preferencesBackups.updatedAt', { date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div> + <div :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.createdAt({ date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div> + <div v-if="profile.updatedAt" :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.updatedAt({ date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div> </div> </div> </template> <div v-else-if="profiles"> - <MkInfo>{{ ts._preferencesBackups.noBackups }}</MkInfo> + <MkInfo>{{ i18n.ts._preferencesBackups.noBackups }}</MkInfo> </div> <MkLoading v-else/> </FormSection> @@ -39,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, ref } from 'vue'; import { v4 as uuid } from 'uuid'; +import { version, host } from '@@/js/config.js'; import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -49,10 +50,8 @@ import { unisonReload } from '@/scripts/unison-reload.js'; import { useStream } from '@/stream.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { version, host } from '@/config.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; -const { t, ts } = i18n; const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'collapseRenotes', @@ -80,7 +79,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'disableCatSpeak', 'disableShowingAnimatedImages', 'emojiStyle', - 'disableDrawer', + 'menuStyle', 'useBlurEffectForModal', 'useBlurEffect', 'showFixedPostForm', @@ -92,7 +91,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'emojiPickerScale', 'emojiPickerWidth', 'emojiPickerHeight', - 'emojiPickerUseDrawerForMobile', + 'emojiPickerStyle', 'defaultSideView', 'menuDisplay', 'reportError', @@ -110,7 +109,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'mediaListWithOneImageAppearance', 'notificationPosition', 'notificationStackAxis', - 'enableCondensedLineForAcct', 'keepScreenOn', 'defaultWithReplies', 'disableStreamingTimeline', @@ -211,15 +209,15 @@ async function saveNew(): Promise<void> { if (!profiles.value) return; const { canceled, result: name } = await os.inputText({ - title: ts._preferencesBackups.inputName, + title: i18n.ts._preferencesBackups.inputName, default: '', }); if (canceled) return; if (Object.values(profiles.value).some(x => x.name === name)) { return os.alert({ - title: ts._preferencesBackups.cannotSave, - text: t('_preferencesBackups.nameAlreadyExists', { name }), + title: i18n.ts._preferencesBackups.cannotSave, + text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }), }); } @@ -248,8 +246,8 @@ function loadFile(): void { if (file.type !== 'application/json') { return os.alert({ type: 'error', - title: ts._preferencesBackups.cannotLoad, - text: ts._preferencesBackups.invalidFile, + title: i18n.ts._preferencesBackups.cannotLoad, + text: i18n.ts._preferencesBackups.invalidFile, }); } @@ -260,7 +258,7 @@ function loadFile(): void { } catch (err) { return os.alert({ type: 'error', - title: ts._preferencesBackups.cannotLoad, + title: i18n.ts._preferencesBackups.cannotLoad, text: (err as any)?.message ?? '', }); } @@ -286,8 +284,8 @@ async function applyProfile(id: string): Promise<void> { const { canceled: cancel1 } = await os.confirm({ type: 'warning', - title: ts._preferencesBackups.apply, - text: t('_preferencesBackups.applyConfirm', { name: profile.name }), + title: i18n.ts._preferencesBackups.apply, + text: i18n.tsx._preferencesBackups.applyConfirm({ name: profile.name }), }); if (cancel1) return; @@ -346,7 +344,7 @@ async function applyProfile(id: string): Promise<void> { const { canceled: cancel2 } = await os.confirm({ type: 'info', - text: ts.reloadToApplySetting, + text: i18n.ts.reloadToApplySetting, }); if (cancel2) return; @@ -358,8 +356,8 @@ async function deleteProfile(id: string): Promise<void> { const { canceled } = await os.confirm({ type: 'info', - title: ts.delete, - text: t('deleteAreYouSure', { x: profiles.value[id].name }), + title: i18n.ts.delete, + text: i18n.tsx.deleteAreYouSure({ x: profiles.value[id].name }), }); if (canceled) return; @@ -374,8 +372,8 @@ async function save(id: string): Promise<void> { const { canceled } = await os.confirm({ type: 'info', - title: ts._preferencesBackups.save, - text: t('_preferencesBackups.saveConfirm', { name }), + title: i18n.ts._preferencesBackups.save, + text: i18n.tsx._preferencesBackups.saveConfirm({ name }), }); if (canceled) return; @@ -394,15 +392,15 @@ async function rename(id: string): Promise<void> { if (!profiles.value) return; const { canceled: cancel1, result: name } = await os.inputText({ - title: ts._preferencesBackups.inputName, + title: i18n.ts._preferencesBackups.inputName, default: '', }); if (cancel1 || profiles.value[id].name === name) return; if (Object.values(profiles.value).some(x => x.name === name)) { return os.alert({ - title: ts._preferencesBackups.cannotSave, - text: t('_preferencesBackups.nameAlreadyExists', { name }), + title: i18n.ts._preferencesBackups.cannotSave, + text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }), }); } @@ -410,8 +408,8 @@ async function rename(id: string): Promise<void> { const { canceled: cancel2 } = await os.confirm({ type: 'info', - title: ts.rename, - text: t('_preferencesBackups.renameConfirm', { old: registry.name, new: name }), + title: i18n.ts.rename, + text: i18n.tsx._preferencesBackups.renameConfirm({ old: registry.name, new: name }), }); if (cancel2) return; @@ -423,25 +421,25 @@ function menu(ev: MouseEvent, profileId: string) { if (!profiles.value) return; return os.popupMenu([{ - text: ts._preferencesBackups.apply, + text: i18n.ts._preferencesBackups.apply, icon: 'ti ti-check', action: () => applyProfile(profileId), }, { type: 'a', - text: ts.download, + text: i18n.ts.download, icon: 'ti ti-download', href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })), download: `${profiles.value[profileId].name}.json`, }, { type: 'divider' }, { - text: ts.rename, + text: i18n.ts.rename, icon: 'ti ti-forms', action: () => rename(profileId), }, { - text: ts._preferencesBackups.save, + text: i18n.ts._preferencesBackups.save, icon: 'ti ti-device-floppy', action: () => save(profileId), }, { type: 'divider' }, { - text: ts.delete, + text: i18n.ts.delete, icon: 'ti ti-trash', action: () => deleteProfile(profileId), danger: true, @@ -463,7 +461,7 @@ onUnmounted(() => { }); definePageMetadata(() => ({ - title: ts.preferencesBackups, + title: i18n.ts.preferencesBackups, icon: 'ti ti-device-floppy', })); </script> diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 6cc19db127..c94cd512f3 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -94,15 +94,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption>{{ i18n.ts._profile.metadataDescription }}</template> </FormSlot> - <MkFolder> - <template #label>{{ i18n.ts.advancedSettings }}</template> - - <div class="_gaps_m"> - <MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch> - <MkSwitch v-if="profile.isCat" v-model="profile.speakAsCat">{{ i18n.ts.flagSpeakAsCat }}<template #caption>{{ i18n.ts.flagSpeakAsCatDescription }}</template></MkSwitch> - <MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch> - </div> - </MkFolder> + <MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false"> + <template #label>{{ i18n.ts._profile.followedMessage }}<span class="_beta">{{ i18n.ts.beta }}</span></template> + <template #caption> + <div>{{ i18n.ts._profile.followedMessageDescription }}</div> + <div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div> + </template> + </MkInput> <MkSelect v-model="reactionAcceptance"> <template #label>{{ i18n.ts.reactionAcceptance }}</template> @@ -112,6 +110,16 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option> <option value="likeOnly">{{ i18n.ts.likeOnly }}</option> </MkSelect> + + <MkFolder> + <template #label>{{ i18n.ts.advancedSettings }}</template> + + <div class="_gaps_m"> + <MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch> + <MkSwitch v-if="profile.isCat" v-model="profile.speakAsCat">{{ i18n.ts.flagSpeakAsCat }}<template #caption>{{ i18n.ts.flagSpeakAsCatDescription }}</template></MkSwitch> + <MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch> + </div> + </MkFolder> </div> </template> @@ -153,6 +161,7 @@ const setMaxBirthDate = () => { const profile = reactive({ name: $i.name, description: $i.description, + followedMessage: $i.followedMessage, location: $i.location, birthday: $i.birthday, listenbrainz: $i.listenbrainz, @@ -209,6 +218,8 @@ function save() { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing description: profile.description || null, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + followedMessage: profile.followedMessage || null, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing location: profile.location || null, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing birthday: profile.birthday || null, diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue index ad07a6b539..e7aef55a53 100644 --- a/packages/frontend/src/pages/settings/theme.vue +++ b/packages/frontend/src/pages/settings/theme.vue @@ -88,19 +88,9 @@ import { uniqueBy } from '@/scripts/array.js'; import { fetchThemes, getThemes } from '@/theme-store.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; +import { reloadAsk } from '@/scripts/reload-ask.js'; import * as os from '@/os.js'; -async function reloadAsk() { - const { canceled } = await os.confirm({ - type: 'info', - text: i18n.ts.reloadToApplySetting, - }); - if (canceled) return; - - unisonReload(); -} - const installedThemes = ref(getThemes()); const builtinThemes = getBuiltinThemesRef(); @@ -148,13 +138,13 @@ watch(syncDeviceDarkMode, () => { } }); -watch(wallpaper, () => { +watch(wallpaper, async () => { if (wallpaper.value == null) { miLocalStorage.removeItem('wallpaper'); } else { miLocalStorage.setItem('wallpaper', wallpaper.value); } - reloadAsk(); + await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); }); onActivated(() => { diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index 058ef69c35..adeaf8550c 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -21,14 +21,41 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <template #label>{{ i18n.ts._webhookSettings.trigger }}</template> - <div class="_gaps_s"> - <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch> - <MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch> - <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch> - <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch> - <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> - <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> - <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> + <div class="_gaps"> + <div class="_gaps_s"> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_follow)" @click="test('follow')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_followed)" @click="test('followed')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_note)" @click="test('note')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_reply)" @click="test('reply')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_mention)" @click="test('mention')"><i class="ti ti-send"></i></MkButton> + </div> + </div> + + <div :class="$style.description"> + {{ i18n.ts._webhookSettings.testRemarks }} + </div> </div> </FormSection> @@ -43,6 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed } from 'vue'; +import * as Misskey from 'misskey-js'; import MkInput from '@/components/MkInput.vue'; import FormSection from '@/components/form/section.vue'; import MkSwitch from '@/components/MkSwitch.vue'; @@ -76,8 +104,8 @@ const event_renote = ref(webhook.on.includes('renote')); const event_reaction = ref(webhook.on.includes('reaction')); const event_mention = ref(webhook.on.includes('mention')); -async function save(): Promise<void> { - const events = []; +function save() { + const events: Misskey.entities.UserWebhook['on'] = []; if (event_follow.value) events.push('follow'); if (event_followed.value) events.push('followed'); if (event_note.value) events.push('note'); @@ -110,8 +138,21 @@ async function del(): Promise<void> { router.push('/settings/webhook'); } +async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise<void> { + await os.apiWithDialog('i/webhooks/test', { + webhookId: props.webhookId, + type, + override: { + secret: secret.value, + url: url.value, + }, + }); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars const headerActions = computed(() => []); +// eslint-disable-next-line @typescript-eslint/no-unused-vars const headerTabs = computed(() => []); definePageMetadata(() => ({ @@ -119,3 +160,30 @@ definePageMetadata(() => ({ icon: 'ti ti-webhook', })); </script> + +<style module lang="scss"> +.switchBox { + display: flex; + align-items: center; + justify-content: start; + + .testButton { + $buttonSize: 28px; + padding: 0; + width: $buttonSize; + min-width: $buttonSize; + max-width: $buttonSize; + height: $buttonSize; + margin-left: auto; + line-height: inherit; + font-size: 90%; + border-radius: 9999px; + } +} + +.description { + font-size: 0.85em; + padding: 8px 0 0 0; + color: var(--fgTransparentWeak); +} +</style> diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index 678299b784..0d261b1af3 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -28,6 +28,7 @@ import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; const props = defineProps<{ tag: string; @@ -51,7 +52,19 @@ async function post() { notes.value?.pagingComponent?.reload(); } -const headerActions = computed(() => []); +const headerActions = computed(() => [{ + icon: 'ti ti-dots', + label: i18n.ts.more, + handler: (ev: MouseEvent) => { + os.popupMenu([{ + text: i18n.ts.genEmbedCode, + icon: 'ti ti-code', + action: () => { + genEmbedCode('tags', props.tag); + }, + }], ev.currentTarget ?? ev.target); + } +}]); const headerTabs = computed(() => []); diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index 1cdccdc244..2fa6eb81ba 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -79,6 +79,8 @@ import tinycolor from 'tinycolor2'; import { v4 as uuid } from 'uuid'; import JSON5 from 'json5'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import MkButton from '@/components/MkButton.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -86,9 +88,7 @@ import MkFolder from '@/components/MkFolder.vue'; import { $i } from '@/account.js'; import { Theme, applyTheme } from '@/scripts/theme.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import * as os from '@/os.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { addTheme } from '@/theme-store.js'; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 55e453b38a..fa147e2151 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -41,7 +41,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; -import { scroll } from '@/scripts/scroll.js'; +import { scroll } from '@@/js/scroll.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; @@ -51,11 +51,10 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { deepMerge } from '@/scripts/merge.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { miLocalStorage } from '@/local-storage.js'; import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; import type { BasicTimelineType } from '@/timelines.js'; -import { useRouter } from '@/router/supplier.js'; provide('shouldOmitHeaderTitle', true); @@ -198,7 +197,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> { }), (channels.length === 0 ? undefined : { type: 'divider' }), { - type: 'link' as const, + type: 'link', icon: 'ti ti-plus', text: i18n.ts.createNew, to: '/channels', @@ -267,16 +266,24 @@ const headerActions = computed(() => { icon: 'ti ti-dots', text: i18n.ts.options, handler: (ev) => { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, - }, isBasicTimeline(src.value) && hasWithReplies(src.value) ? { - type: 'switch', - text: i18n.ts.showRepliesToOthersInTimeline, - ref: withReplies, - disabled: onlyFiles, - } : undefined, { + }); + + if (isBasicTimeline(src.value) && hasWithReplies(src.value)) { + menuItems.push({ + type: 'switch', + text: i18n.ts.showRepliesToOthersInTimeline, + ref: withReplies, + disabled: onlyFiles, + }); + } + + menuItems.push({ type: 'switch', text: i18n.ts.withSensitive, ref: withSensitive, @@ -285,7 +292,9 @@ const headerActions = computed(() => { text: i18n.ts.fileAttachedOnly, ref: onlyFiles, disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false, - }], ev.currentTarget ?? ev.target); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); }, }, ]; diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index b26d21ef57..396e6eb14a 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkTimeline from '@/components/MkTimeline.vue'; -import { scroll } from '@/scripts/scroll.js'; +import { scroll } from '@@/js/scroll.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index e82ec0cb97..a403dc9e34 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -51,6 +51,11 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> </div> </div> + <div v-if="user.followedMessage != null" class="followedMessage"> + <div style="border: solid 1px var(--love); border-radius: 6px; background: color-mix(in srgb, var(--love), transparent 90%); padding: 6px 8px;"> + <Mfm :text="user.followedMessage" :author="user"/> + </div> + </div> <div v-if="user.roles.length > 0" class="roles"> <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }"> <MkA v-adaptive-bg :to="`/roles/${role.id}`"> @@ -180,7 +185,7 @@ import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkButton from '@/components/MkButton.vue'; -import { getScrollPosition } from '@/scripts/scroll.js'; +import { getScrollPosition } from '@@/js/scroll.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; @@ -567,6 +572,11 @@ onUnmounted(() => { filter: drop-shadow(1px 1px 3px rgba(#000, 0.2)); } + > .followedMessage { + padding: 24px 24px 0 154px; + font-size: 0.9em; + } + > .roles { padding: 24px 24px 0 154px; font-size: 0.95em; @@ -749,6 +759,10 @@ onUnmounted(() => { margin: auto; } + > .followedMessage { + padding: 16px 16px 0 16px; + } + > .roles { padding: 16px 16px 0 16px; justify-content: center; diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 31911649ac..5a41100bf1 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; -import { host, version } from '@/config.js'; +import { host, version } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue index 252b1a2955..ee8d4e1d62 100644 --- a/packages/frontend/src/pages/welcome.timeline.note.vue +++ b/packages/frontend/src/pages/welcome.timeline.note.vue @@ -84,7 +84,7 @@ onUpdated(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); + background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); } } diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 045f424cda..16d558cc91 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -24,7 +24,7 @@ import * as Misskey from 'misskey-js'; import { onUpdated, ref, shallowRef } from 'vue'; import XNote from '@/pages/welcome.timeline.note.vue'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; const notes = ref<Misskey.entities.Note[]>([]); const isScrolling = ref(false); diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue index 915fe35025..38d257506c 100644 --- a/packages/frontend/src/pages/welcome.vue +++ b/packages/frontend/src/pages/welcome.vue @@ -15,7 +15,7 @@ import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XSetup from './welcome.setup.vue'; import XEntrance from './welcome.entrance.a.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { fetchInstance } from '@/instance.js'; diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index ab17f92ac6..aab4bf5d44 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -3,15 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue'; -import type { RouteDef } from '@/nirax.js'; -import { IRouter, Router } from '@/nirax.js'; +import { AsyncComponentLoader, defineAsyncComponent } from 'vue'; +import type { IRouter, RouteDef } from '@/nirax.js'; +import { Router } from '@/nirax.js'; import { $i, iAmModerator } from '@/account.js'; import MkLoading from '@/pages/_loading_.vue'; import MkError from '@/pages/_error_.vue'; -import { setMainRouter } from '@/router/main.js'; -const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ +export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ loader: loader, loadingComponent: MkLoading, errorComponent: MkError, @@ -471,21 +470,13 @@ const routes: RouteDef[] = [{ name: 'relays', component: page(() => import('@/pages/admin/relays.vue')), }, { - path: '/instance-block', - name: 'instance-block', - component: page(() => import('@/pages/admin/instance-block.vue')), - }, { - path: '/proxy-account', - name: 'proxy-account', - component: page(() => import('@/pages/admin/proxy-account.vue')), - }, { path: '/external-services', name: 'external-services', component: page(() => import('@/pages/admin/external-services.vue')), }, { - path: '/other-settings', - name: 'other-settings', - component: page(() => import('@/pages/admin/other-settings.vue')), + path: '/performance', + name: 'performance', + component: page(() => import('@/pages/admin/performance.vue')), }, { path: '/server-rules', name: 'server-rules', @@ -608,36 +599,6 @@ const routes: RouteDef[] = [{ component: page(() => import('@/pages/not-found.vue')), }]; -function createRouterImpl(path: string): IRouter { +export function createMainRouter(path: string): IRouter { return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue'))); } - -/** - * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。 - * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能) - */ -export function setupRouter(app: App) { - app.provide('routerFactory', createRouterImpl); - - const mainRouter = createRouterImpl(location.pathname + location.search + location.hash); - - window.addEventListener('popstate', (event) => { - mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); - }); - - mainRouter.addListener('push', ctx => { - window.history.pushState({ key: ctx.key }, '', ctx.path); - }); - - mainRouter.addListener('same', () => { - window.scroll({ top: 0, behavior: 'smooth' }); - }); - - mainRouter.addListener('replace', ctx => { - window.history.replaceState({ key: ctx.key }, '', ctx.path); - }); - - mainRouter.init(); - - setMainRouter(mainRouter); -} diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts index 7a3fde131e..709c508741 100644 --- a/packages/frontend/src/router/main.ts +++ b/packages/frontend/src/router/main.ts @@ -3,10 +3,41 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ShallowRef } from 'vue'; import { EventEmitter } from 'eventemitter3'; import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js'; +import type { App, ShallowRef } from 'vue'; + +/** + * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。 + * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能) + */ +export function setupRouter(app: App, routerFactory: ((path: string) => IRouter)): void { + app.provide('routerFactory', routerFactory); + + const mainRouter = routerFactory(location.pathname + location.search + location.hash); + + window.addEventListener('popstate', (event) => { + mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); + }); + + mainRouter.addListener('push', ctx => { + window.history.pushState({ key: ctx.key }, '', ctx.path); + }); + + mainRouter.addListener('same', () => { + window.scroll({ top: 0, behavior: 'smooth' }); + }); + + mainRouter.addListener('replace', ctx => { + window.history.replaceState({ key: ctx.key }, '', ctx.path); + }); + + mainRouter.init(); + + setMainRouter(mainRouter); +} + function getMainRouter(): IRouter { const router = mainRouterHolder; if (!router) { diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 98a0c61752..46aed49330 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -4,13 +4,13 @@ */ import { utils, values } from '@syuilo/aiscript'; +import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; -import { url, lang } from '@/config.js'; -import { nyaize } from '@/scripts/nyaize.js'; +import { url, lang } from '@@/js/config.js'; export function aiScriptReadline(q: string): Promise<string> { return new Promise(ok => { @@ -87,7 +87,7 @@ export function createAiScriptEnv(opts) { }), 'Mk:nyaize': values.FN_NATIVE(([text]) => { utils.assertString(text); - return values.STR(nyaize(text.value)); + return values.STR(Misskey.nyaize(text.value)); }), }; } diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index fa3fcac2e7..2b386bebb8 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -27,6 +27,8 @@ export type AsUiContainer = AsUiComponentBase & { font?: 'serif' | 'sans-serif' | 'monospace'; borderWidth?: number; borderColor?: string; + borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset'; + borderRadius?: number; padding?: number; rounded?: boolean; hidden?: boolean; @@ -173,6 +175,10 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, if (borderWidth) utils.assertNumber(borderWidth); const borderColor = def.value.get('borderColor'); if (borderColor) utils.assertString(borderColor); + const borderStyle = def.value.get('borderStyle'); + if (borderStyle) utils.assertString(borderStyle); + const borderRadius = def.value.get('borderRadius'); + if (borderRadius) utils.assertNumber(borderRadius); const padding = def.value.get('padding'); if (padding) utils.assertNumber(padding); const rounded = def.value.get('rounded'); @@ -191,6 +197,8 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, font: font?.value, borderWidth: borderWidth?.value, borderColor: borderColor?.value, + borderStyle: borderStyle?.value, + borderRadius: borderRadius?.value, padding: padding?.value, rounded: rounded?.value, hidden: hidden?.value, diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts index 8fc857f84f..c3c3f419a9 100644 --- a/packages/frontend/src/scripts/check-reaction-permissions.ts +++ b/packages/frontend/src/scripts/check-reaction-permissions.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -import { UnicodeEmojiDef } from './emojilist.js'; +import { UnicodeEmojiDef } from '@@/js/emojilist.js'; export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean { if (typeof emoji === 'string') return true; // UnicodeEmojiDefにも無い絵文字であれば文字列で来る。Unicode絵文字であることには変わりないので常にリアクション可能とする; diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index e94027d302..6710d9826e 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -3,17 +3,17 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getHighlighterCore, loadWasm } from 'shiki/core'; +import { createHighlighterCore, loadWasm } from 'shiki/core'; import darkPlus from 'shiki/themes/dark-plus.mjs'; import { bundledThemesInfo } from 'shiki/themes'; import { bundledLanguagesInfo } from 'shiki/langs'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import { unique } from './array.js'; import { deepClone } from './clone.js'; import { deepMerge } from './merge.js'; import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core'; import { ColdDeviceStorage } from '@/store.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; let _highlighter: HighlighterCore | null = null; @@ -69,7 +69,7 @@ async function initHighlighter() { ]); const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript'); - const highlighter = await getHighlighterCore({ + const highlighter = await createHighlighterCore({ themes, langs: [ ...(jsLangInfo ? [async () => await jsLangInfo.import()] : []), diff --git a/packages/frontend/src/scripts/collapsed.ts b/packages/frontend/src/scripts/collapsed.ts deleted file mode 100644 index 4ec88a3c65..0000000000 --- a/packages/frontend/src/scripts/collapsed.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; - -export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean { - const collapsed = note.cw == null && ( - note.text != null && ( - (note.text.includes('$[x2')) || - (note.text.includes('$[x3')) || - (note.text.includes('$[x4')) || - (note.text.includes('$[scale')) || - (note.text.split('\n').length > 9) || - (note.text.length > 500) || - (urls.length >= 4) - ) || note.files.length >= 5 - ); - - return collapsed; -} diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend/src/scripts/emoji-base.ts deleted file mode 100644 index 16a5a6aa5b..0000000000 --- a/packages/frontend/src/scripts/emoji-base.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const twemojiSvgBase = '/twemoji'; -const fluentEmojiPngBase = '/fluent-emoji'; -const tossfaceSvgBase = '/tossface'; - -export function char2twemojiFilePath(char: string): string { - let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); - if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.join('-'); - return `${twemojiSvgBase}/${fileName}.svg`; -} - -export function char2fluentEmojiFilePath(char: string): string { - let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); - // Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25 - if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char); - if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.map(x => x!.padStart(4, '0')).join('-'); - return `${fluentEmojiPngBase}/${fileName}.png`; -} - -export function char2tossfaceFilePath(char: string): string { - let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); - // Twemoji is the only emoji font which still supports the shibuya 50 emoji to this day - if (codes[0]?.startsWith('e50a')) return char2twemojiFilePath(char); - // Tossface does not use the fe0f modifier - codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.join('-'); - return `${tossfaceSvgBase}/${fileName}.svg`; -} diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts deleted file mode 100644 index 6565feba97..0000000000 --- a/packages/frontend/src/scripts/emojilist.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const; - -export type UnicodeEmojiDef = { - name: string; - char: string; - category: typeof unicodeEmojiCategories[number]; -} - -// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -import _emojilist from '../emojilist.json'; - -export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ - name: x[1] as string, - char: x[0] as string, - category: unicodeEmojiCategories[x[2]], -})); - -const unicodeEmojisMap = new Map<string, UnicodeEmojiDef>( - emojilist.map(x => [x.char, x]), -); - -const _indexByChar = new Map<string, number>(); -const _charGroupByCategory = new Map<string, string[]>(); -for (let i = 0; i < emojilist.length; i++) { - const emo = emojilist[i]; - _indexByChar.set(emo.char, i); - - if (_charGroupByCategory.has(emo.category)) { - _charGroupByCategory.get(emo.category)?.push(emo.char); - } else { - _charGroupByCategory.set(emo.category, [emo.char]); - } -} - -export const emojiCharByCategory = _charGroupByCategory; - -export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string { - // Colorize it because emojilist.json assumes that - return unicodeEmojisMap.get(colorizeEmoji(char)) - // カラースタイル絵文字がjsonに無い場合はテキストスタイル絵文字にフォールバックする - ?? unicodeEmojisMap.get(char) - // それでも見つからない場合はそのまま返す(絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する) - ?? char; -} - -export function getEmojiName(char: string): string { - // Colorize it because emojilist.json assumes that - const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char); - if (idx === undefined) { - // 絵文字情報がjsonに無い場合は名前の取得が出来ないのでそのまま返すしか無い - return char; - } else { - return emojilist[idx].name; - } -} - -/** - * テキストスタイル絵文字(U+260Eなどの1文字で表現される絵文字)をカラースタイル絵文字に変換します(VS16:U+FE0Fを付与)。 - */ -export function colorizeEmoji(char: string) { - return char.length === 1 ? `${char}\uFE0F` : char; -} - -export interface CustomEmojiFolderTree { - value: string; - category: string; - children: CustomEmojiFolderTree[]; -} diff --git a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts deleted file mode 100644 index 992f6e9a16..0000000000 --- a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function extractAvgColorFromBlurhash(hash: string) { - return typeof hash === 'string' - ? '#' + [...hash.slice(2, 6)] - .map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x)) - .reduce((a, c) => a * 83 + c, 0) - .toString(16) - .padStart(6, '0') - : undefined; -} diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts index eb2da5ad86..81278b17ea 100644 --- a/packages/frontend/src/scripts/focus.ts +++ b/packages/frontend/src/scripts/focus.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@/scripts/scroll.js'; +import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js'; import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js'; type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement; diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts index 60884d08d3..a85ee01e26 100644 --- a/packages/frontend/src/scripts/gen-search-query.ts +++ b/packages/frontend/src/scripts/gen-search-query.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -import { host as localHost } from '@/config.js'; +import { host as localHost } from '@@/js/config.js'; export async function genSearchQuery(v: any, q: string) { let host: string; diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index 108648d640..c8ab9238d3 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -9,7 +9,7 @@ import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { defaultStore } from '@/store.js'; function rename(file: Misskey.entities.DriveFile) { @@ -87,8 +87,10 @@ async function deleteFile(file: Misskey.entities.DriveFile) { export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] { const isImage = file.type.startsWith('image/'); - let menu; - menu = [{ + + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'link', to: `/my/drive/file/${file.id}`, text: i18n.ts._fileViewer.title, @@ -109,14 +111,20 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss text: i18n.ts.describeFile, icon: 'ti ti-text-caption', action: () => describe(file), - }, ...isImage ? [{ - text: i18n.ts.cropImage, - icon: 'ti ti-crop', - action: () => os.cropImage(file, { - aspectRatio: NaN, - uploadFolder: folder ? folder.id : folder, - }), - }] : [], { type: 'divider' }, { + }); + + if (isImage) { + menuItems.push({ + text: i18n.ts.cropImage, + icon: 'ti ti-crop', + action: () => os.cropImage(file, { + aspectRatio: NaN, + uploadFolder: folder ? folder.id : folder, + }), + }); + } + + menuItems.push({ type: 'divider' }, { text: i18n.ts.createNoteFromTheFile, icon: 'ti ti-pencil', action: () => os.post({ @@ -138,17 +146,17 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss icon: 'ti ti-trash', danger: true, action: () => deleteFile(file), - }]; + }); if (defaultStore.state.devMode) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFileId, action: () => { copyToClipboard(file.id); }, - }]); + }); } - return menu; + return menuItems; } diff --git a/packages/frontend/src/scripts/get-embed-code.ts b/packages/frontend/src/scripts/get-embed-code.ts new file mode 100644 index 0000000000..158ab9c7f8 --- /dev/null +++ b/packages/frontend/src/scripts/get-embed-code.ts @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { defineAsyncComponent } from 'vue'; +import { v4 as uuid } from 'uuid'; +import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js'; +import { url } from '@@/js/config.js'; +import * as os from '@/os.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js'; + +const MOBILE_THRESHOLD = 500; + +/** + * パラメータを正規化する(埋め込みコード作成用) + * @param params パラメータ + * @returns 正規化されたパラメータ + */ +export function normalizeEmbedParams(params: EmbedParams): Record<string, string> { + // paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す + const normalizedParams: Record<string, string> = {}; + for (const key in params) { + // デフォルトの値と同じならparamsに含めない + if (params[key] == null || params[key] === defaultEmbedParams[key]) { + continue; + } + switch (typeof params[key]) { + case 'number': + normalizedParams[key] = params[key].toString(); + break; + case 'boolean': + normalizedParams[key] = params[key] ? 'true' : 'false'; + break; + default: + normalizedParams[key] = params[key]; + break; + } + } + return normalizedParams; +} + +/** + * 埋め込みコードを生成(iframe IDの発番もやる) + */ +export function getEmbedCode(path: string, params?: EmbedParams): string { + const iframeId = 'v1_' + uuid(); // 将来embed.jsのバージョンが上がったとき用にv1_を付けておく + + let paramString = ''; + if (params) { + const searchParams = new URLSearchParams(normalizeEmbedParams(params)); + paramString = searchParams.toString() === '' ? '' : '?' + searchParams.toString(); + } + + const iframeCode = [ + `<iframe src="${url + path + paramString}" data-misskey-embed-id="${iframeId}" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" style="border: none; width: 100%; max-width: 500px; height: 300px; color-scheme: light dark;"></iframe>`, + `<script defer src="${url}/embed.js"></script>`, + ]; + return iframeCode.join('\n'); +} + +/** + * 埋め込みコードを生成してコピーする(カスタマイズ機能つき) + * + * カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください + */ +export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) { + const _params = { ...params }; + + if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) { + _params.maxHeight = 700; + } + + // PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー + if (window.innerWidth < MOBILE_THRESHOLD) { + copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params)); + os.success(); + } else { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), { + entity, + id, + params: _params, + }, { + closed: () => dispose(), + }); + } +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index a7ec4ce6d7..0fabd38d7f 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -12,15 +12,16 @@ import { instance } from '@/instance.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { defaultStore, noteActions } from '@/store.js'; import { miLocalStorage } from '@/local-storage.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import { clipsCache, favoritedChannelsCache } from '@/cache.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { isSupportShare } from '@/scripts/navigator.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -66,6 +67,11 @@ export async function getNoteClipMenu(props: { }); if (props.currentClip?.id === clip.id) props.isDeleted.value = true; } + } else if (err.id === 'f0dba960-ff73-4615-8df4-d6ac5d9dc118') { + os.alert({ + type: 'error', + text: i18n.ts.clipNoteLimitExceeded, + }); } else { os.alert({ type: 'error', @@ -93,11 +99,13 @@ export async function getNoteClipMenu(props: { const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', + default: null, label: i18n.ts.name, }, description: { type: 'string', required: false, + default: null, multiline: true, label: i18n.ts.description, }, @@ -162,6 +170,19 @@ export function getCopyNoteOriginLinkMenu(note: misskey.entities.Note, text: str }; } +function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined { + if (note.url != null || note.uri != null) return undefined; + if (['specified', 'followers'].includes(note.visibility)) return undefined; + + return { + icon: 'ti ti-code', + text, + action: (): void => { + genEmbedCode('notes', note.id); + }, + }; +} + export function getNoteMenu(props: { note: Misskey.entities.Note; translation: Ref<Misskey.entities.NotesTranslateResponse | null>; @@ -267,7 +288,7 @@ export function getNoteMenu(props: { title: i18n.ts.numberOfDays, }); - if (canceled) return; + if (canceled || days == null) return; os.apiWithDialog('admin/promo/create', { noteId: appearNote.id, @@ -298,170 +319,180 @@ export function getNoteMenu(props: { props.translation.value = res; } - let menu: MenuItem[]; + const menuItems: MenuItem[] = []; + if ($i) { const statePromise = misskeyApi('notes/state', { noteId: appearNote.id, }); - menu = [ - ...( - props.currentClip?.userId === $i.id ? [{ - icon: 'ti ti-backspace', - text: i18n.ts.unclip, - danger: true, - action: unclip, - }, { type: 'divider' }] : [] - ), { - icon: 'ti ti-info-circle', - text: i18n.ts.details, - action: openDetail, - }, { - icon: 'ti ti-copy', - text: i18n.ts.copyContent, - action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) - , (appearNote.url || appearNote.uri) ? - getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') - : undefined, - (appearNote.url || appearNote.uri) ? { + if (props.currentClip?.userId === $i.id) { + menuItems.push({ + icon: 'ti ti-backspace', + text: i18n.ts.unclip, + danger: true, + action: unclip, + }, { type: 'divider' }); + } + + menuItems.push({ + icon: 'ti ti-info-circle', + text: i18n.ts.details, + action: openDetail, + }, { + icon: 'ti ti-copy', + text: i18n.ts.copyContent, + action: copyContent, + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); + + if (appearNote.url || appearNote.uri) { + menuItems.push({ icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, - } : undefined, - ...(isSupportShare() ? [{ + }); + } else { + menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); + } + + if (isSupportShare()) { + menuItems.push({ icon: 'ti ti-share', text: i18n.ts.share, action: share, - }] : []), - $i && $i.policies.canUseTranslator && instance.translatorAvailable ? { + }); + } + + if ($i.policies.canUseTranslator && instance.translatorAvailable) { + menuItems.push({ icon: 'ti ti-language-hiragana', text: i18n.ts.translate, action: translate, - } : undefined, - { type: 'divider' }, - statePromise.then(state => state.isFavorited ? { - icon: 'ti ti-star-off', - text: i18n.ts.unfavorite, - action: () => toggleFavorite(false), - } : { - icon: 'ti ti-star', - text: i18n.ts.favorite, - action: () => toggleFavorite(true), - }), - { - type: 'parent' as const, - icon: 'ti ti-paperclip', - text: i18n.ts.clip, - children: () => getNoteClipMenu(props), + }); + } + + menuItems.push({ type: 'divider' }); + + menuItems.push(statePromise.then(state => state.isFavorited ? { + icon: 'ti ti-star-off', + text: i18n.ts.unfavorite, + action: () => toggleFavorite(false), + } : { + icon: 'ti ti-star', + text: i18n.ts.favorite, + action: () => toggleFavorite(true), + })); + + menuItems.push({ + type: 'parent', + icon: 'ti ti-paperclip', + text: i18n.ts.clip, + children: () => getNoteClipMenu(props), + }); + + menuItems.push(statePromise.then(state => state.isMutedThread ? { + icon: 'ti ti-message-off', + text: i18n.ts.unmuteThread, + action: () => toggleThreadMute(false), + } : { + icon: 'ti ti-message-off', + text: i18n.ts.muteThread, + action: () => toggleThreadMute(true), + })); + + if (appearNote.userId === $i.id) { + if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) { + menuItems.push({ + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => togglePin(false), + }); + } else { + menuItems.push({ + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => togglePin(true), + }); + } + } + + menuItems.push({ + type: 'parent', + icon: 'ti ti-user', + text: i18n.ts.user, + children: async () => { + const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); + const { menu, cleanup } = getUserMenu(user); + cleanups.push(cleanup); + return menu; }, - statePromise.then(state => state.isMutedThread ? { - icon: 'ti ti-message-off', - text: i18n.ts.unmuteThread, - action: () => toggleThreadMute(false), - } : { - icon: 'ti ti-message-off', - text: i18n.ts.muteThread, - action: () => toggleThreadMute(true), - }), - appearNote.userId === $i.id ? ($i.pinnedNoteIds ?? []).includes(appearNote.id) ? { - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => togglePin(false), - } : { - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => togglePin(true), - } : undefined, - { - type: 'parent' as const, - icon: 'ti ti-user', - text: i18n.ts.user, + }); + + if (appearNote.userId !== $i.id) { + menuItems.push({ type: 'divider' }); + menuItems.push(getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse)); + } + + if (appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin)) { + menuItems.push({ type: 'divider' }); + menuItems.push({ + type: 'parent', + icon: 'ti ti-device-tv', + text: i18n.ts.channel, children: async () => { - const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); - const { menu, cleanup } = getUserMenu(user); - cleanups.push(cleanup); - return menu; - }, - }, - /* - ...($i.isModerator || $i.isAdmin ? [ - { type: 'divider' }, - { - icon: 'ti ti-speakerphone', - text: i18n.ts.promote, - action: promote - }] - : [] - ),*/ - ...(appearNote.userId !== $i.id ? [ - { type: 'divider' }, - appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined, - ] - : [] - ), - ...(appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin) ? [ - { type: 'divider' }, - { - type: 'parent' as const, - icon: 'ti ti-device-tv', - text: i18n.ts.channel, - children: async () => { - const channelChildMenu = [] as MenuItem[]; + const channelChildMenu = [] as MenuItem[]; - const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); + const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); - if (channel.pinnedNoteIds.includes(appearNote.id)) { - channelChildMenu.push({ - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => os.apiWithDialog('channels/update', { - channelId: appearNote.channel!.id, - pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), - }), - }); - } else { - channelChildMenu.push({ - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => os.apiWithDialog('channels/update', { - channelId: appearNote.channel!.id, - pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], - }), - }); - } - return channelChildMenu; - }, + if (channel.pinnedNoteIds.includes(appearNote.id)) { + channelChildMenu.push({ + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), + }), + }); + } else { + channelChildMenu.push({ + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], + }), + }); + } + return channelChildMenu; }, - ] - : [] - ), - ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ - { type: 'divider' }, - appearNote.userId === $i.id ? { + }); + } + + if (appearNote.userId === $i.id || $i.isModerator || $i.isAdmin) { + menuItems.push({ type: 'divider' }); + if (appearNote.userId === $i.id) { + menuItems.push({ icon: 'ph-pencil-simple ph-bold ph-lg', text: i18n.ts.edit, action: edit, - } : undefined, - { + }); + menuItems.push({ icon: 'ti ti-edit', text: i18n.ts.deleteAndEdit, - danger: true, action: delEdit, - }, - { - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: del, - }] - : [] - )] - .filter(x => x !== undefined); + }); + } + menuItems.push({ + icon: 'ti ti-trash', + text: i18n.ts.delete, + danger: true, + action: del, + }); + } } else { - menu = [{ + menuItems.push({ icon: 'ti ti-info-circle', text: i18n.ts.details, action: openDetail, @@ -469,38 +500,45 @@ export function getNoteMenu(props: { icon: 'ti ti-copy', text: i18n.ts.copyContent, action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) - , (appearNote.url || appearNote.uri) ? - getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') - : undefined, - (appearNote.url || appearNote.uri) ? { - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); - }, - } : undefined] - .filter(x => x !== undefined); + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); + + if (appearNote.url || appearNote.uri) { + menuItems.push( + getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') + ); + menuItems.push({ + icon: 'ti ti-external-link', + text: i18n.ts.showOnRemote, + action: () => { + window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); + }, + }); + } else { + menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); + } } if (noteActions.length > 0) { - menu = menu.concat([{ type: "divider" }, ...noteActions.map(action => ({ + menuItems.push({ type: 'divider' }); + + menuItems.push(...noteActions.map(action => ({ icon: 'ti ti-plug', text: action.title, action: () => { action.handler(appearNote); }, - }))]); + }))); } if (defaultStore.state.devMode) { - menu = menu.concat([{ type: "divider" }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyNoteId, action: () => { copyToClipboard(appearNote.id); + os.success(); }, - }]); + }); } const cleanup = () => { @@ -511,7 +549,7 @@ export function getNoteMenu(props: { }; return { - menu, + menu: menuItems, cleanup, }; } diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 33f16a68aa..d15279d633 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -8,7 +8,7 @@ import { defineAsyncComponent, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { host, url } from '@/config.js'; +import { host, url } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore, userActions } from '@/store.js'; @@ -17,7 +17,8 @@ import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-pe import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; import { mainRouter } from '@/router/main.js'; -import { MenuItem } from '@/types/menu.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; +import type { MenuItem } from '@/types/menu.js'; export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { const meId = $i ? $i.id : null; @@ -147,123 +148,154 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); } - let menu: MenuItem[] = [{ + const menuItems: MenuItem[] = []; + + menuItems.push({ icon: 'ti ti-at', text: i18n.ts.copyUsername, action: () => { copyToClipboard(`@${user.username}@${user.host ?? host}`); }, - }, ...( notesSearchAvailable && (user.host == null || canSearchNonLocalNotes) ? [{ - icon: 'ti ti-search', - text: i18n.ts.searchThisUsersNotes, - action: () => { - router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`); - }, - }] : []) - , ...(iAmModerator ? [{ - icon: 'ti ti-user-exclamation', - text: i18n.ts.moderation, - action: () => { - router.push(`/admin/user/${user.id}`); - }, - }] : []), { + }); + + if (notesSearchAvailable && (user.host == null || canSearchNonLocalNotes)) { + menuItems.push({ + icon: 'ti ti-search', + text: i18n.ts.searchThisUsersNotes, + action: () => { + router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`); + }, + }); + } + + if (iAmModerator) { + menuItems.push({ + icon: 'ti ti-user-exclamation', + text: i18n.ts.moderation, + action: () => { + router.push(`/admin/user/${user.id}`); + }, + }); + } + + menuItems.push({ icon: 'ti ti-rss', text: i18n.ts.copyRSS, action: () => { copyToClipboard(`${user.host ?? host}/@${user.username}.atom`); }, - }, ...(user.host != null && user.url != null ? [{ - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - if (user.url == null) return; - window.open(user.url, '_blank', 'noopener'); - }, - }] : []), { + }); + + if (user.host != null && user.url != null) { + menuItems.push({ + icon: 'ti ti-external-link', + text: i18n.ts.showOnRemote, + action: () => { + if (user.url == null) return; + window.open(user.url, '_blank', 'noopener'); + }, + }); + } else { + menuItems.push({ + icon: 'ti ti-code', + text: i18n.ts.genEmbedCode, + type: 'parent', + children: [{ + text: i18n.ts.noteOfThisUser, + action: () => { + genEmbedCode('user-timeline', user.id); + }, + }], // TODO: ユーザーカードの埋め込みなど + }); + } + + menuItems.push({ icon: 'ti ti-share', text: i18n.ts.copyProfileUrl, action: () => { const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; copyToClipboard(`${url}/${canonical}`); }, - }, ...($i ? [{ - icon: 'ti ti-mail', - text: i18n.ts.sendMessage, - action: () => { - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; - os.post({ specified: user, initialText: `${canonical} ` }); - }, - }, { type: 'divider' }, { - icon: 'ti ti-pencil', - text: i18n.ts.editMemo, - action: () => { - editMemo(); - }, - }, { - type: 'parent', - icon: 'ti ti-list', - text: i18n.ts.addToList, - children: async () => { - const lists = await userListsCache.fetch(); - return lists.map(list => { - const isListed = ref(list.userIds.includes(user.id)); - cleanups.push(watch(isListed, () => { - if (isListed.value) { - os.apiWithDialog('users/lists/push', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds.push(user.id); - }); - } else { - os.apiWithDialog('users/lists/pull', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds.splice(list.userIds.indexOf(user.id), 1); + }); + + if ($i) { + menuItems.push({ + icon: 'ti ti-mail', + text: i18n.ts.sendMessage, + action: () => { + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; + os.post({ specified: user, initialText: `${canonical} ` }); + }, + }, { type: 'divider' }, { + icon: 'ti ti-pencil', + text: i18n.ts.editMemo, + action: editMemo, + }, { + type: 'parent', + icon: 'ti ti-list', + text: i18n.ts.addToList, + children: async () => { + const lists = await userListsCache.fetch(); + return lists.map(list => { + const isListed = ref(list.userIds?.includes(user.id) ?? false); + cleanups.push(watch(isListed, () => { + if (isListed.value) { + os.apiWithDialog('users/lists/push', { + listId: list.id, + userId: user.id, + }).then(() => { + list.userIds?.push(user.id); + }); + } else { + os.apiWithDialog('users/lists/pull', { + listId: list.id, + userId: user.id, + }).then(() => { + list.userIds?.splice(list.userIds?.indexOf(user.id), 1); + }); + } + })); + + return { + type: 'switch', + text: list.name, + ref: isListed, + }; + }); + }, + }, { + type: 'parent', + icon: 'ti ti-antenna', + text: i18n.ts.addToAntenna, + children: async () => { + const antennas = await antennasCache.fetch(); + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; + return antennas.filter((a) => a.src === 'users').map(antenna => ({ + text: antenna.name, + action: async () => { + await os.apiWithDialog('antennas/update', { + antennaId: antenna.id, + name: antenna.name, + keywords: antenna.keywords, + excludeKeywords: antenna.excludeKeywords, + src: antenna.src, + userListId: antenna.userListId, + users: [...antenna.users, canonical], + caseSensitive: antenna.caseSensitive, + withReplies: antenna.withReplies, + withFile: antenna.withFile, + notify: antenna.notify, }); - } + antennasCache.delete(); + }, })); - - return { - type: 'switch', - text: list.name, - ref: isListed, - }; - }); - }, - }, { - type: 'parent', - icon: 'ti ti-antenna', - text: i18n.ts.addToAntenna, - children: async () => { - const antennas = await antennasCache.fetch(); - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; - return antennas.filter((a) => a.src === 'users').map(antenna => ({ - text: antenna.name, - action: async () => { - await os.apiWithDialog('antennas/update', { - antennaId: antenna.id, - name: antenna.name, - keywords: antenna.keywords, - excludeKeywords: antenna.excludeKeywords, - src: antenna.src, - userListId: antenna.userListId, - users: [...antenna.users, canonical], - caseSensitive: antenna.caseSensitive, - withReplies: antenna.withReplies, - withFile: antenna.withFile, - notify: antenna.notify, - }); - antennasCache.delete(); - }, - })); - }, - }] : [])] as any; + }, + }); + } if ($i && meId !== user.id) { if (iAmModerator) { - menu = menu.concat([{ + menuItems.push({ type: 'parent', icon: 'ti ti-badges', text: i18n.ts.roles, @@ -301,13 +333,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }, })); }, - }]); + }); } // フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため //if (user.isFollowing) { - const withRepliesRef = ref(user.withReplies); - menu = menu.concat([{ + const withRepliesRef = ref(user.withReplies ?? false); + + menuItems.push({ type: 'switch', icon: 'ti ti-messages', text: i18n.ts.showRepliesToOthersInTimeline, @@ -316,7 +349,8 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off', text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes, action: toggleNotify, - }]); + }); + watch(withRepliesRef, (withReplies) => { misskeyApi('following/update', { userId: user.id, @@ -327,7 +361,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); //} - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, action: toggleMute, @@ -339,70 +373,68 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter icon: 'ti ti-ban', text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, action: toggleBlock, - }]); + }); if (user.isFollowed) { - menu = menu.concat([{ + menuItems.push({ icon: 'ti ti-link-off', text: i18n.ts.breakFollow, action: invalidateFollow, - }]); + }); } - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-exclamation-circle', text: i18n.ts.reportAbuse, action: reportAbuse, - }]); + }); } if (user.host !== null) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-refresh', text: i18n.ts.updateRemoteUser, action: userInfoUpdate, - }]); + }); } if (defaultStore.state.devMode) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyUserId, action: () => { copyToClipboard(user.id); }, - }]); + }); } if ($i && meId === user.id) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-pencil', text: i18n.ts.editProfile, action: () => { router.push('/settings/profile'); }, - }]); + }); } if (userActions.length > 0) { - menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({ + menuItems.push({ type: 'divider' }, ...userActions.map(action => ({ icon: 'ti ti-plug', text: action.title, action: () => { action.handler(user); }, - }))]); + }))); } - const cleanup = () => { - if (_DEV_) console.log('user menu cleanup', cleanups); - for (const cl of cleanups) { - cl(); - } - }; - return { - menu, - cleanup, + menu: menuItems, + cleanup: () => { + if (_DEV_) console.log('user menu cleanup', cleanups); + for (const cl of cleanups) { + cl(); + } + }, }; } diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts deleted file mode 100644 index c2f44a33cc..0000000000 --- a/packages/frontend/src/scripts/i18n.ts +++ /dev/null @@ -1,294 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { ILocale, ParameterizedString } from '../../../../locales/index.js'; - -type FlattenKeys<T extends ILocale, TPrediction> = keyof { - [K in keyof T as T[K] extends ILocale - ? FlattenKeys<T[K], TPrediction> extends infer C extends string - ? `${K & string}.${C}` - : never - : T[K] extends TPrediction - ? K - : never]: T[K]; -}; - -type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString>> = TKey extends `${infer K}.${infer C}` - // @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。 - ? ParametersOf<T[K], C> - : TKey extends keyof T - ? T[TKey] extends ParameterizedString<infer P> - ? P - : never - : never; - -type Tsx<T extends ILocale> = { - readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString<infer P> - ? (arg: { readonly [_ in P]: string | number }) => string - // @ts-expect-error -- 証明省略 - : Tsx<T[K]>; -}; - -export class I18n<T extends ILocale> { - private tsxCache?: Tsx<T>; - - constructor(public locale: T) { - //#region BIND - this.t = this.t.bind(this); - //#endregion - } - - public get ts(): T { - if (_DEV_) { - class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> { - get(target: TTarget, p: string | symbol): unknown { - const value = target[p as keyof TTarget]; - - if (typeof value === 'object') { - return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>()); - } - - if (typeof value === 'string') { - const parameters = Array.from(value.matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter); - - if (parameters.length) { - console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`); - } - - return value; - } - - console.error(`Unexpected locale key: ${String(p)}`); - - return p; - } - } - - return new Proxy(this.locale, new Handler()); - } - - return this.locale; - } - - public get tsx(): Tsx<T> { - if (_DEV_) { - if (this.tsxCache) { - return this.tsxCache; - } - - class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> { - get(target: TTarget, p: string | symbol): unknown { - const value = target[p as keyof TTarget]; - - if (typeof value === 'object') { - return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>()); - } - - if (typeof value === 'string') { - const quasis: string[] = []; - const expressions: string[] = []; - let cursor = 0; - - while (~cursor) { - const start = value.indexOf('{', cursor); - - if (!~start) { - quasis.push(value.slice(cursor)); - break; - } - - quasis.push(value.slice(cursor, start)); - - const end = value.indexOf('}', start); - - expressions.push(value.slice(start + 1, end)); - - cursor = end + 1; - } - - if (!expressions.length) { - console.error(`Unexpected locale key: ${String(p)}`); - - return () => value; - } - - return (arg) => { - let str = quasis[0]; - - for (let i = 0; i < expressions.length; i++) { - if (!Object.hasOwn(arg, expressions[i])) { - console.error(`Missing locale parameters: ${expressions[i]} at ${String(p)}`); - } - - str += arg[expressions[i]] + quasis[i + 1]; - } - - return str; - }; - } - - console.error(`Unexpected locale key: ${String(p)}`); - - return p; - } - } - - return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx<T>; - } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.tsxCache) { - return this.tsxCache; - } - - function build(target: ILocale): Tsx<T> { - const result = {} as Tsx<T>; - - for (const k in target) { - if (!Object.hasOwn(target, k)) { - continue; - } - - const value = target[k as keyof typeof target]; - - if (typeof value === 'object') { - result[k] = build(value as ILocale); - } else if (typeof value === 'string') { - const quasis: string[] = []; - const expressions: string[] = []; - let cursor = 0; - - while (~cursor) { - const start = value.indexOf('{', cursor); - - if (!~start) { - quasis.push(value.slice(cursor)); - break; - } - - quasis.push(value.slice(cursor, start)); - - const end = value.indexOf('}', start); - - expressions.push(value.slice(start + 1, end)); - - cursor = end + 1; - } - - if (!expressions.length) { - continue; - } - - result[k] = (arg) => { - let str = quasis[0]; - - for (let i = 0; i < expressions.length; i++) { - str += arg[expressions[i]] + quasis[i + 1]; - } - - return str; - }; - } - } - return result; - } - - return this.tsxCache = build(this.locale); - } - - /** - * @deprecated なるべくこのメソッド使うよりも ts 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも - */ - public t<TKey extends FlattenKeys<T, string>>(key: TKey): string; - /** - * @deprecated なるべくこのメソッド使うよりも tsx 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも - */ - public t<TKey extends FlattenKeys<T, ParameterizedString>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string; - public t(key: string, args?: { readonly [_: string]: string | number }) { - let str: string | ParameterizedString | ILocale = this.locale; - - for (const k of key.split('.')) { - str = str[k]; - - if (_DEV_) { - if (typeof str === 'undefined') { - console.error(`Unexpected locale key: ${key}`); - return key; - } - } - } - - if (args) { - if (_DEV_) { - const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter)); - - if (missing.length) { - console.error(`Missing locale parameters: ${missing.join(', ')} at ${key}`); - } - } - - for (const [k, v] of Object.entries(args)) { - const search = `{${k}}`; - - if (_DEV_) { - if (!(str as string).includes(search)) { - console.error(`Unexpected locale parameter: ${k} at ${key}`); - } - } - - str = (str as string).replace(search, v.toString()); - } - } - - return str; - } -} - -if (import.meta.vitest) { - const { describe, expect, it } = import.meta.vitest; - - describe('i18n', () => { - it('t', () => { - const i18n = new I18n({ - foo: 'foo', - bar: { - baz: 'baz', - qux: 'qux {0}' as unknown as ParameterizedString<'0'>, - quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, - }, - }); - - expect(i18n.t('foo')).toBe('foo'); - expect(i18n.t('bar.baz')).toBe('baz'); - expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); - expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); - }); - it('ts', () => { - const i18n = new I18n({ - foo: 'foo', - bar: { - baz: 'baz', - qux: 'qux {0}' as unknown as ParameterizedString<'0'>, - quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, - }, - }); - - expect(i18n.ts.foo).toBe('foo'); - expect(i18n.ts.bar.baz).toBe('baz'); - }); - it('tsx', () => { - const i18n = new I18n({ - foo: 'foo', - bar: { - baz: 'baz', - qux: 'qux {0}' as unknown as ParameterizedString<'0'>, - quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, - }, - }); - - expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); - expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); - }); - }); -} diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts index 6b511f2a5f..20f51660c7 100644 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -10,10 +10,11 @@ import { set as iset, del as idel, } from 'idb-keyval'; +import { miLocalStorage } from '@/local-storage.js'; -const fallbackName = (key: string) => `idbfallback::${key}`; +const PREFIX = 'idbfallback::'; -let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true; +let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true; // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 // バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと @@ -38,15 +39,15 @@ if (idbAvailable) { export async function get(key: string) { if (idbAvailable) return iget(key); - return JSON.parse(window.localStorage.getItem(fallbackName(key))); + return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); } export async function set(key: string, val: any) { if (idbAvailable) return iset(key, val); - return window.localStorage.setItem(fallbackName(key), JSON.stringify(val)); + return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); } export async function del(key: string) { if (idbAvailable) return idel(key); - return window.localStorage.removeItem(fallbackName(key)); + return miLocalStorage.removeItem(`${PREFIX}${key}`); } diff --git a/packages/frontend/src/scripts/initialize-sw.ts b/packages/frontend/src/scripts/initialize-sw.ts index 1517e4e1e8..867ebf19ed 100644 --- a/packages/frontend/src/scripts/initialize-sw.ts +++ b/packages/frontend/src/scripts/initialize-sw.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { lang } from '@/config.js'; +import { lang } from '@@/js/config.js'; export async function initializeSw() { if (!('serviceWorker' in navigator)) return; diff --git a/packages/frontend/src/scripts/intl-const.ts b/packages/frontend/src/scripts/intl-const.ts index aaa4f0a86e..385f59ec39 100644 --- a/packages/frontend/src/scripts/intl-const.ts +++ b/packages/frontend/src/scripts/intl-const.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { lang } from '@/config.js'; +import { lang } from '@@/js/config.js'; export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP'); diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index 099a22163a..78eba35ead 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -3,51 +3,32 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { query } from '@/scripts/url.js'; -import { url } from '@/config.js'; +import { MediaProxy } from '@@/js/media-proxy.js'; +import { url } from '@@/js/config.js'; import { instance } from '@/instance.js'; -export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { - const localProxy = `${url}/proxy`; +let _mediaProxy: MediaProxy | null = null; - if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { - // もう既にproxyっぽそうだったらurlを取り出す - imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; +export function getProxiedImageUrl(...args: Parameters<MediaProxy['getProxiedImageUrl']>): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${mustOrigin ? localProxy : instance.mediaProxy}/${ - type === 'preview' ? 'preview.webp' - : 'image.webp' - }?${query({ - url: imageUrl, - ...(!noFallback ? { 'fallback': '1' } : {}), - ...(type ? { [type]: '1' } : {}), - ...(mustOrigin ? { origin: '1' } : {}), - })}`; + return _mediaProxy.getProxiedImageUrl(...args); } -export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { - if (imageUrl == null) return null; - return getProxiedImageUrl(imageUrl, type); -} - -export function getStaticImageUrl(baseUrl: string): string { - const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url); - - if (u.href.startsWith(`${url}/emoji/`)) { - // もう既にemojiっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; +export function getProxiedImageUrlNullable(...args: Parameters<MediaProxy['getProxiedImageUrlNullable']>): string | null { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - if (u.href.startsWith(instance.mediaProxy + '/')) { - // もう既にproxyっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; + return _mediaProxy.getProxiedImageUrlNullable(...args); +} + +export function getStaticImageUrl(...args: Parameters<MediaProxy['getStaticImageUrl']>): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${instance.mediaProxy}/static.webp?${query({ - url: u.href, - static: '1', - })}`; + return _mediaProxy.getStaticImageUrl(...args); } diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts index 63acf9d3de..2911469cdd 100644 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ b/packages/frontend/src/scripts/mfm-function-picker.ts @@ -6,7 +6,7 @@ import { Ref, nextTick } from 'vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { MFM_TAGS } from '@/const.js'; +import { MFM_TAGS } from '@@/js/const.js'; import type { MenuItem } from '@/types/menu.js'; /** diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts index 49fb6f9e59..1b1159fd01 100644 --- a/packages/frontend/src/scripts/misskey-api.ts +++ b/packages/frontend/src/scripts/misskey-api.ts @@ -5,7 +5,7 @@ import * as Misskey from 'misskey-js'; import { ref } from 'vue'; -import { apiUrl } from '@/config.js'; +import { apiUrl } from '@@/js/config.js'; import { $i } from '@/account.js'; export const pendingApiRequestsCount = ref(0); diff --git a/packages/frontend/src/scripts/nyaize.ts b/packages/frontend/src/scripts/nyaize.ts deleted file mode 100644 index 5e6fa298d1..0000000000 --- a/packages/frontend/src/scripts/nyaize.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const koRegex1 = /[나-낳]/g; -const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm; -const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm; - -function ifAfter(prefix, fn) { - const preLen = prefix.length; - const regex = new RegExp(prefix, 'i'); - return (x, pos, string) => { - return pos > 0 && string.substring(pos - preLen, pos).match(regex) ? fn(x) : x; - }; -} - -export function nyaize(text: string): string { - return text - // ja-JP - .replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ') - // en-US - .replace(/a/gi, ifAfter('n', x => x === 'A' ? 'YA' : 'ya')) - .replace(/ing/gi, ifAfter('morn', x => x === 'ING' ? 'YAN' : 'yan')) - .replace(/one/gi, ifAfter('every', x => x === 'ONE' ? 'NYAN' : 'nyan')) - // ko-KR - .replace(koRegex1, match => String.fromCharCode( - match.charCodeAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0), - )) - .replace(koRegex2, '다냥') - .replace(koRegex3, '냥'); -} diff --git a/packages/frontend/src/scripts/player-url-transform.ts b/packages/frontend/src/scripts/player-url-transform.ts index 53b2a9e441..39c6df6500 100644 --- a/packages/frontend/src/scripts/player-url-transform.ts +++ b/packages/frontend/src/scripts/player-url-transform.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import { hostname } from '@/config.js'; +import { hostname } from '@@/js/config.js'; export function transformPlayerUrl(url: string): string { const urlObj = new URL(url); diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts index 1caa2dfc21..5b141222e8 100644 --- a/packages/frontend/src/scripts/popout.ts +++ b/packages/frontend/src/scripts/popout.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { appendQuery } from './url.js'; -import * as config from '@/config.js'; +import { appendQuery } from '@@/js/url.js'; +import * as config from '@@/js/config.js'; export function popout(path: string, w?: HTMLElement) { let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path; diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts index 31a9ac1ad9..11b6f52ddd 100644 --- a/packages/frontend/src/scripts/post-message.ts +++ b/packages/frontend/src/scripts/post-message.ts @@ -18,7 +18,7 @@ export type MiPostMessageEvent = { * 親フレームにイベントを送信 */ export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void { - window.postMessage({ + window.parent.postMessage({ type, payload, }, '*'); diff --git a/packages/frontend/src/scripts/reload-ask.ts b/packages/frontend/src/scripts/reload-ask.ts new file mode 100644 index 0000000000..733d91b85a --- /dev/null +++ b/packages/frontend/src/scripts/reload-ask.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; +import { unisonReload } from '@/scripts/unison-reload.js'; + +let isReloadConfirming = false; + +export async function reloadAsk(opts: { + unison?: boolean; + reason?: string; +}) { + if (isReloadConfirming) { + return; + } + + isReloadConfirming = true; + + const { canceled } = await os.confirm(opts.reason == null ? { + type: 'info', + text: i18n.ts.reloadConfirm, + } : { + type: 'info', + title: i18n.ts.reloadConfirm, + text: opts.reason, + }).finally(() => { + isReloadConfirming = false; + }); + + if (canceled) return; + + if (opts.unison) { + unisonReload(); + } else { + location.reload(); + } +} diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts deleted file mode 100644 index 6bfcef6c36..0000000000 --- a/packages/frontend/src/scripts/safe-parse.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeParseFloat(str: unknown): number | null { - if (typeof str !== 'string' || str === '') return null; - const num = parseFloat(str); - if (isNaN(num)) return null; - return num; -} diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts deleted file mode 100644 index 0edf4e9eba..0000000000 --- a/packages/frontend/src/scripts/safe-uri-decode.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeURIDecode(str: string): string { - try { - return decodeURIComponent(str); - } catch { - return str; - } -} diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts deleted file mode 100644 index f0274034b5..0000000000 --- a/packages/frontend/src/scripts/scroll.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -type ScrollBehavior = 'auto' | 'smooth' | 'instant'; - -export function getScrollContainer(el: HTMLElement | null): HTMLElement | null { - if (el == null || el.tagName === 'HTML') return null; - const overflow = window.getComputedStyle(el).getPropertyValue('overflow-y'); - if (overflow === 'scroll' || overflow === 'auto') { - return el; - } else { - return getScrollContainer(el.parentElement); - } -} - -export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top = 0) { - if (!el.parentElement) return top; - const data = el.dataset.stickyContainerHeaderHeight; - const newTop = data ? Number(data) + top : top; - if (el === container) return newTop; - return getStickyTop(el.parentElement, container, newTop); -} - -export function getStickyBottom(el: HTMLElement, container: HTMLElement | null = null, bottom = 0) { - if (!el.parentElement) return bottom; - const data = el.dataset.stickyContainerFooterHeight; - const newBottom = data ? Number(data) + bottom : bottom; - if (el === container) return newBottom; - return getStickyBottom(el.parentElement, container, newBottom); -} - -export function getScrollPosition(el: HTMLElement | null): number { - const container = getScrollContainer(el); - return container == null ? window.scrollY : container.scrollTop; -} - -export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { - // とりあえず評価してみる - if (el.isConnected && isTopVisible(el)) { - cb(); - if (once) return null; - } - - const container = getScrollContainer(el) ?? window; - - const onScroll = ev => { - if (!document.body.contains(el)) return; - if (isTopVisible(el, tolerance)) { - cb(); - if (once) removeListener(); - } - }; - - function removeListener() { container.removeEventListener('scroll', onScroll); } - - container.addEventListener('scroll', onScroll, { passive: true }); - return removeListener; -} - -export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { - const container = getScrollContainer(el); - - // とりあえず評価してみる - if (el.isConnected && isBottomVisible(el, tolerance, container)) { - cb(); - if (once) return null; - } - - const containerOrWindow = container ?? window; - const onScroll = ev => { - if (!document.body.contains(el)) return; - if (isBottomVisible(el, 1, container)) { - cb(); - if (once) removeListener(); - } - }; - - function removeListener() { - containerOrWindow.removeEventListener('scroll', onScroll); - } - - containerOrWindow.addEventListener('scroll', onScroll, { passive: true }); - return removeListener; -} - -export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) { - const container = getScrollContainer(el); - if (container == null) { - window.scroll(options); - } else { - container.scroll(options); - } -} - -/** - * Scroll to Top - * @param el Scroll container element - * @param options Scroll options - */ -export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) { - scroll(el, { top: 0, ...options }); -} - -/** - * Scroll to Bottom - * @param el Content element - * @param options Scroll options - * @param container Scroll container element - */ -export function scrollToBottom( - el: HTMLElement, - options: ScrollToOptions = {}, - container = getScrollContainer(el), -) { - if (container) { - container.scroll({ top: el.scrollHeight - container.clientHeight + getStickyTop(el, container) || 0, ...options }); - } else { - window.scroll({ - top: (el.scrollHeight - window.innerHeight + getStickyTop(el, container) + (window.innerWidth <= 500 ? 96 : 0)) || 0, - ...options, - }); - } -} - -export function isTopVisible(el: HTMLElement, tolerance = 1): boolean { - const scrollTop = getScrollPosition(el); - return scrollTop <= tolerance; -} - -export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) { - if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance; - return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance; -} - -// https://ja.javascript.info/size-and-scroll-window#ref-932 -export function getBodyScrollHeight() { - return Math.max( - document.body.scrollHeight, document.documentElement.scrollHeight, - document.body.offsetHeight, document.documentElement.offsetHeight, - document.body.clientHeight, document.documentElement.clientHeight, - ); -} diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts new file mode 100644 index 0000000000..cb0e607fcb --- /dev/null +++ b/packages/frontend/src/scripts/stream-mock.ts @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EventEmitter } from 'eventemitter3'; +import * as Misskey from 'misskey-js'; +import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js'; + +type AnyOf<T extends Record<any, any>> = T[keyof T]; +type OmitFirst<T extends any[]> = T extends [any, ...infer R] ? R : never; + +/** + * Websocket無効化時に使うStreamのモック(なにもしない) + */ +export class StreamMock extends EventEmitter<StreamEvents> implements IStream { + public readonly state = 'initializing'; + + constructor(...args: ConstructorParameters<typeof Misskey.Stream>) { + super(); + // do nothing + } + + public useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock<Channels[C]> { + return new ChannelConnectionMock(this, channel, name); + } + + public removeSharedConnection(connection: any): void { + // do nothing + } + + public removeSharedConnectionPool(pool: any): void { + // do nothing + } + + public disconnectToChannel(): void { + // do nothing + } + + public send(typeOrPayload: string): void + public send(typeOrPayload: string, payload: any): void + public send(typeOrPayload: Record<string, any> | any[]): void + public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void { + // do nothing + } + + public ping(): void { + // do nothing + } + + public heartbeat(): void { + // do nothing + } + + public close(): void { + // do nothing + } +} + +class ChannelConnectionMock<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> { + public id = ''; + public name?: string; // for debug + public inCount = 0; // for debug + public outCount = 0; // for debug + public channel: string; + + constructor(stream: IStream, ...args: OmitFirst<ConstructorParameters<typeof Misskey.ChannelConnection<Channel>>>) { + super(); + + this.channel = args[0]; + this.name = args[1]; + } + + public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void { + // do nothing + } + + public dispose(): void { + // do nothing + } +} diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index e59643b09c..bd3cddde67 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -5,11 +5,11 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import { deepClone } from './clone.js'; import type { BundledTheme } from 'shiki/themes'; import { globalEvents } from '@/events.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; import { miLocalStorage } from '@/local-storage.js'; export type Theme = { @@ -54,7 +54,7 @@ export const getBuiltinThemes = () => Promise.all( 'd-u0', 'rosepine', 'rosepine-dawn', - ].map(name => import(`@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)), + ].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)), ); export const getBuiltinThemesRef = () => { @@ -78,6 +78,8 @@ export function applyTheme(theme: Theme, persist = true) { const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; + document.documentElement.dataset.colorScheme = colorScheme; + // Deep copy const _theme = deepClone(theme); diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index 3e947183c9..22dce609c6 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -9,10 +9,11 @@ import { v4 as uuid } from 'uuid'; import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; import { getCompressionConfig } from './upload/compress-config.js'; import { defaultStore } from '@/store.js'; -import { apiUrl } from '@/config.js'; +import { apiUrl } from '@@/js/config.js'; import { $i } from '@/account.js'; import { alert } from '@/os.js'; import { i18n } from '@/i18n.js'; +import { instance } from '@/instance.js'; type Uploading = { id: string; @@ -39,6 +40,15 @@ export function uploadFile( if (folder && typeof folder === 'object') folder = folder.id; + if (file.size > instance.maxFileSize) { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, + }); + return Promise.reject(); + } + return new Promise((resolve, reject) => { const id = uuid(); diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend/src/scripts/url.ts deleted file mode 100644 index 5a8265af9e..0000000000 --- a/packages/frontend/src/scripts/url.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* objを検査して - * 1. 配列に何も入っていない時はクエリを付けない - * 2. プロパティがundefinedの時はクエリを付けない - * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) - */ -export function query(obj: Record<string, any>): string { - const params = Object.entries(obj) - .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) - .reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); - - return Object.entries(params) - .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) - .join('&'); -} - -export function appendQuery(url: string, query: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; -} - -export function extractDomain(url: string) { - const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im); - return match ? match[1] : null; -} diff --git a/packages/frontend/src/scripts/use-document-visibility.ts b/packages/frontend/src/scripts/use-document-visibility.ts deleted file mode 100644 index a8f4d5e03a..0000000000 --- a/packages/frontend/src/scripts/use-document-visibility.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { onMounted, onUnmounted, ref, Ref } from 'vue'; - -export function useDocumentVisibility(): Ref<DocumentVisibilityState> { - const visibility = ref(document.visibilityState); - - const onChange = (): void => { - visibility.value = document.visibilityState; - }; - - onMounted(() => { - document.addEventListener('visibilitychange', onChange); - }); - - onUnmounted(() => { - document.removeEventListener('visibilitychange', onChange); - }); - - return visibility; -} diff --git a/packages/frontend/src/scripts/use-form.ts b/packages/frontend/src/scripts/use-form.ts new file mode 100644 index 0000000000..0d505fe466 --- /dev/null +++ b/packages/frontend/src/scripts/use-form.ts @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { computed, Reactive, reactive, watch } from 'vue'; + +function copy<T>(v: T): T { + return JSON.parse(JSON.stringify(v)); +} + +function unwrapReactive<T>(v: Reactive<T>): T { + return JSON.parse(JSON.stringify(v)); +} + +export function useForm<T extends Record<string, any>>(initialState: T, save: (newState: T) => Promise<void>) { + const currentState = reactive<T>(copy(initialState)); + const previousState = reactive<T>(copy(initialState)); + + const modifiedStates = reactive<Record<keyof T, boolean>>({} as any); + for (const key in currentState) { + modifiedStates[key] = false; + } + const modified = computed(() => Object.values(modifiedStates).some(v => v)); + const modifiedCount = computed(() => Object.values(modifiedStates).filter(v => v).length); + + watch([currentState, previousState], () => { + for (const key in modifiedStates) { + modifiedStates[key] = currentState[key] !== previousState[key]; + } + }, { deep: true }); + + async function _save() { + await save(unwrapReactive(currentState)); + for (const key in currentState) { + previousState[key] = copy(currentState[key]); + } + } + + function discard() { + for (const key in currentState) { + currentState[key] = copy(previousState[key]); + } + } + + return { + state: currentState, + savedState: previousState, + modifiedStates, + modified, + modifiedCount, + save: _save, + discard, + }; +} diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend/src/scripts/use-interval.ts deleted file mode 100644 index b50e78c3cc..0000000000 --- a/packages/frontend/src/scripts/use-interval.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'; - -export function useInterval(fn: () => void, interval: number, options: { - immediate: boolean; - afterMounted: boolean; -}): (() => void) | undefined { - if (Number.isNaN(interval)) return; - - let intervalId: number | null = null; - - if (options.afterMounted) { - onMounted(() => { - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - }); - } else { - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - } - - const clear = () => { - if (intervalId) window.clearInterval(intervalId); - intervalId = null; - }; - - onActivated(() => { - if (intervalId) return; - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - }); - - onDeactivated(() => { - clear(); - }); - - onUnmounted(() => { - clear(); - }); - - return clear; -} diff --git a/packages/frontend/src/scripts/worker-multi-dispatch.ts b/packages/frontend/src/scripts/worker-multi-dispatch.ts deleted file mode 100644 index 6b3fcd9383..0000000000 --- a/packages/frontend/src/scripts/worker-multi-dispatch.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -function defaultUseWorkerNumber(prev: number, totalWorkers: number) { - return prev + 1; -} - -export class WorkerMultiDispatch<POST = any, RETURN = any> { - private symbol = Symbol('WorkerMultiDispatch'); - private workers: Worker[] = []; - private terminated = false; - private prevWorkerNumber = 0; - private getUseWorkerNumber = defaultUseWorkerNumber; - private finalizationRegistry: FinalizationRegistry<symbol>; - - constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) { - this.getUseWorkerNumber = getUseWorkerNumber; - for (let i = 0; i < concurrency; i++) { - this.workers.push(workerConstructor()); - } - - this.finalizationRegistry = new FinalizationRegistry(() => { - this.terminate(); - }); - this.finalizationRegistry.register(this, this.symbol); - - if (_DEV_) console.log('WorkerMultiDispatch: Created', this); - } - - public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) { - let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length); - workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length; - if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber); - this.prevWorkerNumber = workerNumber; - - // 不毛だがunionをoverloadに突っ込めない - // https://stackoverflow.com/questions/66507585/overload-signatures-union-types-and-no-overload-matches-this-call-error - // https://github.com/microsoft/TypeScript/issues/14107 - if (Array.isArray(options)) { - this.workers[workerNumber].postMessage(message, options); - } else { - this.workers[workerNumber].postMessage(message, options); - } - return workerNumber; - } - - public addListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) { - this.workers.forEach(worker => { - worker.addEventListener('message', callback, options); - }); - } - - public removeListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) { - this.workers.forEach(worker => { - worker.removeEventListener('message', callback, options); - }); - } - - public terminate() { - this.terminated = true; - if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this); - this.workers.forEach(worker => { - worker.terminate(); - }); - this.workers = []; - this.finalizationRegistry.unregister(this); - } - - public isTerminated() { - return this.terminated; - } - - public getWorkers() { - return this.workers; - } - - public getSymbol() { - return this.symbol; - } -} diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index ab5fbf0dd1..2df5b04a64 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -5,11 +5,13 @@ import { markRaw, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { hemisphere } from '@@/js/intl-const.js'; +import lightTheme from '@@/themes/l-cherry.json5'; +import darkTheme from '@@/themes/d-ice.json5'; import { miLocalStorage } from './local-storage.js'; import { searchEngineMap } from './scripts/search-engine-map.js'; import type { SoundType } from '@/scripts/sound.js'; import { Storage } from '@/pizzax.js'; -import { hemisphere } from '@/scripts/intl-const.js'; interface PostFormAction { title: string, @@ -300,9 +302,9 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'twemoji', // twemoji / fluentEmoji / native }, - disableDrawer: { + menuStyle: { where: 'device', - default: false, + default: 'auto' as 'auto' | 'popup' | 'drawer', }, useBlurEffectForModal: { where: 'device', @@ -364,9 +366,9 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 2, }, - emojiPickerUseDrawerForMobile: { + emojiPickerStyle: { where: 'device', - default: true, + default: 'auto' as 'auto' | 'popup' | 'drawer', }, recentlyUsedEmojis: { where: 'device', @@ -464,9 +466,9 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'horizontal' as 'vertical' | 'horizontal', }, - enableCondensedLineForAcct: { + enableCondensedLine: { where: 'device', - default: false, + default: true, }, additionalUnicodeEmojiIndexes: { where: 'device', @@ -532,10 +534,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - contextMenu: { + contextMenu: { where: 'device', default: 'app' as 'app' | 'appWithShift' | 'native', - }, + }, sound_masterVolume: { where: 'device', @@ -594,8 +596,6 @@ interface Watcher { /** * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ) */ -import lightTheme from '@/themes/l-cherry.json5'; -import darkTheme from '@/themes/d-ice.json5'; export class ColdDeviceStorage { public static default = { @@ -632,7 +632,7 @@ export class ColdDeviceStorage { public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void { // 呼び出し側のバグ等で undefined が来ることがある // undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (value === undefined) { console.error(`attempt to store undefined value for key '${key}'`); return; diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index 0d5bd78b09..e63dac951c 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -6,18 +6,21 @@ import * as Misskey from 'misskey-js'; import { markRaw } from 'vue'; import { $i } from '@/account.js'; -import { wsOrigin } from '@/config.js'; +import { wsOrigin } from '@@/js/config.js'; +// TODO: No WebsocketモードでStreamMockが使えそう +//import { StreamMock } from '@/scripts/stream-mock.js'; // heart beat interval in ms const HEART_BEAT_INTERVAL = 1000 * 60; -let stream: Misskey.Stream | null = null; -let timeoutHeartBeat: ReturnType<typeof setTimeout> | null = null; +let stream: Misskey.IStream | null = null; +let timeoutHeartBeat: number | null = null; let lastHeartbeatCall = 0; -export function useStream(): Misskey.Stream { +export function useStream(): Misskey.IStream { if (stream) return stream; + // TODO: No Websocketモードもここで判定 stream = markRaw(new Misskey.Stream(wsOrigin, $i ? { token: $i.token, } : null)); diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 62ba7a08d5..d990a706b3 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -33,21 +33,14 @@ --minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px))); --minBottomSpacing: var(--minBottomSpacingMobile); + //--ad: rgb(255 169 0 / 10%); + @media (max-width: 500px) { --margin: var(--marginHalf); } --avatar: 48px; --thread-width: 2px; - - //--ad: rgb(255 169 0 / 10%); - --eventFollow: #36aed2; - --eventRenote: #36d298; - --eventReply: #007aff; - --eventReactionHeart: #dd2e44; - --eventReaction: #e99a0b; - --eventAchievement: #cb9a11; - --eventOther: #88a6b7; } html.radius-misskey { @@ -289,11 +282,11 @@ rt { background: var(--accent); &:not(:disabled):hover { - background: var(--X8); + background: hsl(from var(--accent) h s calc(l + 5)); } &:not(:disabled):active { - background: var(--X9); + background: hsl(from var(--accent) h s calc(l - 5)); } } @@ -303,11 +296,11 @@ rt { background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } &:not(:disabled):active { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } } @@ -412,6 +405,16 @@ rt { vertical-align: top; } +._modified { + margin-left: 0.7em; + font-size: 65%; + padding: 2px 3px; + color: var(--warn); + border: solid 1px var(--warn); + border-radius: 4px; + vertical-align: top; +} + ._table { > ._row { display: flex; @@ -481,7 +484,7 @@ rt { --fg: #693410; } -html[data-color-mode=dark] ._woodenFrame { +html[data-color-scheme=dark] ._woodenFrame { --bg: #1d0c02; --fg: #F1E8DC; --panel: #192320; diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5 deleted file mode 100644 index 7b70aa1e09..0000000000 --- a/packages/frontend/src/themes/_dark.json5 +++ /dev/null @@ -1,102 +0,0 @@ -// ダークテーマのベーステーマ -// このテーマが直接使われることは無い -{ - id: 'dark', - - name: 'Dark', - author: 'syuilo', - desc: 'Default dark theme', - kind: 'dark', - - props: { - accent: '#86b300', - accentDarken: ':darken<10<@accent', - accentLighten: ':lighten<10<@accent', - accentedBg: ':alpha<0.15<@accent', - focus: ':alpha<0.3<@accent', - bg: '#000', - acrylicBg: ':alpha<0.5<@bg', - fg: '#dadada', - fgTransparentWeak: ':alpha<0.75<@fg', - fgTransparent: ':alpha<0.5<@fg', - fgHighlighted: ':lighten<3<@fg', - fgOnAccent: '#fff', - fgOnWhite: '#333', - divider: 'rgba(255, 255, 255, 0.1)', - indicator: '@accent', - panel: ':lighten<3<@bg', - panelHighlight: ':lighten<3<@panel', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - panelBorder: '" solid 1px var(--divider)', - thread: ':lighten<12<@panel', - acrylicPanel: ':alpha<0.5<@panel', - windowHeader: ':alpha<0.85<@panel', - popup: ':lighten<3<@panel', - shadow: 'rgba(0, 0, 0, 0.3)', - header: ':alpha<0.7<@panel', - navBg: '@panel', - navFg: '@fg', - navHoverFg: ':lighten<17<@fg', - navActive: '@accent', - navIndicator: '@indicator', - link: '#44a4c1', - hashtag: '#ff9156', - mention: '@accent', - mentionMe: '@mention', - renote: '#229e82', - modalBg: 'rgba(0, 0, 0, 0.5)', - scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - dateLabelFg: '@fg', - infoBg: '#253142', - infoFg: '#fff', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - switchBg: 'rgba(255, 255, 255, 0.15)', - buttonBg: 'rgba(255, 255, 255, 0.05)', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - switchOffBg: 'rgba(255, 255, 255, 0.1)', - switchOffFg: ':alpha<0.8<@fg', - switchOnBg: '@accentedBg', - switchOnFg: '@accent', - inputBorder: 'rgba(255, 255, 255, 0.1)', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - driveFolderBg: ':alpha<0.3<@accent', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - badge: '#31b1ce', - messageBg: '@bg', - success: '#86b300', - error: '#ec4137', - warn: '#ecb637', - codeString: '#ffb675', - codeNumber: '#cfff9e', - codeBoolean: '#c59eff', - deckBg: '#000', - htmlThemeColor: '@bg', - X2: ':darken<2<@panel', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - }, - - codeHighlighter: { - base: 'one-dark-pro', - }, -} diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend/src/themes/_light.json5 deleted file mode 100644 index d797aec734..0000000000 --- a/packages/frontend/src/themes/_light.json5 +++ /dev/null @@ -1,102 +0,0 @@ -// ライトテーマのベーステーマ -// このテーマが直接使われることは無い -{ - id: 'light', - - name: 'Light', - author: 'syuilo', - desc: 'Default light theme', - kind: 'light', - - props: { - accent: '#86b300', - accentDarken: ':darken<10<@accent', - accentLighten: ':lighten<10<@accent', - accentedBg: ':alpha<0.15<@accent', - focus: ':alpha<0.3<@accent', - bg: '#fff', - acrylicBg: ':alpha<0.5<@bg', - fg: '#5f5f5f', - fgTransparentWeak: ':alpha<0.75<@fg', - fgTransparent: ':alpha<0.5<@fg', - fgHighlighted: ':darken<3<@fg', - fgOnAccent: '#fff', - fgOnWhite: '#333', - divider: 'rgba(0, 0, 0, 0.1)', - indicator: '@accent', - panel: ':lighten<3<@bg', - panelHighlight: ':darken<3<@panel', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - panelBorder: '" solid 1px var(--divider)', - thread: ':darken<12<@panel', - acrylicPanel: ':alpha<0.5<@panel', - windowHeader: ':alpha<0.85<@panel', - popup: ':lighten<3<@panel', - shadow: 'rgba(0, 0, 0, 0.1)', - header: ':alpha<0.7<@panel', - navBg: '@panel', - navFg: '@fg', - navHoverFg: ':darken<17<@fg', - navActive: '@accent', - navIndicator: '@indicator', - link: '#44a4c1', - hashtag: '#ff9156', - mention: '@accent', - mentionMe: '@mention', - renote: '#229e82', - modalBg: 'rgba(0, 0, 0, 0.3)', - scrollbarHandle: 'rgba(0, 0, 0, 0.2)', - scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', - dateLabelFg: '@fg', - infoBg: '#e5f5ff', - infoFg: '#72818a', - infoWarnBg: '#fff0db', - infoWarnFg: '#8f6e31', - switchBg: 'rgba(0, 0, 0, 0.15)', - buttonBg: 'rgba(0, 0, 0, 0.05)', - buttonHoverBg: 'rgba(0, 0, 0, 0.1)', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - switchOffBg: 'rgba(0, 0, 0, 0.1)', - switchOffFg: '@panel', - switchOnBg: '@accent', - switchOnFg: '@fgOnAccent', - inputBorder: 'rgba(0, 0, 0, 0.1)', - inputBorderHover: 'rgba(0, 0, 0, 0.2)', - listItemHoverBg: 'rgba(0, 0, 0, 0.03)', - driveFolderBg: ':alpha<0.3<@accent', - wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', - badge: '#31b1ce', - messageBg: '@bg', - success: '#86b300', - error: '#ec4137', - warn: '#ecb637', - codeString: '#b98710', - codeNumber: '#0fbbbb', - codeBoolean: '#62b70c', - deckBg: ':darken<3<@bg', - htmlThemeColor: '@bg', - X2: ':darken<2<@panel', - X3: 'rgba(0, 0, 0, 0.05)', - X4: 'rgba(0, 0, 0, 0.1)', - X5: 'rgba(0, 0, 0, 0.05)', - X6: 'rgba(0, 0, 0, 0.25)', - X7: 'rgba(0, 0, 0, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.1)', - X12: 'rgba(0, 0, 0, 0.1)', - X13: 'rgba(0, 0, 0, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - }, - - codeHighlighter: { - base: 'catppuccin-latte', - }, -} diff --git a/packages/frontend/src/themes/d-astro.json5 b/packages/frontend/src/themes/d-astro.json5 deleted file mode 100644 index fee25cc4a4..0000000000 --- a/packages/frontend/src/themes/d-astro.json5 +++ /dev/null @@ -1,76 +0,0 @@ -{ - id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea', - base: 'dark', - name: 'Mi Astro Dark', - author: 'syuilo', - props: { - bg: '#232125', - fg: '#efdab9', - link: '#78b0a0', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: '#2a272b', - accent: '#81c08b', - header: ':alpha<0.7<@bg', - infoBg: '#253142', - infoFg: '#fff', - renote: '#659CC8', - shadow: 'rgba(0, 0, 0, 0.3)', - divider: 'rgba(255, 255, 255, 0.1)', - hashtag: '#ff9156', - mention: '#ffd152', - modalBg: 'rgba(0, 0, 0, 0.5)', - success: '#86b300', - buttonBg: 'rgba(255, 255, 255, 0.05)', - acrylicBg: ':alpha<0.5<@bg', - indicator: '@accent', - mentionMe: '#fb5d38', - messageBg: '@bg', - navActive: '@accent', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - navHoverFg: ':lighten<17<@fg', - dateLabelFg: '@fg', - inputBorder: 'rgba(255, 255, 255, 0.1)', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@accent', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - buttonGradateA: '@accent', - buttonGradateB: ':hue<-20<@accent', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':lighten<3<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - htmlThemeColor: '@bg', - fgOnWhite: '@accent', - panelHighlight: ':lighten<3<@panel', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - X2: ':darken<2<@panel', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - }, -} diff --git a/packages/frontend/src/themes/d-botanical.json5 b/packages/frontend/src/themes/d-botanical.json5 deleted file mode 100644 index 62208d2378..0000000000 --- a/packages/frontend/src/themes/d-botanical.json5 +++ /dev/null @@ -1,26 +0,0 @@ -{ - id: '504debaf-4912-6a4c-5059-1db08a76b737', - - name: 'Mi Botanical Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: 'rgb(148, 179, 0)', - bg: 'rgb(37, 38, 36)', - fg: 'rgb(216, 212, 199)', - fgHighlighted: '#fff', - fgOnWhite: '@accent', - divider: 'rgba(255, 255, 255, 0.14)', - panel: 'rgb(47, 47, 44)', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - header: ':alpha<0.7<@panel', - navBg: '#363636', - renote: '@accent', - mention: 'rgb(212, 153, 76)', - mentionMe: 'rgb(212, 210, 76)', - hashtag: '#5bcbb0', - link: '@accent', - }, -} diff --git a/packages/frontend/src/themes/d-cherry.json5 b/packages/frontend/src/themes/d-cherry.json5 deleted file mode 100644 index f9638124c2..0000000000 --- a/packages/frontend/src/themes/d-cherry.json5 +++ /dev/null @@ -1,21 +0,0 @@ -{ - id: '679b3b87-a4e9-4789-8696-b56c15cc33b0', - - name: 'Mi Cherry Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: 'rgb(255, 89, 117)', - bg: 'rgb(28, 28, 37)', - fg: 'rgb(236, 239, 244)', - fgOnWhite: '@accent', - panel: 'rgb(35, 35, 47)', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '@accent', - divider: 'rgb(63, 63, 80)', - }, -} diff --git a/packages/frontend/src/themes/d-dark.json5 b/packages/frontend/src/themes/d-dark.json5 deleted file mode 100644 index ae4f7d53f5..0000000000 --- a/packages/frontend/src/themes/d-dark.json5 +++ /dev/null @@ -1,26 +0,0 @@ -{ - id: '8050783a-7f63-445a-b270-36d0f6ba1677', - - name: 'Mi Dark', - author: 'syuilo', - desc: 'Default light theme', - - base: 'dark', - - props: { - bg: '#232323', - fg: 'rgb(199, 209, 216)', - fgHighlighted: '#fff', - fgOnWhite: '@accent', - divider: 'rgba(255, 255, 255, 0.14)', - panel: '#2d2d2d', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - header: ':alpha<0.7<@panel', - navBg: '#363636', - renote: '@accent', - mention: '#da6d35', - mentionMe: '#d44c4c', - hashtag: '#4cb8d4', - link: '@accent', - }, -} diff --git a/packages/frontend/src/themes/d-future.json5 b/packages/frontend/src/themes/d-future.json5 deleted file mode 100644 index f2c1f3eb86..0000000000 --- a/packages/frontend/src/themes/d-future.json5 +++ /dev/null @@ -1,27 +0,0 @@ -{ - id: '32a637ef-b47a-4775-bb7b-bacbb823f865', - - name: 'Mi Future Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#63e2b7', - bg: '#101014', - fg: '#D5D5D6', - fgHighlighted: '#fff', - fgOnAccent: '#000', - fgOnWhite: '@accent', - divider: 'rgba(255, 255, 255, 0.1)', - panel: '#18181c', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - renote: '@accent', - mention: '#f2c97d', - mentionMe: '@accent', - hashtag: '#70c0e8', - link: '#e88080', - buttonGradateA: '@accent', - buttonGradateB: ':saturate<30<:hue<30<@accent', - }, -} diff --git a/packages/frontend/src/themes/d-green-lime.json5 b/packages/frontend/src/themes/d-green-lime.json5 deleted file mode 100644 index ca4e688fdb..0000000000 --- a/packages/frontend/src/themes/d-green-lime.json5 +++ /dev/null @@ -1,24 +0,0 @@ -{ - id: '02816013-8107-440f-877e-865083ffe194', - - name: 'Mi Green+Lime Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#b4e900', - bg: '#0C1210', - fg: '#dee7e4', - fgHighlighted: '#fff', - fgOnAccent: '#192320', - fgOnWhite: '@accent', - divider: '#e7fffb24', - panel: '#192320', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - popup: '#293330', - renote: '@accent', - mentionMe: '#ffaa00', - link: '#24d7ce', - }, -} diff --git a/packages/frontend/src/themes/d-green-orange.json5 b/packages/frontend/src/themes/d-green-orange.json5 deleted file mode 100644 index c2539816e2..0000000000 --- a/packages/frontend/src/themes/d-green-orange.json5 +++ /dev/null @@ -1,24 +0,0 @@ -{ - id: 'dc489603-27b5-424a-9b25-1ff6aec9824a', - - name: 'Mi Green+Orange Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#e97f00', - bg: '#0C1210', - fg: '#dee7e4', - fgHighlighted: '#fff', - fgOnAccent: '#192320', - fgOnWhite: '@accent', - divider: '#e7fffb24', - panel: '#192320', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - popup: '#293330', - renote: '@accent', - mentionMe: '#b4e900', - link: '#24d7ce', - }, -} diff --git a/packages/frontend/src/themes/d-ice.json5 b/packages/frontend/src/themes/d-ice.json5 deleted file mode 100644 index b4abc0cacb..0000000000 --- a/packages/frontend/src/themes/d-ice.json5 +++ /dev/null @@ -1,14 +0,0 @@ -{ - id: '66e7e5a9-cd43-42cd-837d-12f47841fa34', - - name: 'Mi Ice Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#47BFE8', - fgOnWhite: '@accent', - bg: '#212526', - }, -} diff --git a/packages/frontend/src/themes/d-persimmon.json5 b/packages/frontend/src/themes/d-persimmon.json5 deleted file mode 100644 index 0ab6523dd7..0000000000 --- a/packages/frontend/src/themes/d-persimmon.json5 +++ /dev/null @@ -1,26 +0,0 @@ -{ - id: 'c503d768-7c70-4db2-a4e6-08264304bc8d', - - name: 'Mi Persimmon Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: 'rgb(206, 102, 65)', - bg: 'rgb(31, 33, 31)', - fg: '#cdd8c7', - fgHighlighted: '#fff', - fgOnWhite: '@accent', - divider: 'rgba(255, 255, 255, 0.14)', - panel: 'rgb(41, 43, 41)', - infoFg: '@fg', - infoBg: '#333c3b', - navBg: '#141714', - renote: '@accent', - mention: '@accent', - mentionMe: '#de6161', - hashtag: '#68bad0', - link: '#a1c758', - }, -} diff --git a/packages/frontend/src/themes/d-u0.json5 b/packages/frontend/src/themes/d-u0.json5 deleted file mode 100644 index 3bd0b9483c..0000000000 --- a/packages/frontend/src/themes/d-u0.json5 +++ /dev/null @@ -1,86 +0,0 @@ -{ - id: '7a5bc13b-df8f-4d44-8e94-4452f0c634bb', - base: 'dark', - name: 'Mi U0 Dark', - props: { - X2: ':darken<2<@panel', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - bg: '#172426', - fg: '#dadada', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - link: '@accent', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: ':lighten<3<@bg', - popup: ':lighten<3<@panel', - accent: '#00a497', - header: ':alpha<0.7<@panel', - infoBg: '#253142', - infoFg: '#fff', - renote: '@accent', - shadow: 'rgba(0, 0, 0, 0.3)', - divider: 'rgba(255, 255, 255, 0.1)', - hashtag: '#e6b422', - mention: '@accent', - modalBg: 'rgba(0, 0, 0, 0.5)', - success: '#86b300', - buttonBg: 'rgba(255, 255, 255, 0.05)', - switchBg: 'rgba(255, 255, 255, 0.15)', - acrylicBg: ':alpha<0.5<@bg', - indicator: '@accent', - mentionMe: '@mention', - messageBg: '@bg', - navActive: '@accent', - accentedBg: ':alpha<0.15<@accent', - codeNumber: '#cfff9e', - codeString: '#ffb675', - fgOnAccent: '#fff', - fgOnWhite: '@accent', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - navHoverFg: ':lighten<17<@fg', - codeBoolean: '#c59eff', - dateLabelFg: '@fg', - inputBorder: 'rgba(255, 255, 255, 0.1)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@indicator', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':lighten<3<@fg', - fgTransparent: ':alpha<0.5<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - htmlThemeColor: '@bg', - panelHighlight: ':lighten<3<@panel', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - deckBg: '#142022', - }, -} diff --git a/packages/frontend/src/themes/l-apricot.json5 b/packages/frontend/src/themes/l-apricot.json5 deleted file mode 100644 index fe1f9f8927..0000000000 --- a/packages/frontend/src/themes/l-apricot.json5 +++ /dev/null @@ -1,23 +0,0 @@ -{ - id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b', - - name: 'Mi Apricot Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: 'rgb(234, 154, 82)', - bg: '#e6e5e2', - fg: 'rgb(149, 143, 139)', - fgOnWhite: '@accent', - panel: '#EEECE8', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '@accent', - inputBorder: 'rgba(0, 0, 0, 0.1)', - inputBorderHover: 'rgba(0, 0, 0, 0.2)', - infoBg: 'rgb(226, 235, 241)', - }, -} diff --git a/packages/frontend/src/themes/l-botanical.json5 b/packages/frontend/src/themes/l-botanical.json5 deleted file mode 100644 index 17e9ca246f..0000000000 --- a/packages/frontend/src/themes/l-botanical.json5 +++ /dev/null @@ -1,30 +0,0 @@ -{ - id: '1100673c-f902-4ccd-93aa-7cb88be56178', - - name: 'Mi Botanical Light', - author: 'ThinaticSystem', - - base: 'light', - - props: { - accent: '#77b58c', - bg: 'e2deda', - fg: '#3d3d3d', - fgHighlighted: '#6bc9a0', - fgOnWhite: '@accent', - divider: '#cfcfcf', - panel: '@X14', - panelHeaderBg: '@panel', - panelHeaderDivider: '@divider', - header: ':alpha<0.7<@panel', - navBg: '@X14', - renote: '#229e92', - mention: '#da6d35', - mentionMe: '#d44c4c', - hashtag: '#4cb8d4', - link: '@accent', - buttonGradateB: ':hue<-70<@accent', - success: '#86b300', - X14: '#ebe7e5' - }, -} diff --git a/packages/frontend/src/themes/l-cherry.json5 b/packages/frontend/src/themes/l-cherry.json5 deleted file mode 100644 index 1189a28fe6..0000000000 --- a/packages/frontend/src/themes/l-cherry.json5 +++ /dev/null @@ -1,22 +0,0 @@ -{ - id: 'ac168876-f737-4074-a3fc-a370c732ef48', - - name: 'Mi Cherry Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: 'rgb(219, 96, 114)', - bg: 'rgb(254, 248, 249)', - fg: 'rgb(152, 13, 26)', - fgOnWhite: '@accent', - panel: 'rgb(255, 255, 255)', - renote: '@accent', - link: 'rgb(156, 187, 5)', - mention: '@accent', - hashtag: '@accent', - divider: 'rgba(134, 51, 51, 0.1)', - inputBorderHover: 'rgb(238, 221, 222)', - }, -} diff --git a/packages/frontend/src/themes/l-coffee.json5 b/packages/frontend/src/themes/l-coffee.json5 deleted file mode 100644 index b64cc73583..0000000000 --- a/packages/frontend/src/themes/l-coffee.json5 +++ /dev/null @@ -1,22 +0,0 @@ -{ - id: '6ed80faa-74f0-42c2-98e4-a64d9e138eab', - - name: 'Mi Coffee Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: '#9f8989', - bg: '#f5f3f3', - fg: '#7f6666', - fgOnWhite: '@accent', - panel: '#fff', - divider: 'rgba(87, 68, 68, 0.1)', - renote: 'rgb(160, 172, 125)', - link: 'rgb(137, 151, 159)', - mention: '@accent', - mentionMe: 'rgb(170, 149, 98)', - hashtag: '@accent', - }, -} diff --git a/packages/frontend/src/themes/l-light.json5 b/packages/frontend/src/themes/l-light.json5 deleted file mode 100644 index 63c2e6d278..0000000000 --- a/packages/frontend/src/themes/l-light.json5 +++ /dev/null @@ -1,21 +0,0 @@ -{ - id: '4eea646f-7afa-4645-83e9-83af0333cd37', - - name: 'Mi Light', - author: 'syuilo', - desc: 'Default light theme', - - base: 'light', - - props: { - bg: '#f9f9f9', - fg: '#676767', - fgOnWhite: '@accent', - divider: '#e8e8e8', - header: ':alpha<0.7<@panel', - navBg: '#fff', - panel: '#fff', - panelHeaderDivider: '@divider', - mentionMe: 'rgb(0, 179, 70)', - }, -} diff --git a/packages/frontend/src/themes/l-rainy.json5 b/packages/frontend/src/themes/l-rainy.json5 deleted file mode 100644 index e7d1d5af00..0000000000 --- a/packages/frontend/src/themes/l-rainy.json5 +++ /dev/null @@ -1,22 +0,0 @@ -{ - id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96', - - name: 'Mi Rainy Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: '#5db0da', - bg: 'rgb(246 248 249)', - fg: '#636b71', - fgOnWhite: '@accent', - panel: '#fff', - divider: 'rgb(230 233 234)', - panelHeaderDivider: '@divider', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '@accent', - }, -} diff --git a/packages/frontend/src/themes/l-sushi.json5 b/packages/frontend/src/themes/l-sushi.json5 deleted file mode 100644 index f1523b698c..0000000000 --- a/packages/frontend/src/themes/l-sushi.json5 +++ /dev/null @@ -1,19 +0,0 @@ -{ - id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c', - - name: 'Mi Sushi Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: '#e36749', - bg: '#f0eee9', - fg: '#5f5f5f', - fgOnWhite: '@accent', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '@accent', - }, -} diff --git a/packages/frontend/src/themes/l-u0.json5 b/packages/frontend/src/themes/l-u0.json5 deleted file mode 100644 index dbc777d493..0000000000 --- a/packages/frontend/src/themes/l-u0.json5 +++ /dev/null @@ -1,85 +0,0 @@ -{ - id: 'e2c940b5-6e9a-4c03-b738-261c720c426d', - base: 'light', - name: 'Mi U0 Light', - props: { - X2: ':darken<2<@panel', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - bg: '#e7e7eb', - fg: '#5f5f5f', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - link: '@accent', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: ':lighten<3<@bg', - popup: ':lighten<3<@panel', - accent: '#478384', - header: ':alpha<0.7<@panel', - infoBg: '#253142', - infoFg: '#fff', - renote: '@accent', - shadow: 'rgba(0, 0, 0, 0.3)', - divider: '#4646461a', - hashtag: '#1f3134', - mention: '@accent', - modalBg: 'rgba(0, 0, 0, 0.5)', - success: '#86b300', - buttonBg: '#0000000d', - switchBg: 'rgba(255, 255, 255, 0.15)', - acrylicBg: ':alpha<0.5<@bg', - indicator: '@accent', - mentionMe: '@mention', - messageBg: '@bg', - navActive: '@accent', - accentedBg: ':alpha<0.15<@accent', - codeNumber: '#cfff9e', - codeString: '#ffb675', - fgOnAccent: '#fff', - fgOnWhite: '@accent', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - navHoverFg: ':lighten<17<@fg', - codeBoolean: '#c59eff', - dateLabelFg: '@fg', - inputBorder: 'rgba(255, 255, 255, 0.1)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@indicator', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: '#0000001a', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':lighten<3<@fg', - fgTransparent: ':alpha<0.5<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - htmlThemeColor: '@bg', - panelHighlight: ':lighten<3<@panel', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - scrollbarHandle: '#74747433', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - }, -} diff --git a/packages/frontend/src/themes/l-vivid.json5 b/packages/frontend/src/themes/l-vivid.json5 deleted file mode 100644 index 3368855b5e..0000000000 --- a/packages/frontend/src/themes/l-vivid.json5 +++ /dev/null @@ -1,80 +0,0 @@ -{ - id: '6128c2a9-5c54-43fe-a47d-17942356470b', - - name: 'Mi Vivid Light', - author: 'syuilo', - - base: 'light', - - props: { - bg: '#fafafa', - fg: '#444', - link: '#ff9400', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: '#fff', - accent: '#008cff', - header: ':alpha<0.7<@panel', - infoBg: '#e5f5ff', - infoFg: '#72818a', - renote: '@accent', - shadow: 'rgba(0, 0, 0, 0.1)', - divider: 'rgba(0, 0, 0, 0.08)', - hashtag: '#92d400', - mention: '@accent', - modalBg: 'rgba(0, 0, 0, 0.3)', - success: '#86b300', - buttonBg: 'rgba(0, 0, 0, 0.05)', - acrylicBg: ':alpha<0.5<@bg', - indicator: '@accent', - mentionMe: '@mention', - messageBg: '@bg', - navActive: '@accent', - infoWarnBg: '#fff0db', - infoWarnFg: '#8f6e31', - navHoverFg: ':darken<17<@fg', - dateLabelFg: '@fg', - inputBorder: 'rgba(0, 0, 0, 0.1)', - inputBorderHover: 'rgba(0, 0, 0, 0.2)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@accent', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(0, 0, 0, 0.1)', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':darken<3<@fg', - fgTransparent: ':alpha<0.5<@fg', - fgOnWhite: '@accent', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - htmlThemeColor: '@bg', - panelHighlight: ':darken<3<@panel', - listItemHoverBg: 'rgba(0, 0, 0, 0.03)', - scrollbarHandle: 'rgba(0, 0, 0, 0.2)', - wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: '@divider', - scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', - X2: ':darken<2<@panel', - X3: 'rgba(0, 0, 0, 0.05)', - X4: 'rgba(0, 0, 0, 0.1)', - X5: 'rgba(0, 0, 0, 0.05)', - X6: 'rgba(0, 0, 0, 0.25)', - X7: 'rgba(0, 0, 0, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.1)', - X12: 'rgba(0, 0, 0, 0.1)', - X13: 'rgba(0, 0, 0, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - }, -} diff --git a/packages/frontend/src/themes/rosepine-dawn.json5 b/packages/frontend/src/themes/rosepine-dawn.json5 deleted file mode 100644 index ff1ca0c996..0000000000 --- a/packages/frontend/src/themes/rosepine-dawn.json5 +++ /dev/null @@ -1,89 +0,0 @@ -{ - id: '919c91ac-c6fa-43dc-a423-3cc84fd67d7c', - base: 'light', - name: ' Rosé Pine Dawn', - description: 'Soho vibes for Misskey, dawn edition', - props: { - accent: '#286983', - accentDarken: ':darken<10<@accent', - accentLighten: ':lighten<10<@accent', - accentedBg: ':alpha<0.15<@accent', - focus: ':alpha<0.3<@accent', - bg: '#faf4ed', - acrylicBg: ':alpha<0.5<@bg', - fg: '#575279', - fgTransparentWeak: ':alpha<0.75<@fg', - fgTransparent: ':alpha<0.5<@fg', - fgHighlighted: ':darken<3<@fg', - fgOnAccent: '#fffaf3', - divider: 'rgba(0, 0, 0, 0.1)', - indicator: '@accent', - panel: ':lighten<3<@bg', - panelHighlight: ':darken<3<@panel', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - panelBorder: '" solid 1px var(--divider)', - acrylicPanel: ':alpha<0.5<@panel', - popup: ':lighten<3<@panel', - shadow: 'rgba(0, 0, 0, 0.1)', - header: ':alpha<0.7<@panel', - navBg: '@panel', - navFg: '@fg', - navHoverFg: ':darken<17<@fg', - navActive: '@accent', - navIndicator: '@indicator', - link: '#56949f', - hashtag: '#ea9d34', - mention: '@accent', - mentionMe: '@mention', - renote: '#56949f', - modalBg: 'rgba(0, 0, 0, 0.3)', - scrollbarHandle: 'rgba(0, 0, 0, 0.2)', - scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', - dateLabelFg: '@fg', - infoBg: '#f2e9e1', - infoFg: '#ea9d34', - infoWarnBg: '#f2e9e1', - infoWarnFg: '#b4637a', - switchBg: 'rgba(0, 0, 0, 0.15)', - cwBg: '#b4637a', - cwFg: '#faf4ed', - cwHoverBg: '#d7827e', - buttonBg: 'rgba(0, 0, 0, 0.05)', - buttonHoverBg: 'rgba(0, 0, 0, 0.1)', - buttonGradateA: '#d7827e', - buttonGradateB: ':hue<20<#d7827e', - inputBorder: 'rgba(0, 0, 0, 0.1)', - inputBorderHover: 'rgba(0, 0, 0, 0.2)', - listItemHoverBg: 'rgba(0, 0, 0, 0.03)', - driveFolderBg: ':alpha<0.3<@accent', - wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', - badge: '#31b1ce', - messageBg: '@bg', - success: '#907aa9', - error: '#b4637a', - warn: '#ea9d34', - codeString: '#b98710', - codeNumber: '#0fbbbb', - codeBoolean: '#62b70c', - htmlThemeColor: '@bg', - X2: ':darken<2<@panel', - X3: 'rgba(0, 0, 0, 0.05)', - X4: 'rgba(0, 0, 0, 0.1)', - X5: 'rgba(0, 0, 0, 0.05)', - X6: 'rgba(0, 0, 0, 0.25)', - X7: 'rgba(0, 0, 0, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.1)', - X12: 'rgba(0, 0, 0, 0.1)', - X13: 'rgba(0, 0, 0, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - }, - author: '@thatonecalculator@stop.voring.me', -}
\ No newline at end of file diff --git a/packages/frontend/src/themes/rosepine.json5 b/packages/frontend/src/themes/rosepine.json5 deleted file mode 100644 index 06516f75fc..0000000000 --- a/packages/frontend/src/themes/rosepine.json5 +++ /dev/null @@ -1,86 +0,0 @@ -{ - id: '3cdfd635-4d5e-4d06-9ba3-20f123f0999b', - base: 'dark', - desc: 'Soho vibes for Misskey', - name: 'Rosé Pine v3', - props: { - X2: ':darken<2<@panel', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - bg: '#191724', - fg: '#e0def4', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - cwBg: '#1f1d2e', - cwFg: '#f6c177', - link: '#9ccfd8', - warn: '#f6c177', - badge: '#ebbcba', - error: '#eb6f92', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: ':lighten<3<@bg', - popup: ':lighten<3<@panel', - accent: '#c4a7e7', - header: ':alpha<0.7<@panel', - infoBg: '#253142', - infoFg: '#fff', - renote: '#31748f', - shadow: 'rgba(0, 0, 0, 0.3)', - divider: 'rgba(255, 255, 255, 0.1)', - hashtag: '#ebbcba', - mention: '@accent', - modalBg: 'rgba(0, 0, 0, 0.5)', - success: '#ebbcba', - buttonBg: 'rgba(255, 255, 255, 0.05)', - switchBg: 'rgba(255, 255, 255, 0.15)', - acrylicBg: ':alpha<0.5<@bg', - cwHoverBg: '#26233a', - indicator: '@accent', - mentionMe: '@mention', - messageBg: '@bg', - navActive: '@accent', - accentedBg: ':alpha<0.15<@accent', - fgOnAccent: '#26233a', - infoWarnBg: '#26233a', - infoWarnFg: '#f6c177', - navHoverFg: ':lighten<17<@fg', - dateLabelFg: '@fg', - inputBorder: 'rgba(255, 255, 255, 0.1)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@indicator', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':lighten<3<@fg', - fgTransparent: ':alpha<0.5<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - buttonGradateA: '@accent', - buttonGradateB: '#ebbcba', - htmlThemeColor: '@bg', - panelHighlight: ':lighten<3<@panel', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - }, - author: '@thatonecalculator@stop.voring.me', -}
\ No newline at end of file diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index 17079b3ddc..4c1fcb9c9b 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -7,7 +7,7 @@ import { defineAsyncComponent } from 'vue'; import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; @@ -41,7 +41,9 @@ function toolsMenuItems(): MenuItem[] { } export function openInstanceMenu(ev: MouseEvent) { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ text: instance.name ?? host, type: 'label', }, { @@ -69,12 +71,18 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.ads, icon: 'ti ti-ad', to: '/ads', - }, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? { - type: 'link', - to: '/invite', - text: i18n.ts.invite, - icon: 'ti ti-user-plus', - } : undefined, { + }); + + if ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) { + menuItems.push({ + type: 'link', + to: '/invite', + text: i18n.ts.invite, + icon: 'ti ti-user-plus', + }); + } + + menuItems.push({ type: 'parent', text: i18n.ts.tools, icon: 'ti ti-tool', @@ -84,50 +92,80 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.inquiry, icon: 'ti ti-help-circle', to: '/contact', - }, (instance.impressumUrl) ? { - type: 'a', - text: i18n.ts.impressum, - icon: 'ti ti-file-invoice', - href: instance.impressumUrl, - target: '_blank', - } : undefined, (instance.tosUrl) ? { - type: 'a', - text: i18n.ts.termsOfService, - icon: 'ti ti-notebook', - href: instance.tosUrl, - target: '_blank', - } : undefined, (instance.privacyPolicyUrl) ? { - type: 'a', - text: i18n.ts.privacyPolicy, - icon: 'ti ti-shield-lock', - href: instance.privacyPolicyUrl, - target: '_blank', - } : undefined, (instance.donationUrl) ? { - type: 'a', - text: i18n.ts.donation, - icon: 'ph-hand-coins ph-bold ph-lg', - href: instance.donationUrl, - target: '_blank', - } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) ? undefined : { type: 'divider' }, { + }); + + if (instance.impressumUrl) { + menuItems.push({ + type: 'a', + text: i18n.ts.impressum, + icon: 'ti ti-file-invoice', + href: instance.impressumUrl, + target: '_blank', + }); + } + + if (instance.tosUrl) { + menuItems.push({ + type: 'a', + text: i18n.ts.termsOfService, + icon: 'ti ti-notebook', + href: instance.tosUrl, + target: '_blank', + }); + } + + if (instance.privacyPolicyUrl) { + menuItems.push({ + type: 'a', + text: i18n.ts.privacyPolicy, + icon: 'ti ti-shield-lock', + href: instance.privacyPolicyUrl, + target: '_blank', + }); + } + + if(instance.donationUrl) { + menuItems.push({ + type: 'a', + text: i18n.ts.donation, + icon: 'ph-hand-coins ph-bold ph-lg', + href: instance.donationUrl, + target: '_blank', + }); + } + + if (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) { + menuItems.push({ type: 'divider' }); + } + + menuItems.push({ type: 'a', text: i18n.ts.document, icon: 'ti ti-bulb', href: 'https://misskey-hub.net/docs/for-users/', target: '_blank', - }, ($i) ? { - text: i18n.ts._initialTutorial.launchTutorial, - icon: 'ti ti-presentation', - action: () => { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, { - closed: () => dispose(), - }); - }, - } : undefined, { + }); + + if ($i) { + menuItems.push({ + text: i18n.ts._initialTutorial.launchTutorial, + icon: 'ti ti-presentation', + action: () => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, { + closed: () => dispose(), + }); + }, + }); + } + + menuItems.push({ type: 'link', text: i18n.ts.aboutMisskey, icon: 'sk-icons sk-shark sk-icons-lg', to: '/about-sharkey', - }], ev.currentTarget ?? ev.target, { + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target, { align: 'left', }); } diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 442b6479dd..e9baa0eab4 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="pendingApiRequestsCount > 0" id="wait"></div> -<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div> +<div v-if="dev" id="devTicker"><span style="animation: dev-ticker-blink 2s infinite;">DEV BUILD</span></div> <div v-if="$i && $i.isBot" id="botWarn"><span>{{ i18n.ts.loggedInAsBot }}</span></div> @@ -263,10 +263,6 @@ if ($i) { font-size: 14px; pointer-events: none; user-select: none; - - > span { - animation: dev-ticker-blink 2s infinite; - } } #devTicker { @@ -280,9 +276,5 @@ if ($i) { font-size: 14px; pointer-events: none; user-select: none; - - > span { - animation: dev-ticker-blink 2s infinite; - } } </style> diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index 9a4ac6c192..f3244b5697 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -82,6 +82,8 @@ function more() { <style lang="scss" module> .root { + --nav-bg-transparent: color(from var(--navBg) srgb r g b / 0.5); + display: flex; flex-direction: column; } @@ -91,7 +93,7 @@ function more() { top: 0; z-index: 1; padding: 20px 0; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } @@ -132,7 +134,7 @@ function more() { position: sticky; bottom: 0; padding: 20px 0; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index c30cf42624..17690df412 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -111,6 +111,7 @@ function more(ev: MouseEvent) { .root { --nav-width: 250px; --nav-icon-only-width: 80px; + --nav-bg-transparent: color(from var(--navBg) srgb r g b / 0.5); flex: 0 0 var(--nav-width); width: var(--nav-width); @@ -144,7 +145,7 @@ function more(ev: MouseEvent) { top: 0; z-index: 1; padding: 20px 0; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } @@ -194,7 +195,7 @@ function more(ev: MouseEvent) { position: sticky; bottom: 0; padding-top: 20px; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } @@ -385,7 +386,7 @@ function more(ev: MouseEvent) { top: 0; z-index: 1; padding: 20px 0; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } @@ -415,7 +416,7 @@ function more(ev: MouseEvent) { position: sticky; bottom: 0; padding-top: 20px; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index 8dad666623..e234bb3a33 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -35,7 +35,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const props = defineProps<{ diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue index 6e1d06eec1..550fc39b00 100644 --- a/packages/frontend/src/ui/_common_/statusbar-rss.vue +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { shuffle } from '@/scripts/shuffle.js'; const props = defineProps<{ diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue index 67f8b109c4..078b595dca 100644 --- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue +++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue @@ -35,7 +35,7 @@ import { ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; import { notePage } from '@/filters/note.js'; diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue index 872c69810c..690366307b 100644 --- a/packages/frontend/src/ui/_common_/statusbars.vue +++ b/packages/frontend/src/ui/_common_/statusbars.vue @@ -40,6 +40,14 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue') --nameMargin: 10px; font-size: 0.85em; + display: flex; + vertical-align: bottom; + width: 100%; + line-height: var(--height); + height: var(--height); + overflow: clip; + contain: strict; + &.verySmall { --nameMargin: 7px; --height: 16px; @@ -64,14 +72,6 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue') font-size: 0.9em; } - display: flex; - vertical-align: bottom; - width: 100%; - line-height: var(--height); - height: var(--height); - overflow: clip; - contain: strict; - &.black { background: #000; color: #fff; diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue index c859626571..96cc24c9b9 100644 --- a/packages/frontend/src/ui/classic.sidebar.vue +++ b/packages/frontend/src/ui/classic.sidebar.vue @@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, computed, watch, ref, shallowRef } from 'vue'; import { openInstanceMenu } from './_common_/common.js'; -// import { host } from '@/config.js'; +// import { host } from '@@/js/config.js'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { openAccountMenu as openAccountMenu_, $i } from '@/account.js'; diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index ce5a22a61d..31bb1ddc14 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, onMounted, provide, ref, computed, shallowRef } from 'vue'; import XSidebar from './classic.sidebar.vue'; import XCommon from './_common_/common.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import { StickySidebar } from '@/scripts/sticky-sidebar.js'; import * as os from '@/os.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; @@ -57,6 +57,8 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { mainRouter } from '@/router/main.js'; +import { isLink } from '@@/js/is-link.js'; + const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue')); const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); @@ -104,12 +106,6 @@ function top() { } function onContextmenu(ev: MouseEvent) { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; if (isLink(ev.target)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; if (window.getSelection().toString() !== '') return; diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 44f1af5f8f..cd4e256056 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -118,7 +118,7 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue'; import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; import { mainRouter } from '@/router/main.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); @@ -451,6 +451,7 @@ body { &:active { color: var(--accent); + background: hsl(from var(--panel) h s calc(l - 2)); } } @@ -460,12 +461,12 @@ body { color: var(--fgOnAccent); &:hover { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); color: var(--fgOnAccent); } &:active { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); color: var(--fgOnAccent); } } diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index 987bd4db55..a41639e71c 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -22,7 +22,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { antennasCache } from '@/cache.js'; import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index 518df7a6fc..661d45b110 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -29,7 +29,7 @@ import * as os from '@/os.js'; import { favoritedChannelsCache } from '@/cache.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 5fed70fc90..5ed3aa754f 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -46,7 +46,7 @@ import { onBeforeUnmount, onMounted, provide, watch, shallowRef, ref, computed } import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column } from './deck-store.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; provide('shouldHeaderThin', true); provide('shouldOmitHeaderTitle', true); @@ -104,7 +104,27 @@ function toggleActive() { } function getMenu() { - let items: MenuItem[] = [{ + const menuItems: MenuItem[] = []; + + if (props.menu) { + menuItems.push(...props.menu, { + type: 'divider', + }); + } + + if (props.refresher) { + menuItems.push({ + icon: 'ti ti-refresh', + text: i18n.ts.reload, + action: () => { + if (props.refresher) { + props.refresher(); + } + }, + }); + } + + menuItems.push({ icon: 'ti ti-settings', text: i18n.ts._deck.configureColumn, action: async () => { @@ -129,74 +149,73 @@ function getMenu() { if (canceled) return; updateColumn(props.column.id, result); }, + }); + + const moveToMenuItems: MenuItem[] = []; + + moveToMenuItems.push({ + icon: 'ti ti-arrow-left', + text: i18n.ts._deck.swapLeft, + action: () => { + swapLeftColumn(props.column.id); + }, }, { - type: 'parent', - text: i18n.ts.move + '...', - icon: 'ti ti-arrows-move', - children: [{ - icon: 'ti ti-arrow-left', - text: i18n.ts._deck.swapLeft, - action: () => { - swapLeftColumn(props.column.id); - }, - }, { - icon: 'ti ti-arrow-right', - text: i18n.ts._deck.swapRight, - action: () => { - swapRightColumn(props.column.id); - }, - }, props.isStacked ? { + icon: 'ti ti-arrow-right', + text: i18n.ts._deck.swapRight, + action: () => { + swapRightColumn(props.column.id); + }, + }); + + if (props.isStacked) { + moveToMenuItems.push({ icon: 'ti ti-arrow-up', text: i18n.ts._deck.swapUp, action: () => { swapUpColumn(props.column.id); }, - } : undefined, props.isStacked ? { + }, { icon: 'ti ti-arrow-down', text: i18n.ts._deck.swapDown, action: () => { swapDownColumn(props.column.id); }, - } : undefined], + }); + } + + menuItems.push({ + type: 'parent', + text: i18n.ts.move + '...', + icon: 'ti ti-arrows-move', + children: moveToMenuItems, }, { icon: 'ti ti-stack-2', text: i18n.ts._deck.stackLeft, action: () => { stackLeftColumn(props.column.id); }, - }, props.isStacked ? { - icon: 'ti ti-window-maximize', - text: i18n.ts._deck.popRight, - action: () => { - popRightColumn(props.column.id); - }, - } : undefined, { type: 'divider' }, { + }); + + if (props.isStacked) { + menuItems.push({ + icon: 'ti ti-window-maximize', + text: i18n.ts._deck.popRight, + action: () => { + popRightColumn(props.column.id); + }, + }); + } + + menuItems.push({ type: 'divider' }, { icon: 'ti ti-trash', text: i18n.ts.remove, danger: true, action: () => { removeColumn(props.column.id); }, - }]; - - if (props.menu) { - items.unshift({ type: 'divider' }); - items = props.menu.concat(items); - } - - if (props.refresher) { - items = [{ - icon: 'ti ti-refresh', - text: i18n.ts.reload, - action: () => { - if (props.refresher) { - props.refresher(); - } - }, - }, ...items]; - } + }); - return items; + return menuItems; } function showSettingsMenu(ev: MouseEvent) { @@ -324,11 +343,11 @@ function onDrop(ev) { > .body { background: transparent !important; + scrollbar-color: var(--scrollbarHandle) transparent; &::-webkit-scrollbar-track { background: transparent; } - scrollbar-color: var(--scrollbarHandle) transparent; } } @@ -338,11 +357,11 @@ function onDrop(ev) { > .body { background: var(--bg) !important; overflow-y: scroll !important; + scrollbar-color: var(--scrollbarHandle) transparent; &::-webkit-scrollbar-track { background: inherit; } - scrollbar-color: var(--scrollbarHandle) transparent; } } } @@ -423,10 +442,10 @@ function onDrop(ev) { box-sizing: border-box; container-type: size; background-color: var(--bg); + scrollbar-color: var(--scrollbarHandle) var(--panel); &::-webkit-scrollbar-track { background: var(--panel); } - scrollbar-color: var(--scrollbarHandle) var(--panel); } </style> diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index a0e318f7eb..8762fb0cce 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -22,7 +22,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { SoundStore } from '@/store.js'; import { userListsCache } from '@/cache.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index 79c9671917..f8c712c371 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -26,7 +26,8 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { useScrollPositionManager } from '@/nirax.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; +import { isLink } from '@@/js/is-link.js'; import { mainRouter } from '@/router/main.js'; defineProps<{ @@ -52,12 +53,6 @@ function back() { function onContextmenu(ev: MouseEvent) { if (!ev.target) return; - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; if (isLink(ev.target as HTMLElement)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return; if (window.getSelection()?.toString() !== '') return; diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index a375e9c574..beb4237978 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -21,7 +21,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 17afa12551..8315f7fca5 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -115,29 +115,41 @@ function onNote() { sound.playMisskeySfxFile(soundSetting.value); } -const menu = computed<MenuItem[]>(() => [{ - icon: 'ti ti-pencil', - text: i18n.ts.timeline, - action: setType, -}, { - icon: 'ti ti-bell', - text: i18n.ts._deck.newNoteNotificationSettings, - action: () => soundSettingsButton(soundSetting), -}, { - type: 'switch', - text: i18n.ts.showRenotes, - ref: withRenotes, -}, hasWithReplies(props.column.tl) ? { - type: 'switch', - text: i18n.ts.showRepliesToOthersInTimeline, - ref: withReplies, - disabled: onlyFiles, -} : undefined, { - type: 'switch', - text: i18n.ts.fileAttachedOnly, - ref: onlyFiles, - disabled: hasWithReplies(props.column.tl) ? withReplies : false, -}]); +const menu = computed<MenuItem[]>(() => { + const menuItems: MenuItem[] = []; + + menuItems.push({ + icon: 'ti ti-pencil', + text: i18n.ts.timeline, + action: setType, + }, { + icon: 'ti ti-bell', + text: i18n.ts._deck.newNoteNotificationSettings, + action: () => soundSettingsButton(soundSetting), + }, { + type: 'switch', + text: i18n.ts.showRenotes, + ref: withRenotes, + }); + + if (hasWithReplies(props.column.tl)) { + menuItems.push({ + type: 'switch', + text: i18n.ts.showRepliesToOthersInTimeline, + ref: withReplies, + disabled: onlyFiles, + }); + } + + menuItems.push({ + type: 'switch', + text: i18n.ts.fileAttachedOnly, + ref: onlyFiles, + disabled: hasWithReplies(props.column.tl) ? withReplies : false, + }); + + return menuItems; +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue index db5eb19c20..9e41c48c5b 100644 --- a/packages/frontend/src/ui/minimum.vue +++ b/packages/frontend/src/ui/minimum.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, provide, ref } from 'vue'; import XCommon from './_common_/common.vue'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import { mainRouter } from '@/router/main.js'; const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index'); diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index d2b5d8cc42..2fdaca775b 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef, Ref } from 'vue'; import XCommon from './_common_/common.vue'; import type MkStickyContainer from '@/components/global/MkStickyContainer.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; @@ -108,9 +108,10 @@ import { $i } from '@/account.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; -import { CURRENT_STICKY_BOTTOM } from '@/const.js'; +import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js'; import { useScrollPositionManager } from '@/nirax.js'; import { mainRouter } from '@/router/main.js'; +import { isLink } from '@@/js/is-link.js'; const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue')); @@ -195,12 +196,6 @@ onMounted(() => { }); const onContextmenu = (ev) => { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; if (isLink(ev.target)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; if (window.getSelection()?.toString() !== '') return; @@ -423,10 +418,12 @@ $widgets-hide-threshold: 1090px; color: var(--fg); &:hover { + background: var(--panelHighlight); color: var(--accent); } &:active { + background: hsl(from var(--panel) h s calc(l - 2)); color: var(--accent); } } @@ -437,12 +434,12 @@ $widgets-hide-threshold: 1090px; color: var(--fgOnAccent); &:hover { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); color: var(--fgOnAccent); } &:active { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); color: var(--fgOnAccent); } } diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index ccc8d8fd97..510b2a4342 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, provide, ref, computed } from 'vue'; import XCommon from './_common_/common.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; import XSigninDialog from '@/components/MkSigninDialog.vue'; diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue index 47502b2af1..ac13d7822f 100644 --- a/packages/frontend/src/ui/zen.vue +++ b/packages/frontend/src/ui/zen.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, provide, ref } from 'vue'; import XCommon from './_common_/common.vue'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; -import { instanceName, ui } from '@/config.js'; +import { instanceName, ui } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { mainRouter } from '@/router/main.js'; diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index 49fd103d37..bcfaaf00ab 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; import { $i } from '@/account.js'; diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index 19843f3949..e4ac1acfc7 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -42,7 +42,7 @@ import { ref } from 'vue'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; const name = 'calendar'; diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index 2ea1253c2b..e91a77beab 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -32,7 +32,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue index 76ccdb3971..d090372b9a 100644 --- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue +++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue @@ -26,7 +26,7 @@ import MkContainer from '@/components/MkContainer.vue'; import MkTagCloud from '@/components/MkTagCloud.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const name = 'instanceCloud'; diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue index ce8b0dd602..014cf01a5d 100644 --- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue +++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { instance } from '@/instance.js'; const name = 'instanceInfo'; diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue index 5c89a06c62..d56ee96ac1 100644 --- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue +++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue @@ -18,7 +18,7 @@ import { ref } from 'vue'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import number from '@/filters/number.js'; diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index e5758662cc..511777a570 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -28,9 +28,9 @@ import * as Misskey from 'misskey-js'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; -import { url as base } from '@/config.js'; +import { url as base } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { infoImageUrl } from '@/instance.js'; const name = 'rss'; diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index 16306ef5ba..b393ecd74b 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -34,8 +34,8 @@ import MarqueeText from '@/components/MkMarquee.vue'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { shuffle } from '@/scripts/shuffle.js'; -import { url as base } from '@/config.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { url as base } from '@@/js/config.js'; +import { useInterval } from '@@/js/use-interval.js'; const name = 'rssTicker'; diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue index b8efd3bda9..3fea1d7053 100644 --- a/packages/frontend/src/widgets/WidgetSlideshow.vue +++ b/packages/frontend/src/widgets/WidgetSlideshow.vue @@ -23,7 +23,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; const name = 'slideshow'; diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index d02f9b8e22..a4685fd1fc 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -40,6 +40,7 @@ import MkContainer from '@/components/MkContainer.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import { i18n } from '@/i18n.js'; import { availableBasicTimelines, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; +import type { MenuItem } from '@/types/menu.js'; const name = 'timeline'; @@ -109,11 +110,26 @@ const choose = async (ev) => { setSrc('list'); }, })); - os.popupMenu([...availableBasicTimelines().map(tl => ({ + + const menuItems: MenuItem[] = []; + + menuItems.push(...availableBasicTimelines().map(tl => ({ text: i18n.ts._timelines[tl], icon: basicTimelineIconClass(tl), action: () => { setSrc(tl); }, - })), antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { + }))); + + if (antennaItems.length > 0) { + menuItems.push({ type: 'divider' }); + menuItems.push(...antennaItems); + } + + if (listItems.length > 0) { + menuItems.push({ type: 'divider' }); + menuItems.push(...listItems); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => { menuOpened.value = false; }); }; diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index 4299181a27..a41db513e8 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -31,7 +31,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue index d9f4dc49ea..72391d622e 100644 --- a/packages/frontend/src/widgets/WidgetUserList.vue +++ b/packages/frontend/src/widgets/WidgetUserList.vue @@ -31,7 +31,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue index 27d3234207..469075e2c4 100644 --- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue +++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue @@ -138,7 +138,7 @@ function onStats(connStats: Misskey.entities.ServerStats) { } function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) { - for (const revStats of statsLog.reverse()) { + for (const revStats of statsLog.toReversed()) { onStats(revStats); } } diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue index d46aaa5f69..d78494b8d2 100644 --- a/packages/frontend/src/widgets/server-metric/net.vue +++ b/packages/frontend/src/widgets/server-metric/net.vue @@ -111,7 +111,7 @@ function onStats(connStats: Misskey.entities.ServerStats) { } function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) { - for (const revStats of statsLog.reverse()) { + for (const revStats of statsLog.toReversed()) { onStats(revStats); } } |