diff options
| author | Henry Jameson <me@hjkos.com> | 2022-08-30 23:54:16 +0300 |
|---|---|---|
| committer | Henry Jameson <me@hjkos.com> | 2022-08-30 23:54:16 +0300 |
| commit | 887fac5addc2416504593cb62a52a3b08f3638e2 (patch) | |
| tree | 74711c1bc7ec9fe991ad15d51b3ecfab7e5e0749 /src/modules | |
| parent | af734afe3694ff3c77d4457b93b02309c4f55d6d (diff) | |
| parent | 8b25febe36a97d113c846928dab22ab36158ee07 (diff) | |
Merge remote-tracking branch 'origin/develop' into scrolltotop
* origin/develop: (59 commits)
a11y
Use dedicated indicator for non-ascii domain names
add a favorites "timeline" shortcut
refactor navigation-entry and use them in other nav items
Update dependency sinon-chai to v3
Update dependency semver to v7
Update dependency vue-router to v4.1.5
Update dependency eslint to v8.23.0
Update dependency vue-template-compiler to v2.7.10
Update dependency @vue/babel-helper-vue-jsx-merge-props to v1.4.0
Update dependency eslint-plugin-promise to v6.0.1
fix lists edit page
change ugly checkbox to a list element that doesn't look too much out of place
a11y
squeeze/stretch pinned items as long as there's enough space for it, hide items that won't fitc
Remove isparta
lint
fix being unable to edit timeline pins on mobile
aria
fix mobile side drawer causing issues
...
Diffstat (limited to 'src/modules')
| -rw-r--r-- | src/modules/api.js | 3 | ||||
| -rw-r--r-- | src/modules/config.js | 1 | ||||
| -rw-r--r-- | src/modules/lists.js | 100 | ||||
| -rw-r--r-- | src/modules/serverSideStorage.js | 223 | ||||
| -rw-r--r-- | src/modules/users.js | 17 |
5 files changed, 298 insertions, 46 deletions
diff --git a/src/modules/api.js b/src/modules/api.js index 80a978f9..f783fa4f 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -15,6 +15,9 @@ const api = { mastoUserSocketStatus: null, followRequests: [] }, + getters: { + followRequestCount: state => state.api.followRequests.length + }, mutations: { setBackendInteractor (state, backendInteractor) { state.backendInteractor = backendInteractor diff --git a/src/modules/config.js b/src/modules/config.js index be30dee3..eeaac917 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -89,7 +89,6 @@ export const defaultState = { contentColumnWidth: '45rem', notifsColumnWidth: '25rem', navbarColumnStretch: false, - listsNavigation: false, greentext: undefined, // instance default useAtIcon: undefined, // instance default mentionLinkDisplay: undefined, // instance default diff --git a/src/modules/lists.js b/src/modules/lists.js index 84c15759..22fed800 100644 --- a/src/modules/lists.js +++ b/src/modules/lists.js @@ -9,27 +9,43 @@ export const mutations = { setLists (state, value) { state.allLists = value }, - setList (state, { id, title }) { - if (!state.allListsObject[id]) { - state.allListsObject[id] = {} + setList (state, { listId, title }) { + if (!state.allListsObject[listId]) { + state.allListsObject[listId] = { accountIds: [] } } - state.allListsObject[id].title = title + state.allListsObject[listId].title = title - if (!find(state.allLists, { id })) { - state.allLists.push({ id, title }) + const entry = find(state.allLists, { id: listId }) + if (!entry) { + state.allLists.push({ id: listId, title }) } else { - find(state.allLists, { id }).title = title + entry.title = title } }, - setListAccounts (state, { id, accountIds }) { - if (!state.allListsObject[id]) { - state.allListsObject[id] = {} + setListAccounts (state, { listId, accountIds }) { + if (!state.allListsObject[listId]) { + state.allListsObject[listId] = { accountIds: [] } } - state.allListsObject[id].accountIds = accountIds + state.allListsObject[listId].accountIds = accountIds }, - deleteList (state, { id }) { - delete state.allListsObject[id] - remove(state.allLists, list => list.id === id) + addListAccount (state, { listId, accountId }) { + if (!state.allListsObject[listId]) { + state.allListsObject[listId] = { accountIds: [] } + } + state.allListsObject[listId].accountIds.push(accountId) + }, + removeListAccount (state, { listId, accountId }) { + if (!state.allListsObject[listId]) { + state.allListsObject[listId] = { accountIds: [] } + } + const { accountIds } = state.allListsObject[listId] + const set = new Set(accountIds) + set.delete(accountId) + state.allListsObject[listId].accountIds = [...set] + }, + deleteList (state, { listId }) { + delete state.allListsObject[listId] + remove(state.allLists, list => list.id === listId) } } @@ -40,37 +56,57 @@ const actions = { createList ({ rootState, commit }, { title }) { return rootState.api.backendInteractor.createList({ title }) .then((list) => { - commit('setList', { id: list.id, title }) + commit('setList', { listId: list.id, title }) return list }) }, - fetchList ({ rootState, commit }, { id }) { - return rootState.api.backendInteractor.getList({ id }) - .then((list) => commit('setList', { id: list.id, title: list.title })) + fetchList ({ rootState, commit }, { listId }) { + return rootState.api.backendInteractor.getList({ listId }) + .then((list) => commit('setList', { listId: list.id, title: list.title })) }, - fetchListAccounts ({ rootState, commit }, { id }) { - return rootState.api.backendInteractor.getListAccounts({ id }) - .then((accountIds) => commit('setListAccounts', { id, accountIds })) + fetchListAccounts ({ rootState, commit }, { listId }) { + return rootState.api.backendInteractor.getListAccounts({ listId }) + .then((accountIds) => commit('setListAccounts', { listId, accountIds })) }, - setList ({ rootState, commit }, { id, title }) { - rootState.api.backendInteractor.updateList({ id, title }) - commit('setList', { id, title }) + setList ({ rootState, commit }, { listId, title }) { + rootState.api.backendInteractor.updateList({ listId, title }) + commit('setList', { listId, title }) }, - setListAccounts ({ rootState, commit }, { id, accountIds }) { - const saved = rootState.lists.allListsObject[id].accountIds || [] + setListAccounts ({ rootState, commit }, { listId, accountIds }) { + const saved = rootState.lists.allListsObject[listId].accountIds || [] const added = accountIds.filter(id => !saved.includes(id)) const removed = saved.filter(id => !accountIds.includes(id)) - commit('setListAccounts', { id, accountIds }) + commit('setListAccounts', { listId, accountIds }) if (added.length > 0) { - rootState.api.backendInteractor.addAccountsToList({ id, accountIds: added }) + rootState.api.backendInteractor.addAccountsToList({ listId, accountIds: added }) } if (removed.length > 0) { - rootState.api.backendInteractor.removeAccountsFromList({ id, accountIds: removed }) + rootState.api.backendInteractor.removeAccountsFromList({ listId, accountIds: removed }) } }, - deleteList ({ rootState, commit }, { id }) { - rootState.api.backendInteractor.deleteList({ id }) - commit('deleteList', { id }) + addListAccount ({ rootState, commit }, { listId, accountId }) { + return rootState + .api + .backendInteractor + .addAccountsToList({ listId, accountIds: [accountId] }) + .then((result) => { + commit('addListAccount', { listId, accountId }) + return result + }) + }, + removeListAccount ({ rootState, commit }, { listId, accountId }) { + return rootState + .api + .backendInteractor + .removeAccountsFromList({ listId, accountIds: [accountId] }) + .then((result) => { + commit('removeListAccount', { listId, accountId }) + return result + }) + }, + deleteList ({ rootState, commit }, { listId }) { + rootState.api.backendInteractor.deleteList({ listId }) + commit('deleteList', { listId }) } } diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index e516a6e6..56164be7 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, cloneDeep, set, get, clamp, flatten, groupBy, findLastIndex, takeRight } from 'lodash' import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' export const VERSION = 1 @@ -14,14 +14,21 @@ 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 // 1001: same as above + reset everything to 0 }, + prefsStorage: { + _journal: [], + simple: { + dontShowUpdateNotifs: false, + collapseNav: false + }, + collections: { + pinnedNavItems: ['home', 'dms', 'chats'] + } + }, // raw data raw: null, // local cache @@ -33,14 +40,43 @@ export const newUserFlags = { updateCounter: CURRENT_UPDATE_COUNTER // new users don't need to see update notification } -const _wrapData = (data) => ({ +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, userName) => ({ ...data, + _user: userName, _timestamp: Date.now(), _version: VERSION }) const _checkValidity = (data) => data._timestamp > 0 && data._version > 0 +const _verifyPrefs = (state) => { + state.prefsStorage = state.prefsStorage || { + simple: {}, + collections: {} + } + Object.entries(defaultState.prefsStorage.simple).forEach(([k, v]) => { + if (typeof v === 'number' || typeof v === 'boolean') return + console.warn(`Preference simple.${k} as invalid type, reinitializing`) + set(state.prefsStorage.simple, k, defaultState.prefsStorage.simple[k]) + }) + Object.entries(defaultState.prefsStorage.collections).forEach(([k, v]) => { + if (Array.isArray(v)) return + console.warn(`Preference collections.${k} as invalid type, reinitializing`) + set(state.prefsStorage.collections, k, defaultState.prefsStorage.collections[k]) + }) +} + export const _getRecentData = (cache, live) => { const result = { recent: null, stale: null, needUpload: false } const cacheValid = _checkValidity(cache || {}) @@ -85,6 +121,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] @@ -93,6 +131,88 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => { })) } +const _mergeJournal = (...journals) => { + // Ignore invalid journal entries + const allJournals = flatten( + journals.map(j => Array.isArray(j) ? j : []) + ).filter(entry => + Object.prototype.hasOwnProperty.call(entry, 'path') && + Object.prototype.hasOwnProperty.call(entry, 'operation') && + Object.prototype.hasOwnProperty.call(entry, 'args') && + Object.prototype.hasOwnProperty.call(entry, 'timestamp') + ) + const grouped = groupBy(allJournals, 'path') + const trimmedGrouped = Object.entries(grouped).map(([path, journal]) => { + // side effect + journal.sort((a, b) => a.timestamp > b.timestamp ? 1 : -1) + + if (path.startsWith('collections')) { + const lastRemoveIndex = findLastIndex(journal, ({ operation }) => operation === 'removeFromCollection') + // everything before last remove is unimportant + if (lastRemoveIndex > 0) { + return journal.slice(lastRemoveIndex) + } else { + // everything else doesn't need trimming + return journal + } + } else if (path.startsWith('simple')) { + // Only the last record is important + return takeRight(journal) + } else { + return journal + } + }) + return flatten(trimmedGrouped) + .sort((a, b) => a.timestamp > b.timestamp ? 1 : -1) +} + +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 = _mergeJournal(staleJournal, recentJournal) + totalJournal.forEach(({ path, timestamp, operation, command, 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 + case 'addToCollection': + set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).add(args[0]))) + break + case 'removeFromCollection': { + const newSet = new Set(get(resultOutput, path)) + newSet.delete(args[0]) + set(resultOutput, path, Array.from(newSet)) + 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?`) + } + }) + return { ...resultOutput, _journal: totalJournal } +} + export const _resetFlags = (totalFlags, knownKeys = defaultState.flagStorage) => { let result = { ...totalFlags } const allFlagKeys = Object.keys(totalFlags) @@ -149,10 +269,17 @@ export const _doMigrations = (cache) => { } export const mutations = { + clearServerSideStorage (state, userData) { + state = { ...cloneDeep(defaultState) } + }, setServerSideStorage (state, userData) { const live = userData.storage state.raw = live let cache = state.cache + if (cache && cache._user !== userData.fqn) { + console.warn('cache belongs to another user! reinitializing local cache!') + cache = null + } cache = _doMigrations(cache) @@ -165,7 +292,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 +308,23 @@ 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) + _verifyPrefs(recent) + _verifyPrefs(stale) + totalPrefs = _mergePrefs(recent.prefsStorage, stale.prefsStorage) } else { totalFlags = recent.flagStorage + totalPrefs = recent.prefsStorage } totalFlags = _resetFlags(totalFlags) - recent.flagStorage = totalFlags + recent.flagStorage = { ...flagsTemplate, ...totalFlags } + recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs } state.dirty = dirty || needsUpload state.cache = recent @@ -199,10 +333,72 @@ 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 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 = [ + ...state.prefsStorage._journal, + { operation: 'set', path, args: [value], timestamp: Date.now() } + ] + state.dirty = true + }, + 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, + { operation: 'addToCollection', path, args: [value], timestamp: Date.now() } + ] + state.dirty = true + }, + 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.delete(value) + set(state.prefsStorage, path, [...collection]) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { operation: 'removeFromCollection', path, args: [value], timestamp: Date.now() } + ] + state.dirty = true + }, + 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, + { operation: 'arrangeCollection', path, args: [value], timestamp: Date.now() } + ] + state.dirty = true + }, + updateCache (state, { username }) { + state.prefsStorage._journal = _mergeJournal(state.prefsStorage._journal) + state.cache = _wrapData({ + flagStorage: toRaw(state.flagStorage), + prefsStorage: toRaw(state.prefsStorage) + }, username) } } @@ -214,15 +410,16 @@ const serverSideStorage = { actions: { pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) { const needPush = state.dirty || force + console.log(needPush) if (!needPush) return - state.cache = _wrapData({ - flagStorage: toRaw(state.flagStorage) - }) + commit('updateCache', { username: rootState.users.currentUser.fqn }) const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } rootState.api.backendInteractor .updateProfile({ params }) - .then((user) => commit('setServerSideStorage', user)) - state.dirty = false + .then((user) => { + commit('setServerSideStorage', user) + state.dirty = false + }) } } } diff --git a/src/modules/users.js b/src/modules/users.js index be0bc997..de28766a 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -171,6 +171,9 @@ export const mutations = { state.relationships[relationship.id] = relationship }) }, + updateUserInLists (state, { id, inLists }) { + state.usersObject[id].inLists = inLists + }, saveBlockIds (state, blockIds) { state.currentUser.blockIds = blockIds }, @@ -298,6 +301,12 @@ const users = { .then((relationships) => store.commit('updateUserRelationship', relationships)) } }, + fetchUserInLists (store, id) { + if (store.state.currentUser) { + store.rootState.api.backendInteractor.fetchUserInLists({ id }) + .then((inLists) => store.commit('updateUserInLists', { id, inLists })) + } + }, fetchBlocks (store) { return store.rootState.api.backendInteractor.fetchBlocks() .then((blocks) => { @@ -509,6 +518,7 @@ const users = { store.dispatch('stopFetchingTimeline', 'friends') store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) store.dispatch('stopFetchingNotifications') + store.dispatch('stopFetchingLists') store.dispatch('stopFetchingFollowRequests') store.commit('clearNotifications') store.commit('resetStatuses') @@ -516,6 +526,7 @@ const users = { store.dispatch('setLastTimeline', 'public-timeline') store.dispatch('setLayoutWidth', windowWidth()) store.dispatch('setLayoutHeight', windowHeight()) + store.commit('clearServerSideStorage') }) }, loginUser (store, accessToken) { @@ -562,6 +573,12 @@ const users = { store.dispatch('startFetchingChats') } + store.dispatch('startFetchingLists') + + if (user.locked) { + store.dispatch('startFetchingFollowRequests') + } + if (store.getters.mergedConfig.useStreamingApi) { store.dispatch('fetchTimeline', 'friends', { since: null }) store.dispatch('fetchNotifications', { since: null }) |
