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.

145 lines
4.4 KiB

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