From aa41cedd932e88b030ecc3cc54848b5aa300eec3 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 10 Aug 2022 02:19:07 +0300 Subject: [PATCH 01/59] initial prefs storage work --- src/modules/serverSideStorage.js | 54 ++++++++++- .../specs/modules/serverSideStorage.spec.js | 94 +++++++++++++++++++ 2 files changed, 144 insertions(+), 4 deletions(-) diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index e516a6e6..08ba48bb 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -1,5 +1,5 @@ import { toRaw } from 'vue' -import { isEqual, cloneDeep } from 'lodash' +import { isEqual, uniqBy, cloneDeep, set } from 'lodash' import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' export const VERSION = 1 @@ -22,6 +22,10 @@ export const defaultState = { // 1000: trim keys to those known by currently running FE // 1001: same as above + reset everything to 0 }, + prefsStorage: { + _journal: [], + simple: {} + }, // raw data raw: null, // local cache @@ -93,6 +97,42 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => { })) } +export const _mergePrefs = (recent, stale, allFlagKeys) => { + if (!stale) return recent + if (!recent) return stale + const { _journal: recentJournal, ...recentData } = recent + const { _journal: staleJournal } = stale + /** Journal entry format: + * path: path to entry in prefsStorage + * timestamp: timestamp of the change + * operation: operation type + * arguments: array of arguments, depends on operation type + * + * currently only supported operation type is "set" which just sets the value + * to requested one. Intended only to be used with simple preferences (boolean, number) + * shouldn't be used with collections! + */ + const resultOutput = { ...recentData } + const totalJournal = uniqBy( + [...recentJournal, ...staleJournal].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), + 'path' + ).reverse() + totalJournal.forEach(({ path, timestamp, operation, args }) => { + if (path.startsWith('_')) { + console.error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`) + return + } + switch (operation) { + case 'set': + set(resultOutput, path, args[0]) + break + default: + console.error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`) + } + }) + return { ...resultOutput, _journal: totalJournal } +} + export const _resetFlags = (totalFlags, knownKeys = defaultState.flagStorage) => { let result = { ...totalFlags } const allFlagKeys = Object.keys(totalFlags) @@ -165,7 +205,8 @@ export const mutations = { if (recent === null) { console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`) recent = _wrapData({ - flagStorage: { ...flagsTemplate } + flagStorage: { ...flagsTemplate }, + prefsStorage: { ...defaultState.prefsStorage } }) } @@ -180,17 +221,21 @@ export const mutations = { const allFlagKeys = _getAllFlags(recent, stale) let totalFlags + let totalPrefs if (dirty) { // Merge the flags - console.debug('Merging the flags...') + console.debug('Merging the data...') totalFlags = _mergeFlags(recent, stale, allFlagKeys) + totalPrefs = _mergePrefs(recent.prefsStorage, stale.prefsStorage) } else { totalFlags = recent.flagStorage + totalPrefs = recent.prefsStorage } totalFlags = _resetFlags(totalFlags) recent.flagStorage = totalFlags + recent.prefsStorage = totalPrefs state.dirty = dirty || needsUpload state.cache = recent @@ -216,7 +261,8 @@ const serverSideStorage = { const needPush = state.dirty || force if (!needPush) return state.cache = _wrapData({ - flagStorage: toRaw(state.flagStorage) + flagStorage: toRaw(state.flagStorage), + prefsStorage: toRaw(state.flagStorage) }) const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } rootState.api.backendInteractor diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js index e06c6ada..6059dc84 100644 --- a/test/unit/specs/modules/serverSideStorage.spec.js +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -7,6 +7,7 @@ import { _getRecentData, _getAllFlags, _mergeFlags, + _mergePrefs, _resetFlags, mutations, defaultState, @@ -28,6 +29,7 @@ describe('The serverSideStorage module', () => { expect(state.cache._version).to.eql(VERSION) expect(state.cache._timestamp).to.be.a('number') expect(state.cache.flagStorage).to.eql(defaultState.flagStorage) + expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage) }) it('should initialize storage with proper flags for new users if none present', () => { @@ -36,6 +38,7 @@ describe('The serverSideStorage module', () => { expect(state.cache._version).to.eql(VERSION) expect(state.cache._timestamp).to.be.a('number') expect(state.cache.flagStorage).to.eql(newUserFlags) + expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage) }) it('should merge flags even if remote timestamp is older', () => { @@ -57,6 +60,9 @@ describe('The serverSideStorage module', () => { flagStorage: { ...defaultState.flagStorage, updateCounter: 1 + }, + prefsStorage: { + ...defaultState.flagStorage, } } } @@ -157,6 +163,94 @@ describe('The serverSideStorage module', () => { }) }) + describe('_mergePrefs', () => { + it('should prefer recent and apply journal to it', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { a: 1, b: 0, c: true }, + _journal: [ + { path: 'simple.b', operation: 'set', args: [0], timestamp: 2 }, + { path: 'simple.c', operation: 'set', args: [true], timestamp: 4 } + ] + }, + // STALE + { + simple: { a: 1, b: 1, c: false }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [1], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [1], timestamp: 3 } + ] + } + ) + ).to.eql({ + simple: { a: 1, b: 1, c: true }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [1], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [1], timestamp: 3 }, + { path: 'simple.c', operation: 'set', args: [true], timestamp: 4 } + ] + }) + }) + + it('should allow setting falsy values', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { a: 1, b: 0, c: false }, + _journal: [ + { path: 'simple.b', operation: 'set', args: [0], timestamp: 2 }, + { path: 'simple.c', operation: 'set', args: [false], timestamp: 4 } + ] + }, + // STALE + { + simple: { a: 0, b: 0, c: true }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [0], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [0], timestamp: 3 } + ] + } + ) + ).to.eql({ + simple: { a: 0, b: 0, c: false }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [0], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [0], timestamp: 3 }, + { path: 'simple.c', operation: 'set', args: [false], timestamp: 4 } + ] + }) + }) + + it('should work with strings', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { a: 'foo' }, + _journal: [ + { path: 'simple.a', operation: 'set', args: ['foo'], timestamp: 2 } + ] + }, + // STALE + { + simple: { a: 'bar' }, + _journal: [ + { path: 'simple.a', operation: 'set', args: ['bar'], timestamp: 4 } + ] + } + ) + ).to.eql({ + simple: { a: 'bar' }, + _journal: [ + { path: 'simple.a', operation: 'set', args: ['bar'], timestamp: 4 } + ] + }) + }) + }) + describe('_resetFlags', () => { it('should reset all known flags to 0 when reset flag is set to > 0 and < 9000', () => { const totalFlags = { a: 0, b: 3, reset: 1 } From 2c0eb29b286edf57d75c6044855ea5be9187493b Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 10 Aug 2022 02:31:41 +0300 Subject: [PATCH 02/59] more prefs storage work + move dontShowUpdateNotifs to prefs --- .../update_notification.js | 4 +-- src/modules/serverSideStorage.js | 23 ++++++++++--- .../specs/modules/serverSideStorage.spec.js | 32 +++++++++++++++++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js index ba008d81..609842c4 100644 --- a/src/components/update_notification/update_notification.js +++ b/src/components/update_notification/update_notification.js @@ -38,7 +38,7 @@ const UpdateNotification = { return !this.$store.state.instance.disableUpdateNotification && this.$store.state.users.currentUser && this.$store.state.serverSideStorage.flagStorage.updateCounter < CURRENT_UPDATE_COUNTER && - !this.$store.state.serverSideStorage.flagStorage.dontShowUpdateNotifs + !this.$store.state.serverSideStorage.prefsStorage.simple.dontShowUpdateNotifs } }, methods: { @@ -48,7 +48,7 @@ const UpdateNotification = { neverShowAgain () { this.toggleShow() this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) - this.$store.commit('setFlag', { flag: 'dontShowUpdateNotifs', value: 1 }) + this.$store.commit('setPreference', { path: 'simple.dontShowUpdateNotifs', value: true }) this.$store.dispatch('pushServerSideStorage') }, dismiss () { diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index 08ba48bb..bb647b97 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -14,9 +14,6 @@ export const defaultState = { // storage of flags - stuff that can only be set and incremented flagStorage: { updateCounter: 0, // Counter for most recent update notification seen - // TODO move to prefsStorage when that becomes a thing since only way - // this can be reset is by complete reset of all flags - dontShowUpdateNotifs: 0, // if user chose to not show update notifications ever again reset: 0 // special flag that can be used to force-reset all flags, debug purposes only // special reset codes: // 1000: trim keys to those known by currently running FE @@ -24,7 +21,9 @@ export const defaultState = { }, prefsStorage: { _journal: [], - simple: {} + simple: { + dontShowUpdateNotifs: false + } }, // raw data raw: null, @@ -248,6 +247,20 @@ export const mutations = { setFlag (state, { flag, value }) { state.flagStorage[flag] = value state.dirty = true + }, + setPreference (state, { path, value }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + set(state.prefsStorage, path, value) + state.prefsStorage._journal = uniqBy( + [ + ...state.prefsStorage._journal, + { command: 'set', path, args: [value], timestamp: Date.now() } + ].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), + 'path' + ).reverse() } } @@ -262,7 +275,7 @@ const serverSideStorage = { if (!needPush) return state.cache = _wrapData({ flagStorage: toRaw(state.flagStorage), - prefsStorage: toRaw(state.flagStorage) + prefsStorage: toRaw(state.prefsStorage) }) const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } rootState.api.backendInteractor diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js index 6059dc84..da790793 100644 --- a/test/unit/specs/modules/serverSideStorage.spec.js +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -105,6 +105,38 @@ describe('The serverSideStorage module', () => { expect(state.cache.flagStorage).to.eql(defaultState.flagStorage) }) }) + describe('setPreference', () => { + const { setPreference } = mutations + + it('should set preference and update journal log accordingly', () => { + const state = cloneDeep(defaultState) + setPreference(state, { path: 'simple.testing', value: 1 }) + expect(state.prefsStorage.simple.testing).to.eql(1) + expect(state.prefsStorage._journal.length).to.eql(1) + expect(state.prefsStorage._journal[0]).to.eql({ + path: 'simple.testing', + command: 'set', + args: [1], + // should have A timestamp, we don't really care what it is + timestamp: state.prefsStorage._journal[0].timestamp + }) + }) + + it('should keep journal to a minimum (one entry per path)', () => { + const state = cloneDeep(defaultState) + setPreference(state, { path: 'simple.testing', value: 1 }) + setPreference(state, { path: 'simple.testing', value: 2 }) + expect(state.prefsStorage.simple.testing).to.eql(1) + expect(state.prefsStorage._journal.length).to.eql(1) + expect(state.prefsStorage._journal[0]).to.eql({ + path: 'simple.testing', + command: 'set', + args: [2], + // should have A timestamp, we don't really care what it is + timestamp: state.prefsStorage._journal[0].timestamp + }) + }) + }) }) describe('helper functions', () => { From 894a506382b18748e889fec69028da9897555446 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 10 Aug 2022 02:59:08 +0300 Subject: [PATCH 03/59] fixes --- src/modules/serverSideStorage.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index bb647b97..1a7e02b3 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -88,6 +88,8 @@ export const _getAllFlags = (recent, stale) => { } export const _mergeFlags = (recent, stale, allFlagKeys) => { + if (!stale.flagStorage) return recent.flagStorage + if (!recent.flagStorage) return stale.flagStorage return Object.fromEntries(allFlagKeys.map(flag => { const recentFlag = recent.flagStorage[flag] const staleFlag = stale.flagStorage[flag] @@ -113,7 +115,10 @@ export const _mergePrefs = (recent, stale, allFlagKeys) => { */ const resultOutput = { ...recentData } const totalJournal = uniqBy( - [...recentJournal, ...staleJournal].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), + [ + ...(Array.isArray(recentJournal) ? recentJournal : []), + ...(Array.isArray(staleJournal) ? staleJournal : []) + ].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), 'path' ).reverse() totalJournal.forEach(({ path, timestamp, operation, args }) => { @@ -243,6 +248,7 @@ export const mutations = { state.cache._timestamp = Math.min(stale._timestamp, recent._timestamp) } state.flagStorage = state.cache.flagStorage + state.prefsStorage = state.cache.prefsStorage }, setFlag (state, { flag, value }) { state.flagStorage[flag] = value From 8a67fe93c2ca56689c478c38e527e2f5a0d8d5ab Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 11 Aug 2022 01:07:26 +0300 Subject: [PATCH 04/59] update link in update notification to be a better one --- src/components/update_notification/update_notification.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/update_notification/update_notification.vue b/src/components/update_notification/update_notification.vue index d0e2499c..00841af2 100644 --- a/src/components/update_notification/update_notification.vue +++ b/src/components/update_notification/update_notification.vue @@ -60,7 +60,7 @@ From 72e238ceb34304cb023a01a84c3f453aadaa775c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 11 Aug 2022 01:07:51 +0300 Subject: [PATCH 05/59] server side storage support for collections + fixes --- src/modules/serverSideStorage.js | 111 +++++++++++++++--- .../specs/modules/serverSideStorage.spec.js | 20 +++- 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index 1a7e02b3..11e66220 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -1,5 +1,5 @@ import { toRaw } from 'vue' -import { isEqual, uniqBy, cloneDeep, set } from 'lodash' +import { isEqual, uniqWith, cloneDeep, set, get, clamp } from 'lodash' import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' export const VERSION = 1 @@ -36,6 +36,17 @@ export const newUserFlags = { updateCounter: CURRENT_UPDATE_COUNTER // new users don't need to see update notification } +export const _moveItemInArray = (array, value, movement) => { + const oldIndex = array.indexOf(value) + const newIndex = oldIndex + movement + const newArray = [...array] + // remove old + newArray.splice(oldIndex, 1) + // add new + newArray.splice(clamp(newIndex, 0, newArray.length + 1), 0, value) + return newArray +} + const _wrapData = (data) => ({ ...data, _timestamp: Date.now(), @@ -98,6 +109,23 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => { })) } +const _mergeJournal = (a, b) => uniqWith( + [ + ...(Array.isArray(a) ? a : []), + ...(Array.isArray(b) ? b : []) + ].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), + (a, b) => { + if (a.operation !== b.operation) return false + switch (a.operation) { + case 'set': + case 'arrangeSet': + return a.path === b.path + default: + return a.path === b.path && a.timestamp === b.timestamp + } + } +).reverse() + export const _mergePrefs = (recent, stale, allFlagKeys) => { if (!stale) return recent if (!recent) return stale @@ -114,13 +142,7 @@ export const _mergePrefs = (recent, stale, allFlagKeys) => { * shouldn't be used with collections! */ const resultOutput = { ...recentData } - const totalJournal = uniqBy( - [ - ...(Array.isArray(recentJournal) ? recentJournal : []), - ...(Array.isArray(staleJournal) ? staleJournal : []) - ].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), - 'path' - ).reverse() + const totalJournal = _mergeJournal(staleJournal, recentJournal) totalJournal.forEach(({ path, timestamp, operation, args }) => { if (path.startsWith('_')) { console.error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`) @@ -130,6 +152,17 @@ export const _mergePrefs = (recent, stale, allFlagKeys) => { case 'set': set(resultOutput, path, args[0]) break + case 'addToCollection': + set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).add(args[0]))) + break + case 'removeFromCollection': + set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).remove(args[0]))) + break + case 'reorderCollection': { + const [value, movement] = args + set(resultOutput, path, _moveItemInArray(get(resultOutput, path), value, movement)) + break + } default: console.error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`) } @@ -260,13 +293,56 @@ export const mutations = { return } set(state.prefsStorage, path, value) - state.prefsStorage._journal = uniqBy( - [ - ...state.prefsStorage._journal, - { command: 'set', path, args: [value], timestamp: Date.now() } - ].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), - 'path' - ).reverse() + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { command: 'set', path, args: [value], timestamp: Date.now() } + ] + }, + addCollectionPreference (state, { path, value }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + const collection = new Set(get(state.prefsStorage, path)) + collection.add(value) + set(state.prefsStorage, path, collection) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { command: 'addToCollection', path, args: [value], timestamp: Date.now() } + ] + }, + removeCollectionPreference (state, { path, value }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + const collection = new Set(get(state.prefsStorage, path)) + collection.remove(value) + set(state.prefsStorage, path, collection) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { command: 'removeFromCollection', path, args: [value], timestamp: Date.now() } + ] + }, + reorderCollectionPreference (state, { path, value, movement }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + const collection = get(state.prefsStorage, path) + const newCollection = _moveItemInArray(collection, value, movement) + set(state.prefsStorage, path, newCollection) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { command: 'arrangeCollection', path, args: [value], timestamp: Date.now() } + ] + }, + updateCache (state) { + state.prefsStorage._journal = _mergeJournal(state.prefsStorage._journal) + state.cache = _wrapData({ + flagStorage: toRaw(state.flagStorage), + prefsStorage: toRaw(state.prefsStorage) + }) } } @@ -279,10 +355,7 @@ const serverSideStorage = { pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) { const needPush = state.dirty || force if (!needPush) return - state.cache = _wrapData({ - flagStorage: toRaw(state.flagStorage), - prefsStorage: toRaw(state.prefsStorage) - }) + commit('updateCache') const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } rootState.api.backendInteractor .updateProfile({ params }) diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js index da790793..ada3b7ca 100644 --- a/test/unit/specs/modules/serverSideStorage.spec.js +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -4,6 +4,7 @@ import { VERSION, COMMAND_TRIM_FLAGS, COMMAND_TRIM_FLAGS_AND_RESET, + _moveItemInArray, _getRecentData, _getAllFlags, _mergeFlags, @@ -62,7 +63,7 @@ describe('The serverSideStorage module', () => { updateCounter: 1 }, prefsStorage: { - ...defaultState.flagStorage, + ...defaultState.prefsStorage } } } @@ -106,7 +107,7 @@ describe('The serverSideStorage module', () => { }) }) describe('setPreference', () => { - const { setPreference } = mutations + const { setPreference, updateCache } = mutations it('should set preference and update journal log accordingly', () => { const state = cloneDeep(defaultState) @@ -122,11 +123,12 @@ describe('The serverSideStorage module', () => { }) }) - it('should keep journal to a minimum (one entry per path)', () => { + it('should keep journal to a minimum (one entry per path for sets)', () => { const state = cloneDeep(defaultState) setPreference(state, { path: 'simple.testing', value: 1 }) setPreference(state, { path: 'simple.testing', value: 2 }) - expect(state.prefsStorage.simple.testing).to.eql(1) + updateCache(state) + expect(state.prefsStorage.simple.testing).to.eql(2) expect(state.prefsStorage._journal.length).to.eql(1) expect(state.prefsStorage._journal[0]).to.eql({ path: 'simple.testing', @@ -140,6 +142,16 @@ describe('The serverSideStorage module', () => { }) describe('helper functions', () => { + describe('_moveItemInArray', () => { + it('should move item according to movement value', () => { + expect(_moveItemInArray([1, 2, 3, 4], 4, -1)).to.eql([1, 2, 4, 3]) + expect(_moveItemInArray([1, 2, 3, 4], 1, 2)).to.eql([2, 3, 1, 4]) + }) + it('should clamp movement to within array', () => { + expect(_moveItemInArray([1, 2, 3, 4], 4, -10)).to.eql([4, 1, 2, 3]) + expect(_moveItemInArray([1, 2, 3, 4], 3, 99)).to.eql([1, 2, 4, 3]) + }) + }) describe('_getRecentData', () => { it('should handle nulls correctly', () => { expect(_getRecentData(null, null)).to.eql({ recent: null, stale: null, needUpload: true }) From 732733f115a863408a339e164ff88f1022c46101 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 11 Aug 2022 02:23:58 +0300 Subject: [PATCH 06/59] add a todo for future --- src/modules/serverSideStorage.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index 11e66220..bff8e4ba 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -110,6 +110,9 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => { } const _mergeJournal = (a, b) => uniqWith( + // TODO use groupBy to group by path, then trim them depending on operations, + // i.e. if field got removed in the end - no need to sort it beforehand, if field + // got re-added no need to remove it and add it etc. [ ...(Array.isArray(a) ? a : []), ...(Array.isArray(b) ? b : []) From 6df99133548fb209bf365b77665931be477f0a30 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 11 Aug 2022 14:30:58 +0300 Subject: [PATCH 07/59] ability to pin items in navigation menu, initial draft version --- src/App.scss | 3 + src/components/nav_panel/nav_panel.js | 111 +++++++++++++- src/components/nav_panel/nav_panel.vue | 140 ++++++++---------- .../settings_modal/tabs/general_tab.vue | 5 - .../timeline_menu/timeline_menu.vue | 2 +- .../timeline_menu/timeline_menu_content.js | 27 +++- .../timeline_menu/timeline_menu_content.vue | 69 +++------ src/modules/config.js | 1 - src/modules/serverSideStorage.js | 9 +- 9 files changed, 221 insertions(+), 146 deletions(-) diff --git a/src/App.scss b/src/App.scss index ab025d63..c75c990a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -756,6 +756,9 @@ option { padding: 0 0.3em; } } +.veryfaint { + opacity: 0.25; +} .login-hint { text-align: center; diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index abeff6bf..758f9af4 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -31,6 +31,66 @@ library.add( faList ) +export const TIMELINES = { + home: { + route: 'friends', + anonRoute: 'public-timeline', + icon: 'home', + label: 'nav.home_timeline', + criteria: ['!private'] + }, + public: { + route: 'public-timeline', + anon: true, + icon: 'users', + label: 'nav.public_tl', + criteria: ['!private'] + }, + twkn: { + route: 'public-external-timeline', + anon: true, + icon: 'globe', + label: 'nav.twkn', + criteria: ['!private', 'federating'] + }, + bookmarks: { + route: 'bookmarks', + icon: 'bookmark', + label: 'nav.bookmarks' + }, + dms: { + route: 'dms', + icon: 'envelope', + label: 'nav.dms' + } +} +export const ROOT_ITEMS = { + interactions: { + route: 'interactions', + icon: 'bell', + label: 'nav.interactions' + }, + chats: { + route: 'chats', + icon: 'comments', + label: 'nav.chats', + badgeGetter: 'unreadChatCount' + }, + friendRequests: { + route: 'friend-requests', + icon: 'user-plus', + label: 'nav.friend_requests', + criteria: ['lockedUser'], + badgeGetter: 'followRequestCount' + }, + about: { + route: 'about', + anon: true, + icon: 'info-circle', + label: 'nav.about' + } +} + const NavPanel = { created () { if (this.currentUser && this.currentUser.locked) { @@ -43,8 +103,11 @@ const NavPanel = { }, data () { return { + collapsed: false, showTimelines: false, - showLists: false + showLists: false, + timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })), + rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k })) } }, methods: { @@ -53,19 +116,57 @@ const NavPanel = { }, toggleLists () { this.showLists = !this.showLists + }, + toggleCollapse () { + this.collapsed = !this.collapsed + }, + isPinned (item) { + return this.pinnedItems.has(item) + }, + togglePin (item) { + if (this.isPinned(item)) { + this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value: item }) + } else { + this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value: item }) + } } }, computed: { - listsNavigation () { - return this.$store.getters.mergedConfig.listsNavigation - }, ...mapState({ currentUser: state => state.users.currentUser, followRequestCount: state => state.api.followRequests.length, privateMode: state => state.instance.private, federating: state => state.instance.federating, - pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, + pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) }), + rootItems () { + return Object + .entries({ ...ROOT_ITEMS }) + .map(([k, v]) => ({ ...v, name: k })) + .filter(({ criteria, anon, anonRoute }) => { + const set = new Set(criteria || []) + if (!this.federating && set.has('federating')) return false + if (this.private && set.has('!private')) return false + if (!this.currentUser && !(anon || anonRoute)) return false + if ((!this.currentUser || !this.currentUser.locked) && set.has('lockedUser')) return false + return true + }) + }, + pinnedList () { + return Object + .entries({ ...TIMELINES, ...ROOT_ITEMS }) + .filter(([k]) => this.pinnedItems.has(k)) + .map(([k, v]) => ({ ...v, name: k })) + .filter(({ criteria, anon, anonRoute }) => { + const set = new Set(criteria || []) + if (!this.federating && set.has('federating')) return false + if (this.private && set.has('!private')) return false + if (!this.currentUser && !(anon || anonRoute)) return false + if (this.currentUser && !this.currentUser.locked && set.has('locked')) return false + return true + }) + }, ...mapGetters(['unreadChatCount']) } } diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 9322e233..99a4571e 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -1,7 +1,33 @@