Add frequently used stickers section at top. Fixes #4
This commit is contained in:
parent
4ce90892f0
commit
aad04ba9b6
24
web/frequently-used.js
Normal file
24
web/frequently-used.js
Normal 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)
|
||||||
|
}
|
@ -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; }
|
||||||
|
65
web/index.js
65
web/index.js
@ -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" : ""}">
|
||||||
<img src=${makeThumbnailURL(pack.stickers[0].url)}
|
${iconOverride ? html`
|
||||||
alt=${pack.stickers[0].body} class="visible" />
|
<img src=${iconOverride} alt=${altOverride} class="visible"/>
|
||||||
|
` : html`
|
||||||
|
<img src=${makeThumbnailURL(pack.stickers[0].url)}
|
||||||
|
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>
|
||||||
`
|
`
|
||||||
|
@ -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
1
web/res/favorite.svg
Normal 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
1
web/res/recent.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user