remnantchat/src/components/Room/useRoomFileShare.ts
Jeremy Kahn 5d3d019cd6
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
2022-11-28 21:18:41 -06:00

169 lines
4.8 KiB
TypeScript

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