diff --git a/web/src/frequently-used.js b/web/src/frequently-used.js index a754e9a5d4216e5fe6a599c6a7ad517b434e2581..d35f6a913cb7a4dd8ca89e47a59db9d0f9268bcd 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 0e9792ef701228f6ce9454d640a2ca048cee7522..d9b6b4453512ab6a0f17797e54ed614f6603f6d0 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"