From a9591043335883fe94cd378d104013225cc040c9 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 21 Feb 2022 11:14:20 -0500 Subject: [PATCH 001/388] Create service worker in app --- src/App.js | 2 ++ src/services/sw/sw.js | 2 ++ src/sw.js | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 1c1b3a8c2..deb413540 100644 --- a/src/App.js +++ b/src/App.js @@ -14,6 +14,7 @@ import EditStatusModal from './components/edit_status_modal/edit_status_modal.vu import PostStatusModal from './components/post_status_modal/post_status_modal.vue' import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue' import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' +import { getOrCreateServiceWorker } from './services/sw/sw' import { windowWidth, windowHeight } from './services/window_utils/window_utils' import { mapGetters } from 'vuex' import { defineAsyncComponent } from 'vue' @@ -65,6 +66,7 @@ export default { if (useInterfaceStore().themeApplied) { this.removeSplash() } + getOrCreateServiceWorker() }, unmounted () { window.removeEventListener('resize', this.updateMobileState) diff --git a/src/services/sw/sw.js b/src/services/sw/sw.js index 2133db222..0eea849b1 100644 --- a/src/services/sw/sw.js +++ b/src/services/sw/sw.js @@ -146,3 +146,5 @@ export function unregisterPushNotifications (token) { ]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`)) } } + +export { getOrCreateServiceWorker } diff --git a/src/sw.js b/src/sw.js index 3c53d5339..af21a8746 100644 --- a/src/sw.js +++ b/src/sw.js @@ -143,4 +143,4 @@ self.addEventListener('notificationclick', (event) => { })) }) -console.log('sw here') +self.addEventListener('fetch', _ => _) From 2b16dd55c408e8b057a08924cb8bbf8dafba1b81 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Thu, 12 Aug 2021 22:42:08 -0400 Subject: [PATCH 002/388] Cache compiled assets in service worker --- src/sw.js | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/sw.js b/src/sw.js index af21a8746..2b2d2f3e9 100644 --- a/src/sw.js +++ b/src/sw.js @@ -85,6 +85,35 @@ const showPushNotification = async (event) => { return Promise.resolve() } +const shouldCache = process.env.NODE_ENV === 'production' +const cacheKey = 'pleroma-fe' +const cacheFiles = self.serviceWorkerOption.assets + +self.addEventListener('install', async (event) => { + if (shouldCache) { + event.waitUntil((async () => { + const cache = await caches.open(cacheKey) + await cache.addAll(cacheFiles) + })()) + } +}) + +self.addEventListener('activate', async (event) => { + if (shouldCache) { + event.waitUntil((async () => { + const cache = await caches.open(cacheKey) + const keys = await cache.keys() + await Promise.all( + keys.filter(request => { + const url = new URL(request.url) + const shouldKeep = cacheFiles.includes(url.pathname) + return !shouldKeep + }).map(k => cache.delete(k)) + ) + })()) + } +}) + self.addEventListener('push', async (event) => { if (event.data) { // Supposedly, we HAVE to return a promise inside waitUntil otherwise it will @@ -143,4 +172,16 @@ self.addEventListener('notificationclick', (event) => { })) }) -self.addEventListener('fetch', _ => _) +self.addEventListener('fetch', async (event) => { + if (shouldCache) { + event.respondWith((async () => { + const r = await caches.match(event.request) + console.log(`[Service Worker] Fetching resource: ${event.request.url}`) + if (r) { + return r + } + const response = await fetch(event.request) + return response + })()) + } +}) From 63490ad2daf6a49ecfa374e38f678d6606ac95bc Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sun, 15 Aug 2021 10:51:49 -0400 Subject: [PATCH 003/388] Cache emojis in service worker --- src/sw.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/sw.js b/src/sw.js index 2b2d2f3e9..94c0a93e8 100644 --- a/src/sw.js +++ b/src/sw.js @@ -88,6 +88,16 @@ const showPushNotification = async (event) => { const shouldCache = process.env.NODE_ENV === 'production' const cacheKey = 'pleroma-fe' const cacheFiles = self.serviceWorkerOption.assets +const emojiCacheKey = 'pleroma-fe-emoji' +const isEmoji = req => { + console.log('req.method=', req.method) + if (req.method !== 'GET') { + return false + } + const url = new URL(req.url) + console.log('pathname=', url.pathname) + return url.pathname.startsWith('/emoji/') +} self.addEventListener('install', async (event) => { if (shouldCache) { @@ -180,7 +190,13 @@ self.addEventListener('fetch', async (event) => { if (r) { return r } + const response = await fetch(event.request) + if (response.ok && isEmoji(event.request)) { + console.log(`[Service Worker] Caching emoji ${event.request.url}`) + const cache = await caches.open(emojiCacheKey) + await cache.put(event.request.clone(), response.clone()) + } return response })()) } From f1f7f5de5126d3aad3ca58d416677225ce080950 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Tue, 11 Jan 2022 23:03:46 -0500 Subject: [PATCH 004/388] Fix the bug where remote media fail to load without media proxy --- src/sw.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/sw.js b/src/sw.js index 94c0a93e8..65ab49c3c 100644 --- a/src/sw.js +++ b/src/sw.js @@ -182,8 +182,12 @@ self.addEventListener('notificationclick', (event) => { })) }) -self.addEventListener('fetch', async (event) => { - if (shouldCache) { +self.addEventListener('fetch', (event) => { + console.log(`[Service Worker] Got: ${event.request.url}`) + console.debug(event.request) + // Do not mess up with remote things + const isSameOrigin = (new URL(event.request.url)).origin === self.location.origin + if (shouldCache && event.request.method === 'GET' && isSameOrigin) { event.respondWith((async () => { const r = await caches.match(event.request) console.log(`[Service Worker] Fetching resource: ${event.request.url}`) @@ -191,13 +195,18 @@ self.addEventListener('fetch', async (event) => { return r } - const response = await fetch(event.request) - if (response.ok && isEmoji(event.request)) { - console.log(`[Service Worker] Caching emoji ${event.request.url}`) - const cache = await caches.open(emojiCacheKey) - await cache.put(event.request.clone(), response.clone()) + try { + const response = await fetch(event.request) + if (response.ok && isEmoji(event.request)) { + console.log(`[Service Worker] Caching emoji ${event.request.url}`) + const cache = await caches.open(emojiCacheKey) + await cache.put(event.request.clone(), response.clone()) + } + return response + } catch (e) { + console.log('error:', e) + throw e } - return response })()) } }) From 6ab98db7ad7a611c35fe5c7077079f6741cfe3f3 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 21 Feb 2022 11:19:10 -0500 Subject: [PATCH 005/388] Clean up debug code --- src/sw.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/sw.js b/src/sw.js index 65ab49c3c..87eb484a5 100644 --- a/src/sw.js +++ b/src/sw.js @@ -90,12 +90,11 @@ const cacheKey = 'pleroma-fe' const cacheFiles = self.serviceWorkerOption.assets const emojiCacheKey = 'pleroma-fe-emoji' const isEmoji = req => { - console.log('req.method=', req.method) if (req.method !== 'GET') { return false } const url = new URL(req.url) - console.log('pathname=', url.pathname) + return url.pathname.startsWith('/emoji/') } @@ -183,14 +182,12 @@ self.addEventListener('notificationclick', (event) => { }) self.addEventListener('fetch', (event) => { - console.log(`[Service Worker] Got: ${event.request.url}`) - console.debug(event.request) // Do not mess up with remote things const isSameOrigin = (new URL(event.request.url)).origin === self.location.origin if (shouldCache && event.request.method === 'GET' && isSameOrigin) { event.respondWith((async () => { const r = await caches.match(event.request) - console.log(`[Service Worker] Fetching resource: ${event.request.url}`) + if (r) { return r } @@ -198,13 +195,11 @@ self.addEventListener('fetch', (event) => { try { const response = await fetch(event.request) if (response.ok && isEmoji(event.request)) { - console.log(`[Service Worker] Caching emoji ${event.request.url}`) const cache = await caches.open(emojiCacheKey) await cache.put(event.request.clone(), response.clone()) } return response } catch (e) { - console.log('error:', e) throw e } })()) From ed04c2ac710cb51f01d34efcfdf8d007fa764c16 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 26 May 2023 16:37:06 -0400 Subject: [PATCH 006/388] Exclude local media requests --- src/sw.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/sw.js b/src/sw.js index 87eb484a5..c53927a32 100644 --- a/src/sw.js +++ b/src/sw.js @@ -97,6 +97,15 @@ const isEmoji = req => { return url.pathname.startsWith('/emoji/') } +const isNotMedia = req => { + console.log('req.method=', req.method) + if (req.method !== 'GET') { + return false + } + const url = new URL(req.url) + console.log('pathname=', url.pathname) + return !url.pathname.startsWith('/media/') +} self.addEventListener('install', async (event) => { if (shouldCache) { @@ -184,7 +193,7 @@ self.addEventListener('notificationclick', (event) => { self.addEventListener('fetch', (event) => { // Do not mess up with remote things const isSameOrigin = (new URL(event.request.url)).origin === self.location.origin - if (shouldCache && event.request.method === 'GET' && isSameOrigin) { + if (shouldCache && event.request.method === 'GET' && isSameOrigin && isNotMedia(event.request)) { event.respondWith((async () => { const r = await caches.match(event.request) From cc4d4ccbeb933738e52c9cfd85549215447ea5d4 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 16 Jun 2023 23:12:20 -0400 Subject: [PATCH 007/388] Treat html file as failure in emoji cache --- src/sw.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/sw.js b/src/sw.js index c53927a32..3e7af8fcb 100644 --- a/src/sw.js +++ b/src/sw.js @@ -193,17 +193,22 @@ self.addEventListener('notificationclick', (event) => { self.addEventListener('fetch', (event) => { // Do not mess up with remote things const isSameOrigin = (new URL(event.request.url)).origin === self.location.origin + const isEmojiSuccessful = (resp) => { + const type = resp.headers.get('Content-Type') + // Backend will revert to index.html if the file does not exist, so text/html for emojis is a failure + return type && !type.includes('text/html') + } if (shouldCache && event.request.method === 'GET' && isSameOrigin && isNotMedia(event.request)) { event.respondWith((async () => { const r = await caches.match(event.request) - if (r) { + if (r && (isEmojiSuccessful(r) || !isEmoji(event.request))) { return r } try { const response = await fetch(event.request) - if (response.ok && isEmoji(event.request)) { + if (response.ok && isEmojiSuccessful(response) && isEmoji(event.request)) { const cache = await caches.open(emojiCacheKey) await cache.put(event.request.clone(), response.clone()) } From 3315901b077512d3dfaa74e20ad64858a569d6b5 Mon Sep 17 00:00:00 2001 From: tusooa Date: Thu, 22 Aug 2024 21:41:14 -0400 Subject: [PATCH 008/388] Add changelog for caching assets --- changelog.d/sw-cache-assets.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/sw-cache-assets.add diff --git a/changelog.d/sw-cache-assets.add b/changelog.d/sw-cache-assets.add new file mode 100644 index 000000000..5f7414eee --- /dev/null +++ b/changelog.d/sw-cache-assets.add @@ -0,0 +1 @@ +Cache assets and emojis with service worker From 67724ad2a437ac380146ea33550e036f1b20cb14 Mon Sep 17 00:00:00 2001 From: tusooa Date: Thu, 22 Aug 2024 21:44:55 -0400 Subject: [PATCH 009/388] Fix useless try-catch --- src/sw.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sw.js b/src/sw.js index 3e7af8fcb..daf4cdd19 100644 --- a/src/sw.js +++ b/src/sw.js @@ -214,6 +214,7 @@ self.addEventListener('fetch', (event) => { } return response } catch (e) { + console.error('[Service worker] error when caching emoji:', e) throw e } })()) From e21bac3d704d42b650b56976b46c42ad9216feaf Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 23 Aug 2024 00:31:03 -0400 Subject: [PATCH 010/388] Implement clearing caches in settings --- .../settings_modal/tabs/general_tab.js | 16 +++++++++++++ .../settings_modal/tabs/general_tab.vue | 23 +++++++++++++++++++ src/i18n/en.json | 5 +++- src/services/sw/sw.js | 6 +++++ src/sw.js | 4 +--- 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index 517f54eb1..9275d9e6f 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -8,6 +8,7 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche import SharedComputedObject from '../helpers/shared_computed_object.js' import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue' +import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faGlobe @@ -98,6 +99,21 @@ const GeneralTab = { methods: { changeDefaultScope (value) { this.$store.dispatch('setProfileOption', { name: 'defaultScope', value }) + }, + clearCache (key) { + clearCache(key) + .then((v) => { + this.$store.dispatch('settingsSaved', { success: true }) + }) + .catch(error => { + this.$store.dispatch('settingsSaved', { error }) + }) + }, + clearAssetCache () { + this.clearCache(cacheKey) + }, + clearEmojiCache () { + this.clearCache(emojiCacheKey) } } } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 02396114f..b8da782fb 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -509,6 +509,29 @@ +
+

