feat: [closes #33] Render inline media (#73)

* refactor: pass inline media upload data to useRoom
* feat: render inline media
* fix: don't rescind inline media file offers
* refactor: send file offer metadata object
* fix: enable re-seeding of inline media files
* feat: show loading indicator for inline media
* feat: rescind any evicted inline media
* feat: display media rendering failure message
* feat: prevent user from uploading file if message is sending
This commit is contained in:
Jeremy Kahn 2022-11-28 21:18:41 -06:00 committed by GitHub
parent ac4cb2d7e0
commit 5d3d019cd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 321 additions and 89 deletions

View File

@ -2,11 +2,11 @@ import { HTMLAttributes, useRef, useEffect, useState } from 'react'
import cx from 'classnames' import cx from 'classnames'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import { Message as IMessage } from 'models/chat' import { Message as IMessage, InlineMedia } from 'models/chat'
import { Message } from 'components/Message' import { Message } from 'components/Message'
export interface ChatTranscriptProps extends HTMLAttributes<HTMLDivElement> { export interface ChatTranscriptProps extends HTMLAttributes<HTMLDivElement> {
messageLog: Array<IMessage> messageLog: Array<IMessage | InlineMedia>
userId: string userId: string
} }

View File

@ -0,0 +1,67 @@
import { useEffect, useRef, useState } from 'react'
import CircularProgress from '@mui/material/CircularProgress'
import { fileTransfer } from 'services/FileTransfer'
import { Typography } from '@mui/material'
type TorrentFiles = Awaited<ReturnType<typeof fileTransfer.download>>
interface InlineMediaProps {
magnetURI: string
}
interface InlineFileProps {
file: TorrentFiles[0]
}
export const InlineFile = ({ file }: InlineFileProps) => {
const containerRef = useRef(null)
const [didRenderingMediaFail, setDidRenderingMediaFail] = useState(false)
useEffect(() => {
const { current: container } = containerRef
if (!container) return
try {
file.appendTo(container)
} catch (e) {
setDidRenderingMediaFail(true)
}
}, [file, containerRef])
return (
<div ref={containerRef}>
{didRenderingMediaFail && (
<Typography sx={{ fontStyle: 'italic' }}>
Media failed to render
</Typography>
)}
</div>
)
}
export const InlineMedia = ({ magnetURI }: InlineMediaProps) => {
const [hasDownloadInitiated, setHasDownloadInitiated] = useState(false)
const [downloadedFiles, setDownloadedFiles] = useState<TorrentFiles>([])
useEffect(() => {
;(async () => {
if (hasDownloadInitiated) return
setHasDownloadInitiated(true)
const files = await fileTransfer.download(magnetURI)
setDownloadedFiles(files)
})()
}, [hasDownloadInitiated, magnetURI])
return (
<>
{hasDownloadInitiated && downloadedFiles.length === 0 ? (
<CircularProgress variant="indeterminate" color="inherit" />
) : (
downloadedFiles.map(file => <InlineFile file={file} key={file.name} />)
)}
</>
)
}

View File

@ -20,13 +20,19 @@ import { CodeProps } from 'react-markdown/lib/ast-to-react'
// @ts-ignore // @ts-ignore
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import { Message as IMessage, isMessageReceived } from 'models/chat' import {
InlineMedia as I_InlineMedia,
Message as IMessage,
isMessageReceived,
isInlineMedia,
} from 'models/chat'
import { PeerNameDisplay } from 'components/PeerNameDisplay' import { PeerNameDisplay } from 'components/PeerNameDisplay'
import { InlineMedia } from './InlineMedia'
import './Message.sass' import './Message.sass'
export interface MessageProps { export interface MessageProps {
message: IMessage message: IMessage | I_InlineMedia
showAuthor: boolean showAuthor: boolean
userId: string userId: string
} }
@ -123,13 +129,17 @@ export const Message = ({ message, showAuthor, userId }: MessageProps) => {
}} }}
maxWidth="85%" maxWidth="85%"
> >
<Markdown {isInlineMedia(message) ? (
components={componentMap} <InlineMedia magnetURI={message.magnetURI} />
remarkPlugins={[remarkGfm]} ) : (
linkTarget="_blank" <Markdown
> components={componentMap}
{message.text} remarkPlugins={[remarkGfm]}
</Markdown> linkTarget="_blank"
>
{message.text}
</Markdown>
)}
</Box> </Box>
</Tooltip> </Tooltip>
</Box> </Box>

