Skip to content

Commit 561b0df

Browse files
authored
Merge pull request #58 from Passimx/test-input
Feat: add recover
2 parents 4443222 + af94dda commit 561b0df

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+750
-815
lines changed

index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
<meta name="robots" content="index, follow"/>
88
<meta name="description" content="Open platform for messaging"/>
99
<meta name="keywords" content="passimx, messaging, chat, anonymously"/>
10-
<meta name="viewport" content="width=device-width, user-scalable=no" />
11-
10+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
1211
<meta property="og:site_name" content="PassimX">
1312
<meta property="og:url" content="https://tons-chat.ru">
1413
<meta property="og:image:width" content="250">

public/languages/ar/translation.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"search": "بحث",
55
"search_chats": "بحث عن الدردشات",
66
"chats_enter_message": "...رسالة",
7+
"messages": "الرسائل",
8+
"delete_chats": "حذف الدردشات",
79
"chats_message_unavailable": "إرسال الرسائل غير متاح",
810
"create_open_chat": "دردشة مفتوحة",
911
"no_chats": "لم يتم العثور على دردشات",
@@ -19,6 +21,7 @@
1921
"reply": "الرد",
2022
"copy_text": "نسخ النص",
2123
"copy_message_link":"نسخ رابط الرسالة",
24+
"recording": "تسجيل",
2225

2326
"description_open_chat": "الفكرة الرئيسية للدردشة المفتوحة هي إنشاء قناة اتصال متاحة للجميع، يمكن ربطها باسم نطاق موقع ويب أو رابط ملف تعريف على وسائل التواصل الاجتماعي أو كلمات رئيسية أخرى. هذا النوع من الدردشة هو الحل الأفضل للتواصل مع جمهور واسع.",
2427
"description_shared_chat": "تتيح دردشة البث لمجموعة معينة من الأشخاص التواصل بينما يمكن للعالم كله المشاهدة. هذا النوع من الدردشة هو الحل الأفضل لنقل الرسائل إلى الجماهير.",

public/languages/ch/translation.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"search": "搜索",
55
"search_chats": "搜索聊天",
66
"chats_enter_message": "消息...",
7+
"messages": "消息",
8+
"delete_chats": "删除聊天",
79
"chats_message_unavailable": "无法发送消息 ",
810
"create_open_chat": "公开聊天",
911
"no_chats": "未找到聊天",
@@ -19,6 +21,7 @@
1921
"reply": "回复",
2022
"copy_text": "复制文本",
2123
"copy_message_link":"复制消息链接",
24+
"recording": "录音",
2225

2326
"description_open_chat": "公开聊天的主要目的是创建一个任何人都可以访问的交流渠道,可以绑定到网站域名、社交媒体个人资料链接或其他关键词。这种聊天模式是与更广泛的人群进行交流的最佳方式。",
2427
"description_shared_chat": "广播聊天允许特定人群进行交流,同时所有人都可以观看。这种聊天模式是将信息传播给大众的最佳解决方案。",

public/languages/en/translation.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"search": "Search",
55
"search_chats": "Search chats",
66
"chats_enter_message": "Message...",
7+
"messages": "Messages",
8+
"delete_chats": "Delete chats",
79
"chats_message_unavailable": "Message sending is unavailable",
810
"create_open_chat": "Open chat",
911
"no_chats": "No chats found",
@@ -19,6 +21,7 @@
1921
"reply": "Reply",
2022
"copy_text": "Copy text",
2123
"copy_message_link":"Copy message link",
24+
"recording": "Recording",
2225

2326
"description_open_chat": "The main idea of an open chat is to create a communication channel accessible to everyone, which can be linked to a website domain name, a social media profile link, or other keywords. This type of chat is the best solution for communicating with a wide audience.",
2427
"description_shared_chat": "A broadcast chat allows a specific group of people to communicate while the whole world can observe. This type of chat is the best solution for broadcasting messages to a large audience.",