{{ $t('settings.cache') }}

+
    +
  • + +
  • +
  • + +
  • +
+
diff --git a/src/i18n/en.json b/src/i18n/en.json index 9d3400d10..304a64ea3 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1049,7 +1049,10 @@ "reset_value": "Reset", "reset_value_tooltip": "Reset draft", "hard_reset_value": "Hard reset", - "hard_reset_value_tooltip": "Remove setting from storage, forcing use of default value" + "hard_reset_value_tooltip": "Remove setting from storage, forcing use of default value", + "cache": "Cache", + "clear_asset_cache": "Clear asset cache", + "clear_emoji_cache": "Clear emoji cache" }, "admin_dash": { "window_title": "Administration", diff --git a/src/services/sw/sw.js b/src/services/sw/sw.js index 0eea849b1..a986e98dd 100644 --- a/src/services/sw/sw.js +++ b/src/services/sw/sw.js @@ -147,4 +147,10 @@ export function unregisterPushNotifications (token) { } } +export const shouldCache = process.env.NODE_ENV === 'production' +export const cacheKey = 'pleroma-fe' +export const emojiCacheKey = 'pleroma-fe-emoji' + +export const clearCache = (key) => caches.delete(key) + export { getOrCreateServiceWorker } diff --git a/src/sw.js b/src/sw.js index daf4cdd19..2ce1a0648 100644 --- a/src/sw.js +++ b/src/sw.js @@ -3,6 +3,7 @@ import { storage } from 'src/lib/storage.js' import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js' import { prepareNotificationObject } from './services/notification_utils/notification_utils.js' +import { shouldCache, cacheKey, emojiCacheKey } from './services/sw/sw.js' import { createI18n } from 'vue-i18n' // Collects all messages for service workers // Needed because service workers cannot use dynamic imports @@ -85,10 +86,7 @@ const showPushNotification = async (event) => { return Promise.resolve() } -const shouldCache = process.env.NODE_ENV === 'production' -const cacheKey = 'pleroma-fe' const cacheFiles = self.serviceWorkerOption.assets -const emojiCacheKey = 'pleroma-fe-emoji' const isEmoji = req => { if (req.method !== 'GET') { return false From bbeafab1efca446c984eae037d78732c8e6fcc69 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 23 Aug 2024 01:45:36 -0400 Subject: [PATCH 011/388] Verify response is not html when pre-caching in install event --- src/sw.js | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/sw.js b/src/sw.js index 2ce1a0648..f9ae8dc17 100644 --- a/src/sw.js +++ b/src/sw.js @@ -105,11 +105,35 @@ const isNotMedia = req => { return !url.pathname.startsWith('/media/') } +const isSuccessful = (resp) => { + if (!resp.ok) { + return false + } + if ((new URL(resp.url)).pathname === '/index.html') { + // For index.html itself, there is no fallback possible. + return true + } + const type = resp.headers.get('Content-Type') + // Backend will revert to index.html if the file does not exist, so text/html for emojis and assets is a failure + return type && !type.includes('text/html') +} + self.addEventListener('install', async (event) => { if (shouldCache) { event.waitUntil((async () => { const cache = await caches.open(cacheKey) - await cache.addAll(cacheFiles) + await Promise.allSettled(cacheFiles.map(async (route) => { + // https://developer.mozilla.org/en-US/docs/Web/API/Cache/add + // originally we used addAll() but it will raise a problem in one edge case: + // when the file for the route is not found, backend will return index.html with code 200 + // but it's wrong, and it's cached, so we end up with a bad cache. + // this can happen when you refresh when you are in the process of upgrading + // the frontend. + const resp = await fetch(route) + if (isSuccessful(resp)) { + await cache.put(route, resp) + } + })) })()) } }) @@ -191,22 +215,17 @@ self.addEventListener('notificationclick', (event) => { self.addEventListener('fetch', (event) => { // Do not mess up with remote things const isSameOrigin = (new URL(event.request.url)).origin === self.location.origin - const isEmojiSuccessful = (resp) => { - const type = resp.headers.get('Content-Type') - // Backend will revert to index.html if the file does not exist, so text/html for emojis is a failure - return type && !type.includes('text/html') - } if (shouldCache && event.request.method === 'GET' && isSameOrigin && isNotMedia(event.request)) { event.respondWith((async () => { const r = await caches.match(event.request) - if (r && (isEmojiSuccessful(r) || !isEmoji(event.request))) { + if (r && (isSuccessful(r) || !isEmoji(event.request))) { return r } try { const response = await fetch(event.request) - if (response.ok && isEmojiSuccessful(response) && isEmoji(event.request)) { + if (response.ok && isSuccessful(response) && isEmoji(event.request)) { const cache = await caches.open(emojiCacheKey) await cache.put(event.request.clone(), response.clone()) } From 8b8f6cbca243f356b6a16c01d6f6900cfc9eccb9 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 23 Aug 2024 02:49:07 -0400 Subject: [PATCH 012/388] Cache assets when cache is missing --- src/sw.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/sw.js b/src/sw.js index f9ae8dc17..aa061e14f 100644 --- a/src/sw.js +++ b/src/sw.js @@ -96,14 +96,16 @@ const isEmoji = req => { return url.pathname.startsWith('/emoji/') } const isNotMedia = req => { - console.log('req.method=', req.method) if (req.method !== 'GET') { return false } const url = new URL(req.url) - console.log('pathname=', url.pathname) return !url.pathname.startsWith('/media/') } +const isAsset = req => { + const url = new URL(req.url) + return cacheFiles.includes(url.pathname) +} const isSuccessful = (resp) => { if (!resp.ok) { @@ -216,17 +218,23 @@ self.addEventListener('fetch', (event) => { // Do not mess up with remote things const isSameOrigin = (new URL(event.request.url)).origin === self.location.origin if (shouldCache && event.request.method === 'GET' && isSameOrigin && isNotMedia(event.request)) { + console.debug('[Service worker] fetch:', event.request.url) event.respondWith((async () => { const r = await caches.match(event.request) + const isEmojiReq = isEmoji(event.request) - if (r && (isSuccessful(r) || !isEmoji(event.request))) { + if (r && isSuccessful(r)) { + console.debug('[Service worker] already cached:', event.request.url) return r } try { const response = await fetch(event.request) - if (response.ok && isSuccessful(response) && isEmoji(event.request)) { - const cache = await caches.open(emojiCacheKey) + if (response.ok && + isSuccessful(response) && + (isEmojiReq || isAsset(event.request))) { + console.debug(`[Service worker] caching ${isEmojiReq ? 'emoji' : 'asset'}:`, event.request.url) + const cache = await caches.open(isEmojiReq ? emojiCacheKey : cacheKey) await cache.put(event.request.clone(), response.clone()) } return response From 652781fcc8ff1384555383bf1d81bc893cc28fc5 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 23 Aug 2024 02:57:44 -0400 Subject: [PATCH 013/388] Do not preload emoji annotations and i18n files --- src/sw.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sw.js b/src/sw.js index aa061e14f..8c83762ee 100644 --- a/src/sw.js +++ b/src/sw.js @@ -123,8 +123,12 @@ const isSuccessful = (resp) => { self.addEventListener('install', async (event) => { if (shouldCache) { event.waitUntil((async () => { + // Do not preload i18n and emoji annotations to speed up loading + const shouldPreload = (route) => { + return !route.startsWith('/static/js/i18n/') && !route.startsWith('/static/js/emoji-annotations/') + } const cache = await caches.open(cacheKey) - await Promise.allSettled(cacheFiles.map(async (route) => { + await Promise.allSettled(cacheFiles.filter(shouldPreload).map(async (route) => { // https://developer.mozilla.org/en-US/docs/Web/API/Cache/add // originally we used addAll() but it will raise a problem in one edge case: // when the file for the route is not found, backend will return index.html with code 200 From 0cb47652b88a0c2a3a7572f784246b7ed51dc2a1 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 4 Mar 2025 18:39:20 -0500 Subject: [PATCH 014/388] Make asset caching work with vite --- build/sw_plugin.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++ src/sw.js | 1 + vite.config.js | 9 ++++++++- 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/build/sw_plugin.js b/build/sw_plugin.js index 90ab856ad..a2c792b7d 100644 --- a/build/sw_plugin.js +++ b/build/sw_plugin.js @@ -11,6 +11,11 @@ const getSWMessagesAsText = async () => { } const projectRoot = dirname(dirname(fileURLToPath(import.meta.url))) +const swEnvName = 'virtual:pleroma-fe/service_worker_env' +const swEnvNameResolved = '\0' + swEnvName +const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };` +const getProdSwEnv = ({ assets }) => `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };` + export const devSwPlugin = ({ swSrc, swDest, @@ -32,12 +37,16 @@ export const devSwPlugin = ({ const name = id.startsWith('/') ? id.slice(1) : id if (name === swDest) { return swFullSrc + } else if (name === swEnvName) { + return swEnvNameResolved } return null }, async load (id) { if (id === swFullSrc) { return readFile(swFullSrc, 'utf-8') + } else if (id === swEnvNameResolved) { + return getDevSwEnv() } return null }, @@ -79,6 +88,21 @@ export const devSwPlugin = ({ contents: await getSWMessagesAsText() })) } + }, { + name: 'sw-env', + setup (b) { + b.onResolve( + { filter: new RegExp('^' + swEnvName + '$') }, + args => ({ + path: args.path, + namespace: 'sw-env' + })) + b.onLoad( + { filter: /.*/, namespace: 'sw-env' }, + () => ({ + contents: getDevSwEnv() + })) + } }] }) const text = res.outputFiles[0].text @@ -126,6 +150,30 @@ export const buildSwPlugin = ({ configFile: false } }, + generateBundle: { + order: 'post', + sequential: true, + async handler (_, bundle) { + const assets = Object.keys(bundle) + .filter(name => !/\.map$/.test(name)) + .map(name => '/' + name) + config.plugins.push({ + name: 'build-sw-env-plugin', + resolveId (id) { + if (id === swEnvName) { + return swEnvNameResolved + } + return null + }, + load (id) { + if (id === swEnvNameResolved) { + return getProdSwEnv({ assets }) + } + return null + } + }) + } + }, closeBundle: { order: 'post', sequential: true, diff --git a/src/sw.js b/src/sw.js index 8c83762ee..7ba910589 100644 --- a/src/sw.js +++ b/src/sw.js @@ -1,5 +1,6 @@ /* eslint-env serviceworker */ +import 'virtual:pleroma-fe/service_worker_env' import { storage } from 'src/lib/storage.js' import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js' import { prepareNotificationObject } from './services/notification_utils/notification_utils.js' diff --git a/vite.config.js b/vite.config.js index 2abf18e50..4f78fc43e 100644 --- a/vite.config.js +++ b/vite.config.js @@ -165,7 +165,14 @@ export default defineConfig(async ({ mode, command }) => { return 'static/js/[name].[hash].js' } }, - chunkFileNames () { + chunkFileNames (chunkInfo) { + if (chunkInfo.facadeModuleId) { + if (chunkInfo.facadeModuleId.includes('node_modules/@kazvmoe-infra/unicode-emoji-json/annotations/')) { + return 'static/js/emoji-annotations/[name].[hash].js' + } else if (chunkInfo.facadeModuleId.includes('src/i18n/')) { + return 'static/js/i18n/[name].[hash].js' + } + } return 'static/js/[name].[hash].js' }, assetFileNames (assetInfo) { From 7f880dea0e33b6a52ad645b212e79867dc5907b3 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 4 Mar 2025 19:37:24 -0500 Subject: [PATCH 015/388] Fix clear cache lint --- src/components/settings_modal/tabs/general_tab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index 9275d9e6f..df28a23e3 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -102,7 +102,7 @@ const GeneralTab = { }, clearCache (key) { clearCache(key) - .then((v) => { + .then(() => { this.$store.dispatch('settingsSaved', { success: true }) }) .catch(error => { From 5e880ed54ff212e270ec8f3b8ab2502267fac8e1 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 20 Mar 2025 19:32:52 +0200 Subject: [PATCH 016/388] added ability to granularly modify objects in simple storage --- src/modules/serverSideStorage.js | 76 +++++++++++++++++-- .../specs/modules/serverSideStorage.spec.js | 73 +++++++++++++++++- 2 files changed, 143 insertions(+), 6 deletions(-) diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index 9a8ea316a..97bf69fe9 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -3,6 +3,7 @@ import { isEqual, cloneDeep, set, + unset, get, clamp, flatten, @@ -35,7 +36,8 @@ export const defaultState = { _journal: [], simple: { dontShowUpdateNotifs: false, - collapseNav: false + collapseNav: false, + filters: {} }, collections: { pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'], @@ -78,11 +80,16 @@ const _verifyPrefs = (state) => { simple: {}, collections: {} } + + // Simple Object.entries(defaultState.prefsStorage.simple).forEach(([k, v]) => { if (typeof v === 'number' || typeof v === 'boolean') return + if (typeof v === 'object' && v != null) return console.warn(`Preference simple.${k} as invalid type, reinitializing`) set(state.prefsStorage.simple, k, defaultState.prefsStorage.simple[k]) }) + + // Collections Object.entries(defaultState.prefsStorage.collections).forEach(([k, v]) => { if (Array.isArray(v)) return console.warn(`Preference collections.${k} as invalid type, reinitializing`) @@ -224,8 +231,27 @@ export const _mergePrefs = (recent, stale) => { } switch (operation) { case 'set': + if (path.startsWith('collections') || path.startsWith('objectCollections')) { + console.error('Illegal operation "set" on a collection') + return + } + if (path.split(/\./g).length <= 1) { + console.error(`Calling set on depth <= 1 (path: ${path}) is not allowed`) + return + } set(resultOutput, path, args[0]) break + case 'unset': + if (path.startsWith('collections') || path.startsWith('objectCollections')) { + console.error('Illegal operation "unset" on a collection') + return + } + if (path.split(/\./g).length <= 2) { + console.error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`) + return + } + unset(resultOutput, path) + break case 'addToCollection': set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).add(args[0]))) break @@ -380,7 +406,15 @@ export const mutations = { }, setPreference (state, { path, value }) { if (path.startsWith('_')) { - console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + console.error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + if (path.startsWith('collections') || path.startsWith('objectCollections')) { + console.error(`Invalid operation 'set' for collection field '${path}', ignoring.`) + return + } + if (path.split(/\./g).length <= 1) { + console.error(`Calling set on depth <= 1 (path: ${path}) is not allowed`) return } set(state.prefsStorage, path, value) @@ -390,14 +424,46 @@ export const mutations = { ] state.dirty = true }, + unsetPreference (state, { path, value }) { + if (path.startsWith('_')) { + console.error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + if (path.startsWith('collections') || path.startsWith('objectCollections')) { + console.error(`Invalid operation 'unset' for collection field '${path}', ignoring.`) + return + } + if (path.split(/\./g).length <= 2) { + console.error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`) + return + } + unset(state.prefsStorage, path, value) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { operation: 'unset', path, args: [], 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]) + if (path.startsWith('collections')) { + const collection = new Set(get(state.prefsStorage, path)) + collection.add(value) + set(state.prefsStorage, path, [...collection]) + } else if (path.startsWith('objectCollections')) { + const { _key } = value + if (!_key && typeof _key !== 'string') { + console.error('Object for storage is missing _key field! ignoring') + return + } + const collection = new Set(get(state.prefsStorage, path + '.index')) + collection.add(_key) + set(state.prefsStorage, path + '.index', [...collection]) + set(state.prefsStorage, path + '.data.' + _key, value) + } state.prefsStorage._journal = [ ...state.prefsStorage._journal, { operation: 'addToCollection', path, args: [value], timestamp: Date.now() } diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js index 1d4021a7a..986cf61e0 100644 --- a/test/unit/specs/modules/serverSideStorage.spec.js +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -107,7 +107,7 @@ describe('The serverSideStorage module', () => { }) }) describe('setPreference', () => { - const { setPreference, updateCache, addCollectionPreference, removeCollectionPreference } = mutations + const { setPreference, unsetPreference, updateCache, addCollectionPreference, removeCollectionPreference } = mutations it('should set preference and update journal log accordingly', () => { const state = cloneDeep(defaultState) @@ -160,6 +160,25 @@ describe('The serverSideStorage module', () => { expect(state.prefsStorage.collections.testing).to.eql([2]) expect(state.prefsStorage._journal.length).to.eql(2) }) + + it('should remove depth = 3 set/unset entries from journal', () => { + const state = cloneDeep(defaultState) + setPreference(state, { path: 'simple.object.foo', value: 1 }) + unsetPreference(state, { path: 'simple.object.foo' }) + updateCache(state, { username: 'test' }) + expect(state.prefsStorage.simple.object).to.not.have.property('foo') + expect(state.prefsStorage._journal.length).to.eql(1) + }) + + it('should not allow unsetting depth <= 2', () => { + const state = cloneDeep(defaultState) + setPreference(state, { path: 'simple.object.foo', value: 1 }) + unsetPreference(state, { path: 'simple.object' }) + unsetPreference(state, { path: 'simple' }) + updateCache(state, { username: 'test' }) + expect(state.prefsStorage.simple.object).to.have.property('foo') + expect(state.prefsStorage._journal.length).to.eql(1) + }) }) }) @@ -315,6 +334,58 @@ describe('The serverSideStorage module', () => { ] }) }) + + it('should work with objects', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { lv2: { lv3: 'foo' } }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'set', args: ['foo'], timestamp: 2 } + ] + }, + // STALE + { + simple: { lv2: { lv3: 'bar' } }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'set', args: ['bar'], timestamp: 4 } + ] + } + ) + ).to.eql({ + simple: { lv2: { lv3: 'bar' } }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'set', args: ['bar'], timestamp: 4 } + ] + }) + }) + + it('should work with unset', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { lv2: { lv3: 'foo' } }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'set', args: ['foo'], timestamp: 2 } + ] + }, + // STALE + { + simple: { lv2: {} }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'unset', args: [], timestamp: 4 } + ] + } + ) + ).to.eql({ + simple: { lv2: {} }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'unset', args: [], timestamp: 4 } + ] + }) + }) }) describe('_resetFlags', () => { From eaf7efdcffd631975f7c1a8b1a964d8da896575c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 23 Mar 2025 17:23:47 +0200 Subject: [PATCH 017/388] throw errors instead of console logs --- src/modules/serverSideStorage.js | 53 ++++++++----------- .../specs/modules/serverSideStorage.spec.js | 16 ++++-- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index 97bf69fe9..0b46f8560 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -226,29 +226,24 @@ export const _mergePrefs = (recent, stale) => { const totalJournal = _mergeJournal(staleJournal, recentJournal) totalJournal.forEach(({ path, operation, args }) => { if (path.startsWith('_')) { - console.error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`) - return + throw new Error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`) } switch (operation) { case 'set': if (path.startsWith('collections') || path.startsWith('objectCollections')) { - console.error('Illegal operation "set" on a collection') - return + throw new Error('Illegal operation "set" on a collection') } if (path.split(/\./g).length <= 1) { - console.error(`Calling set on depth <= 1 (path: ${path}) is not allowed`) - return + throw new Error(`Calling set on depth <= 1 (path: ${path}) is not allowed`) } set(resultOutput, path, args[0]) break case 'unset': if (path.startsWith('collections') || path.startsWith('objectCollections')) { - console.error('Illegal operation "unset" on a collection') - return + throw new Error('Illegal operation "unset" on a collection') } if (path.split(/\./g).length <= 2) { - console.error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`) - return + throw new Error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`) } unset(resultOutput, path) break @@ -267,7 +262,7 @@ export const _mergePrefs = (recent, stale) => { break } default: - console.error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`) + throw new Error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`) } }) return { ...resultOutput, _journal: totalJournal } @@ -406,16 +401,16 @@ export const mutations = { }, setPreference (state, { path, value }) { if (path.startsWith('_')) { - console.error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) - return + throw new Error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) } if (path.startsWith('collections') || path.startsWith('objectCollections')) { - console.error(`Invalid operation 'set' for collection field '${path}', ignoring.`) - return + throw new Error(`Invalid operation 'set' for collection field '${path}', ignoring.`) } if (path.split(/\./g).length <= 1) { - console.error(`Calling set on depth <= 1 (path: ${path}) is not allowed`) - return + throw new Error(`Calling set on depth <= 1 (path: ${path}) is not allowed`) + } + if (path.split(/\./g).length > 3) { + throw new Error(`Calling set on depth > 3 (path: ${path}) is not allowed`) } set(state.prefsStorage, path, value) state.prefsStorage._journal = [ @@ -426,16 +421,16 @@ export const mutations = { }, unsetPreference (state, { path, value }) { if (path.startsWith('_')) { - console.error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) - return + throw new Error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) } if (path.startsWith('collections') || path.startsWith('objectCollections')) { - console.error(`Invalid operation 'unset' for collection field '${path}', ignoring.`) - return + throw new Error(`Invalid operation 'unset' for collection field '${path}', ignoring.`) } if (path.split(/\./g).length <= 2) { - console.error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`) - return + throw new Error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`) + } + if (path.split(/\./g).length > 3) { + throw new Error(`Calling unset on depth > 3 (path: ${path}) is not allowed`) } unset(state.prefsStorage, path, value) state.prefsStorage._journal = [ @@ -446,8 +441,7 @@ export const mutations = { }, addCollectionPreference (state, { path, value }) { if (path.startsWith('_')) { - console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) - return + throw new Error(`tried to edit internal (starts with _) field '${path}'`) } if (path.startsWith('collections')) { const collection = new Set(get(state.prefsStorage, path)) @@ -456,8 +450,7 @@ export const mutations = { } else if (path.startsWith('objectCollections')) { const { _key } = value if (!_key && typeof _key !== 'string') { - console.error('Object for storage is missing _key field! ignoring') - return + throw new Error('Object for storage is missing _key field!') } const collection = new Set(get(state.prefsStorage, path + '.index')) collection.add(_key) @@ -472,8 +465,7 @@ export const mutations = { }, removeCollectionPreference (state, { path, value }) { if (path.startsWith('_')) { - console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) - return + throw new Error(`tried to edit internal (starts with _) field '${path}', ignoring.`) } const collection = new Set(get(state.prefsStorage, path)) collection.delete(value) @@ -486,8 +478,7 @@ export const mutations = { }, reorderCollectionPreference (state, { path, value, movement }) { if (path.startsWith('_')) { - console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) - return + throw new Error(`tried to edit internal (starts with _) field '${path}', ignoring.`) } const collection = get(state.prefsStorage, path) const newCollection = _moveItemInArray(collection, value, movement) diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js index 986cf61e0..fc0402402 100644 --- a/test/unit/specs/modules/serverSideStorage.spec.js +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -173,11 +173,17 @@ describe('The serverSideStorage module', () => { it('should not allow unsetting depth <= 2', () => { const state = cloneDeep(defaultState) setPreference(state, { path: 'simple.object.foo', value: 1 }) - unsetPreference(state, { path: 'simple.object' }) - unsetPreference(state, { path: 'simple' }) - updateCache(state, { username: 'test' }) - expect(state.prefsStorage.simple.object).to.have.property('foo') - expect(state.prefsStorage._journal.length).to.eql(1) + expect(() => unsetPreference(state, { path: 'simple' })).to.throw() + expect(() => unsetPreference(state, { path: 'simple.object' })).to.throw() + }) + + it('should not allow (un)setting depth > 3', () => { + const state = cloneDeep(defaultState) + setPreference(state, { path: 'simple.object', value: {} }) + expect(() => setPreference(state, { path: 'simple.object.lv3', value: 1 })).to.not.throw() + expect(() => setPreference(state, { path: 'simple.object.lv3.lv4', value: 1})).to.throw() + expect(() => unsetPreference(state, { path: 'simple.object.lv3', value: 1 })).to.not.throw() + expect(() => unsetPreference(state, { path: 'simple.object.lv3.lv4', value: 1})).to.throw() }) }) }) From f347897b2916ebc8eefe911e05018cf45d17bb9e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 23 Mar 2025 20:03:25 +0200 Subject: [PATCH 018/388] migrate to pinia --- src/components/mobile_nav/mobile_nav.js | 17 +- src/components/nav_panel/nav_panel.js | 18 +- src/components/navigation/navigation_entry.js | 18 +- src/components/navigation/navigation_pins.js | 9 +- .../status_action_buttons.js | 16 +- .../update_notification.js | 16 +- src/modules/index.js | 2 - src/modules/users.js | 7 +- src/{modules => stores}/serverSideStorage.js | 373 +++++++++--------- .../specs/modules/serverSideStorage.spec.js | 171 ++++---- 10 files changed, 330 insertions(+), 317 deletions(-) rename src/{modules => stores}/serverSideStorage.js (61%) diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index 10a0892f4..2085d24e3 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -1,14 +1,19 @@ import SideDrawer from '../side_drawer/side_drawer.vue' import Notifications from '../notifications/notifications.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue' +import GestureService from '../../services/gesture_service/gesture_service' +import NavigationPins from 'src/components/navigation/navigation_pins.vue' + import { unseenNotificationsFromStore, countExtraNotifications } from '../../services/notification_utils/notification_utils' -import GestureService from '../../services/gesture_service/gesture_service' -import NavigationPins from 'src/components/navigation/navigation_pins.vue' + import { mapGetters } from 'vuex' import { mapState } from 'pinia' +import { useAnnouncementsStore } from 'src/stores/announcements' +import { useServerSideStorageStore } from 'src/stores/serverSideStorage' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes, @@ -18,7 +23,6 @@ import { faMinus, faCheckDouble } from '@fortawesome/free-solid-svg-icons' -import { useAnnouncementsStore } from 'src/stores/announcements' library.add( faTimes, @@ -71,10 +75,9 @@ const MobileNav = { return this.$route.name === 'chat' }, ...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']), - ...mapGetters(['unreadChatCount']), - chatsPinned () { - return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats') - }, + ...mapState(useServerSideStorageStore, { + pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems).has('chats') + }), shouldConfirmLogout () { return this.$store.getters.mergedConfig.modalOnLogout }, diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index 9d569729f..681aaf05b 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -7,7 +7,9 @@ import { filterNavigation } from 'src/components/navigation/filter.js' import NavigationEntry from 'src/components/navigation/navigation_entry.vue' import NavigationPins from 'src/components/navigation/navigation_pins.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' + import { useAnnouncementsStore } from 'src/stores/announcements' +import { useServerSideStorageStore } from 'src/stores/serverSideStorage' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -76,19 +78,19 @@ const NavPanel = { this.editMode = !this.editMode }, toggleCollapse () { - this.$store.commit('setPreference', { path: 'simple.collapseNav', value: !this.collapsed }) - this.$store.dispatch('pushServerSideStorage') + useServerSideStorageStore().setPreference({ path: 'simple.collapseNav', value: !this.collapsed }) + useServerSideStorageStore().pushServerSideStorage() }, isPinned (item) { return this.pinnedItems.has(item) }, togglePin (item) { if (this.isPinned(item)) { - this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value: item }) + useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedNavItems', value: item }) } else { - this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value: item }) + useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedNavItems', value: item }) } - this.$store.dispatch('pushServerSideStorage') + useServerSideStorageStore().pushServerSideStorage() } }, computed: { @@ -96,14 +98,16 @@ const NavPanel = { unreadAnnouncementCount: 'unreadAnnouncementCount', supportsAnnouncements: store => store.supportsAnnouncements }), + ...mapPiniaState(useServerSideStorageStore, { + collapsed: store => store.prefsStorage.simple.collapseNav, + pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems) + }), ...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, - pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems), - collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav, bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable }), timelinesItems () { diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js index dbef18fcd..11db1c9e3 100644 --- a/src/components/navigation/navigation_entry.js +++ b/src/components/navigation/navigation_entry.js @@ -3,8 +3,10 @@ import { routeTo } from 'src/components/navigation/navigation.js' import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faThumbtack } from '@fortawesome/free-solid-svg-icons' -import { mapStores } from 'pinia' +import { mapStores, mapState as mapPiniaState } from 'pinia' + import { useAnnouncementsStore } from 'src/stores/announcements' +import { useServerSideStorageStore } from 'src/stores/serverSideStorage' library.add(faThumbtack) @@ -19,11 +21,11 @@ const NavigationEntry = { }, togglePin (value) { if (this.isPinned(value)) { - this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value }) + useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedNavItems', value }) } else { - this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value }) + useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedNavItems', value }) } - this.$store.dispatch('pushServerSideStorage') + useServerSideStorageStore().pushServerSideStorage() } }, computed: { @@ -35,9 +37,11 @@ const NavigationEntry = { }, ...mapStores(useAnnouncementsStore), ...mapState({ - currentUser: state => state.users.currentUser, - pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) - }) + currentUser: state => state.users.currentUser + }), + ...mapPiniaState(useServerSideStorageStore, { + pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems) + }), } } diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js index f9cdef71b..50acbbaf1 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -20,6 +20,7 @@ import { import { useListsStore } from 'src/stores/lists' import { useAnnouncementsStore } from 'src/stores/announcements' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' +import { useServerSideStorageStore } from 'src/stores/serverSideStorage' library.add( faUsers, @@ -54,15 +55,17 @@ const NavPanel = { supportsAnnouncements: store => store.supportsAnnouncements }), ...mapPiniaState(useBookmarkFoldersStore, { - bookmarks: getBookmarkFolderEntries + bookmarks: getBookmarkFolderEntries, + }), + ...mapPiniaState(useServerSideStorageStore, { + pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems) }), ...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, - pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable }), pinnedList () { if (!this.currentUser) { diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js index 6d4e4fbc0..6f84ce8b5 100644 --- a/src/components/status_action_buttons/status_action_buttons.js +++ b/src/components/status_action_buttons/status_action_buttons.js @@ -1,10 +1,12 @@ -import { mapState } from 'vuex' +import { mapState } from 'pinia' import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' import ActionButtonContainer from './action_button_container.vue' import Popover from 'src/components/popover/popover.vue' import genRandomSeed from 'src/services/random_seed/random_seed.service.js' +import { useServerSideStorageStore } from 'src/stores/serverSideStorage' + import { BUTTONS } from './buttons_definitions.js' import { library } from '@fortawesome/fontawesome-svg-core' @@ -36,8 +38,8 @@ const StatusActionButtons = { ActionButtonContainer }, computed: { - ...mapState({ - pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedStatusActions) + ...mapState(useServerSideStorageStore, { + pinnedItems: store => new Set(store.prefsStorage.collections.pinnedStatusActions) }), buttons () { return BUTTONS.filter(x => x.if ? x.if(this.funcArg) : true) @@ -101,12 +103,12 @@ const StatusActionButtons = { return this.pinnedItems.has(button.name) }, unpin (button) { - this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedStatusActions', value: button.name }) - this.$store.dispatch('pushServerSideStorage') + useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedStatusActions', value: button.name }) + useServerSideStorageStore().pushServerSideStorage() }, pin (button) { - this.$store.commit('addCollectionPreference', { path: 'collections.pinnedStatusActions', value: button.name }) - this.$store.dispatch('pushServerSideStorage') + useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedStatusActions', value: button.name }) + useServerSideStorageStore().pushServerSideStorage() }, getComponent (button) { if (!this.$store.state.users.currentUser && button.anonLink) { diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js index 308449698..1dbae0bba 100644 --- a/src/components/update_notification/update_notification.js +++ b/src/components/update_notification/update_notification.js @@ -3,6 +3,8 @@ import { library } from '@fortawesome/fontawesome-svg-core' import pleromaTanMask from 'src/assets/pleromatan_apology_mask.png' import pleromaTanFoxMask from 'src/assets/pleromatan_apology_fox_mask.png' +import { useServerSideStorageStore } from 'src/stores/serverSideStorage' + import { faTimes } from '@fortawesome/free-solid-svg-icons' @@ -36,8 +38,8 @@ const UpdateNotification = { shouldShow () { return !this.$store.state.instance.disableUpdateNotification && this.$store.state.users.currentUser && - this.$store.state.serverSideStorage.flagStorage.updateCounter < CURRENT_UPDATE_COUNTER && - !this.$store.state.serverSideStorage.prefsStorage.simple.dontShowUpdateNotifs + useServerSideStorageStore().flagStorage.updateCounter < CURRENT_UPDATE_COUNTER && + !useServerSideStorageStore().prefsStorage.simple.dontShowUpdateNotifs } }, methods: { @@ -46,13 +48,13 @@ const UpdateNotification = { }, neverShowAgain () { this.toggleShow() - this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) - this.$store.commit('setPreference', { path: 'simple.dontShowUpdateNotifs', value: true }) - this.$store.dispatch('pushServerSideStorage') + useServerSideStorageStore().setFlag({ flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) + useServerSideStorageStore().setPreference({ path: 'simple.dontShowUpdateNotifs', value: true }) + useServerSideStorageStore().pushServerSideStorage() }, dismiss () { - this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) - this.$store.dispatch('pushServerSideStorage') + useServerSideStorageStore().setFlag({ flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) + useServerSideStorageStore().pushServerSideStorage() } }, mounted () { diff --git a/src/modules/index.js b/src/modules/index.js index e1c68aa67..5bcc1ca94 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -5,7 +5,6 @@ import users from './users.js' import api from './api.js' import config from './config.js' import profileConfig from './profileConfig.js' -import serverSideStorage from './serverSideStorage.js' import adminSettings from './adminSettings.js' import authFlow from './auth_flow.js' import oauthTokens from './oauth_tokens.js' @@ -20,7 +19,6 @@ export default { api, config, profileConfig, - serverSideStorage, adminSettings, authFlow, oauthTokens, diff --git a/src/modules/users.js b/src/modules/users.js index f52de9597..f6c05ca21 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -4,8 +4,10 @@ import apiService from '../services/api/api.service.js' import oauthApi from '../services/new_api/oauth.js' import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash' import { registerPushNotifications, unregisterPushNotifications } from '../services/sw/sw.js' + import { useInterfaceStore } from 'src/stores/interface.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { useServerSideStorageStore } from 'src/stores/serverSideStorage' // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, obj, item) => { @@ -605,7 +607,8 @@ const users = { user.muteIds = [] user.domainMutes = [] commit('setCurrentUser', user) - commit('setServerSideStorage', user) + + useServerSideStorageStore().setServerSideStorage(user) commit('addNewUsers', [user]) dispatch('fetchEmoji') @@ -615,7 +618,7 @@ const users = { // Set our new backend interactor commit('setBackendInteractor', backendInteractorService(accessToken)) - dispatch('pushServerSideStorage') + useServerSideStorageStore().pushServerSideStorage() if (user.token) { dispatch('setWsToken', user.token) diff --git a/src/modules/serverSideStorage.js b/src/stores/serverSideStorage.js similarity index 61% rename from src/modules/serverSideStorage.js rename to src/stores/serverSideStorage.js index 0b46f8560..2a19b2cf2 100644 --- a/src/modules/serverSideStorage.js +++ b/src/stores/serverSideStorage.js @@ -1,3 +1,4 @@ +import { defineStore } from 'pinia' import { toRaw } from 'vue' import { isEqual, @@ -323,200 +324,194 @@ export const _doMigrations = (cache) => { return cache } -export const mutations = { - clearServerSideStorage (state) { - const blankState = { ...cloneDeep(defaultState) } - Object.keys(state).forEach(k => { - state[k] = blankState[k] - }) +export const useServerSideStorageStore = defineStore('serverSideStorage', { + state() { + return 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) - - let { recent, stale, needUpload } = _getRecentData(cache, live) - - const userNew = userData.created_at > NEW_USER_DATE - const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage - let dirty = false - - if (recent === null) { - console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`) - recent = _wrapData({ - flagStorage: { ...flagsTemplate }, - prefsStorage: { ...defaultState.prefsStorage } - }) - } - - if (!needUpload && recent && stale) { - console.debug('Checking if data needs merging...') - // discarding timestamps and versions - /* eslint-disable no-unused-vars */ - const { _timestamp: _0, _version: _1, ...recentData } = recent - const { _timestamp: _2, _version: _3, ...staleData } = stale - /* eslint-enable no-unused-vars */ - dirty = !isEqual(recentData, staleData) - console.debug(`Data ${dirty ? 'needs' : 'doesn\'t need'} merging`) - } - - const allFlagKeys = _getAllFlags(recent, stale) - let totalFlags - let totalPrefs - if (dirty) { - // Merge 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 = { ...flagsTemplate, ...totalFlags } - recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs } - - state.dirty = dirty || needUpload - state.cache = recent - // set local timestamp to smaller one if we don't have any changes - if (stale && recent && !state.dirty) { - 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('_')) { - throw new Error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) - } - if (path.startsWith('collections') || path.startsWith('objectCollections')) { - throw new Error(`Invalid operation 'set' for collection field '${path}', ignoring.`) - } - if (path.split(/\./g).length <= 1) { - throw new Error(`Calling set on depth <= 1 (path: ${path}) is not allowed`) - } - if (path.split(/\./g).length > 3) { - throw new Error(`Calling set on depth > 3 (path: ${path}) is not allowed`) - } - set(state.prefsStorage, path, value) - state.prefsStorage._journal = [ - ...state.prefsStorage._journal, - { operation: 'set', path, args: [value], timestamp: Date.now() } - ] - state.dirty = true - }, - unsetPreference (state, { path, value }) { - if (path.startsWith('_')) { - throw new Error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) - } - if (path.startsWith('collections') || path.startsWith('objectCollections')) { - throw new Error(`Invalid operation 'unset' for collection field '${path}', ignoring.`) - } - if (path.split(/\./g).length <= 2) { - throw new Error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`) - } - if (path.split(/\./g).length > 3) { - throw new Error(`Calling unset on depth > 3 (path: ${path}) is not allowed`) - } - unset(state.prefsStorage, path, value) - state.prefsStorage._journal = [ - ...state.prefsStorage._journal, - { operation: 'unset', path, args: [], timestamp: Date.now() } - ] - state.dirty = true - }, - addCollectionPreference (state, { path, value }) { - if (path.startsWith('_')) { - throw new Error(`tried to edit internal (starts with _) field '${path}'`) - } - if (path.startsWith('collections')) { - const collection = new Set(get(state.prefsStorage, path)) - collection.add(value) - set(state.prefsStorage, path, [...collection]) - } else if (path.startsWith('objectCollections')) { - const { _key } = value - if (!_key && typeof _key !== 'string') { - throw new Error('Object for storage is missing _key field!') - } - const collection = new Set(get(state.prefsStorage, path + '.index')) - collection.add(_key) - set(state.prefsStorage, path + '.index', [...collection]) - set(state.prefsStorage, path + '.data.' + _key, value) - } - state.prefsStorage._journal = [ - ...state.prefsStorage._journal, - { operation: 'addToCollection', path, args: [value], timestamp: Date.now() } - ] - state.dirty = true - }, - removeCollectionPreference (state, { path, value }) { - if (path.startsWith('_')) { - throw new Error(`tried to edit internal (starts with _) field '${path}', ignoring.`) - } - 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('_')) { - throw new Error(`tried to edit internal (starts with _) field '${path}', ignoring.`) - } - 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) - } -} - -const serverSideStorage = { - state: { - ...cloneDeep(defaultState) - }, - mutations, actions: { - pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) { - const needPush = state.dirty || force + clearServerSideStorage () { + const blankState = { ...cloneDeep(defaultState) } + Object.keys(this).forEach(k => { + this[k] = blankState[k] + }) + }, + setServerSideStorage (userData) { + const live = userData.storage + this.raw = live + let cache = this.cache + if (cache && cache._user !== userData.fqn) { + console.warn('Cache belongs to another user! reinitializing local cache!') + cache = null + } + + cache = _doMigrations(cache) + + let { recent, stale, needUpload } = _getRecentData(cache, live) + + const userNew = userData.created_at > NEW_USER_DATE + const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage + let dirty = false + + if (recent === null) { + console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`) + recent = _wrapData({ + flagStorage: { ...flagsTemplate }, + prefsStorage: { ...defaultState.prefsStorage } + }) + } + + if (!needUpload && recent && stale) { + console.debug('Checking if data needs merging...') + // discarding timestamps and versions + /* eslint-disable no-unused-vars */ + const { _timestamp: _0, _version: _1, ...recentData } = recent + const { _timestamp: _2, _version: _3, ...staleData } = stale + /* eslint-enable no-unused-vars */ + dirty = !isEqual(recentData, staleData) + console.debug(`Data ${dirty ? 'needs' : 'doesn\'t need'} merging`) + } + + const allFlagKeys = _getAllFlags(recent, stale) + let totalFlags + let totalPrefs + if (dirty) { + // Merge 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 = { ...flagsTemplate, ...totalFlags } + recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs } + + this.dirty = dirty || needUpload + this.cache = recent + // 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 + }, + setFlag ({ flag, value }) { + this.flagStorage[flag] = value + this.dirty = true + }, + setPreference ({ path, value }) { + if (path.startsWith('_')) { + throw new Error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) + } + if (path.startsWith('collections') || path.startsWith('objectCollections')) { + throw new Error(`Invalid operation 'set' for collection field '${path}', ignoring.`) + } + if (path.split(/\./g).length <= 1) { + throw new Error(`Calling set on depth <= 1 (path: ${path}) is not allowed`) + } + if (path.split(/\./g).length > 3) { + throw new Error(`Calling set on depth > 3 (path: ${path}) is not allowed`) + } + set(this.prefsStorage, path, value) + this.prefsStorage._journal = [ + ...this.prefsStorage._journal, + { operation: 'set', path, args: [value], timestamp: Date.now() } + ] + this.dirty = true + }, + unsetPreference ({ path, value }) { + if (path.startsWith('_')) { + throw new Error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) + } + if (path.startsWith('collections') || path.startsWith('objectCollections')) { + throw new Error(`Invalid operation 'unset' for collection field '${path}', ignoring.`) + } + if (path.split(/\./g).length <= 2) { + throw new Error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`) + } + if (path.split(/\./g).length > 3) { + throw new Error(`Calling unset on depth > 3 (path: ${path}) is not allowed`) + } + unset(this.prefsStorage, path, value) + this.prefsStorage._journal = [ + ...this.prefsStorage._journal, + { operation: 'unset', path, args: [], timestamp: Date.now() } + ] + this.dirty = true + }, + addCollectionPreference ({ path, value }) { + if (path.startsWith('_')) { + throw new Error(`tried to edit internal (starts with _) field '${path}'`) + } + if (path.startsWith('collections')) { + const collection = new Set(get(this.prefsStorage, path)) + collection.add(value) + set(this.prefsStorage, path, [...collection]) + } else if (path.startsWith('objectCollections')) { + const { _key } = value + if (!_key && typeof _key !== 'string') { + throw new Error('Object for storage is missing _key field!') + } + const collection = new Set(get(this.prefsStorage, path + '.index')) + collection.add(_key) + set(this.prefsStorage, path + '.index', [...collection]) + set(this.prefsStorage, path + '.data.' + _key, value) + } + this.prefsStorage._journal = [ + ...this.prefsStorage._journal, + { operation: 'addToCollection', path, args: [value], timestamp: Date.now() } + ] + this.dirty = true + }, + removeCollectionPreference ({ path, value }) { + if (path.startsWith('_')) { + throw new Error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + } + const collection = new Set(get(this.prefsStorage, path)) + collection.delete(value) + set(this.prefsStorage, path, [...collection]) + this.prefsStorage._journal = [ + ...this.prefsStorage._journal, + { operation: 'removeFromCollection', path, args: [value], timestamp: Date.now() } + ] + this.dirty = true + }, + reorderCollectionPreference ({ path, value, movement }) { + if (path.startsWith('_')) { + throw new Error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + } + const collection = get(this.prefsStorage, path) + const newCollection = _moveItemInArray(collection, value, movement) + set(this.prefsStorage, path, newCollection) + this.prefsStorage._journal = [ + ...this.prefsStorage._journal, + { operation: 'arrangeCollection', path, args: [value], timestamp: Date.now() } + ] + this.dirty = true + }, + updateCache ({ username }) { + this.prefsStorage._journal = _mergeJournal(this.prefsStorage._journal) + this.cache = _wrapData({ + flagStorage: toRaw(this.flagStorage), + prefsStorage: toRaw(this.prefsStorage) + }, username) + }, + pushServerSideStorage ({ force = false } = {}) { + const needPush = this.dirty || force if (!needPush) return - commit('updateCache', { username: rootState.users.currentUser.fqn }) - const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } - rootState.api.backendInteractor + this.updateCache({ username: window.vuex.state.users.currentUser.fqn }) + const params = { pleroma_settings_store: { 'pleroma-fe': this.cache } } + window.vuex.state.api.backendInteractor .updateProfile({ params }) .then((user) => { - commit('setServerSideStorage', user) - state.dirty = false + this.setServerSideStorage(user) + this.dirty = false }) } } -} - -export default serverSideStorage +}) diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js index fc0402402..607bf9263 100644 --- a/test/unit/specs/modules/serverSideStorage.spec.js +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -1,4 +1,5 @@ import { cloneDeep } from 'lodash' +import { setActivePinia, createPinia } from 'pinia' import { VERSION, @@ -10,49 +11,50 @@ import { _mergeFlags, _mergePrefs, _resetFlags, - mutations, defaultState, - newUserFlags -} from 'src/modules/serverSideStorage.js' + newUserFlags, + useServerSideStorageStore, +} from 'src/stores/serverSideStorage.js' describe('The serverSideStorage module', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + describe('mutations', () => { describe('setServerSideStorage', () => { - const { setServerSideStorage } = mutations const user = { created_at: new Date('1999-02-09'), storage: {} } it('should initialize storage if none present', () => { - const state = cloneDeep(defaultState) - setServerSideStorage(state, user) - 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) + const store = useServerSideStorageStore() + store.setServerSideStorage(store, user) + expect(store.cache._version).to.eql(VERSION) + expect(store.cache._timestamp).to.be.a('number') + expect(store.cache.flagStorage).to.eql(defaultState.flagStorage) + expect(store.cache.prefsStorage).to.eql(defaultState.prefsStorage) }) it('should initialize storage with proper flags for new users if none present', () => { - const state = cloneDeep(defaultState) - setServerSideStorage(state, { ...user, created_at: new Date() }) - 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) + const store = useServerSideStorageStore() + store.setServerSideStorage({ ...user, created_at: new Date() }) + expect(store.cache._version).to.eql(VERSION) + expect(store.cache._timestamp).to.be.a('number') + expect(store.cache.flagStorage).to.eql(newUserFlags) + expect(store.cache.prefsStorage).to.eql(defaultState.prefsStorage) }) it('should merge flags even if remote timestamp is older', () => { - const state = { - ...cloneDeep(defaultState), - cache: { - _timestamp: Date.now(), - _version: VERSION, - ...cloneDeep(defaultState) - } + const store = useServerSideStorageStore() + store.cache = { + _timestamp: Date.now(), + _version: VERSION, + ...cloneDeep(defaultState) } - setServerSideStorage( - state, + + store.setServerSideStorage( { ...user, storage: { @@ -68,19 +70,18 @@ describe('The serverSideStorage module', () => { } } ) - expect(state.cache.flagStorage).to.eql({ + + expect(store.cache.flagStorage).to.eql({ ...defaultState.flagStorage, updateCounter: 1 }) }) it('should reset local timestamp to remote if contents are the same', () => { - const state = { - ...cloneDeep(defaultState), - cache: null - } - setServerSideStorage( - state, + const store = useServerSideStorageStore() + store.cache = null + + store.setServerSideStorage( { ...user, storage: { @@ -93,97 +94,95 @@ describe('The serverSideStorage module', () => { } } ) - expect(state.cache._timestamp).to.eql(123) - expect(state.flagStorage.updateCounter).to.eql(999) - expect(state.cache.flagStorage.updateCounter).to.eql(999) + expect(store.cache._timestamp).to.eql(123) + expect(store.flagStorage.updateCounter).to.eql(999) + expect(store.cache.flagStorage.updateCounter).to.eql(999) }) it('should remote version if local missing', () => { - const state = cloneDeep(defaultState) - setServerSideStorage(state, user) - expect(state.cache._version).to.eql(VERSION) - expect(state.cache._timestamp).to.be.a('number') - expect(state.cache.flagStorage).to.eql(defaultState.flagStorage) + const store = useServerSideStorageStore() + store.setServerSideStorage(store, user) + expect(store.cache._version).to.eql(VERSION) + expect(store.cache._timestamp).to.be.a('number') + expect(store.cache.flagStorage).to.eql(defaultState.flagStorage) }) }) describe('setPreference', () => { - const { setPreference, unsetPreference, updateCache, addCollectionPreference, removeCollectionPreference } = 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({ + const store = useServerSideStorageStore() + store.setPreference({ path: 'simple.testing', value: 1 }) + expect(store.prefsStorage.simple.testing).to.eql(1) + expect(store.prefsStorage._journal.length).to.eql(1) + expect(store.prefsStorage._journal[0]).to.eql({ path: 'simple.testing', operation: 'set', args: [1], // should have A timestamp, we don't really care what it is - timestamp: state.prefsStorage._journal[0].timestamp + timestamp: store.prefsStorage._journal[0].timestamp }) }) it('should keep journal to a minimum', () => { - const state = cloneDeep(defaultState) - setPreference(state, { path: 'simple.testing', value: 1 }) - setPreference(state, { path: 'simple.testing', value: 2 }) - addCollectionPreference(state, { path: 'collections.testing', value: 2 }) - removeCollectionPreference(state, { path: 'collections.testing', value: 2 }) - updateCache(state, { username: 'test' }) - expect(state.prefsStorage.simple.testing).to.eql(2) - expect(state.prefsStorage.collections.testing).to.eql([]) - expect(state.prefsStorage._journal.length).to.eql(2) - expect(state.prefsStorage._journal[0]).to.eql({ + const store = useServerSideStorageStore() + store.setPreference({ path: 'simple.testing', value: 1 }) + store.setPreference({ path: 'simple.testing', value: 2 }) + store.addCollectionPreference({ path: 'collections.testing', value: 2 }) + store.removeCollectionPreference({ path: 'collections.testing', value: 2 }) + store.updateCache({ username: 'test' }) + expect(store.prefsStorage.simple.testing).to.eql(2) + expect(store.prefsStorage.collections.testing).to.eql([]) + expect(store.prefsStorage._journal.length).to.eql(2) + expect(store.prefsStorage._journal[0]).to.eql({ path: 'simple.testing', operation: 'set', args: [2], // should have A timestamp, we don't really care what it is - timestamp: state.prefsStorage._journal[0].timestamp + timestamp: store.prefsStorage._journal[0].timestamp }) - expect(state.prefsStorage._journal[1]).to.eql({ + expect(store.prefsStorage._journal[1]).to.eql({ path: 'collections.testing', operation: 'removeFromCollection', args: [2], // should have A timestamp, we don't really care what it is - timestamp: state.prefsStorage._journal[1].timestamp + timestamp: store.prefsStorage._journal[1].timestamp }) }) it('should remove duplicate entries from journal', () => { - const state = cloneDeep(defaultState) - setPreference(state, { path: 'simple.testing', value: 1 }) - setPreference(state, { path: 'simple.testing', value: 1 }) - addCollectionPreference(state, { path: 'collections.testing', value: 2 }) - addCollectionPreference(state, { path: 'collections.testing', value: 2 }) - updateCache(state, { username: 'test' }) - expect(state.prefsStorage.simple.testing).to.eql(1) - expect(state.prefsStorage.collections.testing).to.eql([2]) - expect(state.prefsStorage._journal.length).to.eql(2) + const store = useServerSideStorageStore() + store.setPreference({ path: 'simple.testing', value: 1 }) + store.setPreference({ path: 'simple.testing', value: 1 }) + store.addCollectionPreference({ path: 'collections.testing', value: 2 }) + store.addCollectionPreference({ path: 'collections.testing', value: 2 }) + store.updateCache({ username: 'test' }) + expect(store.prefsStorage.simple.testing).to.eql(1) + expect(store.prefsStorage.collections.testing).to.eql([2]) + expect(store.prefsStorage._journal.length).to.eql(2) }) it('should remove depth = 3 set/unset entries from journal', () => { - const state = cloneDeep(defaultState) - setPreference(state, { path: 'simple.object.foo', value: 1 }) - unsetPreference(state, { path: 'simple.object.foo' }) - updateCache(state, { username: 'test' }) - expect(state.prefsStorage.simple.object).to.not.have.property('foo') - expect(state.prefsStorage._journal.length).to.eql(1) + const store = useServerSideStorageStore() + store.setPreference({ path: 'simple.object.foo', value: 1 }) + store.unsetPreference({ path: 'simple.object.foo' }) + store.updateCache(store, { username: 'test' }) + expect(store.prefsStorage.simple.object).to.not.have.property('foo') + expect(store.prefsStorage._journal.length).to.eql(1) }) it('should not allow unsetting depth <= 2', () => { - const state = cloneDeep(defaultState) - setPreference(state, { path: 'simple.object.foo', value: 1 }) - expect(() => unsetPreference(state, { path: 'simple' })).to.throw() - expect(() => unsetPreference(state, { path: 'simple.object' })).to.throw() + const store = useServerSideStorageStore() + store.setPreference({ path: 'simple.object.foo', value: 1 }) + expect(() => store.unsetPreference({ path: 'simple' })).to.throw() + expect(() => store.unsetPreference({ path: 'simple.object' })).to.throw() }) it('should not allow (un)setting depth > 3', () => { - const state = cloneDeep(defaultState) - setPreference(state, { path: 'simple.object', value: {} }) - expect(() => setPreference(state, { path: 'simple.object.lv3', value: 1 })).to.not.throw() - expect(() => setPreference(state, { path: 'simple.object.lv3.lv4', value: 1})).to.throw() - expect(() => unsetPreference(state, { path: 'simple.object.lv3', value: 1 })).to.not.throw() - expect(() => unsetPreference(state, { path: 'simple.object.lv3.lv4', value: 1})).to.throw() + const store = useServerSideStorageStore() + store.setPreference({ path: 'simple.object', value: {} }) + expect(() => store.setPreference({ path: 'simple.object.lv3', value: 1 })).to.not.throw() + expect(() => store.setPreference({ path: 'simple.object.lv3.lv4', value: 1})).to.throw() + expect(() => store.unsetPreference({ path: 'simple.object.lv3', value: 1 })).to.not.throw() + expect(() => store.unsetPreference({ path: 'simple.object.lv3.lv4', value: 1})).to.throw() }) }) }) From 7115b0e22c4111eb70a5ee44696831dd67629558 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 24 Mar 2025 01:12:26 +0200 Subject: [PATCH 019/388] reorder code to make more sense --- src/stores/serverSideStorage.js | 142 ++++++++++++++++---------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/src/stores/serverSideStorage.js b/src/stores/serverSideStorage.js index 2a19b2cf2..e9ab571e3 100644 --- a/src/stores/serverSideStorage.js +++ b/src/stores/serverSideStorage.js @@ -329,77 +329,6 @@ export const useServerSideStorageStore = defineStore('serverSideStorage', { return cloneDeep(defaultState) }, actions: { - clearServerSideStorage () { - const blankState = { ...cloneDeep(defaultState) } - Object.keys(this).forEach(k => { - this[k] = blankState[k] - }) - }, - setServerSideStorage (userData) { - const live = userData.storage - this.raw = live - let cache = this.cache - if (cache && cache._user !== userData.fqn) { - console.warn('Cache belongs to another user! reinitializing local cache!') - cache = null - } - - cache = _doMigrations(cache) - - let { recent, stale, needUpload } = _getRecentData(cache, live) - - const userNew = userData.created_at > NEW_USER_DATE - const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage - let dirty = false - - if (recent === null) { - console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`) - recent = _wrapData({ - flagStorage: { ...flagsTemplate }, - prefsStorage: { ...defaultState.prefsStorage } - }) - } - - if (!needUpload && recent && stale) { - console.debug('Checking if data needs merging...') - // discarding timestamps and versions - /* eslint-disable no-unused-vars */ - const { _timestamp: _0, _version: _1, ...recentData } = recent - const { _timestamp: _2, _version: _3, ...staleData } = stale - /* eslint-enable no-unused-vars */ - dirty = !isEqual(recentData, staleData) - console.debug(`Data ${dirty ? 'needs' : 'doesn\'t need'} merging`) - } - - const allFlagKeys = _getAllFlags(recent, stale) - let totalFlags - let totalPrefs - if (dirty) { - // Merge 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 = { ...flagsTemplate, ...totalFlags } - recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs } - - this.dirty = dirty || needUpload - this.cache = recent - // 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 - }, setFlag ({ flag, value }) { this.flagStorage[flag] = value this.dirty = true @@ -501,6 +430,77 @@ export const useServerSideStorageStore = defineStore('serverSideStorage', { prefsStorage: toRaw(this.prefsStorage) }, username) }, + clearServerSideStorage () { + const blankState = { ...cloneDeep(defaultState) } + Object.keys(this).forEach(k => { + this[k] = blankState[k] + }) + }, + setServerSideStorage (userData) { + const live = userData.storage + this.raw = live + let cache = this.cache + if (cache && cache._user !== userData.fqn) { + console.warn('Cache belongs to another user! reinitializing local cache!') + cache = null + } + + cache = _doMigrations(cache) + + let { recent, stale, needUpload } = _getRecentData(cache, live) + + const userNew = userData.created_at > NEW_USER_DATE + const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage + let dirty = false + + if (recent === null) { + console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`) + recent = _wrapData({ + flagStorage: { ...flagsTemplate }, + prefsStorage: { ...defaultState.prefsStorage } + }) + } + + if (!needUpload && recent && stale) { + console.debug('Checking if data needs merging...') + // discarding timestamps and versions + /* eslint-disable no-unused-vars */ + const { _timestamp: _0, _version: _1, ...recentData } = recent + const { _timestamp: _2, _version: _3, ...staleData } = stale + /* eslint-enable no-unused-vars */ + dirty = !isEqual(recentData, staleData) + console.debug(`Data ${dirty ? 'needs' : 'doesn\'t need'} merging`) + } + + const allFlagKeys = _getAllFlags(recent, stale) + let totalFlags + let totalPrefs + if (dirty) { + // Merge 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 = { ...flagsTemplate, ...totalFlags } + recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs } + + this.dirty = dirty || needUpload + this.cache = recent + // 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 + }, pushServerSideStorage ({ force = false } = {}) { const needPush = this.dirty || force if (!needPush) return From 5d47ac04b0febff479556db0644010e65e185854 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 25 Mar 2025 11:56:17 +0200 Subject: [PATCH 020/388] implement migration --- package.json | 1 + src/lib/persisted_state.js | 1 - src/modules/users.js | 37 +++++++++++++++++++++++++++++++-- src/stores/serverSideStorage.js | 2 +- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 10c0ce613..cb5378a86 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "querystring-es3": "0.2.1", "url": "0.11.4", "utf8": "3.0.0", + "uuid": "8.3.2", "vue": "3.5.13", "vue-i18n": "10", "vue-router": "4.5.0", diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index e6ed05f28..878d95aa8 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -18,7 +18,6 @@ const saveImmedeatelyActions = [ 'markNotificationsAsSeen', 'clearCurrentUser', 'setCurrentUser', - 'setServerSideStorage', 'setHighlight', 'setOption', 'setClientData', diff --git a/src/modules/users.js b/src/modules/users.js index f6c05ca21..6a25ba832 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -1,13 +1,15 @@ +import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash' +import { v4 as uuidv4 } from 'uuid'; + import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import { windowWidth, windowHeight } from '../services/window_utils/window_utils' import apiService from '../services/api/api.service.js' import oauthApi from '../services/new_api/oauth.js' -import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash' import { registerPushNotifications, unregisterPushNotifications } from '../services/sw/sw.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useOAuthStore } from 'src/stores/oauth.js' -import { useServerSideStorageStore } from 'src/stores/serverSideStorage' +import { useServerSideStorageStore, CONFIG_MIGRATION } from 'src/stores/serverSideStorage' // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, obj, item) => { @@ -618,6 +620,37 @@ const users = { // Set our new backend interactor commit('setBackendInteractor', backendInteractorService(accessToken)) + + // Do server-side storage migrations + + const { configMigration } = useServerSideStorageStore().flagStorage + + // Wordfilter migration + if (configMigration < 1) { + // Debug snippet to clean up storage + /* + Object.keys(useServerSideStorageStore().prefsStorage.simple.muteFilters).forEach(key => { + useServerSideStorageStore().unsetPreference({ path: 'simple.muteFilters.' + key, value: null }) + }) + */ + + // Convert existing wordfilter into synced one + store.rootState.config.muteWords.forEach(word => { + const uniqueId = uuidv4() + + useServerSideStorageStore().setPreference({ + path: 'simple.muteFilters.' + uniqueId, + value: { + type: 'word', + value: word + } + }) + }) + } + + + // Update the flag + useServerSideStorageStore().setflag({ flag: 'configMigration', value: CONFIG_MIGRATION }) useServerSideStorageStore().pushServerSideStorage() if (user.token) { diff --git a/src/stores/serverSideStorage.js b/src/stores/serverSideStorage.js index e9ab571e3..1f71803ac 100644 --- a/src/stores/serverSideStorage.js +++ b/src/stores/serverSideStorage.js @@ -38,7 +38,7 @@ export const defaultState = { simple: { dontShowUpdateNotifs: false, collapseNav: false, - filters: {} + muteFilters: {} }, collections: { pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'], From 57aa8818a93f77ab8a765c6872f2e9c309c2d9dc Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 25 Mar 2025 19:01:32 +0200 Subject: [PATCH 021/388] UI for new filters --- .../settings_modal/helpers/setting.js | 1 - .../settings_modal/tabs/filtering_tab.js | 107 ++++++++++-- .../settings_modal/tabs/filtering_tab.scss | 61 +++++++ .../settings_modal/tabs/filtering_tab.vue | 152 +++++++++++++++++- src/i18n/en.json | 17 ++ src/modules/users.js | 38 +++-- src/stores/serverSideStorage.js | 2 + 7 files changed, 339 insertions(+), 39 deletions(-) create mode 100644 src/components/settings_modal/tabs/filtering_tab.scss diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js index 2dc9653ea..df137157a 100644 --- a/src/components/settings_modal/helpers/setting.js +++ b/src/components/settings_modal/helpers/setting.js @@ -78,7 +78,6 @@ export default { }, computed: { draft: { - // TODO allow passing shared draft object? get () { if (this.realSource === 'admin' || this.path == null) { return get(this.$store.state.adminSettings.draft, this.canonPath) diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js index fbace15df..8517e0135 100644 --- a/src/components/settings_modal/tabs/filtering_tab.js +++ b/src/components/settings_modal/tabs/filtering_tab.js @@ -1,15 +1,19 @@ -import { filter, trim, debounce } from 'lodash' +import { mapState, mapActions } from 'pinia' +import { useServerSideStorageStore } from 'src/stores/serverSideStorage' +import { v4 as uuidv4 } from 'uuid'; + import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' import UnitSetting from '../helpers/unit_setting.vue' import IntegerSetting from '../helpers/integer_setting.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' +import Select from 'src/components/select/select.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' const FilteringTab = { data () { return { - muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n'), replyVisibilityOptions: ['all', 'following', 'self'].map(mode => ({ key: mode, value: mode, @@ -21,26 +25,95 @@ const FilteringTab = { BooleanSetting, ChoiceSetting, UnitSetting, - IntegerSetting + IntegerSetting, + Checkbox, + Select }, computed: { ...SharedComputedObject(), - muteWordsString: { - get () { - return this.muteWordsStringLocal - }, - set (value) { - this.muteWordsStringLocal = value - this.debouncedSetMuteWords(value) + ...mapState( + useServerSideStorageStore, + { + muteFilters: store => Object.entries(store.prefsStorage.simple.muteFilters), + muteFiltersObject: store => store.prefsStorage.simple.muteFilters } + ) + }, + methods: { + ...mapActions(useServerSideStorageStore, ['setPreference', 'unsetPreference', 'pushServerSideStorage']), + getDatetimeLocal (timestamp) { + const date = new Date(timestamp) + const datetime = [ + date.getFullYear(), + '-', + date.getMonth() < 9 ? ('0' + (date.getMonth() + 1)) : (date.getMonth() + 1), + '-', + date.getDate() < 10 ? ('0' + date.getDate()) : date.getDate(), + 'T', + date.getHours() < 10 ? ('0' + date.getHours()) : date.getHours(), + ':', + date.getMinutes() < 10 ? ('0' + date.getMinutes()) : date.getMinutes(), + ].join('') + return datetime }, - debouncedSetMuteWords () { - return debounce((value) => { - this.$store.dispatch('setOption', { - name: 'muteWords', - value: filter(value.split('\n'), (word) => trim(word).length > 0) - }) - }, 1000) + checkRegexValid (id) { + const filter = this.muteFiltersObject[id] + if (filter.type !== 'regexp') return true + const { value } = filter + let valid = true + try { + new RegExp(value) + } catch (e) { + valid = false + console.error('Invalid RegExp: ' + value) + } + return valid + }, + createFilter () { + const filter = { + type: 'word', + value: '', + name: 'New Filter', + enabled: true, + expires: null, + hide: false, + order: this.muteFilters.length + 2 + } + const newId = uuidv4() + + this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter }) + this.pushServerSideStorage() + }, + copyFilter (id) { + const filter = { ...this.muteFiltersObject[id] } + const newId = uuidv4() + + this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter }) + this.pushServerSideStorage() + }, + deleteFilter (id) { + this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null }) + this.pushServerSideStorage() + }, + updateFilter(id, field, value) { + const filter = { ...this.muteFiltersObject[id] } + if (field === 'expires-never') { + // filter[field] = value + if (!value) { + const offset = 1000 * 60 * 60 * 24 * 14 // 2 weeks + const date = Date.now() + offset + filter.expires = date + } else { + filter.expires = null + } + } else if (field === 'expires') { + const parsed = Date.parse(value) + filter.expires = parsed.valueOf() + } else { + filter[field] = value + } + this.setPreference({ path: 'simple.muteFilters.' + id , value: filter }) + this.pushServerSideStorage() } }, // Updating nested properties diff --git a/src/components/settings_modal/tabs/filtering_tab.scss b/src/components/settings_modal/tabs/filtering_tab.scss new file mode 100644 index 000000000..ec98e094d --- /dev/null +++ b/src/components/settings_modal/tabs/filtering_tab.scss @@ -0,0 +1,61 @@ +.filtering-tab { + .muteFilterContainer { + border: 1px solid var(--border); + border-radius: var(--roundness); + + height: 20vh; + overflow-y: auto; + } + + .mute-filter { + border: 1px solid var(--border); + border-radius: var(--roundness); + margin: 0.5em; + padding: 0.5em; + + display: grid; + grid-template-columns: fit-content() 1fr fit-content(); + align-items: baseline; + grid-gap: 0.5em; + } + + .filter-name { + grid-column: 1 / span 2; + grid-row: 1; + } + + .alert, .button-default { + display: inline-block; + line-height: 2; + padding: 0 0.5em; + } + + .filter-enabled { + grid-column: 3; + grid-row: 1; + + text-align: right; + } + + .filter-field { + display: grid; + grid-template-columns: subgrid; + grid-template-rows: subgrid; + grid-column: 1 / span 3; + align-items: baseline; + + label { + grid-column: 1; + text-align: right; + } + + .filter-field-value { + grid-column: 2 / span 2; + } + } + + .filter-buttons { + grid-column: 1 / span 3; + justify-self: end; + } +} diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue index 32325d423..538ab047c 100644 --- a/src/components/settings_modal/tabs/filtering_tab.vue +++ b/src/components/settings_modal/tabs/filtering_tab.vue @@ -1,5 +1,5 @@