diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 2fdab6862c..de5bc2cc3d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1312,6 +1312,7 @@ noteOfThisUser: "このユーザーのノート一覧" clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。" performance: "パフォーマンス" modified: "変更あり" +modifiedAt: "変更日時" discard: "破棄" thereAreNChanges: "{n}件の変更があります" signinWithPasskey: "パスキーでログイン" diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts index b32369aa11..6a03a91783 100644 --- a/packages/frontend/src/preferences.ts +++ b/packages/frontend/src/preferences.ts @@ -36,11 +36,12 @@ const io: StorageProvider = { const cloudData = await misskeyApi('i/registry/get', { scope: ['client', 'preferences', 'sync'], key: syncGroup + ':' + ctx.key, - }) as [any, any][]; + }) as [any, any, any][]; const target = cloudData.find(([scope]) => isSameScope(scope, ctx.scope)); if (target == null) return null; return { value: target[1], + meta: target[2], }; } catch (err: any) { if (err.code === 'NO_SUCH_KEY') { // TODO: いちいちエラーキャッチするのは面倒なのでキーが無くてもエラーにならない maybe-get のようなエンドポイントをバックエンドに実装する @@ -52,12 +53,12 @@ const io: StorageProvider = { }, cloudSet: async (ctx) => { - let cloudData: [any, any][] = []; + let cloudData: [any, any, any][] = []; try { cloudData = await misskeyApi('i/registry/get', { scope: ['client', 'preferences', 'sync'], key: syncGroup + ':' + ctx.key, - }) as [any, any][]; + }) as [any, any, any][]; } catch (err: any) { if (err.code === 'NO_SUCH_KEY') { // TODO: いちいちエラーキャッチするのは面倒なのでキーが無くてもエラーにならない maybe-get のようなエンドポイントをバックエンドに実装する cloudData = []; @@ -69,9 +70,9 @@ const io: StorageProvider = { const i = cloudData.findIndex(([scope]) => isSameScope(scope, ctx.scope)); if (i === -1) { - cloudData.push([ctx.scope, ctx.value]); + cloudData.push([ctx.scope, ctx.value, ctx.meta]); } else { - cloudData[i] = [ctx.scope, ctx.value]; + cloudData[i] = [ctx.scope, ctx.value, ctx.meta]; } await misskeyApi('i/registry/set', { @@ -86,10 +87,10 @@ const io: StorageProvider = { const fetchings = ctx.needs.map(need => io.cloudGet(need).then(res => [need.key, res] as const)); const cloudDatas = await Promise.all(fetchings); - const res = {} as Partial>; + const res = {} as Partial>; for (const cloudData of cloudDatas) { if (cloudData[1] != null) { - res[cloudData[0]] = cloudData[1].value; + res[cloudData[0]] = cloudData[1]; } } diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts index 438c7f6ff4..12b84706bb 100644 --- a/packages/frontend/src/preferences/manager.ts +++ b/packages/frontend/src/preferences/manager.ts @@ -36,7 +36,7 @@ type Scope = Partial<{ type ValueMeta = Partial<{ sync: boolean; - // TODO: modifiedAtをここでも(Record個別に)持っておいた方が値の新旧比較に使えて便利そう + modifiedAt?: number; }>; type PrefRecord = [scope: Scope, value: ValueOf, meta: ValueMeta]; @@ -89,9 +89,9 @@ export type PossiblyNonNormalizedPreferencesProfile = Omit PossiblyNonNormalizedPreferencesProfile | null; save: (ctx: { profile: PreferencesProfile; }) => void; - cloudGetBulk: (ctx: { needs: { key: K; scope: Scope; }[] }) => Promise>>>; - cloudGet: (ctx: { key: K; scope: Scope; }) => Promise<{ value: ValueOf; } | null>; - cloudSet: (ctx: { key: K; scope: Scope; value: ValueOf; }) => Promise; + cloudGetBulk: (ctx: { needs: { key: K; scope: Scope; }[] }) => Promise; meta: { modifiedAt: ValueMeta['modifiedAt'] }; }>>>; + cloudGet: (ctx: { key: K; scope: Scope; }) => Promise<{ value: ValueOf; meta: { modifiedAt: ValueMeta['modifiedAt'] }; } | null>; + cloudSet: (ctx: { key: K; scope: Scope; value: ValueOf; meta: { modifiedAt: ValueMeta['modifiedAt'] }; }) => Promise; }; type PreferencesDefinitionRecord infer R ? R : Default> = { @@ -244,13 +244,13 @@ export class PreferencesManager extends EventEmitter { } // TODO: desync対策 cloudの値のfetchが正常に完了していない状態でcommitすると多分値が上書きされる - public commit(key: K, value: ValueOf) { + public commit(key: K, value: ValueOf): PrefRecord | null { const currentAccount = this.currentAccount; // TSを黙らせるため const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除 if (deepEqual(this.s[key], v)) { if (_DEV_) console.log('(skip) prefer:commit', key, v); - return; + return null; } if (_DEV_) console.log('prefer:commit', key, v); @@ -260,30 +260,39 @@ export class PreferencesManager extends EventEmitter { const record = this.getMatchedRecordOf(key); if (parseScope(record[0]).account == null && isAccountDependentKey(key) && currentAccount != null) { - this.profile.preferences[key].push([makeScope({ + const newRecord = [makeScope({ server: host, account: currentAccount.id, - }), v, {}]); + }), v, { + modifiedAt: Date.now(), + }] as PrefRecord; + this.profile.preferences[key].push(newRecord); this.save(); - return; + return newRecord; } if (parseScope(record[0]).server == null && isServerDependentKey(key)) { - this.profile.preferences[key].push([makeScope({ + const newRecord = [makeScope({ server: host, - }), v, {}]); + }), v, { + modifiedAt: Date.now(), + }] as PrefRecord; + this.profile.preferences[key].push(newRecord); this.save(); - return; + return newRecord; } record[1] = v; + record[2].modifiedAt = Date.now(); this.save(); if (record[2].sync) { // awaitの必要なし // TODO: リクエストを間引く - this.io.cloudSet({ key, scope: record[0], value: record[1] }); + this.io.cloudSet({ key, scope: record[0], value: record[1], meta: { modifiedAt: record[2].modifiedAt } }); } + + return record; } /** @@ -360,8 +369,9 @@ export class PreferencesManager extends EventEmitter { if (record[2].sync && Object.hasOwn(cloudValues, key) && cloudValues[key] !== undefined) { const cloudValue = cloudValues[key]; if (!deepEqual(cloudValue, record[1])) { - this.rewriteRawState(key, cloudValue); - record[1] = cloudValue; + this.rewriteRawState(key, cloudValue.value); + record[1] = cloudValue.value; + record[2].modifiedAt = cloudValue.meta.modifiedAt; modified = true; if (_DEV_) console.log('cloud fetched', key, cloudValue); } @@ -503,12 +513,12 @@ export class PreferencesManager extends EventEmitter { newValue = resolvedValue; } - this.commit(key, newValue); + const commitedRecord = this.commit(key, newValue); const done = os.waiting(); try { - await this.io.cloudSet({ key, scope: record[0], value: newValue }); + await this.io.cloudSet({ key, scope: record[0], value: newValue, meta: { modifiedAt: record[2].modifiedAt } }); } catch (err) { done(); @@ -607,6 +617,11 @@ export class PreferencesManager extends EventEmitter { icon: 'ti ti-cloud-cog', text: i18n.ts.syncBetweenDevices, ref: sync, + }, { + type: 'divider', + }, { + type: 'label', + text: i18n.ts.modifiedAt + ': ' + (this.getMatchedRecordOf(key)[2].modifiedAt ? new Date(this.getMatchedRecordOf(key)[2].modifiedAt!).toLocaleString() : '-'), }]; } } diff --git a/packages/i18n/src/autogen/locale.ts b/packages/i18n/src/autogen/locale.ts index 33fdfdd1ff..7e1f3192ed 100644 --- a/packages/i18n/src/autogen/locale.ts +++ b/packages/i18n/src/autogen/locale.ts @@ -5260,6 +5260,10 @@ export interface Locale extends ILocale { * 変更あり */ "modified": string; + /** + * 変更日時 + */ + "modifiedAt": string; /** * 破棄 */