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 }; }