public/languages/es/translation.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
"search": "Buscar",
55
"search_chats": "Buscar chats",
66
"chats_enter_message": "Mensaje...",
7-
"create_open_chat": "Chat abierto",
7+
"messages": "Mensajes",
8+
"delete_chats": "Eliminar chats",
89
"chats_message_unavailable": "Message sending is unavailable",
10+
"create_open_chat": "Chat abierto",
911
"no_chats": "No se encontraron chats",
1012
"create_shared_chat": "Chat de transmisión",
1113
"create_public_chat": "Chat público",
@@ -19,6 +21,7 @@
1921
"reply": "Responder",
2022
"copy_text": "Copiar texto",
2123
"copy_message_link":"Copiar enlace del mensaje",
24+
"recording": "Grabación",
2225

2326
"description_open_chat": "La idea principal de un chat abierto es crear un canal de comunicación accesible para todos, que se pueda vincular a un nombre de dominio de un sitio web, un perfil en redes sociales u otras palabras clave. Este tipo de chat es la mejor solución para comunicarse con un público amplio.",
2427
"description_shared_chat": "Un chat de transmisión permite que un grupo específico de personas se comunique mientras el resto del mundo puede observar. Este tipo de chat es la mejor solución para transmitir mensajes a las masas.",

public/languages/ru/translation.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"search": "Поиск",
55
"search_chats": "Поиск чатов",
66
"chats_enter_message": "Сообщение...",
7+
"messages": "Сообщения",
8+
"delete_chats": "Удалить чаты",
79
"chats_message_unavailable": "Отправка сообщений недоступна",
810
"create_open_chat": "Открытый чат",
911
"no_chats": "Чаты не найдены",
@@ -19,6 +21,7 @@
1921
"reply": "Ответить",
2022
"copy_text": "Копировать текст",
2123
"copy_message_link":"Копировать ссылку сообщения",
24+
"recording": "Запись",
2225

2326
"description_open_chat": "Основной идеей открытого чата является возможность создавать доступный для каждого канал общения, который можно привязать к доменному имени сайта, ссылке на профиль из соц. сетей или же по другим ключевым словам. Этот вид чата является лучшим решением для общения с широкой публикой.",
2427
"description_shared_chat": "Трансляционный чат позволяет определенному кругу лиц вести коммуникацию, за которой может наблюдать весь мир. Этот вид чата является лучшим решением для трансляции сообщений в широкие массы.",

src/components/chat-item/index.module.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
}
1111

