forked from Shiloh/remnantchat
* feat(embed): [#92] hide unnecessary UI when embedded * feat(embed): [#92] provide embed code
This commit is contained in:
parent
1b43b4aa00
commit
7acf267558
@ -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.
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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"
|
||||
|
@ -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: () => {},
|
||||
|
@ -1,3 +1,7 @@
|
||||
import { AlertProps } from '@mui/material/Alert'
|
||||
|
||||
export type AlertOptions = Pick<AlertProps, 'severity'>
|
||||
|
||||
export enum QueryParamKeys {
|
||||
IS_EMBEDDED = 'embed',
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user