diff --git a/README.md b/README.md index 408f3ab..d5a5118 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/Bootstrap.tsx b/src/Bootstrap.tsx index a0d32f9..8416a95 100644 --- a/src/Bootstrap.tsx +++ b/src/Bootstrap.tsx @@ -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={} /> + } + /> ) : ( <> diff --git a/src/components/PasswordPrompt/PasswordPrompt.tsx b/src/components/PasswordPrompt/PasswordPrompt.tsx new file mode 100644 index 0000000..3a6ce21 --- /dev/null +++ b/src/components/PasswordPrompt/PasswordPrompt.tsx @@ -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) => { + event.preventDefault() + onPasswordEntered(password) + } + + const handlePasswordChange = (e: ChangeEvent) => { + setPassword(e.target.value) + } + + return ( + +
+ Room Password + + + 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. + + + If there is a mismatch, you will be in the room but be unable to + connect to others. An error will not be shown. + + + + + + +
+
+ ) +} diff --git a/src/components/PasswordPrompt/index.tsx b/src/components/PasswordPrompt/index.tsx new file mode 100644 index 0000000..4b0d9aa --- /dev/null +++ b/src/components/PasswordPrompt/index.tsx @@ -0,0 +1 @@ +export * from './PasswordPrompt' diff --git a/src/components/Room/Room.tsx b/src/components/Room/Room.tsx index 94ae867..264424e 100644 --- a/src/components/Room/Room.tsx +++ b/src/components/Room/Room.tsx @@ -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, diff --git a/src/components/Room/useRoom.ts b/src/components/Room/useRoom.ts index d3501b4..90bb0dc 100644 --- a/src/components/Room/useRoom.ts +++ b/src/components/Room/useRoom.ts @@ -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[] = [sendPeerId(userId, peerId)] + + if (isPublicRoom) { + promises.push( + sendMessageTranscript(messageLog.filter(isMessageReceived), peerId) + ) + } + + await Promise.all(promises) } catch (e) { console.error(e) } diff --git a/src/config/routes.ts b/src/config/routes.ts index b7ed83e..479576e 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -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', diff --git a/src/pages/About/About.tsx b/src/pages/About/About.tsx index d0db4da..f98f2df 100644 --- a/src/pages/About/About.tsx +++ b/src/pages/About/About.tsx @@ -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 diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 9c26705..358e537 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -37,9 +37,16 @@ export function Home({ userId }: HomeProps) { const handleFormSubmit = (event: React.SyntheticEvent) => { event.preventDefault() + } + + const handleJoinPublicRoomClick = () => { navigate(`/public/${roomName}`) } + const handleJoinPrivateRoomClick = () => { + navigate(`/private/${roomName}`) + } + return (
@@ -64,15 +71,32 @@ export function Home({ userId }: HomeProps) { /> - + + +
diff --git a/src/pages/PrivateRoom/PrivateRoom.tsx b/src/pages/PrivateRoom/PrivateRoom.tsx new file mode 100644 index 0000000..a47a22d --- /dev/null +++ b/src/pages/PrivateRoom/PrivateRoom.tsx @@ -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 ? ( + + ) : ( + + ) +} diff --git a/src/pages/PrivateRoom/index.ts b/src/pages/PrivateRoom/index.ts new file mode 100644 index 0000000..1421114 --- /dev/null +++ b/src/pages/PrivateRoom/index.ts @@ -0,0 +1 @@ +export * from './PrivateRoom'