1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
// 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/.
import { html, render, Component } from "https://unpkg.com/htm/preact/index.mjs?module"
import { Spinner } from "./spinner.js"
import { sendSticker } from "./widget-api.js"
// 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.
const PACKS_BASE_URL = "packs"
// This is updated from packs/index.json
let HOMESERVER_URL = "https://matrix-client.matrix.org"
const makeThumbnailURL = mxc => `${HOMESERVER_URL}/_matrix/media/r0/thumbnail/${mxc.substr(6)}?height=128&width=128&method=scale`
// We need to detect iOS webkit because it has a bug related to scrolling non-fixed divs
const isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/)
class App extends Component {
constructor(props) {
super(props)
this.state = {
packs: [],
loading: true,
error: null,
}
this.imageObserver = null
this.packListRef = null
}
componentDidMount() {
fetch(`${PACKS_BASE_URL}/index.json`).then(async indexRes => {
if (indexRes.status >= 400) {
this.setState({
loading: false,
error: indexRes.status !== 404 ? indexRes.statusText : null,
})
return
}
const indexData = await indexRes.json()
HOMESERVER_URL = indexData.homeserver_url || HOMESERVER_URL
// TODO only load pack metadata when scrolled into view?
for (const packFile of indexData.packs) {
const packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`)
const packData = await packRes.json()
this.setState({
packs: [...this.state.packs, packData],
loading: false,
})
}
}, error => this.setState({ loading: false, error }))
this.imageObserver = new IntersectionObserver(this.observeImageIntersections, {
rootMargin: "100px",
})
this.sectionObserver = new IntersectionObserver(this.observeSectionIntersections)
}
observeImageIntersections(intersections) {
for (const entry of intersections) {
const img = entry.target.children.item(0)
if (entry.isIntersecting) {
img.setAttribute("src", img.getAttribute("data-src"))
img.classList.add("visible")
} else {
img.removeAttribute("src")
img.classList.remove("visible")
}
}
}
observeSectionIntersections(intersections) {
for (const entry of intersections) {
const packID = entry.target.getAttribute("data-pack-id")
const navElement = document.getElementById(`nav-${packID}`)
if (entry.isIntersecting) {
navElement.classList.add("visible")
} else {
navElement.classList.remove("visible")
}
}
}
componentDidUpdate() {
for (const elem of this.packListRef.getElementsByClassName("sticker")) {
this.imageObserver.observe(elem)
}
for (const elem of this.packListRef.children) {
this.sectionObserver.observe(elem)
}
}
componentWillUnmount() {
this.imageObserver.disconnect()
this.sectionObserver.disconnect()
}
render() {
if (this.state.loading) {
return html`<main class="spinner"><${Spinner} size=${80} green /></main>`
} else if (this.state.error) {
return html`<main class="error">
<h1>Failed to load packs</h1>
<p>${this.state.error}</p>
</main>`
} else if (this.state.packs.length === 0) {
return html`<main class="empty"><h1>No packs found 😿</h1></main>`
}
return html`<main class="has-content">
<nav>
${this.state.packs.map(pack => html`<${NavBarItem} id=${pack.id} pack=${pack}/>`)}
</nav>
<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}/>`)}
</div>
</main>`
}
}
const NavBarItem = ({ pack }) => html`
<a href="#pack-${pack.id}" id="nav-${pack.id}" data-pack-id=${pack.id} title=${pack.title}>
<div class="sticker">
<img src=${makeThumbnailURL(pack.stickers[0].url)}
alt=${pack.stickers[0].body} class="visible" />
</div>
</a>
`
const Pack = ({ pack }) => html`
<section class="stickerpack" id="pack-${pack.id}" data-pack-id=${pack.id}>
<h1>${pack.title}</h1>
<div class="sticker-list">
${pack.stickers.map(sticker => html`
<${Sticker} key=${sticker["net.maunium.telegram.sticker"].id} content=${sticker}/>
`)}
</div>
</section>
`
const Sticker = ({ content }) => html`
<div class="sticker" onClick=${() => sendSticker(content)}>
<img data-src=${makeThumbnailURL(content.url)} alt=${content.body} />
</div>
`
render(html`<${App} />`, document.body)
|