diff --git a/src/components/Room/usePeerVerification.ts b/src/components/Room/usePeerVerification.ts index c4cd43ccc..f277ef464 100644 --- a/src/components/Room/usePeerVerification.ts +++ b/src/components/Room/usePeerVerification.ts @@ -3,36 +3,89 @@ import { ShellContext } from 'contexts/ShellContext' import { Peer, PeerVerificationState } from 'models/chat' import { encryption } from 'services/Encryption' import { PeerRoom } from 'lib/PeerRoom' -import { groupActionNamespace, PeerAction } from 'models/network' +import { PeerAction } from 'models/network' import { verificationTimeout } from 'config/messaging' import { usePeerNameDisplay } from 'components/PeerNameDisplay' +import { usePeerAction } from 'hooks/usePeerAction' interface UserPeerVerificationProps { peerRoom: PeerRoom privateKey: CryptoKey + isDirectMessageRoom: boolean encryptionService?: typeof encryption } export const usePeerVerification = ({ peerRoom, privateKey, + isDirectMessageRoom, encryptionService = encryption, }: UserPeerVerificationProps) => { const { updatePeer, peerList, showAlert } = useContext(ShellContext) + const namespace = isDirectMessageRoom ? 'dm' : 'g' const { getDisplayUsername } = usePeerNameDisplay() - const [sendVerificationTokenEncrypted, receiveVerificationTokenEncrypted] = - peerRoom.makeAction( - PeerAction.VERIFICATION_TOKEN_ENCRYPTED, - groupActionNamespace - ) + const [sendVerificationTokenEncrypted] = usePeerAction({ + peerAction: PeerAction.VERIFICATION_TOKEN_ENCRYPTED, + peerRoom, + namespace, + onReceive: async (encryptedVerificationToken, peerId) => { + try { + const decryptedVerificationToken = + await encryptionService.decryptString( + privateKey, + encryptedVerificationToken + ) + + await sendVerificationTokenRaw(decryptedVerificationToken, [peerId]) + } catch (e) { + console.error(e) + } + }, + }) + + const [sendVerificationTokenRaw] = usePeerAction({ + peerAction: PeerAction.VERIFICATION_TOKEN_RAW, + peerRoom, + namespace, + onReceive: (decryptedVerificationToken, peerId) => { + const matchingPeer = peerList.find(peer => peer.peerId === peerId) + + if (!matchingPeer) { + throw new Error(`peerId not found: ${peerId}`) + } + + const { verificationToken, verificationTimer } = matchingPeer + + if (decryptedVerificationToken !== verificationToken) { + updatePeer(peerId, { + verificationState: PeerVerificationState.UNVERIFIED, + verificationTimer: null, + }) + + showAlert( + `Verification for ${getDisplayUsername(matchingPeer.userId)} failed`, + { + severity: 'error', + } + ) + + throw new Error( + `Verification token for peerId ${peerId} does not match. [expected: ${verificationToken}] [received: ${decryptedVerificationToken}]` + ) + } + + if (verificationTimer) { + clearTimeout(verificationTimer) + } - const [sendVerificationTokenRaw, receiveVerificationTokenRaw] = - peerRoom.makeAction( - PeerAction.VERIFICATION_TOKEN_RAW, - groupActionNamespace - ) + updatePeer(peerId, { + verificationState: PeerVerificationState.VERIFIED, + verificationTimer: null, + }) + }, + }) const initPeerVerification = useCallback( async (peer: Peer) => { @@ -82,69 +135,16 @@ export const usePeerVerification = ({ const [scheduledPeerToVerify, setScheduledPeerToVerify] = useState(null) useEffect(() => { - if (scheduledPeerToVerify === null) return + if (scheduledPeerToVerify === null || isDirectMessageRoom) return initPeerVerification(scheduledPeerToVerify) setScheduledPeerToVerify(null) - }, [scheduledPeerToVerify, initPeerVerification]) + }, [scheduledPeerToVerify, initPeerVerification, isDirectMessageRoom]) // NOTE: END HACKY WORKAROUND const verifyPeer = (peer: Peer) => { setScheduledPeerToVerify(peer) } - receiveVerificationTokenEncrypted( - async (encryptedVerificationToken, peerId) => { - try { - const decryptedVerificationToken = - await encryptionService.decryptString( - privateKey, - encryptedVerificationToken - ) - - await sendVerificationTokenRaw(decryptedVerificationToken, [peerId]) - } catch (e) { - console.error(e) - } - } - ) - - receiveVerificationTokenRaw((decryptedVerificationToken, peerId) => { - const matchingPeer = peerList.find(peer => peer.peerId === peerId) - - if (!matchingPeer) { - throw new Error(`peerId not found: ${peerId}`) - } - - const { verificationToken, verificationTimer } = matchingPeer - - if (decryptedVerificationToken !== verificationToken) { - updatePeer(peerId, { - verificationState: PeerVerificationState.UNVERIFIED, - verificationTimer: null, - }) - - showAlert( - `Verification for ${getDisplayUsername(matchingPeer.userId)} failed`, - { - severity: 'error', - } - ) - - throw new Error( - `Verification token for peerId ${peerId} does not match. [expected: ${verificationToken}] [received: ${decryptedVerificationToken}]` - ) - } - - if (verificationTimer) { - clearTimeout(verificationTimer) - } - - updatePeer(peerId, { - verificationState: PeerVerificationState.VERIFIED, - verificationTimer: null, - }) - }) - return { verifyPeer } } diff --git a/src/components/Room/useRoom.ts b/src/components/Room/useRoom.ts index 563229e81..8a38aec6f 100644 --- a/src/components/Room/useRoom.ts +++ b/src/components/Room/useRoom.ts @@ -6,11 +6,7 @@ import { useDebounce } from '@react-hook/debounce' import { ShellContext } from 'contexts/ShellContext' import { SettingsContext } from 'contexts/SettingsContext' -import { - directMessageActionNamespace, - groupActionNamespace, - PeerAction, -} from 'models/network' +import { PeerAction } from 'models/network' import { AudioState, Message, @@ -36,6 +32,7 @@ import { PeerRoom, PeerHookType } from 'lib/PeerRoom' import { notification } from 'services/Notification' import { fileTransfer } from 'lib/FileTransfer' import { AllowedKeyType, encryption } from 'services/Encryption' +import { usePeerAction } from 'hooks/usePeerAction' import { messageTranscriptSizeLimit } from 'config/messaging' @@ -72,6 +69,7 @@ export function useRoom( const isPrivate = password !== undefined const isDirectMessageRoom = typeof targetPeerId === 'string' + const namespace = isDirectMessageRoom ? 'dm' : 'g' const { peerList, @@ -188,15 +186,15 @@ export function useRoom( ] ) - const peerActionNamespace = isDirectMessageRoom - ? directMessageActionNamespace - : groupActionNamespace - - const [sendTypingStatusChange, receiveTypingStatusChange] = - peerRoom.makeAction( - PeerAction.TYPING_STATUS_CHANGE, - peerActionNamespace - ) + const [sendTypingStatusChange] = usePeerAction({ + peerAction: PeerAction.TYPING_STATUS_CHANGE, + peerRoom, + namespace, + onReceive: (typingStatus, peerId) => { + const { isTyping } = typingStatus + updatePeer(peerId, { isTyping }) + }, + }) const [isTyping, setIsTypingDebounced, setIsTyping] = useDebounce( false, @@ -250,58 +248,14 @@ export function useRoom( if (isShowingMessages) setUnreadMessages(0) }, [isShowingMessages, setUnreadMessages]) - const [sendPeerMetadata, receivePeerMetadata] = - peerRoom.makeAction( - PeerAction.PEER_METADATA, - peerActionNamespace - ) - - const [sendMessageTranscript, receiveMessageTranscript] = peerRoom.makeAction< - Array - >(PeerAction.MESSAGE_TRANSCRIPT, peerActionNamespace) - - const [sendPeerMessage, receivePeerMessage] = - peerRoom.makeAction(PeerAction.MESSAGE, peerActionNamespace) - - const [sendPeerInlineMedia, receivePeerInlineMedia] = - peerRoom.makeAction( - PeerAction.MEDIA_MESSAGE, - peerActionNamespace - ) - - const { privateKey } = settingsContext.getUserSettings() - - const { verifyPeer } = usePeerVerification({ + const [sendPeerMetadata] = usePeerAction({ + peerAction: PeerAction.PEER_METADATA, peerRoom, - privateKey, - encryptionService, - }) - - const sendMessage = async (message: string) => { - if (isMessageSending) return - - const unsentMessage: UnsentMessage = { - authorId: userId, - text: message, - timeSent: timeService.now(), - id: getUuid(), - } - - setIsTyping(false) - setIsMessageSending(true) - setMessageLog([...messageLog, unsentMessage]) - - await sendPeerMessage(unsentMessage, targetPeerId) - - setMessageLog([ - ...messageLog, - { ...unsentMessage, timeReceived: timeService.now() }, - ]) - setIsMessageSending(false) - } - - receivePeerMetadata( - async ({ userId, customUsername, publicKeyString }, peerId: string) => { + namespace, + onReceive: async ( + { userId, customUsername, publicKeyString }, + peerId: string + ) => { const publicKey = await encryptionService.parseCryptoKeyString( publicKeyString, AllowedKeyType.PUBLIC @@ -346,89 +300,171 @@ export function useRoom( showAlert(`${oldUsername} is now ${newUsername}`) } } - } - ) + }, + }) - receiveMessageTranscript(transcript => { - if (messageLog.length) return + const [sendMessageTranscript] = usePeerAction< + Array + >({ + peerAction: PeerAction.MESSAGE_TRANSCRIPT, + peerRoom, + namespace, + onReceive: transcript => { + if (messageLog.length) return - setMessageLog(transcript) + setMessageLog(transcript) + }, }) - receivePeerMessage((message, peerId) => { - const userSettings = settingsContext.getUserSettings() + const [sendPeerMessage] = usePeerAction({ + peerAction: PeerAction.MESSAGE, + peerRoom, + namespace, + onReceive: (message, peerId) => { + if (isDirectMessageRoom && peerId !== targetPeerId) { + return + } - if (!isShowingMessages) { - setUnreadMessages(unreadMessages + 1) - } + const userSettings = settingsContext.getUserSettings() - if (!tabHasFocus || !isShowingMessages) { - if (userSettings.playSoundOnNewMessage) { - newMessageAudio.play() + if (!isShowingMessages) { + setUnreadMessages(unreadMessages + 1) } - if (userSettings.showNotificationOnNewMessage) { - const displayUsername = getDisplayUsername(message.authorId) + if (!tabHasFocus || !isShowingMessages) { + if (userSettings.playSoundOnNewMessage) { + newMessageAudio.play() + } + + if (userSettings.showNotificationOnNewMessage) { + const displayUsername = getDisplayUsername(message.authorId) - notification.showNotification(`${displayUsername}: ${message.text}`) + notification.showNotification(`${displayUsername}: ${message.text}`) + } } - } - setMessageLog([ - ...messageLog, - { ...message, timeReceived: timeService.now() }, - ]) - updatePeer(peerId, { isTyping: false }) + setMessageLog([ + ...messageLog, + { ...message, timeReceived: timeService.now() }, + ]) + updatePeer(peerId, { isTyping: false }) + }, }) - peerRoom.onPeerJoin(PeerHookType.NEW_PEER, (peerId: string) => { - showAlert(`Someone has joined the room`, { - severity: 'success', - }) - ;(async () => { - try { - const publicKeyString = - await encryptionService.stringifyCryptoKey(publicKey) + const [sendPeerInlineMedia] = usePeerAction({ + peerAction: PeerAction.MEDIA_MESSAGE, + peerRoom, + namespace, + onReceive: inlineMedia => { + const userSettings = settingsContext.getUserSettings() - const promises: Promise[] = [ - sendPeerMetadata({ userId, customUsername, publicKeyString }, peerId), - ] + if (!tabHasFocus) { + if (userSettings.playSoundOnNewMessage) { + newMessageAudio.play() + } - if (!isPrivate) { - promises.push( - sendMessageTranscript(messageLog.filter(isMessageReceived), peerId) + if (userSettings.showNotificationOnNewMessage) { + notification.showNotification( + `${getDisplayUsername(inlineMedia.authorId)} shared media` ) } - - await Promise.all(promises) - } catch (e) { - console.error(e) } - })() + + setMessageLog([ + ...messageLog, + { ...inlineMedia, timeReceived: timeService.now() }, + ]) + }, }) - peerRoom.onPeerLeave(PeerHookType.NEW_PEER, (peerId: string) => { - const peerIndex = peerList.findIndex(peer => peer.peerId === peerId) - const doesPeerExist = peerIndex !== -1 - - showAlert( - `${ - doesPeerExist - ? getDisplayUsername(peerList[peerIndex].userId) - : 'Someone' - } has left the room`, - { - severity: 'warning', - } - ) + const { privateKey } = settingsContext.getUserSettings() - if (doesPeerExist) { - const peerListClone = [...peerList] - peerListClone.splice(peerIndex, 1) - setPeerList(peerListClone) - } + const { verifyPeer } = usePeerVerification({ + peerRoom, + privateKey, + encryptionService, + isDirectMessageRoom, }) + const sendMessage = async (message: string) => { + if (isMessageSending) return + + const unsentMessage: UnsentMessage = { + authorId: userId, + text: message, + timeSent: timeService.now(), + id: getUuid(), + } + + setIsTyping(false) + setIsMessageSending(true) + setMessageLog([...messageLog, unsentMessage]) + + await sendPeerMessage(unsentMessage, targetPeerId) + + setMessageLog([ + ...messageLog, + { ...unsentMessage, timeReceived: timeService.now() }, + ]) + setIsMessageSending(false) + } + + if (!isDirectMessageRoom) { + peerRoom.onPeerJoin(PeerHookType.NEW_PEER, (peerId: string) => { + showAlert(`Someone has joined the room`, { + severity: 'success', + }) + ;(async () => { + try { + const publicKeyString = + await encryptionService.stringifyCryptoKey(publicKey) + + const promises: Promise[] = [ + sendPeerMetadata( + { userId, customUsername, publicKeyString }, + peerId + ), + ] + + if (!isPrivate) { + promises.push( + sendMessageTranscript( + messageLog.filter(isMessageReceived), + peerId + ) + ) + } + + await Promise.all(promises) + } catch (e) { + console.error(e) + } + })() + }) + + peerRoom.onPeerLeave(PeerHookType.NEW_PEER, (peerId: string) => { + const peerIndex = peerList.findIndex(peer => peer.peerId === peerId) + const doesPeerExist = peerIndex !== -1 + + showAlert( + `${ + doesPeerExist + ? getDisplayUsername(peerList[peerIndex].userId) + : 'Someone' + } has left the room`, + { + severity: 'warning', + } + ) + + if (doesPeerExist) { + const peerListClone = [...peerList] + peerListClone.splice(peerIndex, 1) + setPeerList(peerListClone) + } + }) + } + const showVideoDisplay = Boolean( selfVideoStream || selfScreenStream || @@ -471,34 +507,10 @@ export function useRoom( setIsTypingDebounced(false) } - receivePeerInlineMedia(inlineMedia => { - const userSettings = settingsContext.getUserSettings() - - if (!tabHasFocus) { - if (userSettings.playSoundOnNewMessage) { - newMessageAudio.play() - } - - if (userSettings.showNotificationOnNewMessage) { - notification.showNotification( - `${getDisplayUsername(inlineMedia.authorId)} shared media` - ) - } - } - - setMessageLog([ - ...messageLog, - { ...inlineMedia, timeReceived: timeService.now() }, - ]) - }) - - receiveTypingStatusChange((typingStatus, peerId) => { - const { isTyping } = typingStatus - updatePeer(peerId, { isTyping }) - }) - useEffect(() => { ;(async () => { + if (isDirectMessageRoom) return + const publicKeyString = await encryptionService.stringifyCryptoKey(publicKey) @@ -508,7 +520,14 @@ export function useRoom( publicKeyString, }) })() - }, [customUsername, userId, sendPeerMetadata, publicKey, encryptionService]) + }, [ + customUsername, + userId, + sendPeerMetadata, + publicKey, + encryptionService, + isDirectMessageRoom, + ]) useEffect(() => { ;(async () => { diff --git a/src/components/Room/useRoomAudio.ts b/src/components/Room/useRoomAudio.ts index 0c536012f..57630a324 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 { groupActionNamespace, PeerAction } from 'models/network' +import { PeerAction } from 'models/network' import { AudioState, Peer, @@ -10,6 +10,7 @@ import { StreamType, } from 'models/chat' import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom' +import { usePeerAction } from 'hooks/usePeerAction' interface UseRoomAudioConfig { peerRoom: PeerRoom @@ -38,34 +39,34 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) { })() }, [audioStream]) - const [sendAudioChange, receiveAudioChange] = peerRoom.makeAction< - Partial - >(PeerAction.AUDIO_CHANGE, groupActionNamespace) - - receiveAudioChange((peerAudioChannelState, peerId) => { - setPeerList(peerList => { - return peerList.map(peer => { - const newPeer: Peer = { ...peer } - - const microphoneAudioChannel = - peerAudioChannelState[AudioChannelName.MICROPHONE] - - if (microphoneAudioChannel) { - if (peer.peerId === peerId) { - newPeer.audioChannelState = { - ...newPeer.audioChannelState, - ...peerAudioChannelState, - } - - if (microphoneAudioChannel === AudioState.STOPPED) { - deletePeerAudio(peerId) + const [sendAudioChange] = usePeerAction>({ + peerAction: PeerAction.AUDIO_CHANGE, + peerRoom, + onReceive: (peerAudioChannelState, peerId) => { + setPeerList(peerList => { + return peerList.map(peer => { + const newPeer: Peer = { ...peer } + + const microphoneAudioChannel = + peerAudioChannelState[AudioChannelName.MICROPHONE] + + if (microphoneAudioChannel) { + if (peer.peerId === peerId) { + newPeer.audioChannelState = { + ...newPeer.audioChannelState, + ...peerAudioChannelState, + } + + if (microphoneAudioChannel === AudioState.STOPPED) { + deletePeerAudio(peerId) + } } } - } - return newPeer + return newPeer + }) }) - }) + }, }) peerRoom.onPeerStream(PeerStreamType.AUDIO, (stream, peerId, metadata) => { diff --git a/src/components/Room/useRoomFileShare.ts b/src/components/Room/useRoomFileShare.ts index 91842a708..d0df7180e 100644 --- a/src/components/Room/useRoomFileShare.ts +++ b/src/components/Room/useRoomFileShare.ts @@ -3,10 +3,11 @@ import { useContext, useEffect, useState } from 'react' import { sleep } from 'lib/sleep' import { RoomContext } from 'contexts/RoomContext' import { ShellContext } from 'contexts/ShellContext' -import { groupActionNamespace, PeerAction } from 'models/network' +import { PeerAction } from 'models/network' import { FileOfferMetadata, Peer } from 'models/chat' import { PeerRoom, PeerHookType } from 'lib/PeerRoom' import { fileTransfer } from 'lib/FileTransfer' +import { usePeerAction } from 'hooks/usePeerAction' interface UseRoomFileShareConfig { onInlineMediaUpload: (files: File[]) => void @@ -32,44 +33,42 @@ export function useRoomFileShare({ const { peerList, setPeerList, showAlert } = shellContext const { peerOfferedFileMetadata, setPeerOfferedFileMetadata } = roomContext - const [sendFileOfferMetadata, receiveFileOfferMetadata] = - peerRoom.makeAction( - PeerAction.FILE_OFFER, - groupActionNamespace - ) - - receiveFileOfferMetadata((fileOfferMetadata, peerId) => { - if (fileOfferMetadata) { - setPeerOfferedFileMetadata({ [peerId]: fileOfferMetadata }) - } else { - const fileOfferMetadata = peerOfferedFileMetadata[peerId] - const { magnetURI, isAllInlineMedia } = fileOfferMetadata - - if ( - fileOfferMetadata && - fileTransfer.isOffering(magnetURI) && - !isAllInlineMedia - ) { - fileTransfer.rescind(magnetURI) + const [sendFileOfferMetadata] = usePeerAction({ + peerAction: PeerAction.FILE_OFFER, + peerRoom, + onReceive: (fileOfferMetadata, peerId) => { + if (fileOfferMetadata) { + setPeerOfferedFileMetadata({ [peerId]: fileOfferMetadata }) + } else { + const fileOfferMetadata = peerOfferedFileMetadata[peerId] + const { magnetURI, isAllInlineMedia } = fileOfferMetadata + + if ( + fileOfferMetadata && + fileTransfer.isOffering(magnetURI) && + !isAllInlineMedia + ) { + fileTransfer.rescind(magnetURI) + } + + const newFileOfferMetadata = { ...peerOfferedFileMetadata } + delete newFileOfferMetadata[peerId] + + setPeerOfferedFileMetadata(newFileOfferMetadata) } - const newFileOfferMetadata = { ...peerOfferedFileMetadata } - delete newFileOfferMetadata[peerId] - - setPeerOfferedFileMetadata(newFileOfferMetadata) - } + const newPeerList = peerList.map(peer => { + const newPeer: Peer = { ...peer } - const newPeerList = peerList.map(peer => { - const newPeer: Peer = { ...peer } + if (peer.peerId === peerId) { + newPeer.offeredFileId = fileOfferMetadata?.magnetURI ?? null + } - if (peer.peerId === peerId) { - newPeer.offeredFileId = fileOfferMetadata?.magnetURI ?? null - } - - return newPeer - }) + return newPeer + }) - setPeerList(newPeerList) + setPeerList(newPeerList) + }, }) const isEveryFileInlineMedia = (files: FileList | null) => diff --git a/src/components/Room/useRoomScreenShare.ts b/src/components/Room/useRoomScreenShare.ts index 8966476f5..1dd263df7 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 { groupActionNamespace, PeerAction } from 'models/network' +import { PeerAction } from 'models/network' import { ScreenShareState, Peer, @@ -12,6 +12,7 @@ import { AudioState, } from 'models/chat' import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom' +import { usePeerAction } from 'hooks/usePeerAction' interface UseRoomScreenShareConfig { peerRoom: PeerRoom @@ -37,28 +38,26 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) { setSelfScreenStream, } = roomContext - const [sendScreenShare, receiveScreenShare] = - peerRoom.makeAction( - PeerAction.SCREEN_SHARE, - groupActionNamespace - ) + const [sendScreenShare] = usePeerAction({ + peerAction: PeerAction.SCREEN_SHARE, + peerRoom, + onReceive: (screenState, peerId) => { + const newPeerList = peerList.map(peer => { + const newPeer: Peer = { ...peer } - receiveScreenShare((screenState, peerId) => { - const newPeerList = peerList.map(peer => { - const newPeer: Peer = { ...peer } + if (peer.peerId === peerId) { + newPeer.screenShareState = screenState - if (peer.peerId === peerId) { - newPeer.screenShareState = screenState - - if (screenState === ScreenShareState.NOT_SHARING) { - deletePeerScreen(peerId) + if (screenState === ScreenShareState.NOT_SHARING) { + deletePeerScreen(peerId) + } } - } - return newPeer - }) + return newPeer + }) - setPeerList(newPeerList) + setPeerList(newPeerList) + }, }) peerRoom.onPeerStream(PeerStreamType.SCREEN, (stream, peerId, metadata) => { diff --git a/src/components/Room/useRoomVideo.ts b/src/components/Room/useRoomVideo.ts index a5f54b6dd..4ded58d66 100644 --- a/src/components/Room/useRoomVideo.ts +++ b/src/components/Room/useRoomVideo.ts @@ -2,10 +2,11 @@ import { useContext, useEffect, useCallback, useState } from 'react' import { RoomContext } from 'contexts/RoomContext' import { ShellContext } from 'contexts/ShellContext' -import { groupActionNamespace, PeerAction } from 'models/network' +import { PeerAction } from 'models/network' import { VideoState, Peer, StreamType } from 'models/chat' import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom' import { isRecord } from 'lib/type-guards' +import { usePeerAction } from 'hooks/usePeerAction' interface UseRoomVideoConfig { peerRoom: PeerRoom @@ -68,27 +69,26 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) { })() }, [peerRoom, selfVideoStream, setSelfVideoStream]) - const [sendVideoChange, receiveVideoChange] = peerRoom.makeAction( - PeerAction.VIDEO_CHANGE, - groupActionNamespace - ) - - receiveVideoChange((videoState, peerId) => { - const newPeerList = peerList.map(peer => { - const newPeer: Peer = { ...peer } + const [sendVideoChange] = usePeerAction({ + peerAction: PeerAction.VIDEO_CHANGE, + peerRoom, + onReceive: (videoState, peerId) => { + const newPeerList = peerList.map(peer => { + const newPeer: Peer = { ...peer } - if (peer.peerId === peerId) { - newPeer.videoState = videoState + if (peer.peerId === peerId) { + newPeer.videoState = videoState - if (videoState === VideoState.STOPPED) { - deletePeerVideo(peerId) + if (videoState === VideoState.STOPPED) { + deletePeerVideo(peerId) + } } - } - return newPeer - }) + return newPeer + }) - setPeerList(newPeerList) + setPeerList(newPeerList) + }, }) peerRoom.onPeerStream(PeerStreamType.VIDEO, (stream, peerId, metadata) => { diff --git a/src/hooks/usePeerAction.ts b/src/hooks/usePeerAction.ts new file mode 100644 index 000000000..9a1381699 --- /dev/null +++ b/src/hooks/usePeerAction.ts @@ -0,0 +1,35 @@ +import { PeerRoom } from 'lib/PeerRoom' +import { PeerAction } from 'models/network' +import { useEffect, useState } from 'react' +import { + ActionProgress, + ActionReceiver, + ActionSender, + DataPayload, +} from 'trystero' + +export const usePeerAction = ({ + peerRoom, + peerAction, + onReceive, + namespace, +}: { + peerRoom: PeerRoom + peerAction: PeerAction + onReceive: Parameters>[0] + namespace?: string +}): [ActionSender, ActionProgress] => { + const [[sender, dispatchReceiver, progress, detatchReceiver]] = useState(() => + peerRoom.makeAction(peerAction, namespace) + ) + + useEffect(() => { + dispatchReceiver(onReceive) + + return () => { + detatchReceiver() + } + }, [detatchReceiver, onReceive, dispatchReceiver]) + + return [sender, progress] +} diff --git a/src/lib/PeerRoom/PeerRoom.ts b/src/lib/PeerRoom/PeerRoom.ts index 367af0b0a..a54e3e14e 100644 --- a/src/lib/PeerRoom/PeerRoom.ts +++ b/src/lib/PeerRoom/PeerRoom.ts @@ -1,9 +1,18 @@ -import { joinRoom, Room, BaseRoomConfig, DataPayload } from 'trystero/torrent' -import { RelayConfig } from 'trystero/torrent' +import { + joinRoom, + Room, + BaseRoomConfig, + DataPayload, + ActionProgress, + ActionReceiver, + ActionSender, + RelayConfig, +} from 'trystero/torrent' import { sleep } from 'lib/sleep' import { StreamType } from 'models/chat' import { PeerAction } from 'models/network' +import memoize from 'fast-memoize' export enum PeerHookType { NEW_PEER = 'NEW_PEER', @@ -169,12 +178,44 @@ export class PeerRoom { return peerConnections } - makeAction = ( - peerAction: PeerAction, - namespace: string - ) => { - return this.room.makeAction(`${namespace}.${peerAction}`) - } + makeAction = memoize( + ( + peerAction: PeerAction, + namespace?: string + ): [ActionSender, ActionReceiver, ActionProgress, () => void] => { + const [sender, receiver, progress] = this.room.makeAction( + `${namespace ?? '_'}.${peerAction}` + ) + + const eventName = `peerRoomAction.${namespace ?? '_'}.${peerAction}` + const eventTarget = new EventTarget() + + let handler: EventListenerOrEventListenerObject | null = null + + const dispatchReceiver: ActionReceiver = callback => { + handler = (event: Event): void => { + // @ts-expect-error + callback(...event.detail) + } + + eventTarget.addEventListener(eventName, handler) + } + + receiver((...args) => { + const customEvent = new CustomEvent(eventName, { + detail: args, + }) + + eventTarget.dispatchEvent(customEvent) + }) + + const detatchDispatchReceiver = () => { + eventTarget.removeEventListener(eventName, handler) + } + + return [sender, dispatchReceiver, progress, detatchDispatchReceiver] + } + ) addStream = ( stream: Parameters[0], diff --git a/src/models/network.ts b/src/models/network.ts index 672da07d0..eac043c7b 100644 --- a/src/models/network.ts +++ b/src/models/network.ts @@ -1,8 +1,3 @@ -// FIXME: Change these to be an enum -export const groupActionNamespace = 'g' -// FIXME: Multiple peer actions are probably overwriting each other -export const directMessageActionNamespace = 'dm' - // NOTE: Action names are limited to 12 characters, otherwise Trystero breaks. export enum PeerAction { MESSAGE = 0, diff --git a/vite.config.ts b/vite.config.ts index 38870844d..83cf69c34 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,6 +14,7 @@ import { manifest } from './manifest' const srcPaths = [ 'components', + 'hooks', 'config', 'contexts', 'lib',