[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.

225 lines
8.4 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
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
5 years ago
  1. import React, { FC, useState, useEffect, useRef } from 'react'
  2. import { useSelector, useDispatch } from 'react-redux'
  3. import { getOrigin } from '../utils'
  4. import { useDeepCompareEffect, useTheme } from '../hooks'
  5. import {
  6. fetchInstallations,
  7. setSelectedInstallation,
  8. setHeight as setComposerHeight,
  9. setError as setComposerError,
  10. saveInstallationSettings,
  11. } from '../actions/composer'
  12. import { showNotification } from '../actions/notifications'
  13. import { createPost } from '../actions/posts'
  14. import { getInstallations, getSelectedInstallation, getError, getHeight as getComposerHeight } from '../selectors/composer'
  15. import { getColorScheme } from '../selectors/theme'
  16. import { AppThunkDispatch, NotificationType, Post } from '../types'
  17. import { IncomingMessageData, OutgoingMessageData } from '../types/communicator'
  18. interface LimiterCollection {
  19. [key: string]: number
  20. }
  21. interface Props {
  22. parent?: Post
  23. onPost?: () => void
  24. }
  25. const Composer: FC<Props> = ({ parent, onPost }) => {
  26. const theme = useTheme()
  27. const colorScheme = useSelector(getColorScheme)
  28. const installations = useSelector(getInstallations)
  29. const installation = useSelector(getSelectedInstallation)
  30. const height = useSelector(getComposerHeight)
  31. const error = useSelector(getError)
  32. const dispatch = useDispatch<AppThunkDispatch>()
  33. const ref = useRef<HTMLIFrameElement>(null)
  34. const [limiters, setLimiters] = useState<LimiterCollection>({})
  35. const composerUrl = installation ? installation.app.composerUrl : undefined
  36. const showComposer = !!composerUrl && !error
  37. useEffect(() => {
  38. dispatch(fetchInstallations())
  39. }, [])
  40. useDeepCompareEffect(() => {
  41. if (!composerUrl) return
  42. if (!installation) return
  43. if (error) return
  44. const listener = async (event: MessageEvent) => {
  45. const origin = getOrigin(composerUrl)
  46. if (event.origin !== origin) return
  47. const postMessage = (message: OutgoingMessageData) => {
  48. if (ref.current && ref.current.contentWindow) {
  49. ref.current.contentWindow.postMessage(JSON.stringify(message), origin)
  50. }
  51. }
  52. const withRateLimit = async (fn: (data: IncomingMessageData) => Promise<void>, data: IncomingMessageData, ms: number = 2000) => {
  53. const last = limiters[data.name] ?? 0
  54. if ((Date.now() - last) > ms) {
  55. await fn(data)
  56. limiters[data.name] = Date.now()
  57. setLimiters(limiters)
  58. } else {
  59. postMessage({
  60. name: data.name,
  61. error: 'Rate limited.',
  62. })
  63. }
  64. }
  65. let data: IncomingMessageData | undefined
  66. try {
  67. data = JSON.parse(event.data)
  68. } catch (err) {
  69. dispatch(setComposerError('Invalid payload'))
  70. return
  71. }
  72. if (!data) return
  73. if (data.publicKey !== installation.app.publicKey) {
  74. const message = 'Invalid publicKey'
  75. dispatch(setComposerError(message))
  76. postMessage({
  77. name: data.name,
  78. error: message,
  79. })
  80. }
  81. switch (data.name) {
  82. case 'init':
  83. postMessage({
  84. name: data.name,
  85. content: {
  86. installationId: installation.id,
  87. settings: installation.settings,
  88. theme,
  89. colorScheme,
  90. parent: parent ? {
  91. text: parent.text,
  92. cover: parent.cover,
  93. attachments: parent.attachments,
  94. data: parent.data,
  95. created: parent.created,
  96. } : undefined,
  97. },
  98. })
  99. break
  100. case 'setHeight':
  101. const { height = 0 } = data.content
  102. dispatch(setComposerHeight(Math.max(Math.min(height, 400), 100)))
  103. postMessage({
  104. name: data.name,
  105. content: {},
  106. })
  107. break
  108. case 'saveSettings':
  109. withRateLimit(async ({ name, content }) => {
  110. try {
  111. await dispatch(saveInstallationSettings(installation.id, content.settings))
  112. postMessage({
  113. name,
  114. content: {
  115. settings: content.settings,
  116. },
  117. })
  118. } catch (error) {
  119. postMessage({
  120. name,
  121. error,
  122. })
  123. dispatch(showNotification(NotificationType.Error, `Error saving settings: ${error.message}`))
  124. }
  125. }, data, 2000)
  126. break
  127. case 'post':
  128. withRateLimit(async ({ name, content }) => {
  129. try {
  130. const postId = await dispatch(createPost({
  131. installation: installation.id,
  132. visible: content.visible,
  133. text: content.text,
  134. cover: content.cover,
  135. attachments: content.attachments,
  136. data: content.data,
  137. parent: parent ? parent.id : undefined,
  138. }))
  139. postMessage({
  140. name,
  141. content: {
  142. postId,
  143. }
  144. })
  145. dispatch(showNotification(NotificationType.Success, `Posted!`))
  146. if (onPost) onPost()
  147. } catch (error) {
  148. postMessage({
  149. name,
  150. error,
  151. })
  152. dispatch(showNotification(NotificationType.Error, `Error posting: ${error.message}`))
  153. }
  154. }, data, 2000)
  155. break
  156. }
  157. }
  158. window.addEventListener('message', listener, false)
  159. return () => {
  160. window.removeEventListener('message', listener, false)
  161. }
  162. }, [installation, parent, error])
  163. const handleClick = (id: string) => {
  164. if (installation && installation.id === id) {
  165. dispatch(setSelectedInstallation())
  166. return
  167. }
  168. dispatch(setSelectedInstallation(id))
  169. }
  170. return (
  171. <div className="composer-container" style={{ borderColor: theme.backgroundSecondary }}>
  172. <div className="composer">
  173. {showComposer &&
  174. <iframe ref={ref} src={composerUrl} scrolling="yes" style={{ height }} />
  175. }
  176. {error && <div className="composer-error" style={{ backgroundColor: theme.backgroundSecondary, color: theme.red }}>Composer Error: {error}</div>}
  177. {(!showComposer && !error) && <div className="composer-empty" style={{ backgroundColor: theme.backgroundSecondary, color: theme.secondary }}>Choose an App.</div>}
  178. </div>
  179. <div className="installations" style={{ backgroundColor: theme.backgroundPrimary }}>
  180. {installations.map(i => (
  181. <div key={i.id} className={installation && installation.id === i.id ? 'selected' : ''} style={{ borderColor: theme.backgroundSecondary }} onClick={() => handleClick(i.id)}>
  182. <img src={i.app.iconImageUrl} alt={i.app.name} style={{ width: 32 }} />
  183. <p style={{ color: theme.text }}>{i.app.name}</p>
  184. </div>
  185. ))}
  186. </div>
  187. </div>
  188. )
  189. }
  190. export default Composer