You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

109 lines
3.3 KiB

4 years ago
4 years ago
  1. // Copyright (c) 2020 Tulir Asokan
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this
  5. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. import { html, render, Component } from "https://unpkg.com/htm/preact/index.mjs?module"
  7. import { Spinner } from "./spinner.js"
  8. import { sendSticker } from "./widget-api.js"
  9. // The base URL for fetching packs. The app will first fetch ${PACK_BASE_URL}/index.json,
  10. // then ${PACK_BASE_URL}/${packFile} for each packFile in the packs object of the index.json file.
  11. const PACKS_BASE_URL = "packs"
  12. // This is updated from packs/index.json
  13. let HOMESERVER_URL = "https://matrix-client.matrix.org"
  14. const makeThumbnailURL = mxc => `${HOMESERVER_URL}/_matrix/media/r0/thumbnail/${mxc.substr(6)}?height=128&width=128&method=scale`
  15. class App extends Component {
  16. constructor(props) {
  17. super(props)
  18. this.state = {
  19. packs: [],
  20. loading: true,
  21. error: null,
  22. }
  23. this.observer = null
  24. }
  25. observeIntersection = intersections => {
  26. for (const entry of intersections) {
  27. const img = entry.target.children.item(0)
  28. if (entry.isIntersecting) {
  29. img.setAttribute("src", img.getAttribute("data-src"))
  30. img.classList.add("visible")
  31. } else {
  32. img.removeAttribute("src")
  33. img.classList.remove("visible")
  34. }
  35. }
  36. }
  37. componentDidMount() {
  38. fetch(`${PACKS_BASE_URL}/index.json`).then(async indexRes => {
  39. if (indexRes.status >= 400) {
  40. this.setState({
  41. loading: false,
  42. error: indexRes.status !== 404 ? indexRes.statusText : null,
  43. })
  44. return
  45. }
  46. const indexData = await indexRes.json()
  47. HOMESERVER_URL = indexData.homeserver_url || HOMESERVER_URL
  48. // TODO only load pack metadata when scrolled into view?
  49. for (const packFile of indexData.packs) {
  50. const packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`)
  51. const packData = await packRes.json()
  52. this.setState({
  53. packs: [...this.state.packs, packData],
  54. loading: false,
  55. })
  56. }
  57. }, error => this.setState({ loading: false, error }))
  58. this.observer = new IntersectionObserver(this.observeIntersection, {
  59. rootMargin: "100px",
  60. threshold: 0,
  61. })
  62. }
  63. componentDidUpdate() {
  64. for (const elem of document.getElementsByClassName("sticker")) {
  65. this.observer.observe(elem)
  66. }
  67. }
  68. componentWillUnmount() {
  69. this.observer.disconnect()
  70. }
  71. render() {
  72. if (this.state.loading) {
  73. return html`<div class="main spinner"><${Spinner} size=${80} green /></div>`
  74. } else if (this.state.error) {
  75. return html`<div class="main error">
  76. <h1>Failed to load packs</h1>
  77. <p>${this.state.error}</p>
  78. </div>`
  79. } else if (this.state.packs.length === 0) {
  80. return html`<div class="main empty"><h1>No packs found :(</h1></div>`
  81. }
  82. return html`<div class="main pack-list">
  83. ${this.state.packs.map(pack => html`<${Pack} id=${pack.id} ...${pack}/>`)}
  84. </div>`
  85. }
  86. }
  87. const Pack = ({ title, stickers }) => html`<div class="stickerpack">
  88. <h1>${title}</h1>
  89. <div class="sticker-list">
  90. ${stickers.map(sticker => html`
  91. <${Sticker} key=${sticker["net.maunium.telegram.sticker"].id} content=${sticker}/>
  92. `)}
  93. </div>
  94. </div>`
  95. const Sticker = ({ content }) => html`<div class="sticker" onClick=${() => sendSticker(content)}>
  96. <img data-src=${makeThumbnailURL(content.url)} alt=${content.body} />
  97. </div>`
  98. render(html`<${App} />`, document.body)