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.7 KiB
198 lines
7.7 KiB
import React, { FC, useState, useEffect, useRef } from 'react'
|
|
import { useSelector, useDispatch } from 'react-redux'
|
|
|
|
import { getOrigin } from 'src/utils'
|
|
import { useConfig, useDeepCompareEffect, useTheme } 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 { getColorScheme } from 'src/selectors/theme'
|
|
import { AppState, Installation, 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 theme = useTheme()
|
|
const colorScheme = useSelector<AppState, string>(getColorScheme)
|
|
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
|
|
|
|
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,
|
|
theme,
|
|
colorScheme,
|
|
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({
|
|
installation: installation.id,
|
|
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="composer-container" style={{ borderColor: theme.backgroundSecondary }}>
|
|
<div className="composer">
|
|
{showComposer &&
|
|
<iframe ref={ref} src={composerUrl} scrolling="yes" style={{ height }} />
|
|
}
|
|
{error && <div className="composer-error" style={{ backgroundColor: theme.backgroundSecondary, color: theme.red }}>Composer Error: {error}</div>}
|
|
{(!showComposer && !error) && <div className="composer-empty" style={{ backgroundColor: theme.backgroundSecondary, color: theme.secondary }}>Choose an App.</div>}
|
|
</div>
|
|
|
|
<div className="installations" style={{ backgroundColor: theme.backgroundPrimary }}>
|
|
{installations.map(i => (
|
|
<div key={i.id} className={installation && installation.id === i.id ? 'selected' : ''} style={{ borderColor: theme.backgroundSecondary }} onClick={() => handleClick(i.id)}>
|
|
<img src={`${config.blobUrl}${i.app.iconImageUrl}`} alt={i.app.name} style={{ width: 32 }} />
|
|
<p style={{ color: theme.text }}>{i.app.name}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Composer
|