forked from Shiloh/remnantchat
* 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:
parent
ac4cb2d7e0
commit
5d3d019cd6
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
67
src/components/Message/InlineMedia.tsx
Normal file
67
src/components/Message/InlineMedia.tsx
Normal 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} />)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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, {
|
||||||
|
@ -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: () => {},
|
||||||
})
|
})
|
||||||
|
@ -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
|
|
||||||
|
@ -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',
|
||||||
|
@ -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 => {
|
||||||
|
Loading…
Reference in New Issue
Block a user