Compare commits
	
		
			7 Commits
		
	
	
		
			2025.3.2-a
			...
			2025.3.2-a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					15685be4cc | ||
| 
						 | 
					8508c4dadc | ||
| 
						 | 
					e594fb0037 | ||
| 
						 | 
					a369721791 | ||
| 
						 | 
					f8e244f48d | ||
| 
						 | 
					8410611512 | ||
| 
						 | 
					caab1ec7c3 | 
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"version": "2025.3.2-alpha.7",
 | 
			
		||||
	"version": "2025.3.2-alpha.8",
 | 
			
		||||
	"codename": "nasubi",
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"type": "git",
 | 
			
		||||
@@ -24,6 +24,7 @@
 | 
			
		||||
		"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
 | 
			
		||||
		"build-storybook": "pnpm --filter frontend build-storybook",
 | 
			
		||||
		"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
 | 
			
		||||
		"build-frontend-search-index": "pnpm --filter frontend build-search-index",
 | 
			
		||||
		"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
 | 
			
		||||
		"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
 | 
			
		||||
		"init": "pnpm migrate",
 | 
			
		||||
 
 | 
			
		||||
@@ -1428,6 +1428,23 @@ async function processVueFile(
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function generateSearchIndex(options: Options, transformedCodeCache: Record<string, string> = {}) {
 | 
			
		||||
	const filePaths = options.targetFilePaths.reduce<string[]>((acc, filePathPattern) => {
 | 
			
		||||
		const matchedFiles = glob.sync(filePathPattern);
 | 
			
		||||
		return [...acc, ...matchedFiles];
 | 
			
		||||
	}, []);
 | 
			
		||||
 | 
			
		||||
	for (const filePath of filePaths) {
 | 
			
		||||
		const id = path.resolve(filePath); // 絶対パスに変換
 | 
			
		||||
		const code = fs.readFileSync(filePath, 'utf-8'); // ファイル内容を読み込む
 | 
			
		||||
		const { transformedCodeCache: newCache } = await processVueFile(code, id, options, transformedCodeCache); // processVueFile 関数を呼び出す
 | 
			
		||||
		transformedCodeCache = newCache; // キャッシュを更新
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	await analyzeVueProps({ ...options, transformedCodeCache }); // 開発サーバー起動時にも analyzeVueProps を実行
 | 
			
		||||
 | 
			
		||||
	return transformedCodeCache; // キャッシュを返す
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Rollup プラグインとして export
 | 
			
		||||
export default function pluginCreateSearchIndex(options: Options): Plugin {
 | 
			
		||||
@@ -1445,19 +1462,7 @@ export default function pluginCreateSearchIndex(options: Options): Plugin {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const filePaths = options.targetFilePaths.reduce<string[]>((acc, filePathPattern) => {
 | 
			
		||||
				const matchedFiles = glob.sync(filePathPattern);
 | 
			
		||||
				return [...acc, ...matchedFiles];
 | 
			
		||||
			}, []);
 | 
			
		||||
 | 
			
		||||
			for (const filePath of filePaths) {
 | 
			
		||||
				const id = path.resolve(filePath); // 絶対パスに変換
 | 
			
		||||
				const code = fs.readFileSync(filePath, 'utf-8'); // ファイル内容を読み込む
 | 
			
		||||
				const { transformedCodeCache: newCache } = await processVueFile(code, id, options, transformedCodeCache); // processVueFile 関数を呼び出す
 | 
			
		||||
				transformedCodeCache = newCache; // キャッシュを更新
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			await analyzeVueProps({ ...options, transformedCodeCache }); // 開発サーバー起動時にも analyzeVueProps を実行
 | 
			
		||||
			transformedCodeCache = await generateSearchIndex(options, transformedCodeCache);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async transform(code, id) {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"watch": "vite",
 | 
			
		||||
		"build": "vite build",
 | 
			
		||||
		"build-search-index": "vite-node --config \"./vite-node.config.ts\" \"./scripts/generate-search-index.ts\"",
 | 
			
		||||
		"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
 | 
			
		||||
		"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
 | 
			
		||||
		"build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static",
 | 
			
		||||
@@ -133,6 +134,7 @@
 | 
			
		||||
		"start-server-and-test": "2.0.10",
 | 
			
		||||
		"storybook": "8.6.4",
 | 
			
		||||
		"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
 | 
			
		||||
		"vite-node": "3.0.8",
 | 
			
		||||
		"vite-plugin-turbosnap": "1.0.3",
 | 
			
		||||
		"vitest": "3.0.8",
 | 
			
		||||
		"vitest-fetch-mock": "0.4.5",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								packages/frontend/scripts/generate-search-index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								packages/frontend/scripts/generate-search-index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { searchIndexes } from '../vite.config.js';
 | 
			
		||||
import { generateSearchIndex } from '../lib/vite-plugin-create-search-index.js';
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	for (const searchIndex of searchIndexes) {
 | 
			
		||||
		await generateSearchIndex(searchIndex);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
@@ -220,28 +220,28 @@ function onMousedown(evt: MouseEvent): void {
 | 
			
		||||
		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 | 
			
		||||
 | 
			
		||||
		&:not(:disabled):hover {
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&:not(:disabled):active {
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.danger {
 | 
			
		||||
		font-weight: bold;
 | 
			
		||||
		color: #ff2a2a;
 | 
			
		||||
		color: var(--MI_THEME-error);
 | 
			
		||||
 | 
			
		||||
		&.primary {
 | 
			
		||||
			color: #fff;
 | 
			
		||||
			background: #ff2a2a;
 | 
			
		||||
			background: var(--MI_THEME-error);
 | 
			
		||||
 | 
			
		||||
			&:not(:disabled):hover {
 | 
			
		||||
				background: #ff4242;
 | 
			
		||||
				background: hsl(from var(--MI_THEME-error) h s calc(l + 10));
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:not(:disabled):active {
 | 
			
		||||
				background: #d42e2e;
 | 
			
		||||
				background: hsl(from var(--MI_THEME-error) h s calc(l - 10));
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -177,12 +177,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue';
 | 
			
		||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
 | 
			
		||||
import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
 | 
			
		||||
import type { Keymap } from '@/utility/hotkey.js';
 | 
			
		||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { isTouchUsing } from '@/utility/touch.js';
 | 
			
		||||
import type { Keymap } from '@/utility/hotkey.js';
 | 
			
		||||
import { isFocusable } from '@/utility/focus.js';
 | 
			
		||||
import { getNodeOrNull } from '@/utility/get-dom-node-or-null.js';
 | 
			
		||||
 | 
			
		||||
@@ -558,11 +558,11 @@ onBeforeUnmount(() => {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.danger {
 | 
			
		||||
		--menuFg: #ff2a2a;
 | 
			
		||||
		--menuFg: var(--MI_THEME-error);
 | 
			
		||||
		--menuHoverFg: #fff;
 | 
			
		||||
		--menuHoverBg: #ff4242;
 | 
			
		||||
		--menuHoverBg: var(--MI_THEME-error);
 | 
			
		||||
		--menuActiveFg: #fff;
 | 
			
		||||
		--menuActiveBg: #d42e2e;
 | 
			
		||||
		--menuActiveBg: hsl(from var(--MI_THEME-error) h s calc(l - 10));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.radio {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div :class="$style.root">
 | 
			
		||||
<div :class="$style.root" @contextmenu.prevent.stop="showMenu($event, true)">
 | 
			
		||||
	<div :class="$style.body">
 | 
			
		||||
		<slot></slot>
 | 
			
		||||
	</div>
 | 
			
		||||
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
		<i v-if="isSyncEnabled" class="ti ti-cloud-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i>
 | 
			
		||||
		<i v-if="isAccountOverrided" class="ti ti-user-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i>
 | 
			
		||||
		<div :class="$style.buttons">
 | 
			
		||||
			<button class="_button" style="color: var(--MI_THEME-fg)" @click="showMenu"><i class="ti ti-dots"></i></button>
 | 
			
		||||
			<button class="_button" style="color: var(--MI_THEME-fg)" @click="showMenu($event)"><i class="ti ti-dots"></i></button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -32,15 +32,22 @@ const props = withDefaults(defineProps<{
 | 
			
		||||
const isAccountOverrided = ref(prefer.isAccountOverrided(props.k));
 | 
			
		||||
const isSyncEnabled = ref(prefer.isSyncEnabled(props.k));
 | 
			
		||||
 | 
			
		||||
function showMenu(ev: MouseEvent) {
 | 
			
		||||
function showMenu(ev: MouseEvent, contextmenu?: boolean) {
 | 
			
		||||
	const i = window.setInterval(() => {
 | 
			
		||||
		isAccountOverrided.value = prefer.isAccountOverrided(props.k);
 | 
			
		||||
		isSyncEnabled.value = prefer.isSyncEnabled(props.k);
 | 
			
		||||
	}, 100);
 | 
			
		||||
	if (contextmenu) {
 | 
			
		||||
		os.contextMenu(prefer.getPerPrefMenu(props.k), ev).then(() => {
 | 
			
		||||
			window.clearInterval(i);
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		os.popupMenu(prefer.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, {
 | 
			
		||||
			onClosing: () => {
 | 
			
		||||
				window.clearInterval(i);
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import { v4 as uuid } from 'uuid';
 | 
			
		||||
import type { PreferencesProfile, StorageProvider } from '@/preferences/profile.js';
 | 
			
		||||
import { cloudBackup } from '@/preferences/utility.js';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage.js';
 | 
			
		||||
import { ProfileManager } from '@/preferences/profile.js';
 | 
			
		||||
import { isSameCond, ProfileManager } from '@/preferences/profile.js';
 | 
			
		||||
import { store } from '@/store.js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
@@ -28,22 +28,26 @@ function createProfileManager(storageProvider: StorageProvider) {
 | 
			
		||||
	return new ProfileManager(profile, storageProvider);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const syncGroup = 'default';
 | 
			
		||||
 | 
			
		||||
const storageProvider: StorageProvider = {
 | 
			
		||||
	save: (ctx) => {
 | 
			
		||||
		miLocalStorage.setItem('preferences', JSON.stringify(ctx.profile));
 | 
			
		||||
		miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	cloudGet: async (ctx) => {
 | 
			
		||||
		// TODO: この取得方法だとアカウントが変わると保存場所も変わってしまうので改修する
 | 
			
		||||
		// 例えば複数アカウントある場合でも設定値を保存するための「プライマリアカウント」を設定できるようにするとか
 | 
			
		||||
		// TODO: keyのcondに応じた取得
 | 
			
		||||
		try {
 | 
			
		||||
			const value = await misskeyApi('i/registry/get', {
 | 
			
		||||
			const cloudData = await misskeyApi('i/registry/get', {
 | 
			
		||||
				scope: ['client', 'preferences', 'sync'],
 | 
			
		||||
				key: ctx.key,
 | 
			
		||||
			});
 | 
			
		||||
				key: syncGroup + ':' + ctx.key,
 | 
			
		||||
			}) as [any, any][];
 | 
			
		||||
			const target = cloudData.find(([cond]) => isSameCond(cond, ctx.cond));
 | 
			
		||||
			if (target == null) return null;
 | 
			
		||||
			return {
 | 
			
		||||
				value,
 | 
			
		||||
				value: target[1],
 | 
			
		||||
			};
 | 
			
		||||
		} catch (err: any) {
 | 
			
		||||
			if (err.code === 'NO_SUCH_KEY') {
 | 
			
		||||
@@ -53,13 +57,51 @@ const storageProvider: StorageProvider = {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	cloudSet: async (ctx) => {
 | 
			
		||||
		let cloudData: [any, any][] = [];
 | 
			
		||||
		try {
 | 
			
		||||
			cloudData = await misskeyApi('i/registry/get', {
 | 
			
		||||
				scope: ['client', 'preferences', 'sync'],
 | 
			
		||||
				key: syncGroup + ':' + ctx.key,
 | 
			
		||||
			}) as [any, any][];
 | 
			
		||||
		} catch (err: any) {
 | 
			
		||||
			if (err.code === 'NO_SUCH_KEY') {
 | 
			
		||||
				cloudData = [];
 | 
			
		||||
			} else {
 | 
			
		||||
				throw err;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const i = cloudData.findIndex(([cond]) => isSameCond(cond, ctx.cond));
 | 
			
		||||
 | 
			
		||||
		if (i === -1) {
 | 
			
		||||
			cloudData.push([ctx.cond, ctx.value]);
 | 
			
		||||
		} else {
 | 
			
		||||
			cloudData[i] = [ctx.cond, ctx.value];
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await misskeyApi('i/registry/set', {
 | 
			
		||||
			scope: ['client', 'preferences', 'sync'],
 | 
			
		||||
			key: ctx.key,
 | 
			
		||||
			value: ctx.value,
 | 
			
		||||
			key: syncGroup + ':' + ctx.key,
 | 
			
		||||
			value: cloudData,
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	cloudGets: async (ctx) => {
 | 
			
		||||
		// TODO: 値の取得を1つのリクエストで済ませたい(バックエンド側でAPIの新設が必要)
 | 
			
		||||
		const fetchings = ctx.needs.map(need => storageProvider.cloudGet(need).then(res => [need.key, res] as const));
 | 
			
		||||
		const cloudDatas = await Promise.all(fetchings);
 | 
			
		||||
 | 
			
		||||
		const res = {} as Partial<Record<string, any>>;
 | 
			
		||||
		for (const cloudData of cloudDatas) {
 | 
			
		||||
			if (cloudData[1] != null) {
 | 
			
		||||
				res[cloudData[0]] = cloudData[1].value;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return res;
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const prefer = createProfileManager(storageProvider);
 | 
			
		||||
 
 | 
			
		||||
@@ -60,6 +60,12 @@ function makeCond(cond: Partial<{
 | 
			
		||||
	return c;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isSameCond(a: Cond, b: Cond): boolean {
 | 
			
		||||
	// null と undefined (キー無し) は区別したくないので == で比較
 | 
			
		||||
	// eslint-disable-next-line eqeqeq
 | 
			
		||||
	return a.server == b.server && a.account == b.account && a.device == b.device;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type PreferencesProfile = {
 | 
			
		||||
	id: string;
 | 
			
		||||
	version: string;
 | 
			
		||||
@@ -73,8 +79,9 @@ export type PreferencesProfile = {
 | 
			
		||||
 | 
			
		||||
export type StorageProvider = {
 | 
			
		||||
	save: (ctx: { profile: PreferencesProfile; }) => void;
 | 
			
		||||
	cloudGet: <K extends keyof PREF>(ctx: { key: K; }) => Promise<{ value: ValueOf<K>; } | null>;
 | 
			
		||||
	cloudSet: <K extends keyof PREF>(ctx: { key: K; value: ValueOf<K>; }) => Promise<void>;
 | 
			
		||||
	cloudGets: <K extends keyof PREF>(ctx: { needs: { key: K; cond: Cond; }[] }) => Promise<Partial<Record<K, ValueOf<K>>>>;
 | 
			
		||||
	cloudGet: <K extends keyof PREF>(ctx: { key: K; cond: Cond; }) => Promise<{ value: ValueOf<K>; } | null>;
 | 
			
		||||
	cloudSet: <K extends keyof PREF>(ctx: { key: K; cond: Cond; value: ValueOf<K>; }) => Promise<void>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class ProfileManager {
 | 
			
		||||
@@ -121,7 +128,7 @@ export class ProfileManager {
 | 
			
		||||
 | 
			
		||||
		this.rewriteRawState(key, value);
 | 
			
		||||
 | 
			
		||||
		const record = this.getMatchedRecord(key);
 | 
			
		||||
		const record = this.getMatchedRecordOf(key);
 | 
			
		||||
		if (parseCond(record[0]).account == null && PREF_DEF[key].accountDependent) {
 | 
			
		||||
			this.profile.preferences[key].push([makeCond({
 | 
			
		||||
				account: `${host}/${$i!.id}`,
 | 
			
		||||
@@ -130,14 +137,14 @@ export class ProfileManager {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		record[1] = value;
 | 
			
		||||
		this.save();
 | 
			
		||||
 | 
			
		||||
		if (record[2].sync) {
 | 
			
		||||
			// awaitの必要なし
 | 
			
		||||
			// TODO: リクエストを間引く
 | 
			
		||||
			this.storageProvider.cloudSet({ key, value });
 | 
			
		||||
			this.storageProvider.cloudSet({ key, cond: record[0], value: record[1] });
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		record[1] = value;
 | 
			
		||||
		this.save();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
@@ -180,39 +187,42 @@ export class ProfileManager {
 | 
			
		||||
	private genStates() {
 | 
			
		||||
		const states = {} as { [K in keyof PREF]: ValueOf<K> };
 | 
			
		||||
		for (const key in PREF_DEF) {
 | 
			
		||||
			const record = this.getMatchedRecord(key);
 | 
			
		||||
			const record = this.getMatchedRecordOf(key);
 | 
			
		||||
			states[key] = record[1];
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return states;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private fetchCloudValues() {
 | 
			
		||||
		// TODO: 値の取得を1つのリクエストで済ませたい(バックエンド側でAPIの新設が必要)
 | 
			
		||||
 | 
			
		||||
		const promises: Promise<void>[] = [];
 | 
			
		||||
	private async fetchCloudValues() {
 | 
			
		||||
		const needs = [] as { key: keyof PREF; cond: Cond; }[];
 | 
			
		||||
		for (const key in PREF_DEF) {
 | 
			
		||||
			const record = this.getMatchedRecord(key);
 | 
			
		||||
			const record = this.getMatchedRecordOf(key);
 | 
			
		||||
			if (record[2].sync) {
 | 
			
		||||
				const getting = this.storageProvider.cloudGet({ key });
 | 
			
		||||
				promises.push(getting.then((res) => {
 | 
			
		||||
					if (res == null) return;
 | 
			
		||||
					const value = res.value;
 | 
			
		||||
					if (value !== this.s[key]) {
 | 
			
		||||
						this.rewriteRawState(key, value);
 | 
			
		||||
						record[1] = value;
 | 
			
		||||
						console.log('cloud fetched', key, value);
 | 
			
		||||
					}
 | 
			
		||||
				}));
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		Promise.all(promises).then(() => {
 | 
			
		||||
			console.log('cloud fetched all');
 | 
			
		||||
			this.save();
 | 
			
		||||
 | 
			
		||||
			console.log(this.s.showFixedPostForm, this.r.showFixedPostForm.value);
 | 
			
		||||
				needs.push({
 | 
			
		||||
					key,
 | 
			
		||||
					cond: record[0],
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const cloudValues = await this.storageProvider.cloudGets({ needs });
 | 
			
		||||
 | 
			
		||||
		for (const key in PREF_DEF) {
 | 
			
		||||
			const record = this.getMatchedRecordOf(key);
 | 
			
		||||
			if (record[2].sync && Object.hasOwn(cloudValues, key) && cloudValues[key] !== undefined) {
 | 
			
		||||
				const cloudValue = cloudValues[key];
 | 
			
		||||
				if (cloudValue !== this.s[key]) {
 | 
			
		||||
					this.rewriteRawState(key, cloudValue);
 | 
			
		||||
					record[1] = cloudValue;
 | 
			
		||||
					console.log('cloud fetched', key, cloudValue);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.save();
 | 
			
		||||
		console.log('cloud fetch completed');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static newProfile(): PreferencesProfile {
 | 
			
		||||
		const data = {} as PreferencesProfile['preferences'];
 | 
			
		||||
@@ -261,7 +271,7 @@ export class ProfileManager {
 | 
			
		||||
		this.storageProvider.save({ profile: this.profile });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getMatchedRecord<K extends keyof PREF>(key: K): PrefRecord<K> {
 | 
			
		||||
	public getMatchedRecordOf<K extends keyof PREF>(key: K): PrefRecord<K> {
 | 
			
		||||
		const records = this.profile.preferences[key];
 | 
			
		||||
 | 
			
		||||
		if ($i == null) return records.find(([cond, v]) => parseCond(cond).account == null)!;
 | 
			
		||||
@@ -302,19 +312,21 @@ export class ProfileManager {
 | 
			
		||||
 | 
			
		||||
		records.splice(index, 1);
 | 
			
		||||
 | 
			
		||||
		this.rewriteRawState(key, this.getMatchedRecord(key)[1]);
 | 
			
		||||
		this.rewriteRawState(key, this.getMatchedRecordOf(key)[1]);
 | 
			
		||||
 | 
			
		||||
		this.save();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public isSyncEnabled<K extends keyof PREF>(key: K): boolean {
 | 
			
		||||
		return this.getMatchedRecord(key)[2].sync ?? false;
 | 
			
		||||
		return this.getMatchedRecordOf(key)[2].sync ?? false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public async enableSync<K extends keyof PREF>(key: K): Promise<{ enabled: boolean; } | null> {
 | 
			
		||||
		if (this.isSyncEnabled(key)) return Promise.resolve(null);
 | 
			
		||||
 | 
			
		||||
		const existing = await this.storageProvider.cloudGet({ key });
 | 
			
		||||
		const record = this.getMatchedRecordOf(key);
 | 
			
		||||
 | 
			
		||||
		const existing = await this.storageProvider.cloudGet({ key, cond: record[0] });
 | 
			
		||||
		if (existing != null) {
 | 
			
		||||
			const { canceled, result } = await os.select({
 | 
			
		||||
				title: i18n.ts.preferenceSyncConflictTitle,
 | 
			
		||||
@@ -340,12 +352,11 @@ export class ProfileManager {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const record = this.getMatchedRecord(key);
 | 
			
		||||
		record[2].sync = true;
 | 
			
		||||
		this.save();
 | 
			
		||||
 | 
			
		||||
		// awaitの必要性は無い
 | 
			
		||||
		this.storageProvider.cloudSet({ key, value: this.s[key] });
 | 
			
		||||
		this.storageProvider.cloudSet({ key, cond: record[0], value: this.s[key] });
 | 
			
		||||
 | 
			
		||||
		return { enabled: true };
 | 
			
		||||
	}
 | 
			
		||||
@@ -353,7 +364,7 @@ export class ProfileManager {
 | 
			
		||||
	public disableSync<K extends keyof PREF>(key: K) {
 | 
			
		||||
		if (!this.isSyncEnabled(key)) return;
 | 
			
		||||
 | 
			
		||||
		const record = this.getMatchedRecord(key);
 | 
			
		||||
		const record = this.getMatchedRecordOf(key);
 | 
			
		||||
		delete record[2].sync;
 | 
			
		||||
		this.save();
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								packages/frontend/vite-node.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/frontend/vite-node.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
import { defineConfig } from 'vite';
 | 
			
		||||
 | 
			
		||||
export default defineConfig({});
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import pluginReplace from '@rollup/plugin-replace';
 | 
			
		||||
import pluginVue from '@vitejs/plugin-vue';
 | 
			
		||||
import { type UserConfig, defineConfig } from 'vite';
 | 
			
		||||
import { defineConfig } from 'vite';
 | 
			
		||||
import type { UserConfig } from 'vite';
 | 
			
		||||
import * as yaml from 'js-yaml';
 | 
			
		||||
import { promises as fsp } from 'fs';
 | 
			
		||||
 | 
			
		||||
@@ -11,12 +12,22 @@ import packageInfo from './package.json' with { type: 'json' };
 | 
			
		||||
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
 | 
			
		||||
import pluginJson5 from './vite.json5.js';
 | 
			
		||||
import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js';
 | 
			
		||||
import type { Options as SearchIndexOptions } from './lib/vite-plugin-create-search-index.js';
 | 
			
		||||
 | 
			
		||||
const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
 | 
			
		||||
const host = url ? (new URL(url)).hostname : undefined;
 | 
			
		||||
 | 
			
		||||
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 検索インデックスの生成設定
 | 
			
		||||
 */
 | 
			
		||||
export const searchIndexes = [{
 | 
			
		||||
	targetFilePaths: ['src/pages/settings/*.vue'],
 | 
			
		||||
	exportFilePath: './src/utility/autogen/settings-search-index.ts',
 | 
			
		||||
	verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
 | 
			
		||||
}] satisfies SearchIndexOptions[];
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Misskeyのフロントエンドにバンドルせず、CDNなどから別途読み込むリソースを記述する。
 | 
			
		||||
 * CDNを使わずにバンドルしたい場合、以下の配列から該当要素を削除orコメントアウトすればOK
 | 
			
		||||
@@ -84,11 +95,7 @@ export function getConfig(): UserConfig {
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		plugins: [
 | 
			
		||||
			pluginCreateSearchIndex({
 | 
			
		||||
				targetFilePaths: ['src/pages/settings/*.vue'],
 | 
			
		||||
				exportFilePath: './src/utility/autogen/settings-search-index.ts',
 | 
			
		||||
				verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
 | 
			
		||||
			}),
 | 
			
		||||
			...searchIndexes.map(options => pluginCreateSearchIndex(options)),
 | 
			
		||||
			pluginVue(),
 | 
			
		||||
			pluginUnwindCssModuleClassName(),
 | 
			
		||||
			pluginJson5(),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "module",
 | 
			
		||||
	"name": "misskey-js",
 | 
			
		||||
	"version": "2025.3.2-alpha.7",
 | 
			
		||||
	"version": "2025.3.2-alpha.8",
 | 
			
		||||
	"description": "Misskey SDK for JavaScript",
 | 
			
		||||
	"license": "MIT",
 | 
			
		||||
	"main": "./built/index.js",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -1040,6 +1040,9 @@ importers:
 | 
			
		||||
      storybook-addon-misskey-theme:
 | 
			
		||||
        specifier: github:misskey-dev/storybook-addon-misskey-theme
 | 
			
		||||
        version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.6.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/components@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/core-events@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/manager-api@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/preview-api@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/theming@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/types@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
 | 
			
		||||
      vite-node:
 | 
			
		||||
        specifier: 3.0.8
 | 
			
		||||
        version: 3.0.8(@types/node@22.13.9)(sass@1.85.1)(terser@5.39.0)(tsx@4.19.3)
 | 
			
		||||
      vite-plugin-turbosnap:
 | 
			
		||||
        specifier: 1.0.3
 | 
			
		||||
        version: 1.0.3
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user