feat: (closes #3) Private rooms (#51)

* feat: present password submit UI as a form
* fix: don't connect to room without password
* feat: password-protect room connections
* feat: disable transcript backfilling for private rooms
This commit is contained in:
Jeremy Kahn 2022-10-27 22:21:35 -05:00 committed by GitHub
parent b02ef3e988
commit 73e09041cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 173 additions and 17 deletions

View File

@ -27,7 +27,7 @@ Open https://chitchatter.im/ and join a room to start chatting with anyone else
## Features
- Multiple peers per room (limited only by the number of peer connections your browser supports).
- The number displayed at the top-right of the screen shows how many peers you are connected to. Your peers are the only ones who can see your message.
- Public and private rooms
- 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

@ -12,6 +12,7 @@ import { About } from 'pages/About'
import { Disclaimer } from 'pages/Disclaimer'
import { Settings } from 'pages/Settings'
import { PublicRoom } from 'pages/PublicRoom'
import { PrivateRoom } from 'pages/PrivateRoom'
import { UserSettings } from 'models/settings'
import { PersistedStorageKeys } from 'models/storage'
import { Shell } from 'components/Shell'
@ -114,6 +115,10 @@ function Bootstrap({
path={routes.PUBLIC_ROOM}
element={<PublicRoom userId={userId} />}
/>
<Route
path={routes.PRIVATE_ROOM}
element={<PrivateRoom userId={userId} />}
/>
</Routes>
) : (
<></>

View File

@ -0,0 +1,65 @@
import { ChangeEvent, useState, SyntheticEvent } from 'react'
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText'
import DialogTitle from '@mui/material/DialogTitle'
interface PasswordPromptProps {
isOpen: boolean
onPasswordEntered: (password: string) => void
}
export const PasswordPrompt = ({
isOpen,
onPasswordEntered,
}: PasswordPromptProps) => {
const [password, setPassword] = useState('')
const handleFormSubmit = (event: SyntheticEvent<HTMLFormElement>) => {
event.preventDefault()
onPasswordEntered(password)
}
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
}
return (
<Dialog open={isOpen}>
<form onSubmit={handleFormSubmit}>
<DialogTitle>Room Password</DialogTitle>
<DialogContent>
<DialogContentText sx={{ mb: 2 }}>
You will only be able to connect to room peers that enter the same
password. Due to the decentralized nature of Chitchatter, it is
impossible to know if the password you enter will match the password
entered by other peers.
</DialogContentText>
<DialogContentText>
If there is a mismatch, you will be in the room but be unable to
connect to others. An error will not be shown.
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="password"
label="Password"
type="password"
fullWidth
variant="standard"
value={password}
onChange={handlePasswordChange}
/>
</DialogContent>
<DialogActions>
<Button type="submit" disabled={password.length === 0}>
Submit
</Button>
</DialogActions>
</form>
</Dialog>
)
}

View File

@ -0,0 +1 @@
export * from './PasswordPrompt'

View File

@ -12,6 +12,7 @@ import { useRoom } from './useRoom'
export interface RoomProps {
appId?: string
getUuid?: typeof uuid
password?: string
roomId: string
userId: string
}
@ -20,6 +21,7 @@ export function Room({
appId = `${encodeURI(window.location.origin)}_${process.env.REACT_APP_NAME}`,
getUuid = uuid,
roomId,
password,
userId,
}: RoomProps) {
const { messageLog, sendMessage, isMessageSending } = useRoom(
@ -27,6 +29,7 @@ export function Room({
appId,
trackerUrls,
rtcConfig,
password,
},
{
roomId,

View File

@ -6,7 +6,12 @@ import { v4 as uuid } from 'uuid'
import { ShellContext } from 'contexts/ShellContext'
import { SettingsContext } from 'contexts/SettingsContext'
import { PeerActions } from 'models/network'
import { Message, ReceivedMessage, UnsentMessage, isMessageReceived } from 'models/chat'
import {
Message,
ReceivedMessage,
UnsentMessage,
isMessageReceived,
} from 'models/chat'
import { funAnimalName } from 'fun-animal-names'
import { getPeerName } from 'components/PeerNameDisplay'
import { NotificationService } from 'services/Notification'
@ -24,10 +29,14 @@ interface UseRoomConfig {
}
export function useRoom(
roomConfig: BaseRoomConfig & TorrentRoomConfig,
{ password, ...roomConfig }: BaseRoomConfig & TorrentRoomConfig,
{ roomId, userId, getUuid = uuid }: UseRoomConfig
) {
const [peerRoom] = useState(() => new PeerRoom(roomConfig, roomId))
const isPublicRoom = !password
const [peerRoom] = useState(
() => new PeerRoom({ password: password ?? roomId, ...roomConfig }, roomId)
)
const [numberOfPeers, setNumberOfPeers] = useState(1) // Includes this peer
const shellContext = useContext(ShellContext)
const settingsContext = useContext(SettingsContext)
@ -140,10 +149,15 @@ export function useRoom(
shellContext.setNumberOfPeers(newNumberOfPeers)
;(async () => {
try {
await Promise.all([
sendPeerId(userId, peerId),
sendMessageTranscript(messageLog.filter(isMessageReceived), peerId),
])
const promises: Promise<any>[] = [sendPeerId(userId, peerId)]
if (isPublicRoom) {
promises.push(
sendMessageTranscript(messageLog.filter(isMessageReceived), peerId)
)
}
await Promise.all(promises)
} catch (e) {
console.error(e)
}

View File

@ -2,6 +2,7 @@ export enum routes {
ABOUT = '/about',
DISCLAIMER = '/disclaimer',
INDEX_HTML = '/index.html',
PRIVATE_ROOM = '/private/:roomId',
PUBLIC_ROOM = '/public/:roomId',
ROOT = '/',
SETTINGS = '/settings',

View File

@ -24,7 +24,9 @@ Chitchatter is a communication tool designed to make secure and private communic
#### Chat rooms
Public rooms can be joined by **anyone** with the room URL. By default, rooms are given a random and un-guessable name. You can name your room whatever you'd like, but keep in mind that simpler room names are more guessable by others. For maximum security, consider using the default room name.
Public rooms can be joined by **anyone** with the room URL. By default, rooms are given a random and unguessable name. You can name your room whatever you'd like, but keep in mind that simpler room names are more guessable by others. For maximum security, consider using the default room name.
Private rooms can only be joined by peers with a matching password. The password must be mutually agreed upon before joining. If peers submit mismatching passwords, they will be in the room but be unable to connect to each other. **No error will be shown** if there is a password mismatch because there is no central arbitrating mechanism by which to detect the mismatch.
To connect to others, share the room URL with a secure tool such as [Burner Note](https://burnernote.com/) or [Yopass](https://yopass.se/). You will be notified when others join the room.
@ -36,11 +38,11 @@ There is [a public list of community rooms](https://github.com/jeremyckahn/chitc
Conversation transcripts are erased from local memory as soon as you close the page or navigate away from the room. Conversations are only ever held in volatile memory and never persisted to any disk by Chitchatter.
When a peer joins a public room with participants already in it, the new peer will automatically request the transcript of the conversation that has already taken place from the other peers. Once all peers leave the room, the conversation is completely erased.
When a peer joins a **public** room with participants already in it, the new peer will automatically request the transcript of the conversation that has already taken place from the other peers. Once all peers leave the room, the conversation is completely erased. Peers joining a **private** room will not get the conversation transcript backfilled.
#### Message Authoring
Chat messages support [GitHub-flavored Markdown](https://github.github.com/gfm/).
Chat messages support [GitHub-flavored Markdown](https://github.github.com/gfm/) with code syntax highlighting.
Press \`Enter\` to send a message. Press \`Shift + Enter\` to insert a line break. Message size is limited to ${Intl.NumberFormat().format(
messageCharacterSizeLimit

View File

@ -37,9 +37,16 @@ export function Home({ userId }: HomeProps) {
const handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault()
}
const handleJoinPublicRoomClick = () => {
navigate(`/public/${roomName}`)
}
const handleJoinPrivateRoomClick = () => {
navigate(`/private/${roomName}`)
}
return (
<Box className="Home">
<main className="mt-6 px-4 max-w-3xl text-center mx-auto">
@ -64,15 +71,32 @@ export function Home({ userId }: HomeProps) {
/>
</Tooltip>
</FormControl>
<Button
variant="contained"
type="submit"
<Box
sx={{
marginTop: 2,
display: 'flex',
justifyContent: 'center',
}}
>
Go to chat room
</Button>
<Button
variant="contained"
onClick={handleJoinPublicRoomClick}
sx={{
marginTop: 2,
}}
>
Join public room
</Button>
<Button
variant="contained"
onClick={handleJoinPrivateRoomClick}
sx={{
marginTop: 2,
marginLeft: 2,
}}
>
Join private room
</Button>
</Box>
</form>
</main>
<Divider sx={{ my: 2 }} />

View File

@ -0,0 +1,40 @@
import { useContext, useEffect, useState } from 'react'
import { Room } from 'components/Room'
import { useParams } from 'react-router-dom'
import { ShellContext } from 'contexts/ShellContext'
import { NotificationService } from 'services/Notification'
import { PasswordPrompt } from 'components/PasswordPrompt/PasswordPrompt'
interface PublicRoomProps {
userId: string
}
export function PrivateRoom({ userId }: PublicRoomProps) {
const { roomId = '' } = useParams()
const { setTitle } = useContext(ShellContext)
const [password, setPassword] = useState('')
useEffect(() => {
NotificationService.requestPermission()
}, [])
useEffect(() => {
setTitle(`Room: ${roomId}`)
}, [roomId, setTitle])
const handlePasswordEntered = (password: string) => {
setPassword(password)
}
const isPasswordEntered = password.length === 0
return isPasswordEntered ? (
<PasswordPrompt
isOpen={isPasswordEntered}
onPasswordEntered={handlePasswordEntered}
/>
) : (
<Room userId={userId} roomId={roomId} password={password} />
)
}

View File

@ -0,0 +1 @@
export * from './PrivateRoom'