View File

@ -37,6 +37,7 @@ export function Room({
}: RoomProps) { }: RoomProps) {
const { const {
isMessageSending, isMessageSending,
handleInlineMediaUpload,
messageLog, messageLog,
peerRoom, peerRoom,
roomContextValue, roomContextValue,
@ -107,7 +108,10 @@ export function Room({
<RoomAudioControls peerRoom={peerRoom} /> <RoomAudioControls peerRoom={peerRoom} />
<RoomVideoControls peerRoom={peerRoom} /> <RoomVideoControls peerRoom={peerRoom} />
<RoomScreenShareControls peerRoom={peerRoom} /> <RoomScreenShareControls peerRoom={peerRoom} />
<RoomFileUploadControls peerRoom={peerRoom} /> <RoomFileUploadControls
peerRoom={peerRoom}
onInlineMediaUpload={handleInlineMediaUpload}
/>
</Box> </Box>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>

View File

@ -1,31 +1,38 @@
import { ChangeEventHandler, useRef } from 'react' import { ChangeEventHandler, useContext, useRef } from 'react'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import UploadFile from '@mui/icons-material/UploadFile' import UploadFile from '@mui/icons-material/UploadFile'
import Cancel from '@mui/icons-material/Cancel' import Cancel from '@mui/icons-material/Cancel'
import Fab from '@mui/material/Fab' import Fab from '@mui/material/Fab'
import Tooltip from '@mui/material/Tooltip' import Tooltip from '@mui/material/Tooltip'
import { RoomContext } from 'contexts/RoomContext'
import { PeerRoom } from 'services/PeerRoom/PeerRoom' import { PeerRoom } from 'services/PeerRoom/PeerRoom'
import { useRoomFileShare } from './useRoomFileShare' import { useRoomFileShare } from './useRoomFileShare'
export interface RoomFileUploadControlsProps { export interface RoomFileUploadControlsProps {
onInlineMediaUpload: (files: File[]) => void
peerRoom: PeerRoom peerRoom: PeerRoom
} }
export function RoomFileUploadControls({ export function RoomFileUploadControls({
peerRoom, peerRoom,
onInlineMediaUpload,
}: RoomFileUploadControlsProps) { }: RoomFileUploadControlsProps) {
const roomContext = useContext(RoomContext)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const { isMessageSending } = roomContext
const { const {
isFileShareButtonEnabled, isFileSharingEnabled,
isSharingFile, isSharingFile,
handleFileShareStart, handleFileShareStart,
handleFileShareStop, handleFileShareStop,
sharedFiles, sharedFiles,
} = useRoomFileShare({ } = useRoomFileShare({
peerRoom, peerRoom,
onInlineMediaUpload,
}) })
const handleToggleScreenShareButtonClick = () => { const handleToggleScreenShareButtonClick = () => {
@ -51,6 +58,8 @@ export function RoomFileUploadControls({
const shareFileLabel = const shareFileLabel =
(sharedFiles && sharedFiles.length === 1 && sharedFiles[0].name) || 'files' (sharedFiles && sharedFiles.length === 1 && sharedFiles[0].name) || 'files'
const disableFileUpload = !isFileSharingEnabled || isMessageSending
return ( return (
<Box <Box
sx={{ sx={{
@ -80,7 +89,7 @@ export function RoomFileUploadControls({
color={isSharingFile ? 'error' : 'success'} color={isSharingFile ? 'error' : 'success'}
aria-label="share screen" aria-label="share screen"
onClick={handleToggleScreenShareButtonClick} onClick={handleToggleScreenShareButtonClick}
disabled={!isFileShareButtonEnabled} disabled={disableFileUpload}
> >
{isSharingFile ? <Cancel /> : <UploadFile />} {isSharingFile ? <Cancel /> : <UploadFile />}
</Fab> </Fab>

View File

@ -11,14 +11,20 @@ import {
Message, Message,
ReceivedMessage, ReceivedMessage,
UnsentMessage, UnsentMessage,
InlineMedia,
ReceivedInlineMedia,
UnsentInlineMedia,
VideoState, VideoState,
ScreenShareState, ScreenShareState,
isMessageReceived, isMessageReceived,
isInlineMedia,
FileOfferMetadata,
} from 'models/chat' } from 'models/chat'
import { getPeerName } from 'components/PeerNameDisplay' import { getPeerName } from 'components/PeerNameDisplay'
import { NotificationService } from 'services/Notification' import { NotificationService } from 'services/Notification'
import { Audio as AudioService } from 'services/Audio' import { Audio as AudioService } from 'services/Audio'
import { PeerRoom, PeerHookType } from 'services/PeerRoom' import { PeerRoom, PeerHookType } from 'services/PeerRoom'
import { fileTransfer } from 'services/FileTransfer'
import { messageTranscriptSizeLimit } from 'config/messaging' import { messageTranscriptSizeLimit } from 'config/messaging'
@ -52,14 +58,30 @@ export function useRoom(
} = useContext(ShellContext) } = useContext(ShellContext)
const settingsContext = useContext(SettingsContext) const settingsContext = useContext(SettingsContext)
const [isMessageSending, setIsMessageSending] = useState(false) const [isMessageSending, setIsMessageSending] = useState(false)
const [messageLog, _setMessageLog] = useState< const [messageLog, _setMessageLog] = useState<Array<Message | InlineMedia>>(
Array<ReceivedMessage | UnsentMessage> []
>([]) )
const [newMessageAudio] = useState( const [newMessageAudio] = useState(
() => new AudioService(process.env.PUBLIC_URL + '/sounds/new-message.aac') () => new AudioService(process.env.PUBLIC_URL + '/sounds/new-message.aac')
) )
const setMessageLog = (messages: Message[]) => { const setMessageLog = (messages: Array<Message | InlineMedia>) => {
if (messages.length > messageTranscriptSizeLimit) {
const evictedMessages = messages.slice(
0,
messages.length - messageTranscriptSizeLimit
)
for (const message of evictedMessages) {
if (
isInlineMedia(message) &&
fileTransfer.isOffering(message.magnetURI)
) {
fileTransfer.rescind(message.magnetURI)
}
}
}
_setMessageLog(messages.slice(-messageTranscriptSizeLimit)) _setMessageLog(messages.slice(-messageTranscriptSizeLimit))
} }
@ -77,12 +99,13 @@ export function useRoom(
Record<string, MediaStream> Record<string, MediaStream>
>({}) >({})
const [peerOfferedFileIds, setPeerOfferedFileIds] = useState< const [peerOfferedFileMetadata, setPeerOfferedFileMetadata] = useState<
Record<string, string> Record<string, FileOfferMetadata>
>({}) >({})
const roomContextValue = useMemo( const roomContextValue = useMemo(
() => ({ () => ({
isMessageSending,
selfVideoStream, selfVideoStream,
setSelfVideoStream, setSelfVideoStream,
peerVideoStreams, peerVideoStreams,
@ -91,10 +114,11 @@ export function useRoom(
setSelfScreenStream, setSelfScreenStream,
peerScreenStreams, peerScreenStreams,
setPeerScreenStreams, setPeerScreenStreams,
peerOfferedFileIds, peerOfferedFileMetadata,
setPeerOfferedFileIds, setPeerOfferedFileMetadata,
}), }),
[ [
isMessageSending,
selfVideoStream, selfVideoStream,
setSelfVideoStream, setSelfVideoStream,
peerVideoStreams, peerVideoStreams,
@ -103,8 +127,8 @@ export function useRoom(
setSelfScreenStream, setSelfScreenStream,
peerScreenStreams, peerScreenStreams,
setPeerScreenStreams, setPeerScreenStreams,
peerOfferedFileIds, peerOfferedFileMetadata,
setPeerOfferedFileIds, setPeerOfferedFileMetadata,
] ]
) )
@ -129,12 +153,15 @@ export function useRoom(
) )
const [sendMessageTranscript, receiveMessageTranscript] = usePeerRoomAction< const [sendMessageTranscript, receiveMessageTranscript] = usePeerRoomAction<
ReceivedMessage[] Array<ReceivedMessage | ReceivedInlineMedia>
>(peerRoom, PeerActions.MESSAGE_TRANSCRIPT) >(peerRoom, PeerActions.MESSAGE_TRANSCRIPT)
const [sendPeerMessage, receivePeerMessage] = const [sendPeerMessage, receivePeerMessage] =
usePeerRoomAction<UnsentMessage>(peerRoom, PeerActions.MESSAGE) usePeerRoomAction<UnsentMessage>(peerRoom, PeerActions.MESSAGE)
const [sendPeerInlineMedia, receivePeerInlineMedia] =
usePeerRoomAction<UnsentInlineMedia>(peerRoom, PeerActions.MEDIA_MESSAGE)
const sendMessage = async (message: string) => { const sendMessage = async (message: string) => {
if (isMessageSending) return if (isMessageSending) return
@ -254,7 +281,48 @@ export function useRoom(
Object.values({ ...peerVideoStreams, ...peerScreenStreams }).length > 0 Object.values({ ...peerVideoStreams, ...peerScreenStreams }).length > 0
) )
const handleInlineMediaUpload = async (files: File[]) => {
const fileOfferId = await fileTransfer.offer(files)
const unsentInlineMedia: UnsentInlineMedia = {
authorId: userId,
magnetURI: fileOfferId,
timeSent: Date.now(),
id: getUuid(),
}
setIsMessageSending(true)
setMessageLog([...messageLog, unsentInlineMedia])
await sendPeerInlineMedia(unsentInlineMedia)
setMessageLog([
...messageLog,
{ ...unsentInlineMedia, timeReceived: Date.now() },
])
setIsMessageSending(false)
}
receivePeerInlineMedia(inlineMedia => {
const userSettings = settingsContext.getUserSettings()
if (!tabHasFocus) {
if (userSettings.playSoundOnNewMessage) {
newMessageAudio.play()
}
if (userSettings.showNotificationOnNewMessage) {
NotificationService.showNotification(
`${getPeerName(inlineMedia.authorId)} shared media`
)
}
}
setMessageLog([...messageLog, { ...inlineMedia, timeReceived: Date.now() }])
})
return { return {
handleInlineMediaUpload,
isMessageSending, isMessageSending,
messageLog, messageLog,
peerRoom, peerRoom,

View File

@ -4,7 +4,7 @@ import { sleep } from 'utils'
import { RoomContext } from 'contexts/RoomContext' import { RoomContext } from 'contexts/RoomContext'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { PeerActions } from 'models/network' import { PeerActions } from 'models/network'
import { Peer } from 'models/chat' import { FileOfferMetadata, Peer } from 'models/chat'
import { PeerRoom, PeerHookType } from 'services/PeerRoom' import { PeerRoom, PeerHookType } from 'services/PeerRoom'
import { fileTransfer } from 'services/FileTransfer/index' import { fileTransfer } from 'services/FileTransfer/index'
@ -12,44 +12,61 @@ import { fileTransfer } from 'services/FileTransfer/index'
import { usePeerRoomAction } from './usePeerRoomAction' import { usePeerRoomAction } from './usePeerRoomAction'
interface UseRoomFileShareConfig { interface UseRoomFileShareConfig {
onInlineMediaUpload: (files: File[]) => void
peerRoom: PeerRoom peerRoom: PeerRoom
} }
export function useRoomFileShare({ peerRoom }: UseRoomFileShareConfig) { const isInlineMediaFile = (file: File) => {
return ['image', 'audio', 'video'].includes(file.type.split('/')[0])
}
export function useRoomFileShare({
onInlineMediaUpload,
peerRoom,
}: UseRoomFileShareConfig) {
const shellContext = useContext(ShellContext) const shellContext = useContext(ShellContext)
const roomContext = useContext(RoomContext) const roomContext = useContext(RoomContext)
const [sharedFiles, setSharedFiles] = useState<FileList | null>(null) const [sharedFiles, setSharedFiles] = useState<FileList | null>(null)
const [selfFileOfferId, setFileOfferId] = useState<string | null>(null) const [selfFileOfferMagnetUri, setFileOfferMagnetUri] = useState<
const [isFileShareButtonEnabled, setIsFileShareButtonEnabled] = useState(true) string | null
>(null)
const [isFileSharingEnabled, setIsFileSharingEnabled] = useState(true)
const { peerList, setPeerList } = shellContext const { peerList, setPeerList } = shellContext
const { peerOfferedFileIds, setPeerOfferedFileIds } = roomContext const { peerOfferedFileMetadata, setPeerOfferedFileMetadata } = roomContext
const [sendFileOfferId, receiveFileOfferId] = usePeerRoomAction< const [sendFileOfferMetadata, receiveFileOfferMetadata] =
string | null usePeerRoomAction<FileOfferMetadata | null>(
>(peerRoom, PeerActions.FILE_OFFER) peerRoom,
PeerActions.FILE_OFFER
)
receiveFileOfferId((fileOfferId, peerId) => { receiveFileOfferMetadata((fileOfferMetadata, peerId) => {
if (fileOfferId) { if (fileOfferMetadata) {
setPeerOfferedFileIds({ [peerId]: fileOfferId }) setPeerOfferedFileMetadata({ [peerId]: fileOfferMetadata })
} else { } else {
const fileOfferId = peerOfferedFileIds[peerId] const fileOfferMetadata = peerOfferedFileMetadata[peerId]
const { magnetURI, isAllInlineMedia } = fileOfferMetadata
if (fileOfferId && fileTransfer.isOffering(fileOfferId)) { if (
fileTransfer.rescind(fileOfferId) fileOfferMetadata &&
fileTransfer.isOffering(magnetURI) &&
!isAllInlineMedia
) {
fileTransfer.rescind(magnetURI)
} }
const newFileOfferIds = { ...peerOfferedFileIds } const newFileOfferMetadata = { ...peerOfferedFileMetadata }
delete newFileOfferIds[peerId] delete newFileOfferMetadata[peerId]
setPeerOfferedFileIds(newFileOfferIds) setPeerOfferedFileMetadata(newFileOfferMetadata)
} }
const newPeerList = peerList.map(peer => { const newPeerList = peerList.map(peer => {
const newPeer: Peer = { ...peer } const newPeer: Peer = { ...peer }
if (peer.peerId === peerId) { if (peer.peerId === peerId) {
newPeer.offeredFileId = fileOfferId newPeer.offeredFileId = fileOfferMetadata?.magnetURI ?? null
} }
return newPeer return newPeer
@ -58,68 +75,93 @@ export function useRoomFileShare({ peerRoom }: UseRoomFileShareConfig) {
setPeerList(newPeerList) setPeerList(newPeerList)
}) })
const isEveryFileInlineMedia = (files: FileList | null) =>
Boolean(files && [...files].every(isInlineMediaFile))
peerRoom.onPeerJoin(PeerHookType.FILE_SHARE, async (peerId: string) => { peerRoom.onPeerJoin(PeerHookType.FILE_SHARE, async (peerId: string) => {
if (!selfFileOfferId) return if (!selfFileOfferMagnetUri) return
// This sleep is needed to prevent this peer from not appearing on other // This sleep is needed to prevent this peer from not appearing on other
// peers' peer lists. This is because Trystero's interaction between // peers' peer lists. This is because Trystero's interaction between
// onPeerJoin and its actions is not totally compatible with React's // onPeerJoin and its actions is not totally compatible with React's
// lifecycle hooks. In this case, the reference to peerList in // lifecycle hooks. In this case, the reference to peerList in
// receiveFileOfferId is out of date and prevents this peer from ever being // receiveFileOfferMetadata is out of date and prevents this peer from ever
// added to the receiver's peer list. Deferring the sendFileOfferId call to // being added to the receiver's peer list. Deferring the
// the next tick serves as a workaround. // sendFileOfferMetadata call to the next tick serves as a workaround.
await sleep(1) await sleep(1)
sendFileOfferId(selfFileOfferId, peerId) sendFileOfferMetadata(
{
magnetURI: selfFileOfferMagnetUri,
isAllInlineMedia: isEveryFileInlineMedia(sharedFiles),
},
peerId
)
}) })
peerRoom.onPeerLeave(PeerHookType.FILE_SHARE, (peerId: string) => { peerRoom.onPeerLeave(PeerHookType.FILE_SHARE, (peerId: string) => {
const fileOfferId = peerOfferedFileIds[peerId] const fileOfferMetadata = peerOfferedFileMetadata[peerId]
if (!fileOfferId) return if (!fileOfferMetadata) return
if (fileTransfer.isOffering(fileOfferId)) { const { magnetURI, isAllInlineMedia } = fileOfferMetadata
fileTransfer.rescind(fileOfferId)
if (fileTransfer.isOffering(magnetURI) && !isAllInlineMedia) {
fileTransfer.rescind(magnetURI)
} }
const newPeerFileOfferIds = { ...peerOfferedFileIds } const newPeerFileOfferMetadata = { ...peerOfferedFileMetadata }
delete newPeerFileOfferIds[peerId] delete newPeerFileOfferMetadata[peerId]
setPeerOfferedFileIds(newPeerFileOfferIds) setPeerOfferedFileMetadata(newPeerFileOfferMetadata)
}) })
const handleFileShareStart = async (files: FileList) => { const handleFileShareStart = async (files: FileList) => {
const inlineMediaFiles = [...files].filter(isInlineMediaFile)
setSharedFiles(files) setSharedFiles(files)
setIsFileShareButtonEnabled(false) setIsFileSharingEnabled(false)
const fileOfferId = await fileTransfer.offer(files) const magnetURI = await fileTransfer.offer(files)
sendFileOfferId(fileOfferId)
setFileOfferId(fileOfferId)
setIsFileShareButtonEnabled(true) if (inlineMediaFiles.length > 0) {
onInlineMediaUpload(inlineMediaFiles)
}
sendFileOfferMetadata({
magnetURI,
isAllInlineMedia: isEveryFileInlineMedia(files),
})
setFileOfferMagnetUri(magnetURI)
setIsFileSharingEnabled(true)
} }
const handleFileShareStop = () => { const handleFileShareStop = () => {
sendFileOfferId(null) sendFileOfferMetadata(null)
setFileOfferId(null) setFileOfferMagnetUri(null)
if (selfFileOfferId && fileTransfer.isOffering(selfFileOfferId)) { if (
fileTransfer.rescind(selfFileOfferId) selfFileOfferMagnetUri &&
fileTransfer.isOffering(selfFileOfferMagnetUri) &&
!isEveryFileInlineMedia(sharedFiles)
) {
fileTransfer.rescind(selfFileOfferMagnetUri)
} }
} }
useEffect(() => { useEffect(() => {
return () => { return () => {
fileTransfer.rescindAll() fileTransfer.rescindAll()
sendFileOfferId(null) sendFileOfferMetadata(null)
} }
}, [sendFileOfferId]) }, [sendFileOfferMetadata])
const isSharingFile = Boolean(selfFileOfferId) const isSharingFile = Boolean(selfFileOfferMagnetUri)
return { return {
handleFileShareStart, handleFileShareStart,
handleFileShareStop, handleFileShareStop,
isFileShareButtonEnabled, isFileSharingEnabled,
isSharingFile, isSharingFile,
sharedFiles, sharedFiles,
} }

View File

@ -38,7 +38,7 @@ export const PeerDownloadFileButton = ({
setDownloadProgress(null) setDownloadProgress(null)
try { try {
await fileTransfer.download(offeredFileId, { onProgress }) await fileTransfer.download(offeredFileId, { doSave: true, onProgress })
} catch (e) { } catch (e) {
if (isError(e)) { if (isError(e)) {
shellContext.showAlert(e.message, { shellContext.showAlert(e.message, {

View File

@ -1,6 +1,8 @@
import { createContext, Dispatch, SetStateAction } from 'react' import { createContext, Dispatch, SetStateAction } from 'react'
import { FileOfferMetadata } from 'models/chat'
interface RoomContextProps { interface RoomContextProps {
isMessageSending: boolean
selfVideoStream: MediaStream | null selfVideoStream: MediaStream | null
setSelfVideoStream: Dispatch<SetStateAction<MediaStream | null>> setSelfVideoStream: Dispatch<SetStateAction<MediaStream | null>>
peerVideoStreams: Record<string, MediaStream> peerVideoStreams: Record<string, MediaStream>
@ -9,11 +11,14 @@ interface RoomContextProps {
setSelfScreenStream: Dispatch<SetStateAction<MediaStream | null>> setSelfScreenStream: Dispatch<SetStateAction<MediaStream | null>>
peerScreenStreams: Record<string, MediaStream> peerScreenStreams: Record<string, MediaStream>
setPeerScreenStreams: Dispatch<SetStateAction<Record<string, MediaStream>>> setPeerScreenStreams: Dispatch<SetStateAction<Record<string, MediaStream>>>
peerOfferedFileIds: Record<string, string> peerOfferedFileMetadata: Record<string, FileOfferMetadata>
setPeerOfferedFileIds: Dispatch<SetStateAction<Record<string, string>>> setPeerOfferedFileMetadata: Dispatch<
SetStateAction<Record<string, FileOfferMetadata>>
>
} }
export const RoomContext = createContext<RoomContextProps>({ export const RoomContext = createContext<RoomContextProps>({
isMessageSending: false,
selfVideoStream: null, selfVideoStream: null,
setSelfVideoStream: () => {}, setSelfVideoStream: () => {},
peerVideoStreams: {}, peerVideoStreams: {},
@ -22,6 +27,6 @@ export const RoomContext = createContext<RoomContextProps>({
setSelfScreenStream: () => {}, setSelfScreenStream: () => {},
peerScreenStreams: {}, peerScreenStreams: {},
setPeerScreenStreams: () => {}, setPeerScreenStreams: () => {},
peerOfferedFileIds: {}, peerOfferedFileMetadata: {},
setPeerOfferedFileIds: () => {}, setPeerOfferedFileMetadata: () => {},
}) })

View File

@ -5,6 +5,22 @@ export interface UnsentMessage {
authorId: string authorId: string
} }
export interface ReceivedMessage extends UnsentMessage {
timeReceived: number
}
export type Message = UnsentMessage | ReceivedMessage
export interface UnsentInlineMedia extends Omit<UnsentMessage, 'text'> {
magnetURI: string
}
export interface ReceivedInlineMedia extends UnsentInlineMedia {
timeReceived: number
}
export type InlineMedia = UnsentInlineMedia | ReceivedInlineMedia
export enum AudioState { export enum AudioState {
PLAYING = 'PLAYING', PLAYING = 'PLAYING',
STOPPED = 'STOPPED', STOPPED = 'STOPPED',
@ -34,12 +50,17 @@ export interface Peer {
offeredFileId: string | null offeredFileId: string | null
} }
export interface ReceivedMessage extends UnsentMessage { export const isMessageReceived = (
timeReceived: number message: Message | InlineMedia
): message is ReceivedMessage | ReceivedInlineMedia => 'timeReceived' in message
export const isInlineMedia = (
message: Message | InlineMedia
): message is InlineMedia => {
return 'magnetURI' in message
} }
export const isMessageReceived = ( export interface FileOfferMetadata {
message: Message magnetURI: string
): message is ReceivedMessage => 'timeReceived' in message isAllInlineMedia: boolean
}
export type Message = UnsentMessage | ReceivedMessage

View File

@ -1,6 +1,7 @@
// NOTE: Action names are limited to 12 characters, otherwise Trystero breaks. // NOTE: Action names are limited to 12 characters, otherwise Trystero breaks.
export enum PeerActions { export enum PeerActions {
MESSAGE = 'MESSAGE', MESSAGE = 'MESSAGE',
MEDIA_MESSAGE = 'MEDIA_MSG',
MESSAGE_TRANSCRIPT = 'MSG_XSCRIPT', MESSAGE_TRANSCRIPT = 'MSG_XSCRIPT',
PEER_NAME = 'PEER_NAME', PEER_NAME = 'PEER_NAME',
AUDIO_CHANGE = 'AUDIO_CHANGE', AUDIO_CHANGE = 'AUDIO_CHANGE',

View File

@ -10,6 +10,7 @@ import { streamSaverUrl } from 'config/streamSaverUrl'
streamSaver.mitm = streamSaverUrl streamSaver.mitm = streamSaverUrl
interface DownloadOpts { interface DownloadOpts {
doSave?: boolean
onProgress?: (progress: number) => void onProgress?: (progress: number) => void
} }
@ -72,7 +73,7 @@ export class FileTransfer {
window.addEventListener('beforeunload', this.handleBeforePageUnload) window.addEventListener('beforeunload', this.handleBeforePageUnload)
} }
async download(magnetURI: string, { onProgress }: DownloadOpts = {}) { async download(magnetURI: string, { onProgress, doSave }: DownloadOpts = {}) {
let torrent = this.torrents[magnetURI] let torrent = this.torrents[magnetURI]
if (!torrent) { if (!torrent) {
@ -104,17 +105,21 @@ export class FileTransfer {
torrent.on('download', handleDownload) torrent.on('download', handleDownload)
try { if (doSave) {
await this.saveTorrentFiles(torrent) try {
} catch (e) { await this.saveTorrentFiles(torrent)
torrent.off('download', handleDownload) } catch (e) {
torrent.off('download', handleDownload)
// Propagate error to the UI // Propagate error to the UI
throw e throw e
}
} }
return torrent.files
} }
async offer(files: FileList) { async offer(files: Parameters<typeof this.webTorrentClient.seed>[0]) {
const { isPrivate } = await detectIncognito() const { isPrivate } = await detectIncognito()
const torrent = await new Promise<Torrent>(res => { const torrent = await new Promise<Torrent>(res => {