feat(embed): [closes #92] Embed support (#177)

* feat(embed): [#92] hide unnecessary UI when embedded
* feat(embed): [#92] provide embed code
This commit is contained in:
Jeremy Kahn 2023-10-08 14:19:46 -04:00 committed by GitHub
parent 1b43b4aa00
commit 7acf267558
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 169 additions and 54 deletions

View File

@ -14,6 +14,7 @@ Chitchatter is a free (as in both price and freedom) communication tool. Designe
- Message content is never persisted to disk on either the client or server
- Decentralized
- There is no API server. All that's required for Chitchatter to function is availability of GitHub for static assets, and public WebTorrent and STUN/TURN relay servers for establishing peer-to-peer communication.
- Embeddable
- [Self-hostable](#self-hosting)
Chitchatter uses the [Create React App](https://github.com/facebook/create-react-app) toolchain. The secure networking and streaming magic would not be possible without [Trystero](https://github.com/dmotz/trystero). File transfer functionality is powered by [`secure-file-transfer`](https://github.com/jeremyckahn/secure-file-transfer).
@ -31,6 +32,7 @@ Open https://chitchatter.im/ and join a room to start chatting with anyone else
- File sharing:
- Unlimited file size transfers.
- Files are encrypted prior to sending and decrypted by the receiver (the key is the room name).
- Embedding into other web apps via `iframe`.
- Markdown support via [`react-markdown`](https://github.com/remarkjs/react-markdown).
- Includes support for syntax highlighting of code.
- Conversation backfilling from peers when a new participant joins.

View File

@ -15,7 +15,7 @@ import { useWindowSize } from '@react-hook/window-size'
import { ShellContext } from 'contexts/ShellContext'
import { SettingsContext } from 'contexts/SettingsContext'
import { AlertOptions } from 'models/shell'
import { AlertOptions, QueryParamKeys } from 'models/shell'
import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
import { ErrorBoundary } from 'components/ErrorBoundary'
@ -41,8 +41,11 @@ export interface ShellProps extends PropsWithChildren {
appNeedsUpdate: boolean
}
const queryParams = new URLSearchParams(window.location.search)
export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
const { getUserSettings, updateUserSettings } = useContext(SettingsContext)
const isEmbedded = queryParams.get(QueryParamKeys.IS_EMBEDDED) !== null
const { colorMode } = getUserSettings()
@ -65,7 +68,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
const [isRoomShareDialogOpen, setIsRoomShareDialogOpen] = useState(false)
const [alertSeverity, setAlertSeverity] = useState<AlertColor>('info')
const [showAppBar, setShowAppBar] = useState(true)
const [showRoomControls, setShowRoomControls] = useState(true)
const [showRoomControls, setShowRoomControls] = useState(!isEmbedded)
const [isFullscreen, setIsFullscreen] = useState(false)
const [title, setTitle] = useState('')
const [alertText, setAlertText] = useState('')
@ -118,6 +121,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
const shellContextValue = useMemo(
() => ({
isEmbedded,
tabHasFocus,
showRoomControls,
setShowRoomControls,
@ -150,6 +154,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
updatePeer,
}),
[
isEmbedded,
isPeerListOpen,
setIsQRCodeDialogOpen,
roomId,
@ -244,9 +249,12 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
} else {
exitFullscreen()
setShowAppBar(true)
setShowRoomControls(true)
if (!isEmbedded) {
setShowRoomControls(true)
}
}
}, [isFullscreen, setShowRoomControls, setShowAppBar])
}, [isFullscreen, setShowRoomControls, setShowAppBar, isEmbedded])
useEffect(() => {
if (isFullscreen) setShowAppBar(showRoomControls)
@ -344,13 +352,15 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
isFullscreen={isFullscreen}
setIsFullscreen={setIsFullscreen}
/>
<Drawer
isDrawerOpen={isDrawerOpen}
onDrawerClose={handleDrawerClose}
theme={theme}
/>
{isEmbedded ? null : (
<Drawer
isDrawerOpen={isDrawerOpen}
onDrawerClose={handleDrawerClose}
theme={theme}
/>
)}
<RouteContent
isDrawerOpen={isDrawerOpen}
isDrawerOpen={isEmbedded ? true : isDrawerOpen}
isPeerListOpen={isPeerListOpen}
showAppBar={showAppBar}
>

View File

@ -85,9 +85,10 @@ export const ShellAppBar = ({
isFullscreen,
setIsFullscreen,
}: ShellAppBarProps) => {
const { peerList } = useContext(ShellContext)
const { peerList, isEmbedded } = useContext(ShellContext)
const handleQRCodeClick = () => setIsQRCodeDialogOpen(true)
const onClickFullscreen = () => setIsFullscreen(!isFullscreen)
return (
<>
<Slide appear={false} in={showAppBar} mountOnEnter unmountOnExit>
@ -104,44 +105,50 @@ export const ShellAppBar = ({
justifyContent: 'space-between',
}}
>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="Open menu"
sx={{ mr: 2, ...(isDrawerOpen && { display: 'none' }) }}
onClick={onDrawerOpen}
>
<Menu />
</IconButton>
{isEmbedded ? null : (
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="Open menu"
sx={{ mr: 2, ...(isDrawerOpen && { display: 'none' }) }}
onClick={onDrawerOpen}
>
<Menu />
</IconButton>
)}
<Typography
variant="h6"
noWrap
component="div"
sx={{ marginRight: 'auto' }}
>
{title}
{isEmbedded ? '' : title}
</Typography>
<Tooltip title="Copy current URL">
<IconButton
size="large"
color="inherit"
aria-label="Copy current URL"
onClick={onLinkButtonClick}
>
<Link />
</IconButton>
</Tooltip>
<Tooltip title="Show QR Code">
<IconButton
size="large"
color="inherit"
aria-label="Show QR Code"
onClick={handleQRCodeClick}
>
<QrCode2 />
</IconButton>
</Tooltip>
{isEmbedded ? null : (
<>
<Tooltip title="Copy current URL">
<IconButton
size="large"
color="inherit"
aria-label="Copy current URL"
onClick={onLinkButtonClick}
>
<Link />
</IconButton>
</Tooltip>
<Tooltip title="Show QR Code">
<IconButton
size="large"
color="inherit"
aria-label="Show QR Code"
onClick={handleQRCodeClick}
>
<QrCode2 />
</IconButton>
</Tooltip>
</>
)}
<Tooltip title="Show Room Controls">
<IconButton
size="large"
@ -152,19 +159,21 @@ export const ShellAppBar = ({
<RoomPreferences />
</IconButton>
</Tooltip>
<Tooltip
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
>
<IconButton
size="large"
edge="end"
color="inherit"
aria-label="fullscreen"
onClick={onClickFullscreen}
{isEmbedded ? null : (
<Tooltip
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
>
{isFullscreen ? <FullscreenExit /> : <Fullscreen />}
</IconButton>
</Tooltip>
<IconButton
size="large"
edge="end"
color="inherit"
aria-label="fullscreen"
onClick={onClickFullscreen}
>
{isFullscreen ? <FullscreenExit /> : <Fullscreen />}
</IconButton>
</Tooltip>
)}
<Tooltip title="Click to show peer list">
<IconButton
size="large"

View File

@ -7,6 +7,7 @@ import { ConnectionTestResults } from 'components/Shell/useConnectionTest'
import { TrackerConnection } from 'services/ConnectionTest/ConnectionTest'
interface ShellContextProps {
isEmbedded: boolean
tabHasFocus: boolean
showRoomControls: boolean
setShowRoomControls: Dispatch<SetStateAction<boolean>>
@ -41,6 +42,7 @@ interface ShellContextProps {
}
export const ShellContext = createContext<ShellContextProps>({
isEmbedded: false,
tabHasFocus: true,
showRoomControls: false,
setShowRoomControls: () => {},

View File

@ -1,3 +1,7 @@
import { AlertProps } from '@mui/material/Alert'
export type AlertOptions = Pick<AlertProps, 'severity'>
export enum QueryParamKeys {
IS_EMBEDDED = 'embed',
}

View File

@ -10,7 +10,28 @@ import IconButton from '@mui/material/IconButton'
import MuiLink from '@mui/material/Link'
import GitHubIcon from '@mui/icons-material/GitHub'
import Cached from '@mui/icons-material/Cached'
import Dialog from '@mui/material/Dialog'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText'
import DialogTitle from '@mui/material/DialogTitle'
import { v4 as uuid } from 'uuid'
// These imports need to be ts-ignored to prevent spurious errors that look
// like this:
//
// Module 'react-markdown' cannot be imported using this construct. The
// specifier only resolves to an ES module, which cannot be imported
// synchronously. Use dynamic import instead. (tsserver 1471)
//
// @ts-ignore
import Markdown from 'react-markdown'
// @ts-ignore
import { CodeProps } from 'react-markdown/lib/ast-to-react'
// @ts-ignore
import remarkGfm from 'remark-gfm'
import { routes } from 'config/routes'
import { ShellContext } from 'contexts/ShellContext'
@ -24,6 +45,7 @@ interface HomeProps {
export function Home({ userId }: HomeProps) {
const { setTitle } = useContext(ShellContext)
const [roomName, setRoomName] = useState(uuid())
const [showEmbedCode, setShowEmbedCode] = useState(false)
const navigate = useNavigate()
useEffect(() => {
@ -47,8 +69,19 @@ export function Home({ userId }: HomeProps) {
navigate(`/private/${roomName}`)
}
const handleGetEmbedCodeClick = () => {
setShowEmbedCode(true)
}
const handleEmbedCodeWindowClose = () => {
setShowEmbedCode(false)
}
const isRoomNameValid = roomName.length > 0
const embedUrl = new URL(`${window.location.origin}/public/${roomName}`)
embedUrl.search = new URLSearchParams({ embed: '1' }).toString()
return (
<Box className="Home">
<main className="mt-6 px-4 max-w-3xl text-center mx-auto">
@ -110,6 +143,18 @@ export function Home({ userId }: HomeProps) {
>
Join private room
</Button>
<Button
variant="contained"
color="secondary"
onClick={handleGetEmbedCodeClick}
sx={{
marginTop: 2,
marginLeft: 2,
}}
disabled={!isRoomNameValid}
>
Get embed code
</Button>
</Box>
</form>
</main>
@ -164,6 +209,49 @@ export function Home({ userId }: HomeProps) {
</MuiLink>
.
</Typography>
<Dialog open={showEmbedCode} onClose={handleEmbedCodeWindowClose}>
<DialogTitle>Room embed code</DialogTitle>
<DialogContent>
<Markdown
remarkPlugins={[remarkGfm]}
components={{
// https://github.com/remarkjs/react-markdown#use-custom-components-syntax-highlight
code({
node,
inline,
className,
children,
style,
...props
}: CodeProps) {
return (
<SyntaxHighlighter
children={String(children).replace(/\n$/, '')}
language="html"
style={materialDark}
PreTag="div"
lineProps={{
style: { wordBreak: 'break-all', whiteSpace: 'pre-wrap' },
}}
wrapLines={true}
{...props}
/>
)
},
}}
>
{`\`\`\`html
<iframe src="${embedUrl}" allow="camera;microphone;display-capture" width="800" height="800" />
\`\`\``}
</Markdown>
<DialogContentText sx={{ mb: 2 }}>
Copy and paste this HTML snippet into your project.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleEmbedCodeWindowClose}>Close</Button>
</DialogActions>
</Dialog>
</Box>
)
}