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.

122 lines
3.8 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 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. this.packListRef = null
  25. }
  26. observeIntersection = intersections => {
  27. for (const entry of intersections) {
  28. const img = entry.target.children.item(0)
  29. if (entry.isIntersecting) {
  30. img.setAttribute("src", img.getAttribute("data-src"))
  31. img.classList.add("visible")
  32. } else {
  33. img.removeAttribute("src")
  34. img.classList.remove("visible")
  35. }
  36. }
  37. }
  38. componentDidMount() {
  39. fetch(`${PACKS_BASE_URL}/index.json`).then(async indexRes => {
  40. if (indexRes.status >= 400) {
  41. this.setState({
  42. loading: false,
  43. error: indexRes.status !== 404 ? indexRes.statusText : null,
  44. })
  45. return
  46. }
  47. const indexData = await indexRes.json()
  48. HOMESERVER_URL = indexData.homeserver_url || HOMESERVER_URL
  49. // TODO only load pack metadata when scrolled into view?
  50. for (const packFile of indexData.packs) {
  51. const packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`)
  52. const packData = await packRes.json()
  53. this.setState({
  54. packs: [...this.state.packs, packData],
  55. loading: false,
  56. })
  57. }
  58. }, error => this.setState({ loading: false, error }))
  59. this.observer = new IntersectionObserver(this.observeIntersection, {
  60. rootMargin: "100px",
  61. threshold: 0,
  62. })
  63. }
  64. componentDidUpdate() {
  65. for (const elem of this.packListRef.getElementsByClassName("sticker")) {
  66. this.observer.observe(elem)
  67. }
  68. }
  69. componentWillUnmount() {
  70. this.observer.disconnect()
  71. }
  72. render() {
  73. if (this.state.loading) {
  74. return html`<main class="spinner"><${Spinner} size=${80} green /></main>`
  75. } else if (this.state.error) {
  76. return html`<main class="error">
  77. <h1>Failed to load packs</h1>
  78. <p>${this.state.error}</p>
  79. </main>`
  80. } else if (this.state.packs.length === 0) {
  81. return html`<main class="empty"><h1>No packs found :(</h1></main>`
  82. }
  83. return html`<main>
  84. <nav>
  85. ${this.state.packs.map(pack => html`<${NavBarItem} id=${pack.id} pack=${pack}/>`)}
  86. </nav>
  87. <div class="pack-list" ref=${elem => this.packListRef = elem}>
  88. ${this.state.packs.map(pack => html`<${Pack} id=${pack.id} pack=${pack}/>`)}
  89. </div>
  90. </main>`
  91. }
  92. }
  93. const NavBarItem = ({ pack }) => html`<a href="#pack-${pack.id}" title=${pack.title}>
  94. <div class="sticker">
  95. <img src=${makeThumbnailURL(pack.stickers[0].url)}
  96. alt=${pack.stickers[0].body} class="visible" />
  97. </div>
  98. </a>`
  99. const Pack = ({ pack }) => html`<section class="stickerpack" id=${`pack-${pack.id}`}>
  100. <h1>${pack.title}</h1>
  101. <div class="sticker-list">
  102. ${pack.stickers.map(sticker => html`
  103. <${Sticker} key=${sticker["net.maunium.telegram.sticker"].id} content=${sticker}/>
  104. `)}
  105. </div>
  106. </section>`
  107. const Sticker = ({ content }) => html`<div class="sticker" onClick=${() => sendSticker(content)}>
  108. <img data-src=${makeThumbnailURL(content.url)} alt=${content.body} />
  109. </div>`
  110. render(html`<${App} />`, document.body)