Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync frequently used stickers with current available packs #58

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 66 additions & 8 deletions web/src/frequently-used.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
90 changes: 58 additions & 32 deletions web/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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)
Expand All @@ -57,29 +64,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)
}
Expand All @@ -88,17 +92,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) {
Expand Down Expand Up @@ -152,9 +165,20 @@ 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) => {
this.stickersByID.set(sticker.id, sticker)
})
}

_loadPacks(disableCache = false) {
const cache = disableCache ? "no-cache" : undefined
fetch(INDEX, { cache }).then(async indexRes => {
return fetch(INDEX, { cache }).then(async indexRes => {
if (indexRes.status >= 400) {
this.setState({
loading: false,
Expand All @@ -163,34 +187,35 @@ class App extends Component {
return
}
const indexData = await indexRes.json()
const packNames = indexData.packs ?? []
HOMESERVER_URL = indexData.homeserver_url || HOMESERVER_URL
// TODO only load pack metadata when scrolled into view?
for (const packFile of indexData.packs) {
const fetchedPacks = await Promise.all(packNames.map(async packFile => {
let packRes
if (packFile.startsWith("https://") || packFile.startsWith("http://")) {
packRes = await fetch(packFile, { cache })
} else {
packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`, { cache })
}
const packData = await packRes.json()
for (const sticker of packData.stickers) {
this.stickersByID.set(sticker.id, sticker)
}
this.setState({
packs: [...this.state.packs, packData],
loading: false,
})
}
this.updateFrequentlyUsed()
return await packRes.json()
}))
this.setState({
packs: fetchedPacks,
loading: false,
})
this.populateStickersByID(fetchedPacks)
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)
}

Expand Down Expand Up @@ -302,6 +327,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"
Expand Down