-
- {{ i18n.ts.name }}
-
-
- {{ i18n.ts.description }}
-
-
- {{ i18n.ts.imageUrl }}
-
-
+
+
+
+
+ {{ i18n.ts.save }}
+ {{ i18n.ts.show }}
+ {{ i18n.ts.delete }}
+
+
+
+
@@ -459,3 +465,10 @@ definePageMetadata(() => ({
title: flash.value ? `${i18n.ts._play.edit}: ${flash.value.title}` : i18n.ts._play.new,
}));
+
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index a6a99ba63..cf10bee0f 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: '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() {
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index 5a9c978da..8c4dfc3b8 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 4ba428d53..c69530b34 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index 7d192bcbe..ce8ec6869 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 058ef69c3..adeaf8550 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
{{ i18n.ts._webhookSettings.trigger }}
-
-
{{ i18n.ts._webhookSettings._events.follow }}
-
{{ i18n.ts._webhookSettings._events.followed }}
-
{{ i18n.ts._webhookSettings._events.note }}
-
{{ i18n.ts._webhookSettings._events.reply }}
-
{{ i18n.ts._webhookSettings._events.renote }}
-
{{ i18n.ts._webhookSettings._events.reaction }}
-
{{ i18n.ts._webhookSettings._events.mention }}
+
+
+
+ {{ i18n.ts._webhookSettings._events.follow }}
+
+
+
+ {{ i18n.ts._webhookSettings._events.followed }}
+
+
+
+ {{ i18n.ts._webhookSettings._events.note }}
+
+
+
+ {{ i18n.ts._webhookSettings._events.reply }}
+
+
+
+ {{ i18n.ts._webhookSettings._events.renote }}
+
+
+
+ {{ i18n.ts._webhookSettings._events.reaction }}
+
+
+
+ {{ i18n.ts._webhookSettings._events.mention }}
+
+
+
+
+
+ {{ i18n.ts._webhookSettings.testRemarks }}
+
@@ -43,6 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 9b7739287..1b3e1ecae 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 50c3beeab..a62fe5d58 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 d5943e8fb..12e2db229 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -40,7 +40,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';
@@ -50,11 +50,11 @@ 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';
-
+
provide('shouldOmitHeaderTitle', true);
const tlComponent = shallowRef
>();
@@ -189,7 +189,7 @@ async function chooseChannel(ev: MouseEvent): Promise {
}),
(channels.length === 0 ? undefined : { type: 'divider' }),
{
- type: 'link' as const,
+ type: 'link',
icon: 'ti ti-plus',
text: i18n.ts.createNew,
to: '/channels',
@@ -258,16 +258,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,
@@ -276,7 +284,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 de6737f37..31a3f1b06 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-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 { 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 3039ec749..8e0292c7f 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -161,7 +161,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkOmit from '@/components/MkOmit.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';
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 5c3125949..a227c7c4b 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.vue b/packages/frontend/src/pages/welcome.timeline.vue
index db326f9e6..732d48361 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([]);
const isScrolling = ref(false);
diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue
index 915fe3502..38d257506 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 995a2055b..75f994b86 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) => defineAsyncComponent({
+export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
loader: loader,
loadingComponent: MkLoading,
errorComponent: MkError,
@@ -240,7 +239,7 @@ const routes: RouteDef[] = [{
origin: 'origin',
},
}, {
- // Legacy Compatibility
+ // Legacy Compatibility
path: '/authorize-follow',
redirect: '/lookup',
loginRequired: true,
@@ -463,22 +462,14 @@ const routes: RouteDef[] = [{
path: '/relays',
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',
@@ -597,32 +588,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('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 7a3fde131..6ee967e6f 100644
--- a/packages/frontend/src/router/main.ts
+++ b/packages/frontend/src/router/main.ts
@@ -3,10 +3,37 @@
* 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('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 98a0c6175..46aed4933 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 {
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/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts
index 8fc857f84..c3c3f419a 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 e94027d30..6710d9826 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/focus.ts b/packages/frontend/src/scripts/focus.ts
index eb2da5ad8..81278b17e 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 60884d08d..a85ee01e2 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 108648d64..c8ab9238d 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 000000000..158ab9c7f
--- /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 {
+ // paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す
+ const normalizedParams: Record = {};
+ 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 = [
+ ``,
+ ``,
+ ];
+ 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 1f1139b22..b5e82399b 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -5,22 +5,23 @@
import { defineAsyncComponent, Ref, ShallowRef } from 'vue';
import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
import { claimAchievement } from './achievements.js';
+import type { MenuItem } from '@/types/menu.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
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 { 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 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;
@@ -98,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,
},
@@ -156,6 +159,19 @@ export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string):
};
}
+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;
@@ -203,6 +219,10 @@ export function getNoteMenu(props: {
});
}
+ function edit(): void {
+ os.edit({ target: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
+ }
+
function toggleFavorite(favorite: boolean): void {
claimAchievement('noteFavorited1');
os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
@@ -250,7 +270,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,
@@ -281,170 +301,23 @@ export function getNoteMenu(props: {
props.translation.value = res;
}
- function edit() {
- os.edit({ target: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
- }
+ const menuItems: MenuItem[] = [];
- let menu: 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) ? {
- icon: 'ti ti-external-link',
- text: i18n.ts.showOnRemote,
- action: () => {
- window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
- },
- } : undefined,
- ...(isSupportShare() ? [{
- icon: 'ti ti-share',
- text: i18n.ts.share,
- action: share,
- }] : []),
- $i && $i.policies.canUseTranslator && instance.translatorAvailable ? {
- 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),
- },
- 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,
- 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[];
+ if (props.currentClip?.userId === $i.id) {
+ menuItems.push({
+ icon: 'ti ti-backspace',
+ text: i18n.ts.unclip,
+ danger: true,
+ action: unclip,
+ }, { type: 'divider' });
+ }
- 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;
- },
- },
- ]
- : []
- ),
- ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
- { type: 'divider' },
- $i.policies.canEditNote || $i.isModerator || $i.isAdmin ? {
- icon: 'ti ti-edit',
- text: i18n.ts.edit,
- action: edit,
- } : undefined,
- appearNote.userId === $i.id ? {
- icon: 'ti ti-edit',
- text: i18n.ts.deleteAndEdit,
- action: delEdit,
- } : undefined,
- {
- icon: 'ti ti-trash',
- text: i18n.ts.delete,
- danger: true,
- action: del,
- }]
- : []
- )]
- .filter(x => x !== undefined);
- } else {
- menu = [{
+ menuItems.push({
icon: 'ti ti-info-circle',
text: i18n.ts.details,
action: openDetail,
@@ -452,35 +325,201 @@ export function getNoteMenu(props: {
icon: 'ti ti-copy',
text: i18n.ts.copyContent,
action: copyContent,
- }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
- , (appearNote.url || appearNote.uri) ? {
- icon: 'ti ti-external-link',
- text: i18n.ts.showOnRemote,
- action: () => {
- window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
+ }, 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');
+ },
+ });
+ } else {
+ menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode));
+ }
+
+ if (isSupportShare()) {
+ menuItems.push({
+ icon: 'ti ti-share',
+ text: i18n.ts.share,
+ action: share,
+ });
+ }
+
+ if ($i.policies.canUseTranslator && instance.translatorAvailable) {
+ menuItems.push({
+ icon: 'ti ti-language-hiragana',
+ text: i18n.ts.translate,
+ action: translate,
+ });
+ }
+
+ 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;
},
- } : undefined]
- .filter(x => x !== undefined);
+ });
+
+ 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 channelChildMenu = [] as MenuItem[];
+
+ 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 (appearNote.userId === $i.id || $i.isModerator || $i.isAdmin) {
+ menuItems.push({ type: 'divider' });
+ if ($i.policies.canEditNote || $i.isModerator || $i.isAdmin) {
+ menuItems.push({
+ icon: 'ti ti-edit',
+ text: i18n.ts.edit,
+ action: edit,
+ });
+ }
+ if (appearNote.userId === $i.id) {
+ menuItems.push({
+ icon: 'ti ti-edit',
+ text: i18n.ts.deleteAndEdit,
+ action: delEdit,
+ });
+ }
+ menuItems.push({
+ icon: 'ti ti-trash',
+ text: i18n.ts.delete,
+ danger: true,
+ action: del,
+ });
+ }
+ } else {
+ 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');
+ },
+ });
+ } 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 = () => {
@@ -491,7 +530,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 33f16a68a..d15279d63 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);
- });
- }
- }));
+ });
- 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) {
+ 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();
+ },
+ }));
+ },
+ });
+ }
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/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts
index 6b511f2a5..20f51660c 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 1517e4e1e..867ebf19e 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 aaa4f0a86..385f59ec3 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 099a22163..78eba35ea 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): 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): 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): 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 9938e534c..bf59fe98a 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 49fb6f9e5..1b1159fd0 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/player-url-transform.ts b/packages/frontend/src/scripts/player-url-transform.ts
index 53b2a9e44..39c6df650 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 1caa2dfc2..5b141222e 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 31a9ac1ad..11b6f52dd 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 000000000..733d91b85
--- /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 6bfcef6c3..000000000
--- 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 0edf4e9eb..000000000
--- 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/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts
new file mode 100644
index 000000000..cb0e607fc
--- /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[keyof T];
+type OmitFirst = T extends [any, ...infer R] ? R : never;
+
+/**
+ * Websocket無効化時に使うStreamのモック(なにもしない)
+ */
+export class StreamMock extends EventEmitter implements IStream {
+ public readonly state = 'initializing';
+
+ constructor(...args: ConstructorParameters) {
+ super();
+ // do nothing
+ }
+
+ public useChannel(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock {
+ 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 | any[]): void
+ public send(typeOrPayload: string | Record | any[], payload?: any): void {
+ // do nothing
+ }
+
+ public ping(): void {
+ // do nothing
+ }
+
+ public heartbeat(): void {
+ // do nothing
+ }
+
+ public close(): void {
+ // do nothing
+ }
+}
+
+class ChannelConnectionMock = any> extends EventEmitter implements IChannelConnection {
+ 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>>) {
+ super();
+
+ this.channel = args[0];
+ this.name = args[1];
+ }
+
+ public send(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 c7f8b3d59..b7e7a5a3f 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 = {
@@ -52,7 +52,7 @@ export const getBuiltinThemes = () => Promise.all(
'd-cherry',
'd-ice',
'd-u0',
- ].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 = () => {
@@ -74,6 +74,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 abb0e1e67..22dce609c 100644
--- a/packages/frontend/src/scripts/upload.ts
+++ b/packages/frontend/src/scripts/upload.ts
@@ -9,7 +9,7 @@ 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';
diff --git a/packages/frontend/src/scripts/use-form.ts b/packages/frontend/src/scripts/use-form.ts
new file mode 100644
index 000000000..0d505fe46
--- /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(v: T): T {
+ return JSON.parse(JSON.stringify(v));
+}
+
+function unwrapReactive(v: Reactive): T {
+ return JSON.parse(JSON.stringify(v));
+}
+
+export function useForm>(initialState: T, save: (newState: T) => Promise) {
+ const currentState = reactive(copy(initialState));
+ const previousState = reactive(copy(initialState));
+
+ const modifiedStates = reactive>({} 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/store.ts b/packages/frontend/src/store.ts
index 437314074..5b10a9a38 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -5,10 +5,12 @@
import { markRaw, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { hemisphere } from '@@/js/intl-const.js';
+import lightTheme from '@@/themes/l-light.json5';
+import darkTheme from '@@/themes/d-green-lime.json5';
import { miLocalStorage } from './local-storage.js';
import type { SoundType } from '@/scripts/sound.js';
import { Storage } from '@/pizzax.js';
-import { hemisphere } from '@/scripts/intl-const.js';
interface PostFormAction {
title: string,
@@ -250,9 +252,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',
@@ -458,10 +460,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',
@@ -520,8 +522,6 @@ interface Watcher {
/**
* 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
*/
-import lightTheme from '@/themes/l-light.json5';
-import darkTheme from '@/themes/d-green-lime.json5';
export class ColdDeviceStorage {
public static default = {
@@ -558,7 +558,7 @@ export class ColdDeviceStorage {
public static set(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 0d5bd78b0..e63dac951 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 | 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 44ef740a2..28a16fd6d 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -17,10 +17,6 @@
--minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px)));
--minBottomSpacing: var(--minBottomSpacingMobile);
- @media (max-width: 500px) {
- --margin: var(--marginHalf);
- }
-
//--ad: rgb(255 169 0 / 10%);
--eventFollow: #36aed2;
--eventRenote: #36d298;
@@ -29,6 +25,10 @@
--eventReaction: #e99a0b;
--eventAchievement: #cb9a11;
--eventOther: #88a6b7;
+
+ @media (max-width: 500px) {
+ --margin: var(--marginHalf);
+ }
}
::selection {
@@ -378,6 +378,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;
@@ -447,7 +457,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/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index 74c302874..f908803f0 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,43 +92,69 @@ 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.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? 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.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) {
+ 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,
to: '/about-misskey',
- }], 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 d7df2d10f..61881c02e 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -39,9 +39,9 @@ SPDX-License-Identifier: AGPL-3.0-only
-DEV BUILD
+DEV BUILD
-{{ i18n.ts.loggedInAsBot }}
+{{ i18n.ts.loggedInAsBot }}