diff --git a/changelog.d/config-migration.fix b/changelog.d/config-migration.fix new file mode 100644 index 000000000..19f9dc6b1 --- /dev/null +++ b/changelog.d/config-migration.fix @@ -0,0 +1 @@ +Fix legacy settings migration being discarded after it ran. diff --git a/changelog.d/fix-chat-post.fix b/changelog.d/fix-chat-post.fix new file mode 100644 index 000000000..60480a1c0 --- /dev/null +++ b/changelog.d/fix-chat-post.fix @@ -0,0 +1 @@ +Fix error dialog when posting in chat diff --git a/changelog.d/media-proxy-sw-cache.fix b/changelog.d/media-proxy-sw-cache.fix new file mode 100644 index 000000000..bcdc59898 --- /dev/null +++ b/changelog.d/media-proxy-sw-cache.fix @@ -0,0 +1 @@ +Prevent service worker from caching media-proxy responses diff --git a/src/components/chat_view/chat_view.js b/src/components/chat_view/chat_view.js index 41dd038a9..ab655cc39 100644 --- a/src/components/chat_view/chat_view.js +++ b/src/components/chat_view/chat_view.js @@ -576,11 +576,14 @@ const Chat = { // Event handlers onPosted(data) { - this.explicitReplyStatus = null - this.$router.push({ - name: 'conversation2', - params: { statusId: data.id }, - }) + // only conversation poster has the returned data + if (this.isConversation) { + this.explicitReplyStatus = null + this.$router.push({ + name: 'conversation2', + params: { statusId: data.id }, + }) + } }, handleVisibilityChange() { this.$nextTick(() => { diff --git a/src/stores/sync_config.js b/src/stores/sync_config.js index 17a60d3ef..4de0c13cb 100644 --- a/src/stores/sync_config.js +++ b/src/stores/sync_config.js @@ -688,83 +688,13 @@ export const useSyncConfigStore = defineStore('sync_config', { ) recent = _wrapData({ flagStorage: { ...flagsTemplate }, - prefsStorage: { ...defaultState.prefsStorage }, + prefsStorage: cloneDeep(defaultState.prefsStorage), }) } recent = recent && (await _doMigrations(recent, this.setPreference)) stale = stale && (await _doMigrations(stale, this.setPreference)) - // Various migrations - console.debug('Migrating from old config') - const vuexState = (await storage.getItem('vuex-lz')) ?? {} - const config = vuexState.config ?? {} - - const migratedEntries = new Set(config._syncMigration ?? []) - console.debug( - `Already migrated Values: ${[...migratedEntries].join() || '[none]'}`, - ) - - Object.entries(oldDefaultConfigSync).forEach(([key, value]) => { - const oldValue = config[key] - const defaultValue = value - - const present = oldValue !== undefined - const migrated = migratedEntries.has(key) - const different = !isEqual(oldValue, defaultValue) - - if (present && !migrated && different) { - console.debug(`Migrating config ${key}: ${oldValue}`) - if (key === 'theme3hacks') { - useLocalConfigStore().set({ - path: 'fontInterface', - value: oldValue.fonts.interface, - }) - useLocalConfigStore().set({ - path: 'fontInput', - value: oldValue.fonts.input, - }) - useLocalConfigStore().set({ - path: 'fontPost', - value: oldValue.fonts.post, - }) - useLocalConfigStore().set({ - path: 'fontMonospace', - value: oldValue.fonts.monospace, - }) - useSyncConfigStore().setSimplePrefAndSave({ - path: 'underlay', - value: oldValue.underlay, - }) - } else if (key == 'muteWords') { - oldValue.forEach((word, order) => { - const uniqueId = uuidv4() - - useSyncConfigStore().setPreference({ - path: 'simple.muteFilters.' + uniqueId, - value: { - type: 'word', - value: word, - name: word, - enabled: true, - expires: null, - hide: false, - order, - }, - }) - }) - } else { - this.setPreference({ path: `simple.${key}`, value: oldValue }) - } - migratedEntries.add(key) - needUpload = true - } - }) - - config._syncMigration = [...migratedEntries] - vuexState.config = config - storage.setItem('vuex-lz', vuexState) - if (!needUpload && recent && stale) { console.debug('Checking if data needs merging...') // discarding timestamps and versions @@ -795,14 +725,99 @@ export const useSyncConfigStore = defineStore('sync_config', { recent.flagStorage = { ...flagsTemplate, ...totalFlags } recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs } - this.dirty = dirty || needUpload this.cache = recent + this.flagStorage = this.cache.flagStorage + this.prefsStorage = this.cache.prefsStorage + if (!Array.isArray(this.prefsStorage._journal)) { + this.prefsStorage._journal = [] + } + + // Various migrations + console.debug('Migrating from old config') + const vuexState = (await storage.getItem('vuex-lz')) ?? {} + const config = vuexState.config ?? {} + + const migratedEntries = new Set(config._syncMigration ?? []) + console.debug( + `Already migrated Values: ${[...migratedEntries].join() || '[none]'}`, + ) + + Object.entries(oldDefaultConfigSync).forEach(([key, value]) => { + const oldValue = config[key] + const defaultValue = value + const migrated = migratedEntries.has(key) + const preferencePath = + key === 'theme3hacks' ? 'simple.underlay' : `simple.${key}` + + const present = oldValue !== undefined + const different = !isEqual(oldValue, defaultValue) + const preferenceHandled = + get(this.prefsStorage, preferencePath) !== undefined || + this.prefsStorage._journal.some( + (entry) => + entry?.path === preferencePath || + entry?.path?.startsWith?.(`${preferencePath}.`), + ) + + if (present && different && !preferenceHandled) { + console.debug(`Migrating config ${key}: ${oldValue}`) + if (key === 'theme3hacks') { + if (!migrated) { + useLocalConfigStore().set({ + path: 'fontInterface', + value: oldValue.fonts.interface, + }) + useLocalConfigStore().set({ + path: 'fontInput', + value: oldValue.fonts.input, + }) + useLocalConfigStore().set({ + path: 'fontPosts', + value: oldValue.fonts.post, + }) + useLocalConfigStore().set({ + path: 'fontMonospace', + value: oldValue.fonts.monospace, + }) + } + this.setPreference({ + path: 'simple.underlay', + value: oldValue.underlay, + }) + } else if (key == 'muteWords') { + oldValue.forEach((word, order) => { + const uniqueId = uuidv4() + + this.setPreference({ + path: 'simple.muteFilters.' + uniqueId, + value: { + type: 'word', + value: word, + name: word, + enabled: true, + expires: null, + hide: false, + order, + }, + }) + }) + } else { + this.setPreference({ path: preferencePath, value: oldValue }) + } + migratedEntries.add(key) + needUpload = true + } + }) + + config._syncMigration = [...migratedEntries] + vuexState.config = config + storage.setItem('vuex-lz', vuexState) + + this.dirty = dirty || needUpload // set local timestamp to smaller one if we don't have any changes if (stale && recent && !this.dirty) { this.cache._timestamp = Math.min(stale._timestamp, recent._timestamp) } - this.flagStorage = this.cache.flagStorage - this.prefsStorage = this.cache.prefsStorage this.pushSyncConfig() }, pushSyncConfig({ force = false } = {}) { diff --git a/src/sw.js b/src/sw.js index 48a4c5e60..f24486469 100644 --- a/src/sw.js +++ b/src/sw.js @@ -116,7 +116,9 @@ const isNotMedia = (req) => { return false } const url = new URL(req.url) - return !url.pathname.startsWith('/media/') + return ( + !url.pathname.startsWith('/media/') && !url.pathname.startsWith('/proxy/') + ) } const isAsset = (req) => { const url = new URL(req.url) diff --git a/test/unit/specs/stores/sync_config.spec.js b/test/unit/specs/stores/sync_config.spec.js index d2e6d42d3..3618068f7 100644 --- a/test/unit/specs/stores/sync_config.spec.js +++ b/test/unit/specs/stores/sync_config.spec.js @@ -1,6 +1,7 @@ import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' +import { useLocalConfigStore } from 'src/stores/local_config.js' import { _getAllFlags, _getRecentData, @@ -16,11 +17,17 @@ import { VERSION, } from 'src/stores/sync_config.js' +import { storage } from 'src/lib/storage.js' + describe('The SyncConfig store', () => { beforeEach(() => { setActivePinia(createPinia()) }) + afterEach(() => { + vi.restoreAllMocks() + }) + describe('mutations', () => { describe('initSyncConfig', () => { const user = { @@ -163,6 +170,204 @@ describe('The SyncConfig store', () => { expect(store.cache._timestamp).to.be.a('number') expect(store.cache.flagStorage).to.eql(defaultState.flagStorage) }) + + it('should recover legacy preferences marked as migrated', async () => { + vi.spyOn(storage, 'getItem').mockResolvedValue({ + config: { + _syncMigration: ['modalOnUnfollow'], + modalOnUnfollow: true, + }, + }) + vi.spyOn(storage, 'setItem').mockResolvedValue() + const store = useSyncConfigStore() + store.pushSyncConfig = vi.fn() + + await store.initSyncConfig({ ...user }) + + expect(store.prefsStorage.simple.modalOnUnfollow).to.eql(true) + expect(store.cache.prefsStorage.simple.modalOnUnfollow).to.eql(true) + expect(store.prefsStorage._journal).to.deep.include({ + path: 'simple.modalOnUnfollow', + operation: 'set', + args: [true], + timestamp: store.prefsStorage._journal[0].timestamp, + }) + expect(store.dirty).to.eql(true) + expect(store.pushSyncConfig).toHaveBeenCalledOnce() + expect(defaultState.prefsStorage.simple.modalOnUnfollow).to.eql( + undefined, + ) + }) + + it('should preserve an explicit synced preference during recovery', async () => { + vi.spyOn(storage, 'getItem').mockResolvedValue({ + config: { + _syncMigration: ['modalOnRepeat'], + modalOnRepeat: true, + }, + }) + vi.spyOn(storage, 'setItem').mockResolvedValue() + const store = useSyncConfigStore() + store.pushSyncConfig = () => { + /* no-op */ + } + + await store.initSyncConfig({ + ...user, + storage: { + _timestamp: 1, + _version: VERSION, + flagStorage: cloneDeep(defaultState.flagStorage), + prefsStorage: { + ...cloneDeep(defaultState.prefsStorage), + simple: { + ...cloneDeep(defaultState.prefsStorage.simple), + modalOnRepeat: false, + }, + }, + }, + }) + + expect(store.prefsStorage.simple.modalOnRepeat).to.eql(false) + }) + + it('should preserve a journaled preference removal during recovery', async () => { + vi.spyOn(storage, 'getItem').mockResolvedValue({ + config: { + _syncMigration: ['modalOnMute'], + modalOnMute: true, + }, + }) + vi.spyOn(storage, 'setItem').mockResolvedValue() + const store = useSyncConfigStore() + store.pushSyncConfig = () => { + /* no-op */ + } + + await store.initSyncConfig({ + ...user, + storage: { + _timestamp: 1, + _version: VERSION, + flagStorage: cloneDeep(defaultState.flagStorage), + prefsStorage: { + ...cloneDeep(defaultState.prefsStorage), + _journal: [ + { + path: 'simple.modalOnMute', + operation: 'unset', + args: [], + timestamp: 1, + }, + ], + }, + }, + }) + + expect(store.prefsStorage.simple.modalOnMute).to.eql(undefined) + }) + + it('should preserve a journaled nested preference during recovery', async () => { + vi.spyOn(storage, 'getItem').mockResolvedValue({ + config: { + _syncMigration: ['notificationVisibility'], + notificationVisibility: { + ...cloneDeep( + defaultState.prefsStorage.simple.notificationVisibility, + ), + likes: false, + }, + }, + }) + vi.spyOn(storage, 'setItem').mockResolvedValue() + const store = useSyncConfigStore() + const setPreference = vi.spyOn(store, 'setPreference') + store.pushSyncConfig = () => { + /* no-op */ + } + + await store.initSyncConfig({ + ...user, + storage: { + _timestamp: 1, + _version: VERSION, + flagStorage: cloneDeep(defaultState.flagStorage), + prefsStorage: { + ...cloneDeep(defaultState.prefsStorage), + _journal: [ + { + path: 'simple.notificationVisibility.likes', + operation: 'unset', + args: [], + timestamp: 1, + }, + ], + }, + }, + }) + + expect(setPreference).not.toHaveBeenCalled() + }) + + it('should recover with a malformed synced journal', async () => { + vi.spyOn(storage, 'getItem').mockResolvedValue({ + config: { + _syncMigration: ['modalOnUnfollow'], + modalOnUnfollow: true, + }, + }) + vi.spyOn(storage, 'setItem').mockResolvedValue() + const store = useSyncConfigStore() + store.pushSyncConfig = () => { + /* no-op */ + } + + await store.initSyncConfig({ + ...user, + storage: { + _timestamp: 1, + _version: VERSION, + flagStorage: cloneDeep(defaultState.flagStorage), + prefsStorage: { + ...cloneDeep(defaultState.prefsStorage), + _journal: [{ path: 1, operation: 'set' }], + }, + }, + }) + + expect(store.prefsStorage.simple.modalOnUnfollow).to.eql(true) + }) + + it('should not replay local theme migration during recovery', async () => { + vi.spyOn(storage, 'getItem').mockResolvedValue({ + config: { + _syncMigration: ['theme3hacks'], + theme3hacks: { + underlay: 'grid', + fonts: { + interface: 'Legacy interface', + input: 'Legacy input', + post: 'Legacy posts', + monospace: 'Legacy monospace', + }, + }, + }, + }) + vi.spyOn(storage, 'setItem').mockResolvedValue() + const localStore = useLocalConfigStore() + localStore.set({ path: 'fontInterface', value: 'Current interface' }) + const store = useSyncConfigStore() + store.pushSyncConfig = () => { + /* no-op */ + } + + await store.initSyncConfig({ ...user }) + + expect(store.prefsStorage.simple.underlay).to.eql('grid') + expect(localStore.prefsStorage.fontInterface).to.eql( + 'Current interface', + ) + }) }) describe('setPreference', () => { it('should set preference and update journal log accordingly', () => {