From 859f4bf528ef370060a19673f58aa6408a2a8ae0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?K=C3=A9vin=20Cocchi?= <kevin.cocchi@gmail.com>
Date: Sat, 27 May 2023 18:21:25 +0200
Subject: [PATCH] Frequently used in sync with packs & way to clear Frequently
 used

---
 web/src/frequently-used.js | 74 +++++++++++++++++++++++++++++++++-----
 web/src/index.js           | 61 ++++++++++++++++++++-----------
 2 files changed, 106 insertions(+), 29 deletions(-)

diff --git a/web/src/frequently-used.js b/web/src/frequently-used.js
index a754e9a..d35f6a9 100644
--- a/web/src/frequently-used.js
+++ b/web/src/frequently-used.js
@@ -13,22 +13,80 @@
 //
 // You should have received a copy of the GNU Affero General Public License
 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
-const FREQUENTLY_USED = JSON.parse(window.localStorage.mauFrequentlyUsedStickerIDs || "{}")
+
+const FREQUENTLY_USED_STORAGE_KEY = 'mauFrequentlyUsedStickerIDs'
+const FREQUENTLY_USED_STORAGE_CACHE_KEY = 'mauFrequentlyUsedStickerCache'
+
+let FREQUENTLY_USED = JSON.parse(window.localStorage[FREQUENTLY_USED_STORAGE_KEY] ?? '{}')
 let FREQUENTLY_USED_SORTED = null
 
-export const add = id => {
-	const [count] = FREQUENTLY_USED[id] || [0]
-	FREQUENTLY_USED[id] = [count + 1, Date.now()]
-	window.localStorage.mauFrequentlyUsedStickerIDs = JSON.stringify(FREQUENTLY_USED)
+const sortFrequentlyUsedEntries = (entry1, entry2) => {
+	const [, [count1, date1]] = entry1
+	const [, [count2, date2]] = entry2
+	return count2 === count1 ? date2 - date1 : count2 - count1
+}
+
+export const setFrequentlyUsedStorage = (frequentlyUsed) => {
+	FREQUENTLY_USED = frequentlyUsed ?? {}
+	window.localStorage[FREQUENTLY_USED_STORAGE_KEY] = JSON.stringify(FREQUENTLY_USED)
 	FREQUENTLY_USED_SORTED = null
 }
 
+export const setFrequentlyUsedCacheStorage = (stickers) => {
+	const toPutInCache = stickers.map(sticker => [sticker.id, sticker])
+	window.localStorage[FREQUENTLY_USED_STORAGE_CACHE_KEY] = JSON.stringify(toPutInCache)
+}
+
+export const add = (id) => {
+	let FREQUENTLY_USED_COPY = { ...FREQUENTLY_USED }
+	const [count] = FREQUENTLY_USED_COPY[id] || [0]
+	FREQUENTLY_USED_COPY[id] = [count + 1, Date.now()]
+	setFrequentlyUsedStorage(FREQUENTLY_USED_COPY)
+}
+
 export const get = (limit = 16) => {
 	if (FREQUENTLY_USED_SORTED === null) {
-		FREQUENTLY_USED_SORTED = Object.entries(FREQUENTLY_USED)
-			.sort(([, [count1, date1]], [, [count2, date2]]) =>
-				count2 === count1 ? date2 - date1 : count2 - count1)
+		FREQUENTLY_USED_SORTED = Object.entries(FREQUENTLY_USED || {})
+			.sort(sortFrequentlyUsedEntries)
 			.map(([emoji]) => emoji)
 	}
 	return FREQUENTLY_USED_SORTED.slice(0, limit)
 }
+
+export const getFromCache = () => {
+	return Object.values(JSON.parse(localStorage[FREQUENTLY_USED_STORAGE_CACHE_KEY] ?? '[]'))
+}
+
+export const remove = (id) => {
+	let FREQUENTLY_USED_COPY = { ...FREQUENTLY_USED }
+	if (FREQUENTLY_USED_COPY[id]) {
+		delete FREQUENTLY_USED_COPY[id]
+		setFrequentlyUsedStorage(FREQUENTLY_USED_COPY)
+	}
+}
+
+export const removeMultiple = (ids) => {
+	let FREQUENTLY_USED_COPY = { ...FREQUENTLY_USED }
+	ids.forEach((id) => {
+		delete FREQUENTLY_USED_COPY[id]
+	})
+	setFrequentlyUsedStorage(FREQUENTLY_USED_COPY)
+}
+
+export const removeAll = setFrequentlyUsedStorage
+
+const compareStorageWith = (packs) => {
+	const stickersIDsFromPacks = packs.map((pack) => pack.stickers).flat().map((sticker) => sticker.id)
+	const stickersIDsFromFrequentlyUsedStorage = get()
+
+	const notFound = stickersIDsFromFrequentlyUsedStorage.filter((id) => !stickersIDsFromPacks.includes(id))
+	const found = stickersIDsFromFrequentlyUsedStorage.filter((id) => !notFound.includes(id))
+
+	return { found, notFound }
+}
+
+export const removeNotFoundFromStorage = (packs) => {
+	const { found, notFound } = compareStorageWith(packs)
+	removeMultiple(notFound)
+	return found
+}
diff --git a/web/src/index.js b/web/src/index.js
index 0e9792e..d9b6b44 100644
--- a/web/src/index.js
+++ b/web/src/index.js
@@ -57,6 +57,13 @@ const defaultState = {
 	},
 }
 
