* 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:
parent
b02ef3e988
commit
73e09041cc
@ -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
|
||||
|
@ -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>
|
||||
) : (
|
||||
<></>
|
||||
|
65
src/components/PasswordPrompt/PasswordPrompt.tsx
Normal file
65
src/components/PasswordPrompt/PasswordPrompt.tsx
Normal 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>
|
||||
)
|
||||
}
|
1
src/components/PasswordPrompt/index.tsx
Normal file
1
src/components/PasswordPrompt/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export * from './PasswordPrompt'
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
type="submit"
|
||||
onClick={handleJoinPublicRoomClick}
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
Go to chat room
|
||||
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 }} />
|
||||
|
40
src/pages/PrivateRoom/PrivateRoom.tsx
Normal file
40
src/pages/PrivateRoom/PrivateRoom.tsx
Normal 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} />
|
||||
)
|
||||
}
|
1
src/pages/PrivateRoom/index.ts
Normal file
1
src/pages/PrivateRoom/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './PrivateRoom'
|
Loading…
x
Reference in New Issue
Block a user