summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-05-31 12:49:10 +0900
committersyuilo <4439005+syuilo@users.noreply.github.com>2025-05-31 12:49:10 +0900
commit0254570fbf94ae99f492a781e07863dc694a12cf (patch)
tree79afbe71cf52d5b0580446c3ccceaf9271ec793c /packages/frontend/src
parentNew Crowdin updates (#16116) (diff)
downloadmisskey-0254570fbf94ae99f492a781e07863dc694a12cf.tar.gz
misskey-0254570fbf94ae99f492a781e07863dc694a12cf.tar.bz2
misskey-0254570fbf94ae99f492a781e07863dc694a12cf.zip
enhance(frontend): 設定の同期をオンにするときに競合したときに値をマージできるように
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/pages/settings/navbar.vue3
-rw-r--r--packages/frontend/src/pages/settings/sounds.vue3
-rw-r--r--packages/frontend/src/preferences/def.ts33
-rw-r--r--packages/frontend/src/preferences/manager.ts52
-rw-r--r--packages/frontend/src/utility/sound.ts4
5 files changed, 73 insertions, 22 deletions
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<keyof typeof sounds.value>) {
- 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<K extends keyof PREF> = PREF[K]['default'];
+type DefaultValues = {
+ [K in keyof PREF]: PREF[K]['default'] extends (...args: any) => infer R ? R : PREF[K]['default'];
+};
+type ValueOf<K extends keyof PREF> = DefaultValues[K];
type Scope = Partial<{
server: string | null; // host
@@ -84,11 +87,22 @@ export type StorageProvider = {
cloudSet: <K extends keyof PREF>(ctx: { key: K; scope: Scope; value: ValueOf<K>; }) => Promise<void>;
};
-export type PreferencesDefinition = Record<string, {
- default: any;
+type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) => infer R ? R : Default> = {
+ default: Default;
accountDependent?: boolean;
serverDependent?: boolean;
-}>;
+ mergeStrategy?: (a: T, b: T) => T;
+};
+
+export type PreferencesDefinition = Record<string, PreferencesDefinitionRecord<any>>;
+
+export function getInitialPrefValue<K extends keyof PREF>(k: K): ValueOf<K> {
+ 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<K> | 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<string, AudioBuffer>();
@@ -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<SoundType, '_driveFile_'>;
+ const default_ = getInitialPrefValue(`sound.on.${operationType}`);
+ const soundName = default_.type as Exclude<SoundType, '_driveFile_'>;
if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`);
playMisskeySfxFileInternal({
type: soundName,