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

198 lines
7.3 KiB

import React, { FC, useState, useEffect, useRef } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'
import { getOrigin } from 'src/utils'
import { useConfig, useDeepCompareEffect } from 'src/hooks'
import { fetchInstallations, setSelectedInstallation, setHeight as setComposerHeight, setError as setComposerError } from 'src/actions/composer'
import { showNotification } from 'src/actions/notifications'
import { createPost } from 'src/actions/posts'
import { getInstallations, getSelectedInstallation, getError, getHeight as getComposerHeight } from 'src/selectors/composer'
import { AppState, Installation, ClassDictionary, AppThunkDispatch, NotificationType, Post } from 'src/types'
import { IncomingMessageData, OutgoingMessageData } from 'src/types/communicator'
interface LimiterCollection {
[key: string]: number
}
interface Props {
parent?: Post
onPost?: () => void
}
const Composer: FC<Props> = ({ parent, onPost }) => {
const installations = useSelector<AppState, Installation[]>(getInstallations)
const installation = useSelector<AppState, Installation | undefined>(getSelectedInstallation)
const height = useSelector<AppState, number>(getComposerHeight)
const error = useSelector<AppState, string | undefined>(getError)
const config = useConfig()
const dispatch = useDispatch<AppThunkDispatch>()
const ref = useRef<HTMLIFrameElement>(null)
const [limiters, setLimiters] = useState<LimiterCollection>({})
const composerUrl = installation ? installation.app.composerUrl : undefined
const showComposer = !!composerUrl && !error
const composerClasses: ClassDictionary = {
composer: true,
'composer-empty': !showComposer,
}
useEffect(() => {
dispatch(fetchInstallations())
}, [])
useDeepCompareEffect(() => {
if (!composerUrl) return
if (!installation) return
if (error) return
const listener = async (event: MessageEvent) => {
const origin = getOrigin(composerUrl)
if (event.origin !== origin) return
const postMessage = (message: OutgoingMessageData) => {
if (ref.current && ref.current.contentWindow) {
ref.current.contentWindow.postMessage(JSON.stringify(message), origin)
}
}
const withRateLimit = async (fn: (data: IncomingMessageData) => Promise<void>, data: IncomingMessageData, ms: number = 2000) => {
const last = limiters[data.name] || 0
if ((Date.now() - last) > ms) {
await fn(data)
limiters[data.name] = Date.now()
setLimiters(limiters)
} else {
postMessage({
name: data.name,
error: 'Rate limited.',
})
}
}
let data: IncomingMessageData | undefined
try {
data = JSON.parse(event.data)
} catch (err) {
dispatch(setComposerError('Invalid payload'))
return
}
if (!data) return
if (data.publicKey !== installation.app.publicKey) {
const message = 'Invalid publicKey'
dispatch(setComposerError(message))
postMessage({
name: data.name,
error: message,
})
}
switch (data.name) {
case 'init':
postMessage({
name: data.name,
content: {
installationId: installation.id,
settings: installation.settings,
parent: parent ? {
text: parent.text,
cover: parent.cover,
attachments: parent.attachments,
data: parent.data,
created: parent.created,
} : undefined,
},
})
break
case 'setHeight':
const { height = 0 } = data.content
dispatch(setComposerHeight(Math.max(Math.min(height, 400), 100)))
postMessage({
name: data.name,
content: {},
})
break
case 'post':
withRateLimit(async ({ name, content }) => {
try {
const postId = await dispatch(createPost({
visible: content.visible,
text: content.text,
cover: content.cover,
attachments: content.attachments,
data: content.data,
parent: parent ? parent.id : undefined,
}))
postMessage({
name,
content: {
postId,
}
})
dispatch(showNotification(NotificationType.Success, `Posted!`))
if (onPost) onPost()
} catch (err) {
postMessage({
name,
error: err,
})
dispatch(showNotification(NotificationType.Error, `Error posting: ${err.message}`))
}
}, data, 2000)
break
}
}
window.addEventListener('message', listener, false)
return () => {
window.removeEventListener('message', listener, false)
}
}, [installation, parent, error])
const handleClick = (id: string) => {
if (installation && installation.id === id) {
dispatch(setSelectedInstallation())
return
}
dispatch(setSelectedInstallation(id))
}
return (
<div className="container composer-container">
<div className={classNames(composerClasses)}>
{showComposer &&
<iframe ref={ref} src={composerUrl} scrolling="yes" style={{ height, width: '100%', overflow: 'hidden' }} />
}
{error && <span className="has-text-danger">Composer Error: {error}</span>}
{(!showComposer && !error) && <span>Choose an App.</span>}
</div>
<div className="installations is-flex">
{installations.map(i => (
<div key={i.id} className={installation && installation.id === i.id ? 'selected' : ''} onClick={() => handleClick(i.id)}>
<img src={`${config.blobUrl}${i.app.iconImageUrl}`} alt={i.app.name} style={{ width: 32 }} />
<p className="is-size-7 has-text-weight-bold">{i.app.name}</p>
</div>
))}
</div>
</div>
)
}
export default Composer