From 41f38194a58805e433fd85c11cebba8d2558e6c6 Mon Sep 17 00:00:00 2001 From: Jeremy Kahn Date: Tue, 19 Nov 2024 21:28:19 -0600 Subject: [PATCH] feat(#141): [wip] render direct message room --- .../ChatTranscript/ChatTranscript.tsx | 37 ++++++----- src/components/Room/Room.test.tsx | 17 +++-- src/components/Room/Room.tsx | 65 +++++++++++-------- src/components/Room/usePeerVerification.ts | 12 +++- src/components/Room/useRoom.ts | 59 +++++++++++++---- src/components/Room/useRoomAudio.ts | 4 +- src/components/Room/useRoomFileShare.ts | 7 +- src/components/Room/useRoomScreenShare.ts | 7 +- src/components/Room/useRoomVideo.ts | 5 +- src/components/Shell/PeerListItem.tsx | 17 ++++- src/components/Shell/Shell.tsx | 40 +++++++++++- src/contexts/ShellContext.ts | 13 ++++ src/lib/PeerRoom/PeerRoom.ts | 7 +- src/models/network.ts | 24 +++---- 14 files changed, 227 insertions(+), 87 deletions(-) diff --git a/src/components/ChatTranscript/ChatTranscript.tsx b/src/components/ChatTranscript/ChatTranscript.tsx index 46e026b20..cf1298049 100644 --- a/src/components/ChatTranscript/ChatTranscript.tsx +++ b/src/components/ChatTranscript/ChatTranscript.tsx @@ -1,17 +1,21 @@ -import { HTMLAttributes, useRef, useEffect, useState, useContext } from 'react' -import Box from '@mui/material/Box' +import { useRef, useEffect, useState, useContext } from 'react' +import Box, { BoxProps } from '@mui/material/Box' import useTheme from '@mui/material/styles/useTheme' import { Message as IMessage, InlineMedia } from 'models/chat' import { Message } from 'components/Message' import { ShellContext } from 'contexts/ShellContext' -export interface ChatTranscriptProps extends HTMLAttributes { +export interface ChatTranscriptProps extends BoxProps { messageLog: Array userId: string } -export const ChatTranscript = ({ messageLog, userId }: ChatTranscriptProps) => { +export const ChatTranscript = ({ + messageLog, + userId, + sx, +}: ChatTranscriptProps) => { const { showRoomControls } = useContext(ShellContext) const theme = useTheme() const boxRef = useRef(null) @@ -62,17 +66,20 @@ export const ChatTranscript = ({ messageLog, userId }: ChatTranscriptProps) => { {messageLog.map((message, idx) => { const previousMessage = messageLog[idx - 1] diff --git a/src/components/Room/Room.test.tsx b/src/components/Room/Room.test.tsx index 3f3fcbf4e..f85e44d4d 100644 --- a/src/components/Room/Room.test.tsx +++ b/src/components/Room/Room.test.tsx @@ -119,7 +119,7 @@ describe('Room', () => { expect(textInput).toHaveValue('') }) - test('message is sent to peer', async () => { + test('message is sent to peers', async () => { render( { await userEvent.type(textInput, 'hello') await userEvent.click(sendButton) - expect(mockMessagedSender).toHaveBeenCalledWith({ - authorId: mockUserId, - text: 'hello', - timeSent: mockNowTime, - id: 'abc123', - }) + expect(mockMessagedSender).toHaveBeenCalledWith( + { + authorId: mockUserId, + text: 'hello', + timeSent: mockNowTime, + id: 'abc123', + }, + null + ) }) }) diff --git a/src/components/Room/Room.tsx b/src/components/Room/Room.tsx index b67be80f4..306a464e5 100644 --- a/src/components/Room/Room.tsx +++ b/src/components/Room/Room.tsx @@ -33,6 +33,7 @@ export interface RoomProps { userId: string encryptionService?: typeof encryption timeService?: typeof time + targetPeerId?: string } export function Room({ @@ -43,6 +44,7 @@ export function Room({ roomId, password, userId, + targetPeerId, }: RoomProps) { const theme = useTheme() const settingsContext = useContext(SettingsContext) @@ -72,9 +74,12 @@ export function Room({ publicKey, encryptionService, timeService, + targetPeerId, } ) + const isDirectMessageRoom = typeof targetPeerId === 'string' + const handleMessageSubmit = async (message: string) => { await sendMessage(message) } @@ -105,32 +110,34 @@ export function Room({ overflow: 'auto', }} > - - - - - - - - - - - - - + {!isDirectMessageRoom && ( + + + + + + + + + + + + + + )} - + (PeerAction.VERIFICATION_TOKEN_ENCRYPTED) + peerRoom.makeAction( + PeerAction.VERIFICATION_TOKEN_ENCRYPTED, + groupActionNamespace + ) const [sendVerificationTokenRaw, receiveVerificationTokenRaw] = - peerRoom.makeAction(PeerAction.VERIFICATION_TOKEN_RAW) + peerRoom.makeAction( + PeerAction.VERIFICATION_TOKEN_RAW, + groupActionNamespace + ) const initPeerVerification = useCallback( async (peer: Peer) => { diff --git a/src/components/Room/useRoom.ts b/src/components/Room/useRoom.ts index 1e9bead99..102956c4f 100644 --- a/src/components/Room/useRoom.ts +++ b/src/components/Room/useRoom.ts @@ -6,7 +6,7 @@ import { useDebounce } from '@react-hook/debounce' import { ShellContext } from 'contexts/ShellContext' import { SettingsContext } from 'contexts/SettingsContext' -import { PeerAction } from 'models/network' +import { groupActionNamespace, PeerAction } from 'models/network' import { AudioState, Message, @@ -44,6 +44,7 @@ interface UseRoomConfig { getUuid?: typeof uuid encryptionService?: typeof encryption timeService?: typeof time + targetPeerId?: string | null } interface UserMetadata extends Record { @@ -58,6 +59,7 @@ export function useRoom( roomId, userId, publicKey, + targetPeerId = null, getUuid = uuid, encryptionService = encryption, timeService = time, @@ -65,6 +67,8 @@ export function useRoom( ) { const isPrivate = password !== undefined + const isDirectMessageRoom = typeof targetPeerId === 'string' + const { peerList, setPeerList, @@ -76,8 +80,14 @@ export function useRoom( customUsername, updatePeer, peerRoomRef, + messageLog: shellMessageLog, + setMessageLog: shellSetMessageLog, } = useContext(ShellContext) + const messageLog = isDirectMessageRoom + ? shellMessageLog.directMessageLog[targetPeerId] ?? [] + : shellMessageLog.groupMessageLog + const [peerRoom] = useState( () => peerRoomRef.current ?? @@ -89,9 +99,6 @@ export function useRoom( const settingsContext = useContext(SettingsContext) const { showActiveTypingStatus } = settingsContext.getUserSettings() const [isMessageSending, setIsMessageSending] = useState(false) - const [messageLog, _setMessageLog] = useState>( - [] - ) const [newMessageAudio] = useState(() => new Audio('/sounds/new-message.aac')) const { getDisplayUsername } = usePeerNameDisplay() @@ -113,7 +120,10 @@ export function useRoom( } } - _setMessageLog(messages.slice(-messageTranscriptSizeLimit)) + shellSetMessageLog( + messages.slice(-messageTranscriptSizeLimit), + targetPeerId + ) } const [isShowingMessages, setIsShowingMessages] = useState(true) @@ -174,8 +184,15 @@ export function useRoom( ] ) + const peerActionNamespace = isDirectMessageRoom + ? targetPeerId.slice(0, 8) + : groupActionNamespace + const [sendTypingStatusChange, receiveTypingStatusChange] = - peerRoom.makeAction(PeerAction.TYPING_STATUS_CHANGE) + peerRoom.makeAction( + PeerAction.TYPING_STATUS_CHANGE, + peerActionNamespace + ) const [isTyping, setIsTypingDebounced, setIsTyping] = useDebounce( false, @@ -191,12 +208,23 @@ export function useRoom( useEffect(() => { return () => { + if (isDirectMessageRoom) return + sendTypingStatusChange({ isTyping: false }) peerRoom.leaveRoom() peerRoomRef.current = null setPeerList([]) + shellSetMessageLog([], targetPeerId) } - }, [peerRoom, setPeerList, sendTypingStatusChange, peerRoomRef]) + }, [ + peerRoom, + setPeerList, + sendTypingStatusChange, + peerRoomRef, + isDirectMessageRoom, + shellSetMessageLog, + targetPeerId, + ]) useEffect(() => { setPassword(password) @@ -219,17 +247,23 @@ export function useRoom( }, [isShowingMessages, setUnreadMessages]) const [sendPeerMetadata, receivePeerMetadata] = - peerRoom.makeAction(PeerAction.PEER_METADATA) + peerRoom.makeAction( + PeerAction.PEER_METADATA, + peerActionNamespace + ) const [sendMessageTranscript, receiveMessageTranscript] = peerRoom.makeAction< Array - >(PeerAction.MESSAGE_TRANSCRIPT) + >(PeerAction.MESSAGE_TRANSCRIPT, peerActionNamespace) const [sendPeerMessage, receivePeerMessage] = - peerRoom.makeAction(PeerAction.MESSAGE) + peerRoom.makeAction(PeerAction.MESSAGE, peerActionNamespace) const [sendPeerInlineMedia, receivePeerInlineMedia] = - peerRoom.makeAction(PeerAction.MEDIA_MESSAGE) + peerRoom.makeAction( + PeerAction.MEDIA_MESSAGE, + peerActionNamespace + ) const { privateKey } = settingsContext.getUserSettings() @@ -252,7 +286,8 @@ export function useRoom( setIsTyping(false) setIsMessageSending(true) setMessageLog([...messageLog, unsentMessage]) - await sendPeerMessage(unsentMessage) + + await sendPeerMessage(unsentMessage, targetPeerId) setMessageLog([ ...messageLog, diff --git a/src/components/Room/useRoomAudio.ts b/src/components/Room/useRoomAudio.ts index 638ae9693..0c536012f 100644 --- a/src/components/Room/useRoomAudio.ts +++ b/src/components/Room/useRoomAudio.ts @@ -1,7 +1,7 @@ import { useContext, useEffect, useCallback, useState } from 'react' import { ShellContext } from 'contexts/ShellContext' -import { PeerAction } from 'models/network' +import { groupActionNamespace, PeerAction } from 'models/network' import { AudioState, Peer, @@ -40,7 +40,7 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) { const [sendAudioChange, receiveAudioChange] = peerRoom.makeAction< Partial - >(PeerAction.AUDIO_CHANGE) + >(PeerAction.AUDIO_CHANGE, groupActionNamespace) receiveAudioChange((peerAudioChannelState, peerId) => { setPeerList(peerList => { diff --git a/src/components/Room/useRoomFileShare.ts b/src/components/Room/useRoomFileShare.ts index 3b04399e9..91842a708 100644 --- a/src/components/Room/useRoomFileShare.ts +++ b/src/components/Room/useRoomFileShare.ts @@ -3,7 +3,7 @@ import { useContext, useEffect, useState } from 'react' import { sleep } from 'lib/sleep' import { RoomContext } from 'contexts/RoomContext' import { ShellContext } from 'contexts/ShellContext' -import { PeerAction } from 'models/network' +import { groupActionNamespace, PeerAction } from 'models/network' import { FileOfferMetadata, Peer } from 'models/chat' import { PeerRoom, PeerHookType } from 'lib/PeerRoom' import { fileTransfer } from 'lib/FileTransfer' @@ -33,7 +33,10 @@ export function useRoomFileShare({ const { peerOfferedFileMetadata, setPeerOfferedFileMetadata } = roomContext const [sendFileOfferMetadata, receiveFileOfferMetadata] = - peerRoom.makeAction(PeerAction.FILE_OFFER) + peerRoom.makeAction( + PeerAction.FILE_OFFER, + groupActionNamespace + ) receiveFileOfferMetadata((fileOfferMetadata, peerId) => { if (fileOfferMetadata) { diff --git a/src/components/Room/useRoomScreenShare.ts b/src/components/Room/useRoomScreenShare.ts index d6411ee42..8966476f5 100644 --- a/src/components/Room/useRoomScreenShare.ts +++ b/src/components/Room/useRoomScreenShare.ts @@ -3,7 +3,7 @@ import { useContext, useEffect, useCallback, useState } from 'react' import { isRecord } from 'lib/type-guards' import { RoomContext } from 'contexts/RoomContext' import { ShellContext } from 'contexts/ShellContext' -import { PeerAction } from 'models/network' +import { groupActionNamespace, PeerAction } from 'models/network' import { ScreenShareState, Peer, @@ -38,7 +38,10 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) { } = roomContext const [sendScreenShare, receiveScreenShare] = - peerRoom.makeAction(PeerAction.SCREEN_SHARE) + peerRoom.makeAction( + PeerAction.SCREEN_SHARE, + groupActionNamespace + ) receiveScreenShare((screenState, peerId) => { const newPeerList = peerList.map(peer => { diff --git a/src/components/Room/useRoomVideo.ts b/src/components/Room/useRoomVideo.ts index 9736b3b24..a5f54b6dd 100644 --- a/src/components/Room/useRoomVideo.ts +++ b/src/components/Room/useRoomVideo.ts @@ -2,7 +2,7 @@ import { useContext, useEffect, useCallback, useState } from 'react' import { RoomContext } from 'contexts/RoomContext' import { ShellContext } from 'contexts/ShellContext' -import { PeerAction } from 'models/network' +import { groupActionNamespace, PeerAction } from 'models/network' import { VideoState, Peer, StreamType } from 'models/chat' import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom' import { isRecord } from 'lib/type-guards' @@ -69,7 +69,8 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) { }, [peerRoom, selfVideoStream, setSelfVideoStream]) const [sendVideoChange, receiveVideoChange] = peerRoom.makeAction( - PeerAction.VIDEO_CHANGE + PeerAction.VIDEO_CHANGE, + groupActionNamespace ) receiveVideoChange((videoState, peerId) => { diff --git a/src/components/Shell/PeerListItem.tsx b/src/components/Shell/PeerListItem.tsx index d049072fa..e02261e45 100644 --- a/src/components/Shell/PeerListItem.tsx +++ b/src/components/Shell/PeerListItem.tsx @@ -16,11 +16,13 @@ import ListItem from '@mui/material/ListItem' import ListItemText from '@mui/material/ListItemText' import Tooltip from '@mui/material/Tooltip' import Typography from '@mui/material/Typography' -import { useState } from 'react' +import useTheme from '@mui/material/styles/useTheme' +import { useContext, useState } from 'react' import { AudioVolume } from 'components/AudioVolume' import { PeerNameDisplay } from 'components/PeerNameDisplay' import { PublicKey } from 'components/PublicKey' +import { Room } from 'components/Room' import { PeerConnectionType } from 'lib/PeerRoom' import { AudioChannel, @@ -28,6 +30,7 @@ import { Peer, PeerVerificationState, } from 'models/chat' +import { SettingsContext } from 'contexts/SettingsContext' import { PeerDownloadFileButton } from './PeerDownloadFileButton' @@ -62,6 +65,9 @@ export const PeerListItem = ({ peerConnectionTypes, peerAudioChannels, }: PeerListItemProps) => { + const theme = useTheme() + const { getUserSettings } = useContext(SettingsContext) + const { userId } = getUserSettings() const [showPeerDialog, setShowPeerDialog] = useState(false) const hasPeerConnection = peer.peerId in peerConnectionTypes @@ -169,6 +175,15 @@ export const PeerListItem = ({ + + + diff --git a/src/components/Shell/Shell.tsx b/src/components/Shell/Shell.tsx index 0100d72f9..3668cd268 100644 --- a/src/components/Shell/Shell.tsx +++ b/src/components/Shell/Shell.tsx @@ -17,7 +17,11 @@ import MuiDrawer from '@mui/material/Drawer' import Link from '@mui/material/Link' import { useWindowSize } from '@react-hook/window-size' -import { ShellContext } from 'contexts/ShellContext' +import { + MessageLog, + ShellContext, + ShellMessageLog, +} from 'contexts/ShellContext' import { SettingsContext } from 'contexts/SettingsContext' import { AlertOptions, QueryParamKeys } from 'models/shell' import { @@ -103,6 +107,36 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { Record >({}) + const [shellMessageLog, setShellMessageLog] = useState({ + groupMessageLog: [], + directMessageLog: {}, + }) + + const messageLog = shellMessageLog + + const setMessageLog = useCallback( + (messageLog: MessageLog, targetPeerId: string | null) => { + setShellMessageLog(prev => { + const isDirectMessageLog = typeof targetPeerId === 'string' + + const shellMessageLog: ShellMessageLog = { + groupMessageLog: isDirectMessageLog + ? prev.groupMessageLog + : messageLog, + directMessageLog: { + ...prev.directMessageLog, + ...(isDirectMessageLog && { + [targetPeerId]: messageLog, + }), + }, + } + + return shellMessageLog + }) + }, + [] + ) + const showAlert = useCallback((message: string, options?: AlertOptions) => { setAlertText(message) setAlertSeverity(options?.severity ?? 'info') @@ -162,6 +196,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { connectionTestResults, updatePeer, peerRoomRef, + messageLog, + setMessageLog, }), [ isEmbedded, @@ -193,6 +229,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { connectionTestResults, updatePeer, peerRoomRef, + messageLog, + setMessageLog, ] ) diff --git a/src/contexts/ShellContext.ts b/src/contexts/ShellContext.ts index 9e4f0be5b..6385a77e5 100644 --- a/src/contexts/ShellContext.ts +++ b/src/contexts/ShellContext.ts @@ -12,6 +12,8 @@ import { AudioChannel, AudioChannelName, AudioState, + InlineMedia, + Message, Peer, PeerAudioChannelState, ScreenShareState, @@ -19,6 +21,13 @@ import { } from 'models/chat' import { AlertOptions } from 'models/shell' +export type MessageLog = (Message | InlineMedia)[] + +export interface ShellMessageLog { + groupMessageLog: MessageLog + directMessageLog: Record +} + interface ShellContextProps { isEmbedded: boolean tabHasFocus: boolean @@ -53,6 +62,8 @@ interface ShellContextProps { connectionTestResults: ConnectionTestResults updatePeer: (peerId: string, updatedProperties: Partial) => void peerRoomRef: MutableRefObject + messageLog: ShellMessageLog + setMessageLog: (messageLog: MessageLog, targetPeerId: string | null) => void } export const ShellContext = createContext({ @@ -94,4 +105,6 @@ export const ShellContext = createContext({ }, updatePeer: () => {}, peerRoomRef: { current: null }, + messageLog: { groupMessageLog: [], directMessageLog: {} }, + setMessageLog: () => {}, }) diff --git a/src/lib/PeerRoom/PeerRoom.ts b/src/lib/PeerRoom/PeerRoom.ts index 5344cb8b2..367af0b0a 100644 --- a/src/lib/PeerRoom/PeerRoom.ts +++ b/src/lib/PeerRoom/PeerRoom.ts @@ -169,8 +169,11 @@ export class PeerRoom { return peerConnections } - makeAction = (peerAction: PeerAction) => { - return this.room.makeAction(peerAction) + makeAction = ( + peerAction: PeerAction, + namespace: string + ) => { + return this.room.makeAction(`${namespace}.${peerAction}`) } addStream = ( diff --git a/src/models/network.ts b/src/models/network.ts index 348a65497..0d531765c 100644 --- a/src/models/network.ts +++ b/src/models/network.ts @@ -1,14 +1,16 @@ +export const groupActionNamespace = 'group' + // NOTE: Action names are limited to 12 characters, otherwise Trystero breaks. export enum PeerAction { - MESSAGE = 'MESSAGE', - MEDIA_MESSAGE = 'MEDIA_MSG', - MESSAGE_TRANSCRIPT = 'MSG_XSCRIPT', - PEER_METADATA = 'PEER_META', - AUDIO_CHANGE = 'AUDIO_CHANGE', - VIDEO_CHANGE = 'VIDEO_CHANGE', - SCREEN_SHARE = 'SCREEN_SHARE', - FILE_OFFER = 'FILE_OFFER', - TYPING_STATUS_CHANGE = 'TYPNG_CHANGE', - VERIFICATION_TOKEN_ENCRYPTED = 'V_TKN_ENC', - VERIFICATION_TOKEN_RAW = 'V_TKN_RAW', + MESSAGE = 0, + MEDIA_MESSAGE, + MESSAGE_TRANSCRIPT, + PEER_METADATA, + AUDIO_CHANGE, + VIDEO_CHANGE, + SCREEN_SHARE, + FILE_OFFER, + TYPING_STATUS_CHANGE, + VERIFICATION_TOKEN_ENCRYPTED, + VERIFICATION_TOKEN_RAW, }