From 7acf2675583e5e340304770ac9e78afcd8687170 Mon Sep 17 00:00:00 2001 From: Jeremy Kahn Date: Sun, 8 Oct 2023 14:19:46 -0400 Subject: [PATCH] feat(embed): [closes #92] Embed support (#177) * feat(embed): [#92] hide unnecessary UI when embedded * feat(embed): [#92] provide embed code --- README.md | 2 + src/components/Shell/Shell.tsx | 30 ++++++--- src/components/Shell/ShellAppBar.tsx | 97 +++++++++++++++------------- src/contexts/ShellContext.ts | 2 + src/models/shell.ts | 4 ++ src/pages/Home/Home.tsx | 88 +++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 6ac45b1..0922ad4 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/components/Shell/Shell.tsx b/src/components/Shell/Shell.tsx index a9070d9..67c3752 100644 --- a/src/components/Shell/Shell.tsx +++ b/src/components/Shell/Shell.tsx @@ -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('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} /> - + {isEmbedded ? null : ( + + )} diff --git a/src/components/Shell/ShellAppBar.tsx b/src/components/Shell/ShellAppBar.tsx index 9caf507..34b8487 100644 --- a/src/components/Shell/ShellAppBar.tsx +++ b/src/components/Shell/ShellAppBar.tsx @@ -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 ( <> @@ -104,44 +105,50 @@ export const ShellAppBar = ({ justifyContent: 'space-between', }} > - - - + {isEmbedded ? null : ( + + + + )} - {title} + {isEmbedded ? '' : title} - - - - - - - - - - + {isEmbedded ? null : ( + <> + + + + + + + + + + + + )} - - - {isFullscreen ? : } - - + + {isFullscreen ? : } + + + )} > @@ -41,6 +42,7 @@ interface ShellContextProps { } export const ShellContext = createContext({ + isEmbedded: false, tabHasFocus: true, showRoomControls: false, setShowRoomControls: () => {}, diff --git a/src/models/shell.ts b/src/models/shell.ts index 8468d39..89a604e 100644 --- a/src/models/shell.ts +++ b/src/models/shell.ts @@ -1,3 +1,7 @@ import { AlertProps } from '@mui/material/Alert' export type AlertOptions = Pick + +export enum QueryParamKeys { + IS_EMBEDDED = 'embed', +} diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index a902738..2054f8c 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -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 (
@@ -110,6 +143,18 @@ export function Home({ userId }: HomeProps) { > Join private room +
@@ -164,6 +209,49 @@ export function Home({ userId }: HomeProps) { . + + Room embed code + + + ) + }, + }} + > + {`\`\`\`html +