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
241 lines
9.1 KiB
// composer.tsx
|
|
// Copyright (C) 2020 Dwayne Harris
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import React, { FC, useState, useEffect, useRef } from 'react'
|
|
import { useSelector, useDispatch } from 'react-redux'
|
|
|
|
import { getOrigin } from '../utils'
|
|
import { useDeepCompareEffect, useTheme } from '../hooks'
|
|
import {
|
|
fetchInstallations,
|
|
setSelectedInstallation,
|
|
setHeight as setComposerHeight,
|
|
setError as setComposerError,
|
|
saveInstallationSettings,
|
|
} from '../actions/composer'
|
|
import { showNotification } from '../actions/notifications'
|
|
import { createPost } from '../actions/posts'
|
|
import { getInstallations, getSelectedInstallation, getError, getHeight as getComposerHeight } from '../selectors/composer'
|
|
import { getColorScheme } from '../selectors/theme'
|
|
import { AppThunkDispatch, NotificationType, Post } from '../types'
|
|
import { IncomingMessageData, OutgoingMessageData } from '../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(getColorScheme)
|
|
const installations = useSelector(getInstallations)
|
|
const installation = useSelector(getSelectedInstallation)
|
|
const height = useSelector(getComposerHeight)
|
|
const error = useSelector(getError)
|
|
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 'saveSettings':
|
|
withRateLimit(async ({ name, content }) => {
|
|
try {
|
|
await dispatch(saveInstallationSettings(installation.id, content.settings))
|
|
|
|
postMessage({
|
|
name,
|
|
content: {
|
|
settings: content.settings,
|
|
},
|
|
})
|
|
} catch (error) {
|
|
postMessage({
|
|
name,
|
|
error,
|
|
})
|
|
|
|
dispatch(showNotification(NotificationType.Error, `Error saving settings: ${error.message}`))
|
|
}
|
|
}, data, 2000)
|
|
|
|
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 (error) {
|
|
postMessage({
|
|
name,
|
|
error,
|
|
})
|
|
|
|
dispatch(showNotification(NotificationType.Error, `Error posting: ${error.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={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
|