+const makeFrequentlyUsedState = ({ stickerIDs, stickers } = {}) => ({
+	id: "frequently-used",
+	title: "Frequently used",
+	stickerIDs: stickerIDs ?? [],
+	stickers: stickers ?? [],
+})
+
 class App extends Component {
 	constructor(props) {
 		super(props)
@@ -68,29 +75,26 @@ class App extends Component {
 			error: null,
 			stickersPerRow: parseInt(localStorage.mauStickersPerRow || "4"),
 			theme: localStorage.mauStickerThemeOverride || this.defaultTheme,
-			frequentlyUsed: {
-				id: "frequently-used",
-				title: "Frequently used",
-				stickerIDs: frequent.get(),
-				stickers: [],
-			},
+			frequentlyUsed: makeFrequentlyUsedState(),
 			filtering: defaultState.filtering,
 		}
+
 		if (!supportedThemes.includes(this.state.theme)) {
 			this.state.theme = "light"
 		}
 		if (!supportedThemes.includes(this.defaultTheme)) {
 			this.defaultTheme = "light"
 		}
-		this.stickersByID = new Map(JSON.parse(localStorage.mauFrequentlyUsedStickerCache || "[]"))
-		this.state.frequentlyUsed.stickers = this._getStickersByID(this.state.frequentlyUsed.stickerIDs)
+
 		this.imageObserver = null
 		this.packListRef = null
 		this.navRef = null
+
 		this.searchStickers = this.searchStickers.bind(this)
 		this.sendSticker = this.sendSticker.bind(this)
 		this.navScroll = this.navScroll.bind(this)
 		this.reloadPacks = this.reloadPacks.bind(this)
+		this.clearFrequentlyUsed = this.clearFrequentlyUsed.bind(this)
 		this.observeSectionIntersections = this.observeSectionIntersections.bind(this)
 		this.observeImageIntersections = this.observeImageIntersections.bind(this)
 	}
@@ -99,17 +103,26 @@ class App extends Component {
 		return ids.map(id => this.stickersByID.get(id)).filter(sticker => !!sticker)
 	}
 
+	_setFrequentlyUsed(stickerIDs = []) {
+		const stickers = this._getStickersByID(stickerIDs)
+		const frequentlyUsed = makeFrequentlyUsedState({ stickerIDs, stickers })
+		this.setState({ frequentlyUsed })
+		frequent.setFrequentlyUsedCacheStorage(stickers)
+	}
+
 	updateFrequentlyUsed() {
 		const stickerIDs = frequent.get()
-		const stickers = this._getStickersByID(stickerIDs)
-		this.setState({
-			frequentlyUsed: {
-				...this.state.frequentlyUsed,
-				stickerIDs,
-				stickers,
-			},
-		})
-		localStorage.mauFrequentlyUsedStickerCache = JSON.stringify(stickers.map(sticker => [sticker.id, sticker]))
+		this._setFrequentlyUsed(stickerIDs)
+	}
+
+	refreshFrequentlyUsed(packs) {
+		const stickerIDs = frequent.removeNotFoundFromStorage(packs)
+		this._setFrequentlyUsed(stickerIDs)
+	}
+
+	clearFrequentlyUsed() {
+		frequent.removeAll()
+		this._setFrequentlyUsed()
 	}
 
 	searchStickers(e) {
@@ -163,6 +176,10 @@ class App extends Component {
 		this._loadPacks(true)
 	}
 
+	_initializeStickersByID(ids) {
+		this.stickersByID = new Map(ids ?? [])
+	}
+
 	async populateStickersByID(allPacks) {
 		const allStickers = allPacks.map(({ stickers }) => stickers).flat()
 		allStickers.forEach((sticker) => {
@@ -202,17 +219,18 @@ class App extends Component {
 				loading: false,
 			})
 			this.populateStickersByID(fetchedPacks)
-			this.updateFrequentlyUsed()
+			this.refreshFrequentlyUsed(fetchedPacks)
 			return fetchedPacks
 		}, error => this.setState({ loading: false, error }))
 	}
 
 	componentDidMount() {
 		document.documentElement.style.setProperty("--stickers-per-row", this.state.stickersPerRow.toString())
+
 		this._loadPacks()
-		this.imageObserver = new IntersectionObserver(this.observeImageIntersections, {
-			rootMargin: "100px",
-		})
+		this._initializeStickersByID(frequent.getFromCache())
+
+		this.imageObserver = new IntersectionObserver(this.observeImageIntersections, { rootMargin: "100px" })
 		this.sectionObserver = new IntersectionObserver(this.observeSectionIntersections)
 	}
 
@@ -357,6 +375,7 @@ const Settings = ({app}) => html`
 		<h1>Settings</h1>
 		<div class="settings-list">
 			<button onClick=${app.reloadPacks}>Reload</button>
+			<button onClick=${app.clearFrequentlyUsed}>Clear frequently used</button>
 			<div>
 				<label for="stickers-per-row">Stickers per row: ${app.state.stickersPerRow}</label>
 				<input type="range" min=2 max=10 id="stickers-per-row" id="stickers-per-row"
-- 
GitLab