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.

175 lines
4.8 KiB

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. // The base URL for fetching packs. The app will first fetch ${PACK_BASE_URL}/index.json,
  8. // then ${PACK_BASE_URL}/${packFile} for each packFile in the packs object of the index.json file.
  9. const PACKS_BASE_URL = "packs"
  10. // This is updated from packs/index.json
  11. let HOMESERVER_URL = "https://matrix-client.matrix.org"
  12. const makeThumbnailURL = mxc => `${HOMESERVER_URL}/_matrix/media/r0/thumbnail/${mxc.substr(6)}?height=128&width=128&method=scale`
  13. class App extends Component {
  14. constructor(props) {
  15. super(props)
  16. this.state = {
  17. packs: [],
  18. loading: true,
  19. error: null,
  20. }
  21. this.observer = null
  22. }
  23. observeIntersection = intersections => {
  24. for (const entry of intersections) {
  25. const img = entry.target.children.item(0)
  26. if (entry.isIntersecting) {
  27. img.setAttribute("src", img.getAttribute("data-src"))
  28. img.classList.add("visible")
  29. } else {
  30. img.removeAttribute("src")
  31. img.classList.remove("visible")
  32. }
  33. }
  34. }
  35. componentDidMount() {
  36. fetch(`${PACKS_BASE_URL}/index.json`).then(async indexRes => {
  37. if (indexRes.status >= 400) {
  38. this.setState({
  39. loading: false,
  40. error: indexRes.status !== 404 ? indexRes.statusText : null,
  41. })
  42. return
  43. }
  44. const indexData = await indexRes.json()
  45. HOMESERVER_URL = indexData.homeserver_url || HOMESERVER_URL
  46. // TODO only load pack metadata when scrolled into view?
  47. for (const packFile of indexData.packs) {
  48. const packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`)
  49. const packData = await packRes.json()
  50. this.setState({
  51. packs: [...this.state.packs, packData],
  52. loading: false,
  53. })
  54. }
  55. }, error => this.setState({ loading: false, error }))
  56. this.observer = new IntersectionObserver(this.observeIntersection, {
  57. rootMargin: "100px",
  58. threshold: 0,
  59. })
  60. }
  61. componentDidUpdate() {
  62. for (const elem of document.getElementsByClassName("sticker")) {
  63. this.observer.observe(elem)
  64. }
  65. }
  66. componentWillUnmount() {
  67. this.observer.disconnect()
  68. }
  69. render() {
  70. if (this.state.loading) {
  71. return html`<div class="main spinner"><${Spinner} size=${80} green /></div>`
  72. } else if (this.state.error) {
  73. return html`<div class="main error">
  74. <h1>Failed to load packs</h1>
  75. <p>${this.state.error}</p>
  76. </div>`
  77. } else if (this.state.packs.length === 0) {
  78. return html`<div class="main empty"><h1>No packs found :(</h1></div>`
  79. }
  80. return html`<div class="main pack-list">
  81. ${this.state.packs.map(pack => html`<${Pack} id=${pack.id} ...${pack}/>`)}
  82. </div>`
  83. }
  84. }
  85. const Spinner = ({ size = 40, noCenter = false, noMargin = false, green = false }) => {
  86. let margin = 0
  87. if (!isNaN(+size)) {
  88. size = +size
  89. margin = noMargin ? 0 : `${Math.round(size / 6)}px`
  90. size = `${size}px`
  91. }
  92. const noInnerMargin = !noCenter || !margin
  93. const comp = html`
  94. <div style="width: ${size}; height: ${size}; margin: ${noInnerMargin ? 0 : margin} 0;"
  95. class="sk-chase ${green && "green"}">
  96. <div class="sk-chase-dot" />
  97. <div class="sk-chase-dot" />
  98. <div class="sk-chase-dot" />
  99. <div class="sk-chase-dot" />
  100. <div class="sk-chase-dot" />
  101. <div class="sk-chase-dot" />
  102. </div>
  103. `
  104. if (!noCenter) {
  105. return html`<div style="margin: ${margin} 0;" class="sk-center-wrapper">${comp}</div>`
  106. }
  107. return comp
  108. }
  109. const Pack = ({ title, stickers }) => html`<div class="stickerpack">
  110. <h1>${title}</h1>
  111. <div class="sticker-list">
  112. ${stickers.map(sticker => html`
  113. <${Sticker} key=${sticker["net.maunium.telegram.sticker"].id} content=${sticker}/>
  114. `)}
  115. </div>
  116. </div>`
  117. const Sticker = ({ content }) => html`<div class="sticker" onClick=${() => sendSticker(content)}>
  118. <img data-src=${makeThumbnailURL(content.url)} alt=${content.body} />
  119. </div>`
  120. function sendSticker(content) {
  121. window.parent.postMessage({
  122. api: "fromWidget",
  123. action: "m.sticker",
  124. requestId: `sticker-${Date.now()}`,
  125. widgetId,
  126. data: {
  127. name: content.body,
  128. content,
  129. },
  130. }, "*")
  131. }
  132. let widgetId = null
  133. window.onmessage = event => {
  134. if (!window.parent || !event.data) {
  135. return
  136. }
  137. const request = event.data
  138. if (!request.requestId || !request.widgetId || !request.action || request.api !== "toWidget") {
  139. return
  140. }
  141. if (widgetId) {
  142. if (widgetId !== request.widgetId) {
  143. return
  144. }
  145. } else {
  146. widgetId = request.widgetId
  147. }
  148. window.parent.postMessage({
  149. ...request,
  150. response: request.action === "capabilities" ? {
  151. capabilities: ["m.sticker"],
  152. } : {
  153. error: { message: "Action not supported" },
  154. },
  155. }, event.origin)
  156. }
  157. render(html`<${App} />`, document.body)