forked from Shiloh/remnantchat
* feat(typing-indicator): wire up handleMessageChange * feat(typing-indicator): send typing: true status * feat(typing-indicator): expire typing state * feat(typing-indicator): update typing state received from peers * refactor(shell): add updatePeer utility * feat(typing-indicator): display peer typing status * feat(typing-indicator): reset typing status when a message is sent * feat(typing-indicator): move indicator below message form * feat(typing-indicator): keep status text to one line
This commit is contained in:
parent
af4cba8449
commit
e597a667a1
34
package-lock.json
generated
34
package-lock.json
generated
@ -13,6 +13,7 @@
|
||||
"@emotion/styled": "^11.10.0",
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"@mui/material": "^5.9.3",
|
||||
"@react-hook/debounce": "^4.0.0",
|
||||
"@react-hook/window-size": "^3.1.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
@ -4587,9 +4588,9 @@
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"node_modules/@react-hook/debounce": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-3.0.0.tgz",
|
||||
"integrity": "sha512-ir/kPrSfAzY12Gre0sOHkZ2rkEmM4fS5M5zFxCi4BnCeXh2nvx9Ujd+U4IGpKCuPA+EQD0pg1eK2NGLvfWejag==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-4.0.0.tgz",
|
||||
"integrity": "sha512-706Xcg+KKWHk9BuZQUQ0ZQKp9zhv3/MbqFenWVfHcynYpSGRVwQTzJRGvPxvsdtXxJv+HfgKTY/O/hEejakwmA==",
|
||||
"dependencies": {
|
||||
"@react-hook/latest": "^1.0.2"
|
||||
},
|
||||
@ -4637,6 +4638,17 @@
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-hook/window-size/node_modules/@react-hook/debounce": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-3.0.0.tgz",
|
||||
"integrity": "sha512-ir/kPrSfAzY12Gre0sOHkZ2rkEmM4fS5M5zFxCi4BnCeXh2nvx9Ujd+U4IGpKCuPA+EQD0pg1eK2NGLvfWejag==",
|
||||
"dependencies": {
|
||||
"@react-hook/latest": "^1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-babel": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
||||
@ -29935,9 +29947,9 @@
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"@react-hook/debounce": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-3.0.0.tgz",
|
||||
"integrity": "sha512-ir/kPrSfAzY12Gre0sOHkZ2rkEmM4fS5M5zFxCi4BnCeXh2nvx9Ujd+U4IGpKCuPA+EQD0pg1eK2NGLvfWejag==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-4.0.0.tgz",
|
||||
"integrity": "sha512-706Xcg+KKWHk9BuZQUQ0ZQKp9zhv3/MbqFenWVfHcynYpSGRVwQTzJRGvPxvsdtXxJv+HfgKTY/O/hEejakwmA==",
|
||||
"requires": {
|
||||
"@react-hook/latest": "^1.0.2"
|
||||
}
|
||||
@ -29970,6 +29982,16 @@
|
||||
"@react-hook/debounce": "^3.0.0",
|
||||
"@react-hook/event": "^1.2.1",
|
||||
"@react-hook/throttle": "^2.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-hook/debounce": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-3.0.0.tgz",
|
||||
"integrity": "sha512-ir/kPrSfAzY12Gre0sOHkZ2rkEmM4fS5M5zFxCi4BnCeXh2nvx9Ujd+U4IGpKCuPA+EQD0pg1eK2NGLvfWejag==",
|
||||
"requires": {
|
||||
"@react-hook/latest": "^1.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@rollup/plugin-babel": {
|
||||
|
@ -9,6 +9,7 @@
|
||||
"@emotion/styled": "^11.10.0",
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"@mui/material": "^5.9.3",
|
||||
"@react-hook/debounce": "^4.0.0",
|
||||
"@react-hook/window-size": "^3.1.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
|
@ -15,11 +15,13 @@ import { messageCharacterSizeLimit } from 'config/messaging'
|
||||
|
||||
interface MessageFormProps {
|
||||
onMessageSubmit: (message: string) => void
|
||||
onMessageChange: (message: string) => void
|
||||
isMessageSending: boolean
|
||||
}
|
||||
|
||||
export const MessageForm = ({
|
||||
onMessageSubmit,
|
||||
onMessageChange,
|
||||
isMessageSending,
|
||||
}: MessageFormProps) => {
|
||||
const textFieldRef = useRef<HTMLInputElement>(null)
|
||||
@ -43,6 +45,7 @@ export const MessageForm = ({
|
||||
const handleMessageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target
|
||||
setTextMessage(value)
|
||||
onMessageChange(value)
|
||||
}
|
||||
|
||||
const submitMessage = () => {
|
||||
@ -68,7 +71,7 @@ export const MessageForm = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleMessageSubmit} className="p-4">
|
||||
<form onSubmit={handleMessageSubmit} className="pt-4 px-4">
|
||||
<Stack direction="row" spacing={2}>
|
||||
<FormControl fullWidth>
|
||||
<TextField
|
||||
|
@ -3,7 +3,7 @@ import Typography, { TypographyProps } from '@mui/material/Typography'
|
||||
import { usePeerNameDisplay } from './usePeerNameDisplay'
|
||||
import { getPeerName } from './getPeerName'
|
||||
|
||||
interface PeerNameDisplayProps extends TypographyProps {
|
||||
export interface PeerNameDisplayProps extends TypographyProps {
|
||||
children: string
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import { RoomFileUploadControls } from './RoomFileUploadControls'
|
||||
import { RoomVideoDisplay } from './RoomVideoDisplay'
|
||||
import { RoomShowMessagesControls } from './RoomShowMessagesControls'
|
||||
import { RoomHideRoomControls } from './RoomHideRoomControls'
|
||||
import { TypingStatusBar } from './TypingStatusBar'
|
||||
|
||||
export interface RoomProps {
|
||||
appId?: string
|
||||
@ -43,6 +44,7 @@ export function Room({
|
||||
const {
|
||||
isMessageSending,
|
||||
handleInlineMediaUpload,
|
||||
handleMessageChange,
|
||||
messageLog,
|
||||
peerRoom,
|
||||
roomContextValue,
|
||||
@ -150,10 +152,14 @@ export function Room({
|
||||
className="grow overflow-auto px-4"
|
||||
/>
|
||||
<Divider />
|
||||
<Box>
|
||||
<MessageForm
|
||||
onMessageSubmit={handleMessageSubmit}
|
||||
isMessageSending={isMessageSending}
|
||||
onMessageChange={handleMessageChange}
|
||||
/>
|
||||
<TypingStatusBar />
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
71
src/components/Room/TypingStatusBar.tsx
Normal file
71
src/components/Room/TypingStatusBar.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import Box from '@mui/material/Box'
|
||||
import { Typography } from '@mui/material'
|
||||
import { useContext } from 'react'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import {
|
||||
PeerNameDisplay,
|
||||
PeerNameDisplayProps,
|
||||
} from 'components/PeerNameDisplay/PeerNameDisplay'
|
||||
|
||||
export const TypingStatusBar = () => {
|
||||
const { peerList } = useContext(ShellContext)
|
||||
const typingPeers = peerList.filter(({ isTyping }) => isTyping)
|
||||
|
||||
const peerNameDisplayProps: Partial<PeerNameDisplayProps> = {
|
||||
variant: 'caption',
|
||||
sx: theme => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
}),
|
||||
}
|
||||
|
||||
let statusMessage = <></>
|
||||
|
||||
if (typingPeers.length === 1) {
|
||||
statusMessage = (
|
||||
<>
|
||||
<PeerNameDisplay {...peerNameDisplayProps}>
|
||||
{typingPeers[0].userId}
|
||||
</PeerNameDisplay>{' '}
|
||||
is typing...
|
||||
</>
|
||||
)
|
||||
} else if (typingPeers.length === 2) {
|
||||
statusMessage = (
|
||||
<>
|
||||
<PeerNameDisplay {...peerNameDisplayProps}>
|
||||
{typingPeers[0].userId}
|
||||
</PeerNameDisplay>{' '}
|
||||
and{' '}
|
||||
<PeerNameDisplay {...peerNameDisplayProps}>
|
||||
{typingPeers[1].userId}
|
||||
</PeerNameDisplay>{' '}
|
||||
are typing...
|
||||
</>
|
||||
)
|
||||
} else if (typingPeers.length > 2) {
|
||||
statusMessage = <>Several people are typing...</>
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={theme => ({
|
||||
color: theme.palette.text.secondary,
|
||||
display: 'block',
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
height: '1.75rem',
|
||||
maxHeight: '1.75rem',
|
||||
overflow: 'hidden',
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
})}
|
||||
>
|
||||
{statusMessage}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -2,6 +2,7 @@ import { useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { BaseRoomConfig } from 'trystero'
|
||||
import { TorrentRoomConfig } from 'trystero/torrent'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { useDebounce } from '@react-hook/debounce'
|
||||
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { SettingsContext } from 'contexts/SettingsContext'
|
||||
@ -19,6 +20,7 @@ import {
|
||||
isMessageReceived,
|
||||
isInlineMedia,
|
||||
FileOfferMetadata,
|
||||
TypingStatus,
|
||||
} from 'models/chat'
|
||||
import { getPeerName, usePeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
import { NotificationService } from 'services/Notification'
|
||||
@ -60,6 +62,7 @@ export function useRoom(
|
||||
setRoomId,
|
||||
setPassword,
|
||||
customUsername,
|
||||
updatePeer,
|
||||
} = useContext(ShellContext)
|
||||
|
||||
const settingsContext = useContext(SettingsContext)
|
||||
@ -151,12 +154,26 @@ export function useRoom(
|
||||
]
|
||||
)
|
||||
|
||||
const [sendTypingStatusChange, receiveTypingStatusChange] =
|
||||
usePeerRoomAction<TypingStatus>(peerRoom, PeerActions.TYPING_STATUS_CHANGE)
|
||||
|
||||
const [isTyping, setIsTypingDebounced, setIsTyping] = useDebounce(
|
||||
false,
|
||||
2000,
|
||||
true
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
sendTypingStatusChange({ isTyping })
|
||||
}, [isTyping, sendTypingStatusChange])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
sendTypingStatusChange({ isTyping: false })
|
||||
peerRoom.leaveRoom()
|
||||
setPeerList([])
|
||||
}
|
||||
}, [peerRoom, setPeerList])
|
||||
}, [peerRoom, setPeerList, sendTypingStatusChange])
|
||||
|
||||
useEffect(() => {
|
||||
setPassword(password)
|
||||
@ -201,6 +218,7 @@ export function useRoom(
|
||||
id: getUuid(),
|
||||
}
|
||||
|
||||
setIsTyping(false)
|
||||
setIsMessageSending(true)
|
||||
setMessageLog([...messageLog, unsentMessage])
|
||||
await sendPeerMessage(unsentMessage)
|
||||
@ -226,6 +244,7 @@ export function useRoom(
|
||||
videoState: VideoState.STOPPED,
|
||||
screenShareState: ScreenShareState.NOT_SHARING,
|
||||
offeredFileId: null,
|
||||
isTyping: false,
|
||||
},
|
||||
])
|
||||
} else {
|
||||
@ -250,7 +269,7 @@ export function useRoom(
|
||||
setMessageLog(transcript)
|
||||
})
|
||||
|
||||
receivePeerMessage(message => {
|
||||
receivePeerMessage((message, peerId) => {
|
||||
const userSettings = settingsContext.getUserSettings()
|
||||
|
||||
if (!isShowingMessages) {
|
||||
@ -272,6 +291,7 @@ export function useRoom(
|
||||
}
|
||||
|
||||
setMessageLog([...messageLog, { ...message, timeReceived: Date.now() }])
|
||||
updatePeer(peerId, { isTyping: false })
|
||||
})
|
||||
|
||||
peerRoom.onPeerJoin(PeerHookType.NEW_PEER, (peerId: string) => {
|
||||
@ -299,18 +319,20 @@ export function useRoom(
|
||||
|
||||
peerRoom.onPeerLeave(PeerHookType.NEW_PEER, (peerId: string) => {
|
||||
const peerIndex = peerList.findIndex(peer => peer.peerId === peerId)
|
||||
const peerExist = peerIndex !== -1
|
||||
const doesPeerExist = peerIndex !== -1
|
||||
|
||||
showAlert(
|
||||
`${
|
||||
peerExist ? getDisplayUsername(peerList[peerIndex].userId) : 'Someone'
|
||||
doesPeerExist
|
||||
? getDisplayUsername(peerList[peerIndex].userId)
|
||||
: 'Someone'
|
||||
} has left the room`,
|
||||
{
|
||||
severity: 'warning',
|
||||
}
|
||||
)
|
||||
|
||||
if (peerExist) {
|
||||
if (doesPeerExist) {
|
||||
const peerListClone = [...peerList]
|
||||
peerListClone.splice(peerIndex, 1)
|
||||
setPeerList(peerListClone)
|
||||
@ -347,6 +369,18 @@ export function useRoom(
|
||||
setIsMessageSending(false)
|
||||
}
|
||||
|
||||
const handleMessageChange = () => {
|
||||
if (isTyping) {
|
||||
setIsTypingDebounced(true)
|
||||
} else {
|
||||
setIsTyping(true)
|
||||
}
|
||||
|
||||
// This queues up the expiration of the typing state. It is effectively
|
||||
// cancelled once this message change handler is called again.
|
||||
setIsTypingDebounced(false)
|
||||
}
|
||||
|
||||
receivePeerInlineMedia(inlineMedia => {
|
||||
const userSettings = settingsContext.getUserSettings()
|
||||
|
||||
@ -365,6 +399,11 @@ export function useRoom(
|
||||
setMessageLog([...messageLog, { ...inlineMedia, timeReceived: Date.now() }])
|
||||
})
|
||||
|
||||
receiveTypingStatusChange((typingStatus, peerId) => {
|
||||
const { isTyping } = typingStatus
|
||||
updatePeer(peerId, { isTyping })
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
sendPeerMetadata({ customUsername, userId })
|
||||
}, [customUsername, userId, sendPeerMetadata])
|
||||
@ -378,6 +417,7 @@ export function useRoom(
|
||||
return {
|
||||
isPrivate,
|
||||
handleInlineMediaUpload,
|
||||
handleMessageChange,
|
||||
isMessageSending,
|
||||
messageLog,
|
||||
peerRoom,
|
||||
|
@ -88,9 +88,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
const [peerAudios, setPeerAudios] = useState<
|
||||
Record<string, HTMLAudioElement>
|
||||
>({})
|
||||
const showAlert = useCallback<
|
||||
(message: string, options?: AlertOptions) => void
|
||||
>((message, options) => {
|
||||
|
||||
const showAlert = useCallback((message: string, options?: AlertOptions) => {
|
||||
setAlertText(message)
|
||||
setAlertSeverity(options?.severity ?? 'info')
|
||||
setIsAlertShowing(true)
|
||||
@ -98,6 +97,21 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
|
||||
const { connectionTestResults } = useConnectionTest()
|
||||
|
||||
const updatePeer = useCallback(
|
||||
(peerId: string, updatedProperties: Partial<Peer>) => {
|
||||
const peerIndex = peerList.findIndex(peer => peer.peerId === peerId)
|
||||
const doesPeerExist = peerIndex !== -1
|
||||
|
||||
if (!doesPeerExist) return
|
||||
|
||||
const peerListClone = [...peerList]
|
||||
const peer = peerList[peerIndex]
|
||||
peerListClone[peerIndex] = { ...peer, ...updatedProperties }
|
||||
setPeerList(peerListClone)
|
||||
},
|
||||
[peerList]
|
||||
)
|
||||
|
||||
const shellContextValue = useMemo(
|
||||
() => ({
|
||||
tabHasFocus,
|
||||
@ -129,6 +143,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
customUsername,
|
||||
setCustomUsername,
|
||||
connectionTestResults,
|
||||
updatePeer,
|
||||
}),
|
||||
[
|
||||
isPeerListOpen,
|
||||
@ -157,6 +172,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
customUsername,
|
||||
setCustomUsername,
|
||||
connectionTestResults,
|
||||
updatePeer,
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -37,6 +37,7 @@ interface ShellContextProps {
|
||||
customUsername: string
|
||||
setCustomUsername: Dispatch<SetStateAction<string>>
|
||||
connectionTestResults: ConnectionTestResults
|
||||
updatePeer: (peerId: string, updatedProperties: Partial<Peer>) => void
|
||||
}
|
||||
|
||||
export const ShellContext = createContext<ShellContextProps>({
|
||||
@ -72,4 +73,5 @@ export const ShellContext = createContext<ShellContextProps>({
|
||||
hasRelay: false,
|
||||
trackerConnection: TrackerConnection.SEARCHING,
|
||||
},
|
||||
updatePeer: () => {},
|
||||
})
|
||||
|
@ -49,6 +49,7 @@ export interface Peer {
|
||||
videoState: VideoState
|
||||
screenShareState: ScreenShareState
|
||||
offeredFileId: string | null
|
||||
isTyping: boolean
|
||||
}
|
||||
|
||||
export const isMessageReceived = (
|
||||
@ -65,3 +66,7 @@ export interface FileOfferMetadata {
|
||||
magnetURI: string
|
||||
isAllInlineMedia: boolean
|
||||
}
|
||||
|
||||
export interface TypingStatus {
|
||||
isTyping: boolean
|
||||
}
|
||||
|
@ -8,4 +8,5 @@ export enum PeerActions {
|
||||
VIDEO_CHANGE = 'VIDEO_CHANGE',
|
||||
SCREEN_SHARE = 'SCREEN_SHARE',
|
||||
FILE_OFFER = 'FILE_OFFER',
|
||||
TYPING_STATUS_CHANGE = 'TYPNG_CHANGE',
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user