blob: c93836958827c6073bc916c452e27d8888fa1209 [file] [log] [blame]
simon26e79f72022-10-05 22:16:08 -04001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
idillonae655dd2022-10-14 18:11:02 -040018import { InputBase } from '@mui/material';
simon07b4eb02022-09-29 17:50:26 -040019import { Stack } from '@mui/system';
idillon3e378fd2022-12-23 11:48:12 -050020import { WebSocketMessageType } from 'jami-web-common';
idillona4b96ab2023-02-01 15:30:12 -050021import { ChangeEvent, FormEvent, useCallback, useMemo, useRef, useState } from 'react';
idillonae655dd2022-10-14 18:11:02 -040022import { useTranslation } from 'react-i18next';
simon07b4eb02022-09-29 17:50:26 -040023
simon5da8ca62022-11-09 15:21:25 -050024import { useAuthContext } from '../contexts/AuthProvider';
idillon07d31cc2022-12-06 22:40:14 -050025import { useConversationContext } from '../contexts/ConversationProvider';
idillona4b96ab2023-02-01 15:30:12 -050026import { useWebSocketContext } from '../contexts/WebSocketProvider';
idillon07d31cc2022-12-06 22:40:14 -050027import { ConversationMember } from '../models/conversation-member';
idillonae655dd2022-10-14 18:11:02 -040028import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
simond47ef9e2022-09-28 22:24:28 -040029import {
30 RecordVideoMessageButton,
31 RecordVoiceMessageButton,
32 SelectEmojiButton,
33 SendMessageButton,
34 UploadFileButton,
simon6b9ddfb2022-10-03 00:04:50 -040035} from './Button';
Larbi Gharibe9af9732021-03-31 15:08:01 +010036
simon6b9ddfb2022-10-03 00:04:50 -040037type SendMessageFormProps = {
38 onSend: (message: string) => void;
idilloncab81d72022-11-08 12:20:00 -050039 openFilePicker: () => void;
simon6b9ddfb2022-10-03 00:04:50 -040040};
41
idillon07d31cc2022-12-06 22:40:14 -050042export default function SendMessageForm({ onSend, openFilePicker }: SendMessageFormProps) {
idillona4b96ab2023-02-01 15:30:12 -050043 const webSocket = useWebSocketContext();
idillon3e378fd2022-12-23 11:48:12 -050044 const { members, conversationId } = useConversationContext();
simond47ef9e2022-09-28 22:24:28 -040045 const [currentMessage, setCurrentMessage] = useState('');
idillon3e378fd2022-12-23 11:48:12 -050046 const composingNotificationTimeRef = useRef(0);
simon5da8ca62022-11-09 15:21:25 -050047 const placeholder = usePlaceholder(members);
Adrien Béraud35e7d7c2021-04-13 03:28:39 -040048
idillon3e378fd2022-12-23 11:48:12 -050049 const notifyComposing = useCallback(() => {
50 const currentTime = new Date().getTime();
51 // The daemon automatically turns off "isComposing" after 12 seconds
52 // We ensure it will stay on at least 4 seconds after the last typed character
53 if (currentTime - composingNotificationTimeRef.current > 8000) {
54 composingNotificationTimeRef.current = currentTime;
idillona4b96ab2023-02-01 15:30:12 -050055 webSocket.send(WebSocketMessageType.ComposingStatus, { conversationId, isWriting: true });
Adrien Béraud88a52442021-04-26 12:11:41 -040056 }
idillon3e378fd2022-12-23 11:48:12 -050057 }, [webSocket, conversationId]);
58
59 const notifyStopcomposing = useCallback(() => {
60 composingNotificationTimeRef.current = 0;
idillonbad51972023-01-18 17:00:38 -050061 webSocket?.send(WebSocketMessageType.ComposingStatus, { conversationId, isWriting: false });
idillon3e378fd2022-12-23 11:48:12 -050062 }, [webSocket, conversationId]);
63
64 const handleSubmit = useCallback(
65 (e: FormEvent<HTMLFormElement>) => {
66 e.preventDefault();
67 if (currentMessage) {
68 onSend(currentMessage);
69 setCurrentMessage('');
70 notifyStopcomposing();
71 }
72 },
73 [currentMessage, onSend, notifyStopcomposing]
74 );
75
76 const handleInputChange = useCallback(
77 (event: ChangeEvent<HTMLInputElement>) => {
78 setCurrentMessage(event.target.value);
79 notifyComposing();
80 },
81 [notifyComposing]
82 );
idillonaedab942022-09-01 14:29:43 -040083
Adrien Béraud023f7cf2022-09-18 14:57:53 -040084 const onEmojiSelected = useCallback(
simon6b9ddfb2022-10-03 00:04:50 -040085 (emoji: string) => setCurrentMessage((currentMessage) => currentMessage + emoji),
simond47ef9e2022-09-28 22:24:28 -040086 [setCurrentMessage]
87 );
Larbi Gharibe9af9732021-03-31 15:08:01 +010088
Adrien Béraud8a438f82021-04-14 16:18:57 -040089 return (
idilloncab81d72022-11-08 12:20:00 -050090 <Stack
91 component="form"
92 onSubmit={handleSubmit}
93 direction="row"
94 alignItems="center"
95 spacing="20px"
96 paddingX="16px"
97 paddingTop="16px"
98 >
99 <UploadFileButton onClick={openFilePicker} />
idillonae655dd2022-10-14 18:11:02 -0400100 <RecordVoiceMessageButton />
101 <RecordVideoMessageButton />
102 <Stack flexGrow={1}>
103 <InputBase
104 placeholder={placeholder}
105 value={currentMessage}
106 onChange={handleInputChange}
107 sx={{
108 fontSize: '15px',
109 color: 'black',
110 '& ::placeholder': {
111 color: '#7E7E7E',
112 opacity: 1,
113 textOverflow: 'ellipsis',
114 },
115 }}
116 />
idillonaedab942022-09-01 14:29:43 -0400117 </Stack>
idillonae655dd2022-10-14 18:11:02 -0400118 <SelectEmojiButton onEmojiSelected={onEmojiSelected} />
119 {currentMessage && <SendMessageButton type="submit" />}
idillonaedab942022-09-01 14:29:43 -0400120 </Stack>
Adrien Béraudab519ff2022-05-03 15:34:48 -0400121 );
Larbi Gharibe9af9732021-03-31 15:08:01 +0100122}
idillonae655dd2022-10-14 18:11:02 -0400123
simon5da8ca62022-11-09 15:21:25 -0500124const usePlaceholder = (members: ConversationMember[]) => {
125 const { account } = useAuthContext();
idillonae655dd2022-10-14 18:11:02 -0400126 const { t } = useTranslation();
127
128 return useMemo(() => {
129 const options: TranslateEnumerationOptions<ConversationMember> = {
130 elementPartialKey: 'member',
idillon07d31cc2022-12-06 22:40:14 -0500131 getElementValue: (member) => member.getDisplayName(),
idillonae655dd2022-10-14 18:11:02 -0400132 translaters: [
133 () =>
134 // The user is chatting with themself
idillon6e8ca412022-12-16 11:22:42 -0500135 t('message_input_placeholder_1', { member0: account?.getDisplayName() }),
136 (interpolations) => t('message_input_placeholder_1', interpolations),
137 (interpolations) => t('message_input_placeholder_2', interpolations),
138 (interpolations) => t('message_input_placeholder_3', interpolations),
139 (interpolations) => t('message_input_placeholder_4', interpolations),
idillonae655dd2022-10-14 18:11:02 -0400140 (interpolations) => t('message_input_placeholder_more', interpolations),
141 ],
142 };
143
144 return translateEnumeration<ConversationMember>(members, options);
145 }, [account, members, t]);
146};