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
|
- Message content is never persisted to disk on either the client or server
|
||||||
- Decentralized
|
- 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.
|
- 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)
|
- [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).
|
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:
|
- File sharing:
|
||||||
- Unlimited file size transfers.
|
- Unlimited file size transfers.
|
||||||
- Files are encrypted prior to sending and decrypted by the receiver (the key is the room name).
|
- 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).
|
- Markdown support via [`react-markdown`](https://github.com/remarkjs/react-markdown).
|
||||||
- Includes support for syntax highlighting of code.
|
- Includes support for syntax highlighting of code.
|
||||||
- Conversation backfilling from peers when a new participant joins.
|
- 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 { ShellContext } from 'contexts/ShellContext'
|
||||||
import { SettingsContext } from 'contexts/SettingsContext'
|
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 { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
|
||||||
import { ErrorBoundary } from 'components/ErrorBoundary'
|
import { ErrorBoundary } from 'components/ErrorBoundary'
|
||||||
|
|
||||||
@ -41,8 +41,11 @@ export interface ShellProps extends PropsWithChildren {
|
|||||||
appNeedsUpdate: boolean
|
appNeedsUpdate: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||||
const { getUserSettings, updateUserSettings } = useContext(SettingsContext)
|
const { getUserSettings, updateUserSettings } = useContext(SettingsContext)
|
||||||
|
const isEmbedded = queryParams.get(QueryParamKeys.IS_EMBEDDED) !== null
|
||||||
|
|
||||||
const { colorMode } = getUserSettings()
|
const { colorMode } = getUserSettings()
|
||||||
|
|
||||||
@ -65,7 +68,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
|||||||
const [isRoomShareDialogOpen, setIsRoomShareDialogOpen] = useState(false)
|
const [isRoomShareDialogOpen, setIsRoomShareDialogOpen] = useState(false)
|
||||||
const [alertSeverity, setAlertSeverity] = useState<AlertColor>('info')
|
const [alertSeverity, setAlertSeverity] = useState<AlertColor>('info')
|
||||||
const [showAppBar, setShowAppBar] = useState(true)
|
const [showAppBar, setShowAppBar] = useState(true)
|
||||||
const [showRoomControls, setShowRoomControls] = useState(true)
|
const [showRoomControls, setShowRoomControls] = useState(!isEmbedded)
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [alertText, setAlertText] = useState('')
|
const [alertText, setAlertText] = useState('')
|
||||||
@ -118,6 +121,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
|||||||
|
|
||||||
const shellContextValue = useMemo(
|
const shellContextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
isEmbedded,
|
||||||
tabHasFocus,
|
tabHasFocus,
|
||||||
showRoomControls,
|
showRoomControls,
|
||||||
setShowRoomControls,
|
setShowRoomControls,
|
||||||
@ -150,6 +154,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
|||||||
updatePeer,
|
updatePeer,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
|
isEmbedded,
|
||||||
isPeerListOpen,
|
isPeerListOpen,
|
||||||
setIsQRCodeDialogOpen,
|
setIsQRCodeDialogOpen,
|
||||||
roomId,
|
roomId,
|
||||||
@ -244,9 +249,12 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
|||||||
} else {
|
} else {
|
||||||
exitFullscreen()
|
exitFullscreen()
|
||||||
setShowAppBar(true)
|
setShowAppBar(true)
|
||||||
setShowRoomControls(true)
|
|
||||||
|
if (!isEmbedded) {
|
||||||
|
setShowRoomControls(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [isFullscreen, setShowRoomControls, setShowAppBar])
|
}, [isFullscreen, setShowRoomControls, setShowAppBar, isEmbedded])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFullscreen) setShowAppBar(showRoomControls)
|
if (isFullscreen) setShowAppBar(showRoomControls)
|
||||||
@ -344,13 +352,15 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
|||||||
isFullscreen={isFullscreen}
|
isFullscreen={isFullscreen}
|
||||||
setIsFullscreen={setIsFullscreen}
|
setIsFullscreen={setIsFullscreen}
|
||||||
/>
|
/>
|
||||||
<Drawer
|
{isEmbedded ? null : (
|
||||||
isDrawerOpen={isDrawerOpen}
|
<Drawer
|
||||||
onDrawerClose={handleDrawerClose}
|
isDrawerOpen={isDrawerOpen}
|
||||||
theme={theme}
|
onDrawerClose={handleDrawerClose}
|
||||||
/>
|
theme={theme}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<RouteContent
|
<RouteContent
|
||||||
isDrawerOpen={isDrawerOpen}
|
isDrawerOpen={isEmbedded ? true : isDrawerOpen}
|
||||||
isPeerListOpen={isPeerListOpen}
|
isPeerListOpen={isPeerListOpen}
|
||||||
showAppBar={showAppBar}
|
showAppBar={showAppBar}
|
||||||
>
|
>
|
||||||
|
@ -85,9 +85,10 @@ export const ShellAppBar = ({
|
|||||||
isFullscreen,
|
isFullscreen,
|
||||||
setIsFullscreen,
|
setIsFullscreen,
|
||||||
}: ShellAppBarProps) => {
|
}: ShellAppBarProps) => {
|
||||||
const { peerList } = useContext(ShellContext)
|
const { peerList, isEmbedded } = useContext(ShellContext)
|
||||||
const handleQRCodeClick = () => setIsQRCodeDialogOpen(true)
|
const handleQRCodeClick = () => setIsQRCodeDialogOpen(true)
|
||||||
const onClickFullscreen = () => setIsFullscreen(!isFullscreen)
|
const onClickFullscreen = () => setIsFullscreen(!isFullscreen)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Slide appear={false} in={showAppBar} mountOnEnter unmountOnExit>
|
<Slide appear={false} in={showAppBar} mountOnEnter unmountOnExit>
|
||||||
@ -104,44 +105,50 @@ export const ShellAppBar = ({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
{isEmbedded ? null : (
|
||||||
size="large"
|
<IconButton
|
||||||
edge="start"
|
size="large"
|
||||||
color="inherit"
|
edge="start"
|
||||||
aria-label="Open menu"
|
color="inherit"
|
||||||
sx={{ mr: 2, ...(isDrawerOpen && { display: 'none' }) }}
|
aria-label="Open menu"
|
||||||
onClick={onDrawerOpen}
|
sx={{ mr: 2, ...(isDrawerOpen && { display: 'none' }) }}
|
||||||
>
|
onClick={onDrawerOpen}
|
||||||
<Menu />
|
>
|
||||||
</IconButton>
|
<Menu />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
noWrap
|
noWrap
|
||||||
component="div"
|
component="div"
|
||||||
sx={{ marginRight: 'auto' }}
|
sx={{ marginRight: 'auto' }}
|
||||||
>
|
>
|
||||||
{title}
|
{isEmbedded ? '' : title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Tooltip title="Copy current URL">
|
{isEmbedded ? null : (
|
||||||
<IconButton
|
<>
|
||||||
size="large"
|
<Tooltip title="Copy current URL">
|
||||||
color="inherit"
|
<IconButton
|
||||||
aria-label="Copy current URL"
|
size="large"
|
||||||
onClick={onLinkButtonClick}
|
color="inherit"
|
||||||
>
|
aria-label="Copy current URL"
|
||||||
<Link />
|
onClick={onLinkButtonClick}
|
||||||
</IconButton>
|
>
|
||||||
</Tooltip>
|
<Link />
|
||||||
<Tooltip title="Show QR Code">
|
</IconButton>
|
||||||
<IconButton
|
</Tooltip>
|
||||||
size="large"
|
<Tooltip title="Show QR Code">
|
||||||
color="inherit"
|
<IconButton
|
||||||
aria-label="Show QR Code"
|
size="large"
|
||||||
onClick={handleQRCodeClick}
|
color="inherit"
|
||||||
>
|
aria-label="Show QR Code"
|
||||||
<QrCode2 />
|
onClick={handleQRCodeClick}
|
||||||
</IconButton>
|
>
|
||||||
</Tooltip>
|
<QrCode2 />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Tooltip title="Show Room Controls">
|
<Tooltip title="Show Room Controls">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="large"
|
size="large"
|
||||||
@ -152,19 +159,21 @@ export const ShellAppBar = ({
|
|||||||
<RoomPreferences />
|
<RoomPreferences />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip
|
{isEmbedded ? null : (
|
||||||
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
<Tooltip
|
||||||
>
|
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
||||||
<IconButton
|
|
||||||
size="large"
|
|
||||||
edge="end"
|
|
||||||
color="inherit"
|
|
||||||
aria-label="fullscreen"
|
|
||||||
onClick={onClickFullscreen}
|
|
||||||
>
|
>
|
||||||
{isFullscreen ? <FullscreenExit /> : <Fullscreen />}
|
<IconButton
|
||||||
</IconButton>
|
size="large"
|
||||||
</Tooltip>
|
edge="end"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="fullscreen"
|
||||||
|
onClick={onClickFullscreen}
|
||||||
|
>
|
||||||
|
{isFullscreen ? <FullscreenExit /> : <Fullscreen />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Tooltip title="Click to show peer list">
|
<Tooltip title="Click to show peer list">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="large"
|
size="large"
|
||||||
|
@ -7,6 +7,7 @@ import { ConnectionTestResults } from 'components/Shell/useConnectionTest'
|
|||||||
import { TrackerConnection } from 'services/ConnectionTest/ConnectionTest'
|
import { TrackerConnection } from 'services/ConnectionTest/ConnectionTest'
|
||||||
|
|
||||||
interface ShellContextProps {
|
interface ShellContextProps {
|
||||||
|
isEmbedded: boolean
|
||||||
tabHasFocus: boolean
|
tabHasFocus: boolean
|
||||||
showRoomControls: boolean
|
showRoomControls: boolean
|
||||||
setShowRoomControls: Dispatch<SetStateAction<boolean>>
|
setShowRoomControls: Dispatch<SetStateAction<boolean>>
|
||||||
@ -41,6 +42,7 @@ interface ShellContextProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShellContext = createContext<ShellContextProps>({
|
export const ShellContext = createContext<ShellContextProps>({
|
||||||
|
isEmbedded: false,
|
||||||
tabHasFocus: true,
|
tabHasFocus: true,
|
||||||
showRoomControls: false,
|
showRoomControls: false,
|
||||||
setShowRoomControls: () => {},
|
setShowRoomControls: () => {},
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
import { AlertProps } from '@mui/material/Alert'
|
import { AlertProps } from '@mui/material/Alert'
|
||||||
|
|
||||||
export type AlertOptions = Pick<AlertProps, 'severity'>
|
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 MuiLink from '@mui/material/Link'
|
||||||
import GitHubIcon from '@mui/icons-material/GitHub'
|
import GitHubIcon from '@mui/icons-material/GitHub'
|
||||||
import Cached from '@mui/icons-material/Cached'
|
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'
|
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 { routes } from 'config/routes'
|
||||||
import { ShellContext } from 'contexts/ShellContext'
|
import { ShellContext } from 'contexts/ShellContext'
|
||||||
@ -24,6 +45,7 @@ interface HomeProps {
|
|||||||
export function Home({ userId }: HomeProps) {
|
export function Home({ userId }: HomeProps) {
|
||||||
const { setTitle } = useContext(ShellContext)
|
const { setTitle } = useContext(ShellContext)
|
||||||
const [roomName, setRoomName] = useState(uuid())
|
const [roomName, setRoomName] = useState(uuid())
|
||||||
|
const [showEmbedCode, setShowEmbedCode] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -47,8 +69,19 @@ export function Home({ userId }: HomeProps) {
|
|||||||
navigate(`/private/${roomName}`)
|
navigate(`/private/${roomName}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGetEmbedCodeClick = () => {
|
||||||
|
setShowEmbedCode(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEmbedCodeWindowClose = () => {
|
||||||
|
setShowEmbedCode(false)
|
||||||
|
}
|
||||||
|
|
||||||
const isRoomNameValid = roomName.length > 0
|
const isRoomNameValid = roomName.length > 0
|
||||||
|
|
||||||
|
const embedUrl = new URL(`${window.location.origin}/public/${roomName}`)
|
||||||
|
embedUrl.search = new URLSearchParams({ embed: '1' }).toString()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="Home">
|
<Box className="Home">
|
||||||
<main className="mt-6 px-4 max-w-3xl text-center mx-auto">
|
<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
|
Join private room
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={handleGetEmbedCodeClick}
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
marginLeft: 2,
|
||||||
|
}}
|
||||||
|
disabled={!isRoomNameValid}
|
||||||
|
>
|
||||||
|
Get embed code
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
@ -164,6 +209,49 @@ export function Home({ userId }: HomeProps) {
|
|||||||
</MuiLink>
|
</MuiLink>
|
||||||
.
|
.
|
||||||
</Typography>
|
</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>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user