From 0254570fbf94ae99f492a781e07863dc694a12cf Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 31 May 2025 12:49:10 +0900 Subject: [PATCH 01/10] =?UTF-8?q?enhance(frontend):=20=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=81=AE=E5=90=8C=E6=9C=9F=E3=82=92=E3=82=AA=E3=83=B3=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=81=A8=E3=81=8D=E3=81=AB=E7=AB=B6=E5=90=88?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=81=A8=E3=81=8D=E3=81=AB=E5=80=A4=E3=82=92?= =?UTF-8?q?=E3=83=9E=E3=83=BC=E3=82=B8=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + locales/index.d.ts | 10 ++-- locales/ja-JP.yml | 7 +-- .../frontend/src/pages/settings/navbar.vue | 3 +- .../frontend/src/pages/settings/sounds.vue | 3 +- packages/frontend/src/preferences/def.ts | 33 +++++++++--- packages/frontend/src/preferences/manager.ts | 52 ++++++++++++++----- packages/frontend/src/utility/sound.ts | 4 +- 8 files changed, 85 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4544e5acba..5c11e6e07e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - Feat: 絵文字をミュート可能にする機能 - 絵文字(ユニコードの絵文字・カスタム絵文字)毎にミュートし、不可視化することができるようになりました - Feat: モバイルデバイスで折りたたまれたUIの展開表示に全画面ページを使用できるように(実験的) +- Enhance: 設定の同期をオンにするときに競合したときに値をマージできるように - Enhance: メモリ使用量を軽減しました - Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加 - Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように diff --git a/locales/index.d.ts b/locales/index.d.ts index 21da0c171a..73bcb2f1c8 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5335,15 +5335,19 @@ export interface Locale extends ILocale { */ "preferenceSyncConflictTitle": string; /** - * 同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか? + * 同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どうしますか? */ "preferenceSyncConflictText": string; /** - * サーバーの設定値 + * 統合する + */ + "preferenceSyncConflictChoiceMerge": string; + /** + * サーバーの設定値で上書き */ "preferenceSyncConflictChoiceServer": string; /** - * デバイスの設定値 + * デバイスの設定値で上書き */ "preferenceSyncConflictChoiceDevice": string; /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b81529790f..c7971507aa 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1329,9 +1329,10 @@ skip: "スキップ" restore: "復元" syncBetweenDevices: "デバイス間で同期" preferenceSyncConflictTitle: "サーバーに設定値が存在します" -preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?" -preferenceSyncConflictChoiceServer: "サーバーの設定値" -preferenceSyncConflictChoiceDevice: "デバイスの設定値" +preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どうしますか?" +preferenceSyncConflictChoiceMerge: "統合する" +preferenceSyncConflictChoiceServer: "サーバーの設定値で上書き" +preferenceSyncConflictChoiceDevice: "デバイスの設定値で上書き" preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル" paste: "ペースト" emojiPalette: "絵文字パレット" diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index b322b03a21..ef698fcd6e 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -69,6 +69,7 @@ import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { prefer } from '@/preferences.js'; import { PREF_DEF } from '@/preferences/def.js'; +import { getInitialPrefValue } from '@/preferences/manager.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -106,7 +107,7 @@ async function save() { } function reset() { - items.value = PREF_DEF.menu.default.map(x => ({ + items.value = getInitialPrefValue('menu').map(x => ({ id: Math.random().toString(), type: x, })); diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index 4461ee1ab1..590db19bca 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -75,6 +75,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue'; import { PREF_DEF } from '@/preferences/def.js'; import MkFeatureBanner from '@/components/MkFeatureBanner.vue'; +import { getInitialPrefValue } from '@/preferences/manager.js'; const notUseSound = prefer.model('sound.notUseSound'); const useSoundOnlyWhenActive = prefer.model('sound.useSoundOnlyWhenActive'); @@ -113,7 +114,7 @@ async function updated(type: keyof typeof sounds.value, sound) { function reset() { for (const sound of Object.keys(sounds.value) as Array) { - const v = PREF_DEF[`sound.on.${sound}`].default; + const v = getInitialPrefValue(`sound.on.${sound}`); prefer.commit(`sound.on.${sound}`, v); sounds.value[sound] = v; } diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts index 8a2cfa23b1..b8a5a84279 100644 --- a/packages/frontend/src/preferences/def.ts +++ b/packages/frontend/src/preferences/def.ts @@ -5,6 +5,7 @@ import * as Misskey from 'misskey-js'; import { hemisphere } from '@@/js/intl-const.js'; +import { v4 as uuid } from 'uuid'; import type { Theme } from '@/theme.js'; import type { SoundType } from '@/utility/sound.js'; import type { Plugin } from '@/plugin.js'; @@ -49,15 +50,15 @@ export const PREF_DEF = { }, widgets: { accountDependent: true, - default: [{ + default: () => [{ name: 'calendar', - id: 'a', place: 'right', data: {}, + id: uuid(), place: 'right', data: {}, }, { name: 'notifications', - id: 'b', place: 'right', data: {}, + id: uuid(), place: 'right', data: {}, }, { name: 'trends', - id: 'c', place: 'right', data: {}, + id: uuid(), place: 'right', data: {}, }] as { name: string; id: string; @@ -76,8 +77,8 @@ export const PREF_DEF = { emojiPalettes: { serverDependent: true, - default: [{ - id: 'a', + default: () => [{ + id: uuid(), name: '', emojis: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], }] as { @@ -85,6 +86,11 @@ export const PREF_DEF = { name: string; emojis: string[]; }[], + mergeStrategy: (a, b) => { + const sameIdExists = a.some(x => b.some(y => x.id === y.id)); + if (sameIdExists) throw new Error(); + return a.concat(b); + }, }, emojiPaletteForReaction: { serverDependent: true, @@ -100,6 +106,11 @@ export const PREF_DEF = { }, themes: { default: [] as Theme[], + mergeStrategy: (a, b) => { + const sameIdExists = a.some(x => b.some(y => x.id === y.id)); + if (sameIdExists) throw new Error(); + return a.concat(b); + }, }, lightTheme: { default: null as Theme | null, @@ -345,9 +356,19 @@ export const PREF_DEF = { }, plugins: { default: [] as Plugin[], + mergeStrategy: (a, b) => { + const sameIdExists = a.some(x => b.some(y => x.installId === y.installId)); + if (sameIdExists) throw new Error(); + const sameNameExists = a.some(x => b.some(y => x.name === y.name)); + if (sameNameExists) throw new Error(); + return a.concat(b); + }, }, mutingEmojis: { default: [] as string[], + mergeStrategy: (a, b) => { + return [...new Set(a.concat(b))]; + }, }, 'sound.masterVolume': { diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts index cac659f1fe..016e1ad85b 100644 --- a/packages/frontend/src/preferences/manager.ts +++ b/packages/frontend/src/preferences/manager.ts @@ -22,7 +22,10 @@ import { deepEqual } from '@/utility/deep-equal.js'; //}; type PREF = typeof PREF_DEF; -type ValueOf = PREF[K]['default']; +type DefaultValues = { + [K in keyof PREF]: PREF[K]['default'] extends (...args: any) => infer R ? R : PREF[K]['default']; +}; +type ValueOf = DefaultValues[K]; type Scope = Partial<{ server: string | null; // host @@ -84,11 +87,22 @@ export type StorageProvider = { cloudSet: (ctx: { key: K; scope: Scope; value: ValueOf; }) => Promise; }; -export type PreferencesDefinition = Record infer R ? R : Default> = { + default: Default; accountDependent?: boolean; serverDependent?: boolean; -}>; + mergeStrategy?: (a: T, b: T) => T; +}; + +export type PreferencesDefinition = Record>; + +export function getInitialPrefValue(k: K): ValueOf { + if (typeof PREF_DEF[k].default === 'function') { // factory + return PREF_DEF[k].default(); + } else { + return PREF_DEF[k].default; + } +} export class PreferencesManager { private storageProvider: StorageProvider; @@ -262,7 +276,7 @@ export class PreferencesManager { public static newProfile(): PreferencesProfile { const data = {} as PreferencesProfile['preferences']; for (const key in PREF_DEF) { - data[key] = [[makeScope({}), PREF_DEF[key].default, {}]]; + data[key] = [[makeScope({}), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]]; } return { id: uuid(), @@ -279,7 +293,7 @@ export class PreferencesManager { for (const key in PREF_DEF) { const records = profileLike.preferences[key]; if (records == null || records.length === 0) { - data[key] = [[makeScope({}), PREF_DEF[key].default, {}]]; + data[key] = [[makeScope({}), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]]; continue; } else { data[key] = records; @@ -367,10 +381,20 @@ export class PreferencesManager { const existing = await this.storageProvider.cloudGet({ key, scope: record[0] }); if (existing != null && !deepEqual(existing.value, record[1])) { - const { canceled, result } = await os.select({ + const merge = (PREF_DEF as PreferencesDefinition)[key].mergeStrategy; + let mergedValue: ValueOf | undefined = undefined; // null と区別したいため + try { + if (merge != null) mergedValue = merge(record[1], existing.value); + } catch (err) { + // nop + } + const { canceled, result: choice } = await os.select({ title: i18n.ts.preferenceSyncConflictTitle, text: i18n.ts.preferenceSyncConflictText, - items: [{ + items: [...(mergedValue !== undefined ? [{ + text: i18n.ts.preferenceSyncConflictChoiceMerge, + value: 'merge', + }] : []), { text: i18n.ts.preferenceSyncConflictChoiceServer, value: 'remote', }, { @@ -380,14 +404,16 @@ export class PreferencesManager { text: i18n.ts.preferenceSyncConflictChoiceCancel, value: null, }], - default: 'remote', + default: mergedValue !== undefined ? 'merge' : 'remote', }); - if (canceled || result == null) return { enabled: false }; + if (canceled || choice == null) return { enabled: false }; - if (result === 'remote') { + if (choice === 'remote') { this.commit(key, existing.value); - } else if (result === 'local') { + } else if (choice === 'local') { // nop + } else if (choice === 'merge') { + this.commit(key, mergedValue!); } } @@ -457,7 +483,7 @@ export class PreferencesManager { text: i18n.ts.resetToDefaultValue, danger: true, action: () => { - this.commit(key, PREF_DEF[key].default); + this.commit(key, getInitialPrefValue(key)); }, }, { type: 'divider', diff --git a/packages/frontend/src/utility/sound.ts b/packages/frontend/src/utility/sound.ts index 217da9c8b2..8e79841647 100644 --- a/packages/frontend/src/utility/sound.ts +++ b/packages/frontend/src/utility/sound.ts @@ -6,6 +6,7 @@ import type { SoundStore } from '@/preferences/def.js'; import { prefer } from '@/preferences.js'; import { PREF_DEF } from '@/preferences/def.js'; +import { getInitialPrefValue } from '@/preferences/manager.js'; let ctx: AudioContext; const cache = new Map(); @@ -133,7 +134,8 @@ export function playMisskeySfx(operationType: OperationType) { playMisskeySfxFile(sound).then((succeed) => { if (!succeed && sound.type === '_driveFile_') { // ドライブファイルが存在しない場合はデフォルトのサウンドを再生する - const soundName = PREF_DEF[`sound.on.${operationType}`].default.type as Exclude; + const default_ = getInitialPrefValue(`sound.on.${operationType}`); + const soundName = default_.type as Exclude; if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`); playMisskeySfxFileInternal({ type: soundName, From 743995e4695af1ade073172f87fc91f1c9b37ba8 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 31 May 2025 14:15:40 +0900 Subject: [PATCH 02/10] enhance(frontend): make pref sync more smart --- packages/frontend/src/os.ts | 24 +++++++--- packages/frontend/src/pref-migrate.ts | 2 +- packages/frontend/src/preferences/def.ts | 35 +++++++++++--- packages/frontend/src/preferences/manager.ts | 50 +++++++++++++++----- 4 files changed, 86 insertions(+), 25 deletions(-) diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index a513ae4902..08291a5595 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -547,14 +547,28 @@ export function success(): Promise { }); } -export function waiting(text?: string | null): () => void { +export function waiting(options: { text?: string } = {}) { window.document.body.setAttribute('inert', 'true'); const showing = ref(true); + const isSuccess = ref(false); + + function done(doneOptions: { success?: boolean } = {}) { + if (doneOptions.success) { + isSuccess.value = true; + window.setTimeout(() => { + showing.value = false; + }, 1000); + } else { + showing.value = false; + } + } + + // NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない) const { dispose } = popup(MkWaitingDialog, { - success: false, + success: isSuccess, showing: showing, - text, + text: options.text, }, { closed: () => { window.document.body.removeAttribute('inert'); @@ -562,9 +576,7 @@ export function waiting(text?: string | null): () => void { }, }); - return () => { - showing.value = false; - }; + return done; } export function form(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType }> { diff --git a/packages/frontend/src/pref-migrate.ts b/packages/frontend/src/pref-migrate.ts index 13ca07cdcc..648349c6fe 100644 --- a/packages/frontend/src/pref-migrate.ts +++ b/packages/frontend/src/pref-migrate.ts @@ -15,7 +15,7 @@ import { i18n } from '@/i18n.js'; // TODO: そのうち消す export function migrateOldSettings() { - os.waiting(i18n.ts.settingsMigrating); + os.waiting({ text: i18n.ts.settingsMigrating }); store.loaded.then(async () => { misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []).then((themes: any) => { diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts index b8a5a84279..86d5c8af98 100644 --- a/packages/frontend/src/preferences/def.ts +++ b/packages/frontend/src/preferences/def.ts @@ -13,6 +13,7 @@ import type { DeviceKind } from '@/utility/device-kind.js'; import type { DeckProfile } from '@/deck.js'; import type { PreferencesDefinition } from './manager.js'; import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js'; +import { deepEqual } from '@/utility/deep-equal.js'; /** サウンド設定 */ export type SoundStore = { @@ -87,9 +88,20 @@ export const PREF_DEF = { emojis: string[]; }[], mergeStrategy: (a, b) => { - const sameIdExists = a.some(x => b.some(y => x.id === y.id)); - if (sameIdExists) throw new Error(); - return a.concat(b); + const mergedItems = [] as (typeof a)[]; + for (const x of a.concat(b)) { + const sameIdItem = mergedItems.find(y => y.id === x.id); + if (sameIdItem != null) { + if (deepEqual(x, sameIdItem)) { // 完全な重複は無視 + continue; + } else { // IDは同じなのに内容が違う場合はマージ不可とする + throw new Error(); + } + } else { + mergedItems.push(x); + } + } + return mergedItems; }, }, emojiPaletteForReaction: { @@ -107,9 +119,20 @@ export const PREF_DEF = { themes: { default: [] as Theme[], mergeStrategy: (a, b) => { - const sameIdExists = a.some(x => b.some(y => x.id === y.id)); - if (sameIdExists) throw new Error(); - return a.concat(b); + const mergedItems = [] as (typeof a)[]; + for (const x of a.concat(b)) { + const sameIdItem = mergedItems.find(y => y.id === x.id); + if (sameIdItem != null) { + if (deepEqual(x, sameIdItem)) { // 完全な重複は無視 + continue; + } else { // IDは同じなのに内容が違う場合はマージ不可とする + throw new Error(); + } + } else { + mergedItems.push(x); + } + } + return mergedItems; }, }, lightTheme: { diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts index 016e1ad85b..ccb8ea0372 100644 --- a/packages/frontend/src/preferences/manager.ts +++ b/packages/frontend/src/preferences/manager.ts @@ -377,14 +377,12 @@ export class PreferencesManager { public async enableSync(key: K): Promise<{ enabled: boolean; } | null> { if (this.isSyncEnabled(key)) return Promise.resolve(null); - const record = this.getMatchedRecordOf(key); - - const existing = await this.storageProvider.cloudGet({ key, scope: record[0] }); - if (existing != null && !deepEqual(existing.value, record[1])) { + // undefined ... cancel + async function resolveConflict(local: ValueOf, remote: ValueOf): Promise | undefined> { const merge = (PREF_DEF as PreferencesDefinition)[key].mergeStrategy; let mergedValue: ValueOf | undefined = undefined; // null と区別したいため try { - if (merge != null) mergedValue = merge(record[1], existing.value); + if (merge != null) mergedValue = merge(local, remote); } catch (err) { // nop } @@ -406,23 +404,51 @@ export class PreferencesManager { }], default: mergedValue !== undefined ? 'merge' : 'remote', }); - if (canceled || choice == null) return { enabled: false }; + if (canceled || choice == null) return undefined; if (choice === 'remote') { - this.commit(key, existing.value); + return remote; } else if (choice === 'local') { - // nop + return local; } else if (choice === 'merge') { - this.commit(key, mergedValue!); + return mergedValue!; } } + const record = this.getMatchedRecordOf(key); + + let newValue = record[1]; + + const existing = await this.storageProvider.cloudGet({ key, scope: record[0] }); + if (existing != null && !deepEqual(record[1], existing.value)) { + const resolvedValue = await resolveConflict(record[1], existing.value); + if (resolvedValue === undefined) return { enabled: false }; // canceled + newValue = resolvedValue; + } + + this.commit(key, newValue); + + const done = os.waiting(); + + try { + await this.storageProvider.cloudSet({ key, scope: record[0], value: newValue }); + } catch (err) { + done(); + + os.alert({ + type: 'error', + title: i18n.ts.somethingHappened, + text: err, + }); + + return { enabled: false }; + } + + done({ success: true }); + record[2].sync = true; this.save(); - // awaitの必要性は無い - this.storageProvider.cloudSet({ key, scope: record[0], value: this.s[key] }); - return { enabled: true }; } From aa4c7a1313a9f3e31df6190d66e8665e8d736574 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 31 May 2025 16:33:03 +0900 Subject: [PATCH 03/10] =?UTF-8?q?fix(frontend):=20=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E4=BE=9D=E5=AD=98=E3=81=8B=E3=81=A4?= =?UTF-8?q?=E5=88=9D=E6=9C=9F=E7=8A=B6=E6=85=8B=E3=81=A7=E3=81=82=E3=82=8B?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E5=80=A4=E3=82=92=E3=82=B5=E3=83=BC=E3=83=90?= =?UTF-8?q?=E3=83=BC=E5=90=8C=E6=9C=9F=E3=81=97=E3=82=88=E3=81=86=E3=81=A8?= =?UTF-8?q?=E3=81=97=E3=81=9F=E9=9A=9B=E3=81=AB=E6=AD=A3=E3=81=97=E3=81=8F?= =?UTF-8?q?=E3=82=B3=E3=83=B3=E3=83=95=E3=83=AA=E3=82=AF=E3=83=88=E6=A4=9C?= =?UTF-8?q?=E5=87=BA=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + packages/frontend/src/preferences/manager.ts | 64 +++++++++++++++----- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c11e6e07e..2cebe29d9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ - フロントエンドの読み込みサイズを軽量化しました - ほとんどの言語のハイライトは問題なく行えますが、互換性の問題により一部の言語が正常にハイライトできなくなる可能性があります。詳しくは https://shiki.style/references/engine-js-compat をご覧ください。 - Fix: チャットに動画ファイルを送付すると、動画の表示が崩れてしまい視聴出来ない問題を修正 +- Fix: アカウント依存かつ初期状態である設定値をサーバー同期しようとした際に正しくコンフリクト検出されない問題を修正 - Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正 - Fix: 一定時間操作がなかったら動画プレイヤーのコントロールを隠すように - Fix: Twitchのクリップがプレイヤーで再生できない問題を修正 diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts index ccb8ea0372..cede145e74 100644 --- a/packages/frontend/src/preferences/manager.ts +++ b/packages/frontend/src/preferences/manager.ts @@ -104,6 +104,8 @@ export function getInitialPrefValue(k: K): ValueOf { } } +// TODO: PreferencesManagerForGuest のような非ログイン専用のクラスを分離すれば$iのnullチェックやaccountがnullであるスコープのレコード挿入などが不要になり綺麗になるかもしれない +// NOTE: accountDependentな設定は初期状態であってもアカウントごとのスコープでレコードを作成しておかないと、サーバー同期する際に正しく動作しなくなる export class PreferencesManager { private storageProvider: StorageProvider; public profile: PreferencesProfile; @@ -139,11 +141,11 @@ export class PreferencesManager { // TODO: 定期的にクラウドの値をフェッチ } - private isAccountDependentKey(key: K): boolean { + private static isAccountDependentKey(key: K): boolean { return (PREF_DEF as PreferencesDefinition)[key].accountDependent === true; } - private isServerDependentKey(key: K): boolean { + private static isServerDependentKey(key: K): boolean { return (PREF_DEF as PreferencesDefinition)[key].serverDependent === true; } @@ -166,7 +168,7 @@ export class PreferencesManager { const record = this.getMatchedRecordOf(key); - if (parseScope(record[0]).account == null && this.isAccountDependentKey(key)) { + if (parseScope(record[0]).account == null && PreferencesManager.isAccountDependentKey(key)) { this.profile.preferences[key].push([makeScope({ server: host, account: $i!.id, @@ -175,7 +177,7 @@ export class PreferencesManager { return; } - if (parseScope(record[0]).server == null && this.isServerDependentKey(key)) { + if (parseScope(record[0]).server == null && PreferencesManager.isServerDependentKey(key)) { this.profile.preferences[key].push([makeScope({ server: host, }), v, {}]); @@ -276,7 +278,19 @@ export class PreferencesManager { public static newProfile(): PreferencesProfile { const data = {} as PreferencesProfile['preferences']; for (const key in PREF_DEF) { - data[key] = [[makeScope({}), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]]; + const v = getInitialPrefValue(key as keyof typeof PREF_DEF); + if (PreferencesManager.isAccountDependentKey(key as keyof typeof PREF_DEF)) { + data[key] = $i ? [[makeScope({}), v, {}], [makeScope({ + server: host, + account: $i.id, + }), v, {}]] : [[makeScope({}), v, {}]]; + } else if (PreferencesManager.isServerDependentKey(key as keyof typeof PREF_DEF)) { + data[key] = [[makeScope({ + server: host, + }), v, {}]]; + } else { + data[key] = [[makeScope({}), v, {}]]; + } } return { id: uuid(), @@ -293,18 +307,36 @@ export class PreferencesManager { for (const key in PREF_DEF) { const records = profileLike.preferences[key]; if (records == null || records.length === 0) { - data[key] = [[makeScope({}), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]]; + const v = getInitialPrefValue(key as keyof typeof PREF_DEF); + if (PreferencesManager.isAccountDependentKey(key as keyof typeof PREF_DEF)) { + data[key] = $i ? [[makeScope({}), v, {}], [makeScope({ + server: host, + account: $i.id, + }), v, {}]] : [[makeScope({}), v, {}]]; + } else if (PreferencesManager.isServerDependentKey(key as keyof typeof PREF_DEF)) { + data[key] = [[makeScope({ + server: host, + }), v, {}]]; + } else { + data[key] = [[makeScope({}), v, {}]]; + } continue; } else { - data[key] = records; - - // alpha段階ではmetaが無かったのでマイグレート - // TODO: そのうち消す - for (const record of data[key] as any[][]) { - if (record.length === 2) { - record.push({}); - } + if ($i && PreferencesManager.isAccountDependentKey(key as keyof typeof PREF_DEF) && !records.some(([scope]) => parseScope(scope).server === host && parseScope(scope).account === $i!.id)) { + data[key] = records.concat([[makeScope({ + server: host, + account: $i.id, + }), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]]); + continue; } + if ($i && PreferencesManager.isServerDependentKey(key as keyof typeof PREF_DEF) && !records.some(([scope]) => parseScope(scope).server === host)) { + data[key] = records.concat([[makeScope({ + server: host, + }), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]]); + continue; + } + + data[key] = records; } } @@ -342,7 +374,7 @@ export class PreferencesManager { public setAccountOverride(key: K) { if ($i == null) return; - if (this.isAccountDependentKey(key)) throw new Error('already account-dependent'); + if (PreferencesManager.isAccountDependentKey(key)) throw new Error('already account-dependent'); if (this.isAccountOverrided(key)) return; const records = this.profile.preferences[key]; @@ -356,7 +388,7 @@ export class PreferencesManager { public clearAccountOverride(key: K) { if ($i == null) return; - if (this.isAccountDependentKey(key)) throw new Error('cannot clear override for this account-dependent property'); + if (PreferencesManager.isAccountDependentKey(key)) throw new Error('cannot clear override for this account-dependent property'); const records = this.profile.preferences[key]; From 0c2d799acd1be72fff7b41e7400019de3cf8ee66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Sat, 31 May 2025 16:34:25 +0900 Subject: [PATCH 04/10] =?UTF-8?q?fix(backend):=20=E3=82=B8=E3=83=A7?= =?UTF-8?q?=E3=83=96=E3=82=AD=E3=83=A5=E3=83=BC=E7=94=BB=E9=9D=A2=E3=81=AE?= =?UTF-8?q?Paused=E3=82=BF=E3=83=96=E3=82=92=E3=82=A2=E3=82=AF=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=83=96=E3=81=AB=E3=81=99=E3=82=8B=E3=81=A8400?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=8C=E8=BF=94=E3=81=A3=E3=81=A6?= =?UTF-8?q?=E3=81=8F=E3=82=8B=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#1612?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): ジョブキュー画面のPausedタブをアクティブにすると400エラーが返ってくるのを修正 * fix CHANGELOG.md --- CHANGELOG.md | 2 +- packages/backend/src/server/api/endpoints/admin/queue/jobs.ts | 2 +- packages/misskey-js/src/autogen/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cebe29d9c..edca4339cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,7 +72,7 @@ - Fix: ミュート対象ユーザーが引用されているノートがRNされたときにミュートを貫通してしまう問題を修正 #16009 - Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように - Fix: コントロールパネルから招待コードを作成すると作成者の情報が記録されない問題を修正 - +- Fix: コントロールパネルのジョブキューページからPausedなジョブ一覧を閲覧できない問題を修正 ## 2025.5.0 diff --git a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts index 155f2c4000..a68e95bf3f 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts @@ -28,7 +28,7 @@ export const paramDef = { type: 'object', properties: { queue: { type: 'string', enum: QUEUE_TYPES }, - state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed'] } }, + state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed', 'paused'] } }, search: { type: 'string' }, }, required: ['queue', 'state'], diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 8960639443..e87bcb4f83 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -9074,7 +9074,7 @@ export type operations = { 'application/json': { /** @enum {string} */ queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; - state: ('active' | 'wait' | 'delayed' | 'completed' | 'failed')[]; + state: ('active' | 'wait' | 'delayed' | 'completed' | 'failed' | 'paused')[]; search?: string; }; }; From 62e333191d55b5b17e937ac49492d0bbae12c4d8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 31 May 2025 07:38:56 +0000 Subject: [PATCH 05/10] Bump version to 2025.5.1-beta.6 --- package.json | 2 +- packages/misskey-js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ae61dc13a8..8b843619eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2025.5.1-beta.5", + "version": "2025.5.1-beta.6", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index d274f43ab4..9465b00622 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2025.5.1-beta.5", + "version": "2025.5.1-beta.6", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", From 14cbc78031513122995a207858b910b6c3d9a0d7 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 31 May 2025 17:41:04 +0900 Subject: [PATCH 06/10] Update packages/frontend/src/pages/admin/custom-emojis-manager.register.vue Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com> --- .../frontend/src/pages/admin/custom-emojis-manager.register.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index e1dabe549f..621ec8a6a8 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.uplaod }} + {{ i18n.ts.upload }} {{ i18n.ts.fromDrive }}
From f14787bd106de241db8ab3b00d8fcbb9fbd4cd2e Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 31 May 2025 18:04:00 +0900 Subject: [PATCH 07/10] =?UTF-8?q?fix(frontend):=20=E3=83=99=E3=83=BC?= =?UTF-8?q?=E3=82=B9=E3=83=AD=E3=83=BC=E3=83=AB=E3=81=AE=E3=80=81=E3=80=8C?= =?UTF-8?q?=E3=82=A2=E3=83=83=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E3=81=AA=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E7=A8=AE?= =?UTF-8?q?=E5=88=A5=E3=80=8D=E3=82=92=E5=A4=89=E6=9B=B4=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=82=82=E6=AD=A3=E3=81=97=E3=81=8F=E4=BF=9D=E5=AD=98=E3=81=95?= =?UTF-8?q?=E3=82=8C=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #16128 --- packages/frontend/src/pages/admin/roles.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index ef6e25685d..70e8153544 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -149,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +