[ABANDONED] React/Redux front end for the Flexor social network.
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.

241 lines
9.1 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
4 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
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
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
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. // composer.tsx
  2. // Copyright (C) 2020 Dwayne Harris
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU General Public License as published by
  5. // the Free Software Foundation, either version 3 of the License, or
  6. // (at your option) any later version.
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU General Public License for more details.
  11. // You should have received a copy of the GNU General Public License
  12. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. import React, { FC, useState, useEffect, useRef } from 'react'
  14. import { useSelector, useDispatch } from 'react-redux'
  15. import { getOrigin } from '../utils'
  16. import { useDeepCompareEffect, useTheme } from '../hooks'
  17. import {
  18. fetchInstallations,
  19. setSelectedInstallation,
  20. setHeight as setComposerHeight,
  21. setError as setComposerError,
  22. saveInstallationSettings,
  23. } from '../actions/composer'
  24. import { showNotification } from '../actions/notifications'
  25. import { createPost } from '../actions/posts'
  26. import { getInstallations, getSelectedInstallation, getError, getHeight as getComposerHeight } from '../selectors/composer'
  27. import { getColorScheme } from '../selectors/theme'
  28. import { AppThunkDispatch, NotificationType, Post } from '../types'
  29. import { IncomingMessageData, OutgoingMessageData } from '../types/communicator'
  30. interface LimiterCollection {
  31. [key: string]: number
  32. }
  33. interface Props {
  34. parent?: Post
  35. onPost?: () => void
  36. }
  37. const Composer: FC<Props> = ({ parent, onPost }) => {
  38. const theme = useTheme()
  39. const colorScheme = useSelector(getColorScheme)
  40. const installations = useSelector(getInstallations)
  41. const installation = useSelector(getSelectedInstallation)
  42. const height = useSelector(getComposerHeight)
  43. const error = useSelector(getError)
  44. const dispatch = useDispatch<AppThunkDispatch>()
  45. const ref = useRef<HTMLIFrameElement>(null)
  46. const [limiters, setLimiters] = useState<LimiterCollection>({})
  47. const composerUrl = installation ? installation.app.composerUrl : undefined
  48. const showComposer = !!composerUrl && !error
  49. useEffect(() => {
  50. dispatch(fetchInstallations())
  51. }, [])
  52. useDeepCompareEffect(() => {
  53. if (!composerUrl) return
  54. if (!installation) return
  55. if (error) return
  56. const listener = async (event: MessageEvent) => {
  57. const origin = getOrigin(composerUrl)
  58. if (event.origin !== origin) return
  59. const postMessage = (message: OutgoingMessageData) => {
  60. if (ref.current && ref.current.contentWindow) {
  61. ref.current.contentWindow.postMessage(JSON.stringify(message), origin)
  62. }
  63. }
  64. const withRateLimit = async (fn: (data: IncomingMessageData) => Promise<void>, data: IncomingMessageData, ms: number = 2000) => {
  65. const last = limiters[data.name] ?? 0
  66. if ((Date.now() - last) > ms) {
  67. await fn(data)
  68. limiters[data.name] = Date.now()
  69. setLimiters(limiters)
  70. } else {
  71. postMessage({
  72. name: data.name,
  73. error: 'Rate limited.',
  74. })
  75. }
  76. }
  77. let data: IncomingMessageData | undefined
  78. try {
  79. data = JSON.parse(event.data)
  80. } catch (err) {
  81. dispatch(setComposerError('Invalid payload'))
  82. return
  83. }
  84. if (!data) return
  85. if (data.publicKey !== installation.app.publicKey) {
  86. const message = 'Invalid publicKey'
  87. dispatch(setComposerError(message))
  88. postMessage({
  89. name: data.name,
  90. error: message,
  91. })
  92. }
  93. switch (data.name) {
  94. case 'init':
  95. postMessage({
  96. name: data.name,
  97. content: {
  98. installationId: installation.id,
  99. settings: installation.settings,
  100. theme,
  101. colorScheme,
  102. parent: parent ? {
  103. text: parent.text,
  104. cover: parent.cover,
  105. attachments: parent.attachments,
  106. data: parent.data,
  107. created: parent.created,
  108. } : undefined,
  109. },
  110. })
  111. break
  112. case 'setHeight':
  113. const { height = 0 } = data.content
  114. dispatch(setComposerHeight(Math.max(Math.min(height, 400), 100)))
  115. postMessage({
  116. name: data.name,
  117. content: {},
  118. })
  119. break
  120. case 'saveSettings':
  121. withRateLimit(async ({ name, content }) => {
  122. try {
  123. await dispatch(saveInstallationSettings(installation.id, content.settings))
  124. postMessage({
  125. name,
  126. content: {
  127. settings: content.settings,
  128. },
  129. })
  130. } catch (error) {
  131. postMessage({
  132. name,
  133. error,
  134. })
  135. dispatch(showNotification(NotificationType.Error, `Error saving settings: ${error.message}`))
  136. }
  137. }, data, 2000)
  138. break
  139. case 'post':
  140. withRateLimit(async ({ name, content }) => {
  141. try {
  142. const postId = await dispatch(createPost({
  143. installation: installation.id,
  144. visible: content.visible,
  145. text: content.text,
  146. cover: content.cover,
  147. attachments: content.attachments,
  148. data: content.data,
  149. parent: parent ? parent.id : undefined,
  150. }))
  151. postMessage({
  152. name,
  153. content: {
  154. postId,
  155. }
  156. })
  157. dispatch(showNotification(NotificationType.Success, `Posted!`))
  158. if (onPost) onPost()
  159. } catch (error) {
  160. postMessage({
  161. name,
  162. error,
  163. })
  164. dispatch(showNotification(NotificationType.Error, `Error posting: ${error.message}`))
  165. }
  166. }, data, 2000)
  167. break
  168. }
  169. }
  170. window.addEventListener('message', listener, false)
  171. return () => {
  172. window.removeEventListener('message', listener, false)
  173. }
  174. }, [installation, parent, error])
  175. const handleClick = (id: string) => {
  176. if (installation && installation.id === id) {
  177. dispatch(setSelectedInstallation())
  178. return
  179. }
  180. dispatch(setSelectedInstallation(id))
  181. }
  182. return (
  183. <div className="composer-container" style={{ borderColor: theme.backgroundSecondary }}>
  184. <div className="composer">
  185. {showComposer &&
  186. <iframe ref={ref} src={composerUrl} scrolling="yes" style={{ height }} />
  187. }
  188. {error && <div className="composer-error" style={{ backgroundColor: theme.backgroundSecondary, color: theme.red }}>Composer Error: {error}</div>}
  189. {(!showComposer && !error) && <div className="composer-empty" style={{ backgroundColor: theme.backgroundSecondary, color: theme.secondary }}>Choose an App.</div>}
  190. </div>
  191. <div className="installations" style={{ backgroundColor: theme.backgroundPrimary }}>
  192. {installations.map(i => (
  193. <div key={i.id} className={installation && installation.id === i.id ? 'selected' : ''} style={{ borderColor: theme.backgroundSecondary }} onClick={() => handleClick(i.id)}>
  194. <img src={i.app.iconImageUrl} alt={i.app.name} style={{ width: 32 }} />
  195. <p style={{ color: theme.text }}>{i.app.name}</p>
  196. </div>
  197. ))}
  198. </div>
  199. </div>
  200. )
  201. }
  202. export default Composer