1212
.chat_item:active {
13-
@media(width <= 600px){
13+
@media(width < 600px){
1414
background-color: #0c518d;
1515
border-radius: 10px;
1616
}
1717
}
18+
1819
.selected_chat{
1920
@media(width > 600px){
2021
background-color: #0c518d;

src/components/input-message/hooks/use-enter.hook.ts

Lines changed: 134 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,57 @@ import styles from '../index.module.css';
33
import { useAppAction, useAppSelector } from '../../../root/store';
44
import { ChatEnum } from '../../../root/types/chat/chat.enum.ts';
55
import { useTranslation } from 'react-i18next';
6-
import { UseEnterHookType } from '../types/use-enter-hook.type.ts';
76
import { createMessage } from '../../../root/api/messages';
87
import { getRawChat } from '../../../root/store/chats/chats.raw.ts';
9-
import { focusToEnd } from '../common/focus-to-end.ts';
108
import { getIsFocused } from './get-is-focused.hook.ts';
9+
import { UseEnterHookType } from '../types/use-enter-hook.type.ts';
10+
import { focusToEnd } from '../common/focus-to-end.ts';
11+
import moment from 'moment/min/moment-with-locales';
12+
13+
let mediaRecorder: MediaRecorder | undefined;
14+
let chunks: Blob[] = [];
1115

1216
export const useEnterHook = (): UseEnterHookType => {
1317
const { t } = useTranslation();
1418
const { update, setChatOnPage } = useAppAction();
1519
const [isShowPlaceholder, setIsShowPlaceholder] = useState<boolean>(true);
1620
const { chatOnPage } = useAppSelector((state) => state.chats);
1721
const { isPhone, isOpenMobileKeyboard } = useAppSelector((state) => state.app);
22+
const [textExist, setTextExist] = useState<boolean>(true);
23+
const [isRecovering, setIsRecovering] = useState<boolean>(false);
24+
const [recoveringTime, setRecoveringTime] = useState<string>();
1825

1926
const placeholder = useMemo((): string => {
2027
const text = chatOnPage?.type === ChatEnum.IS_SYSTEM ? 'chats_message_unavailable' : 'chats_enter_message';
28+
if (isRecovering) return `${t('recording')}: ${recoveringTime}`;
2129
return t(text);
22-
}, [chatOnPage?.type, t]);
30+
}, [chatOnPage?.type, t, isRecovering, recoveringTime]);
31+
32+
useEffect(() => {
33+
if (!isRecovering) return;
34+
let handler: NodeJS.Timeout;
35+
const startTime = Date.now();
36+
37+
const updateTime = () => {
38+
const duration = moment.duration(Date.now() - startTime);
39+
40+
const minutes = Math.floor(duration.asMinutes()); // минуты
41+
const seconds = duration.seconds(); // секунды (0-59)
42+
const milliseconds = Math.floor(duration.milliseconds() / 10); // сотые доли (0-99)
43+
44+
setRecoveringTime(
45+
`${minutes}:${seconds.toString().padStart(2, '0')},${milliseconds.toString().padStart(2, '0')}`,
46+
);
47+
handler = setTimeout(updateTime, 20);
48+
};
49+
50+
updateTime();
51+
52+
return () => {
53+
setRecoveringTime(undefined);
54+
clearTimeout(handler);
55+
};
56+
}, [isRecovering]);
2357

2458
const onInput = useCallback(() => {
2559
const el = document.getElementById(styles.new_message)!;
@@ -29,12 +63,14 @@ export const useEnterHook = (): UseEnterHookType => {
2963
setIsShowPlaceholder(isEmpty);
3064
if (chatOnPage?.id) {
3165
const isText = !!el.innerText.replace(/^\n+|\n+$/g, '').trim()?.length;
66+
setTextExist(isText);
3267

3368
update({ id: chatOnPage.id, inputMessage: isText ? el.innerText : undefined });
3469
}
35-
}, [chatOnPage?.id]);
70+
}, [chatOnPage?.id, textExist]);
3671

3772
const sendMessage = useCallback(async () => {
73+
alert(2);
3874
if (!chatOnPage?.id) return;
3975
const element = document.getElementById(styles.new_message)!;
4076
const isFocused = isPhone ? isOpenMobileKeyboard : getIsFocused();
@@ -45,6 +81,7 @@ export const useEnterHook = (): UseEnterHookType => {
4581
element.innerText = '';
4682
if (isFocused) element.focus();
4783
setIsShowPlaceholder(true);
84+
setTextExist(false);
4885

4986
if (getRawChat(chatOnPage.id)) update({ id: chatOnPage.id, inputMessage: undefined, answerMessage: undefined });
5087
else setChatOnPage({ answerMessage: undefined });
@@ -60,13 +97,33 @@ export const useEnterHook = (): UseEnterHookType => {
6097

6198
element.innerText = text ?? '';
6299
setIsShowPlaceholder(!text);
100+
setTextExist(!!text);
101+
102+
if (isRecovering && mediaRecorder) {
103+
mediaRecorder.stop();
104+
}
63105

64106
// 300 - время анимации, иначе быстро отрабатывает анимация
65107
if (!isPhone) setTimeout(() => focusToEnd(element), 300);
66108
}, [chatOnPage?.id, isPhone]);
67109

68110
useEffect(() => {
111+
if (!chatOnPage) return;
69112
const element = document.getElementById(styles.new_message)!;
113+
if (isRecovering) {
114+
element.removeAttribute('contentEditable');
115+
} else element.setAttribute('contentEditable', `${chatOnPage.type !== ChatEnum.IS_SYSTEM}`);
116+
}, [isRecovering, chatOnPage]);
117+
118+
useEffect(() => {
119+
if (chatOnPage?.type === ChatEnum.IS_SYSTEM) return;
120+
const element = document.getElementById(styles.new_message)!;
121+
const background = document.getElementById(styles.background)!;
122+
const buttonStartRecover = document.getElementById(styles.microphone)!;
123+
const sendMessageButton = document.getElementById(styles.button_input_block)!;
124+
const microphoneButton = document.getElementById(styles.button_microphone_block);
125+
const buttonMicrophoneDelete = document.getElementById(styles.button_microphone_delete);
126+
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
70127

71128
const preventDefault = (event: KeyboardEvent) => {
72129
if (event.code === 'Enter' && !isPhone && !event.shiftKey) event.preventDefault();
@@ -105,26 +162,96 @@ export const useEnterHook = (): UseEnterHookType => {
105162

106163
if (chatOnPage?.id) {
107164
const isText = !!element.innerText.replace(/^\n+|\n+$/g, '').trim()?.length;
165+
setTextExist(isText);
108166

109167
update({ id: chatOnPage.id, inputMessage: isText ? element.innerText : undefined });
110168
}
111169
};
112170

171+
const mobileFocus = () => {
172+
background.style.paddingBottom = '0px';
173+
};
174+
175+
const mobileFocusOut = () => {
176+
background.style.paddingBottom = 'env(safe-area-inset-bottom, 32px)';
177+
};
178+
179+
const stopRecover = async () => {
180+
if (mediaRecorder) mediaRecorder.stop();
181+
};
182+
183+
const startRecover = async () => {
184+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
185+
186+
if (isRecovering && mediaRecorder) {
187+
mediaRecorder.stop();
188+
189+
if (stream) stream.getTracks().forEach((track) => track.stop());
190+
return;
191+
}
192+
193+
buttonStartRecover.classList.add(styles.recover_color);
194+
setIsRecovering(true);
195+
mediaRecorder = new MediaRecorder(stream);
196+
197+
mediaRecorder.ondataavailable = (event) => {
198+
chunks.push(event.data);
199+
};
200+
201+
mediaRecorder.onstop = () => {
202+
if (stream) stream.getTracks().forEach((track) => track.stop());
203+
buttonStartRecover.classList.remove(styles.recover_color);
204+
setIsRecovering(false);
205+
206+
// todo
207+
// перенести в отправку на сервер
208+
// // Создаем Blob из кусочков
209+
// const audioBlob = new Blob(chunks, { type: 'audio/webm' });
210+
211+
// // Создаем URL для воспроизведения
212+
// const audioUrl = URL.createObjectURL(audioBlob);
213+
214+
// // Воспроизведение
215+
// const audio = new Audio(audioUrl);
216+
// audio.play();
217+
chunks = [];
218+
};
219+
220+
// Запускаем запись
221+
mediaRecorder.start();
222+
};
223+
113224
element.addEventListener('keypress', preventDefault);
114225
element.addEventListener('keyup', send);
115226
element.addEventListener('paste', paste);
116227
element.addEventListener('input', onInput);
228+
sendMessageButton.addEventListener('click', sendMessage);
229+
microphoneButton?.addEventListener('mousedown', startRecover);
230+
buttonMicrophoneDelete?.addEventListener('mousedown', stopRecover);
231+
if (isStandalone && isPhone) {
232+
element.addEventListener('focus', mobileFocus);
233+
element.addEventListener('focusout', mobileFocusOut);
234+
}
117235

118236
return () => {
119237
element.removeEventListener('keypress', preventDefault);
120238
element.removeEventListener('keyup', send);
121239
element.removeEventListener('paste', paste);
122240
element.removeEventListener('input', onInput);
241+
sendMessageButton.removeEventListener('click', sendMessage);
242+
microphoneButton?.removeEventListener('mousedown', startRecover);
243+
buttonMicrophoneDelete?.removeEventListener('mousedown', stopRecover);
244+
245+
if (isStandalone && isPhone) {
246+
element.removeEventListener('focus', mobileFocus);
247+
element.removeEventListener('focusout', mobileFocusOut);
248+
}
123249
};
124-
}, [chatOnPage?.id, isPhone]);
250+
}, [chatOnPage?.id, isPhone, sendMessage, isRecovering]);
125251

126252
const setEmoji = useCallback(
127253
(emoji: string) => {
254+
if (isRecovering) return;
128255
const chatInput = document.getElementById(styles.new_message)!;
129256
const isFocused = isPhone ? isOpenMobileKeyboard : getIsFocused();
130257

@@ -161,8 +288,8 @@ export const useEnterHook = (): UseEnterHookType => {
161288
else chatInput.blur();
162289
onInput();
163290
},
164-
[chatOnPage?.id, isPhone, isOpenMobileKeyboard],
291+
[chatOnPage?.id, isPhone, isOpenMobileKeyboard, isRecovering],
165292
);
166293

167-
return [sendMessage, setEmoji, placeholder, isShowPlaceholder];
294+
return [textExist, recoveringTime, setEmoji, placeholder, isShowPlaceholder];
168295
};

0 commit comments

Comments
 (0)