Add frequently used stickers section at top. Fixes #4

This commit is contained in:
Tulir Asokan 2020-09-06 18:25:28 +03:00
parent 4ce90892f0
commit aad04ba9b6
6 changed files with 89 additions and 10 deletions

24
web/frequently-used.js Normal file
View File

@ -0,0 +1,24 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
const FREQUENTLY_USED = JSON.parse(window.localStorage.mauFrequentlyUsedStickerIDs || "{}")
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)
FREQUENTLY_USED_SORTED = null
}
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)
.map(([emoji]) => emoji)
}
return FREQUENTLY_USED_SORTED.slice(0, limit)
}

View File

@ -37,6 +37,10 @@ nav {
nav > a > div.sticker { nav > a > div.sticker {
width: 12vw; width: 12vw;
height: 12vw; } height: 12vw; }
nav > a > div.sticker.icon > img {
width: 70%;
height: 70%;
padding: 15%; }
div.pack-list, nav { div.pack-list, nav {
scrollbar-width: none; } scrollbar-width: none; }

View File

@ -5,7 +5,8 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
import { html, render, Component } from "https://unpkg.com/htm/preact/index.mjs?module" import { html, render, Component } from "https://unpkg.com/htm/preact/index.mjs?module"
import { Spinner } from "./spinner.js" import { Spinner } from "./spinner.js"
import { sendSticker } from "./widget-api.js" import * as widgetAPI from "./widget-api.js"
import * as frequent from "./frequently-used.js"
// The base URL for fetching packs. The app will first fetch ${PACK_BASE_URL}/index.json, // The base URL for fetching packs. The app will first fetch ${PACK_BASE_URL}/index.json,
// then ${PACK_BASE_URL}/${packFile} for each packFile in the packs object of the index.json file. // then ${PACK_BASE_URL}/${packFile} for each packFile in the packs object of the index.json file.
@ -26,9 +27,35 @@ class App extends Component {
packs: [], packs: [],
loading: true, loading: true,
error: null, error: null,
frequentlyUsed: {
id: "frequently-used",
title: "Frequently used",
stickerIDs: frequent.get(),
stickers: [],
},
} }
this.stickersByID = new Map(JSON.parse(localStorage.mauFrequentlyUsedStickerCache || "[]"))
this.state.frequentlyUsed.stickers = this._getStickersByID(this.state.frequentlyUsed.stickerIDs)
this.imageObserver = null this.imageObserver = null
this.packListRef = null this.packListRef = null
this.sendSticker = this.sendSticker.bind(this)
}
_getStickersByID(ids) {
return ids.map(id => this.stickersByID.get(id)).filter(sticker => !!sticker)
}
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]))
} }
componentDidMount() { componentDidMount() {
@ -46,11 +73,15 @@ class App extends Component {
for (const packFile of indexData.packs) { for (const packFile of indexData.packs) {
const packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`) const packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`)
const packData = await packRes.json() const packData = await packRes.json()
for (const sticker of packData.stickers) {
this.stickersByID.set(sticker.id, sticker)
}
this.setState({ this.setState({
packs: [...this.state.packs, packData], packs: [...this.state.packs, packData],
loading: false, loading: false,
}) })
} }
this.updateFrequentlyUsed()
}, error => this.setState({ loading: false, error })) }, error => this.setState({ loading: false, error }))
this.imageObserver = new IntersectionObserver(this.observeImageIntersections, { this.imageObserver = new IntersectionObserver(this.observeImageIntersections, {
@ -98,6 +129,14 @@ class App extends Component {
this.sectionObserver.disconnect() this.sectionObserver.disconnect()
} }
sendSticker(evt) {
const id = evt.currentTarget.getAttribute("data-sticker-id")
const sticker = this.stickersByID.get(id)
frequent.add(id)
this.updateFrequentlyUsed()
widgetAPI.sendSticker(sticker)
}
render() { render() {
if (this.state.loading) { if (this.state.loading) {
return html`<main class="spinner"><${Spinner} size=${80} green /></main>` return html`<main class="spinner"><${Spinner} size=${80} green /></main>`
@ -111,10 +150,12 @@ class App extends Component {
} }
return html`<main class="has-content"> return html`<main class="has-content">
<nav> <nav>
<${NavBarItem} pack=${this.state.frequentlyUsed} iconOverride="res/recent.svg" altOverride="🕓️" />
${this.state.packs.map(pack => html`<${NavBarItem} id=${pack.id} pack=${pack}/>`)} ${this.state.packs.map(pack => html`<${NavBarItem} id=${pack.id} pack=${pack}/>`)}
</nav> </nav>
<div class="pack-list ${isMobileSafari ? "ios-safari-hack" : ""}" ref=${elem => this.packListRef = elem}> <div class="pack-list ${isMobileSafari ? "ios-safari-hack" : ""}" ref=${elem => this.packListRef = elem}>
${this.state.packs.map(pack => html`<${Pack} id=${pack.id} pack=${pack}/>`)} <${Pack} pack=${this.state.frequentlyUsed} send=${this.sendSticker} />
${this.state.packs.map(pack => html`<${Pack} id=${pack.id} pack=${pack} send=${this.sendSticker} />`)}
</div> </div>
</main>` </main>`
} }
@ -128,29 +169,33 @@ const scrollToSection = (evt, id) => {
evt.preventDefault() evt.preventDefault()
} }
const NavBarItem = ({ pack }) => html` const NavBarItem = ({ pack, iconOverride = null, altOverride = null }) => html`
<a href="#pack-${pack.id}" id="nav-${pack.id}" data-pack-id=${pack.id} title=${pack.title} <a href="#pack-${pack.id}" id="nav-${pack.id}" data-pack-id=${pack.id} title=${pack.title}
onClick=${isMobileSafari ? (evt => scrollToSection(evt, pack.id)) : undefined}> onClick=${isMobileSafari ? (evt => scrollToSection(evt, pack.id)) : undefined}>
<div class="sticker"> <div class="sticker ${iconOverride ? "icon" : ""}">
${iconOverride ? html`
<img src=${iconOverride} alt=${altOverride} class="visible"/>
` : html`
<img src=${makeThumbnailURL(pack.stickers[0].url)} <img src=${makeThumbnailURL(pack.stickers[0].url)}
alt=${pack.stickers[0].body} class="visible" /> alt=${pack.stickers[0].body} class="visible" />
`}
</div> </div>
</a> </a>
` `
const Pack = ({ pack }) => html` const Pack = ({ pack, send }) => html`
<section class="stickerpack" id="pack-${pack.id}" data-pack-id=${pack.id}> <section class="stickerpack" id="pack-${pack.id}" data-pack-id=${pack.id}>
<h1>${pack.title}</h1> <h1>${pack.title}</h1>
<div class="sticker-list"> <div class="sticker-list">
${pack.stickers.map(sticker => html` ${pack.stickers.map(sticker => html`
<${Sticker} key=${sticker.id} content=${sticker}/> <${Sticker} key=${sticker.id} content=${sticker} send=${send}/>
`)} `)}
</div> </div>
</section> </section>
` `
const Sticker = ({ content }) => html` const Sticker = ({ content, send }) => html`
<div class="sticker" onClick=${() => sendSticker(content)}> <div class="sticker" onClick=${send} data-sticker-id=${content.id}>
<img data-src=${makeThumbnailURL(content.url)} alt=${content.body} /> <img data-src=${makeThumbnailURL(content.url)} alt=${content.body} />
</div> </div>
` `

View File

@ -54,6 +54,10 @@ nav
> div.sticker > div.sticker
width: $nav-sticker-size width: $nav-sticker-size
height: $nav-sticker-size height: $nav-sticker-size
> div.sticker.icon > img
width: 70%
height: 70%
padding: 15%
div.pack-list, nav div.pack-list, nav
scrollbar-width: none scrollbar-width: none

1
web/res/favorite.svg Normal file
View File

@ -0,0 +1 @@
<svg height='100px' width='100px' fill="#000000" xmlns:x="http://ns.adobe.com/Extensibility/1.0/" xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/" xmlns:graph="http://ns.adobe.com/Graphs/1.0/" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"><g><g i:extraneous="self"><path d="M77,95.3c-0.7,0-1.4-0.2-2-0.6l-25.1-16l-25.1,16c-1.3,0.8-2.9,0.8-4.1-0.1c-1.2-0.9-1.8-2.4-1.4-3.9l7.5-28.9l-23-18.9 c-1.2-1-1.6-2.5-1.2-4c0.5-1.4,1.8-2.4,3.3-2.5l29.8-1.8L46.6,6.9c1.1-2.8,5.7-2.8,6.8,0l10.9,27.8L94,36.4 c1.5,0.1,2.8,1.1,3.3,2.5c0.5,1.4,0,3-1.2,4l-23,19l7.5,28.8c0.4,1.5-0.2,3-1.4,3.9C78.6,95,77.8,95.3,77,95.3z M49.9,70.6 c0.7,0,1.4,0.2,2,0.6l19.2,12.3l-5.7-22c-0.4-1.4,0.1-2.9,1.2-3.8l17.6-14.5l-22.7-1.4c-1.4-0.1-2.7-1-3.2-2.3L50,18.3l-8.3,21.2 c-0.5,1.3-1.8,2.2-3.2,2.3l-22.8,1.4l17.5,14.4c1.1,0.9,1.6,2.4,1.2,3.8l-5.7,22.1l19.2-12.2C48.5,70.8,49.2,70.6,49.9,70.6z"></path></g></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
web/res/recent.svg Normal file
View File

@ -0,0 +1 @@
<svg height='100px' width='100px' fill="#000000" xmlns:x="http://ns.adobe.com/Extensibility/1.0/" xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/" xmlns:graph="http://ns.adobe.com/Graphs/1.0/" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"><g><g i:extraneous="self"><g><path d="M84,17.3c-17.8-17.8-46.7-18-64.8-0.5l-6.4-6.4c-0.8-0.8-2-1.1-3.2-0.8C8.7,9.9,7.7,10.9,7.5,12L2.6,35.6 c-0.2,1.1,0.1,2.1,0.8,2.8c0.8,0.8,1.8,1.1,2.9,0.8l23.6-4.9c1.2-0.2,2.1-1,2.4-2.2c0.3-1.1,0-2.4-0.8-3.2L25,22.6 C39.9,8.3,63.6,8.5,78.2,23.1C93,37.9,93,62,78.2,76.9C71,84.1,61.5,88,51.3,88s-19.7-4-26.9-11.1c-5.6-5.6-9.3-12.7-10.6-20.5 c-0.4-2.2-2.5-3.8-4.7-3.4c-2.2,0.4-3.7,2.5-3.4,4.7c1.6,9.5,6.1,18.1,12.9,24.9c8.7,8.7,20.3,13.5,32.7,13.5s24-4.8,32.7-13.5 C102,64.6,102,35.3,84,17.3z"></path><path d="M51.6,21c-2.3,0-4.1,1.8-4.1,4.1V50c0,1.5,0.8,2.9,2.1,3.6l13.2,7.3c0.6,0.4,1.3,0.5,2,0.5c1.4,0,2.8-0.8,3.6-2.1 c1.1-2,0.4-4.5-1.6-5.6l-11.1-6.1V25.1C55.7,22.8,53.9,21,51.6,21z"></path></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB