Translation for chat pages

This commit is contained in:
Nicola Benaglia 2025-05-16 19:48:07 +02:00
parent 4f35730db6
commit cb336133c6
10 changed files with 120 additions and 52 deletions

View File

@ -1,4 +1,4 @@
import React, { import {
useCallback, useCallback,
useContext, useContext,
useEffect, useEffect,
@ -50,6 +50,8 @@ import CloseIcon from '@mui/icons-material/Close';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import ImageIcon from '@mui/icons-material/Image'; import ImageIcon from '@mui/icons-material/Image';
import { messageHasImage } from '../../utils/chat'; import { messageHasImage } from '../../utils/chat';
import { useTranslation } from 'react-i18next';
const uid = new ShortUniqueId({ length: 5 }); const uid = new ShortUniqueId({ length: 5 });
const uidImages = new ShortUniqueId({ length: 12 }); const uidImages = new ShortUniqueId({ length: 12 });
@ -75,8 +77,8 @@ export const ChatGroup = ({
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isMoved, setIsMoved] = useState(false); const [isMoved, setIsMoved] = useState(false);
const [openSnack, setOpenSnack] = React.useState(false); const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = React.useState(null); const [infoSnack, setInfoSnack] = useState(null);
const hasInitialized = useRef(false); const hasInitialized = useRef(false);
const [isFocusedParent, setIsFocusedParent] = useState(false); const [isFocusedParent, setIsFocusedParent] = useState(false);
const [replyMessage, setReplyMessage] = useState(null); const [replyMessage, setReplyMessage] = useState(null);
@ -93,8 +95,8 @@ export const ChatGroup = ({
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const lastReadTimestamp = useRef(null); const lastReadTimestamp = useRef(null);
const handleUpdateRef = useRef(null); const handleUpdateRef = useRef(null);
const { t } = useTranslation(['auth', 'core', 'group']);
const getTimestampEnterChat = async (selectedGroup) => { const getTimestampEnterChat = async (selectedGroup) => {
try { try {
@ -129,7 +131,12 @@ export const ChatGroup = ({
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || 'An error occurred'); rej(
error.message ||
t('core:message.error.generic', {
postProcess: 'capitalizeFirst',
})
);
}); });
}); });
} catch (error) { } catch (error) {
@ -154,9 +161,6 @@ export const ChatGroup = ({
return Array.from(uniqueMembers); return Array.from(uniqueMembers);
}, [messages]); }, [messages]);
const triggerRerender = () => {
forceUpdate(); // Trigger re-render by updating the state
};
const setEditorRef = (editorInstance) => { const setEditorRef = (editorInstance) => {
editorRef.current = editorInstance; editorRef.current = editorInstance;
}; };
@ -168,6 +172,7 @@ export const ChatGroup = ({
} }
return []; return [];
}, [selectedGroup, queueChats]); }, [selectedGroup, queueChats]);
const tempChatReferences = useMemo(() => { const tempChatReferences = useMemo(() => {
if (!selectedGroup) return []; if (!selectedGroup) return [];
if (queueChats[selectedGroup]) { if (queueChats[selectedGroup]) {
@ -184,12 +189,6 @@ export const ChatGroup = ({
} }
}, [secretKey]); }, [secretKey]);
// const getEncryptedSecretKey = useCallback(()=> {
// const response = getResource()
// const decryptResponse = decryptResource()
// return
// }, [])
const checkForFirstSecretKeyNotification = (messages) => { const checkForFirstSecretKeyNotification = (messages) => {
messages?.forEach((message) => { messages?.forEach((message) => {
try { try {
@ -250,7 +249,6 @@ export const ChatGroup = ({
const dataRemovedBlock = responseData?.filter((item) => { const dataRemovedBlock = responseData?.filter((item) => {
return !isUserBlocked(item?.sender, item?.senderName); return !isUserBlocked(item?.sender, item?.senderName);
}); });
decryptMessages(dataRemovedBlock, false); decryptMessages(dataRemovedBlock, false);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -273,6 +271,7 @@ export const ChatGroup = ({
const filterUIMessages = encryptedMessages.filter( const filterUIMessages = encryptedMessages.filter(
(item) => !isExtMsg(item.data) (item) => !isExtMsg(item.data)
); );
const decodedUIMessages = const decodedUIMessages =
decodeBase64ForUIChatMessages(filterUIMessages); decodeBase64ForUIChatMessages(filterUIMessages);
@ -280,6 +279,7 @@ export const ChatGroup = ({
...decodedUIMessages, ...decodedUIMessages,
...response, ...response,
]; ];
const combineUIAndExtensionMsgs = processWithNewMessages( const combineUIAndExtensionMsgs = processWithNewMessages(
combineUIAndExtensionMsgsBefore.map((item) => ({ combineUIAndExtensionMsgsBefore.map((item) => ({
...item, ...item,
@ -287,16 +287,24 @@ export const ChatGroup = ({
})), })),
selectedGroup selectedGroup
); );
res(combineUIAndExtensionMsgs); res(combineUIAndExtensionMsgs);
if (isInitiated) { if (isInitiated) {
const formatted = combineUIAndExtensionMsgs const formatted = combineUIAndExtensionMsgs
.filter((rawItem) => !rawItem?.chatReference) .filter((rawItem) => !rawItem?.chatReference)
.map((item) => { .map((item) => {
const message = (
<p>
{t('group:message.generic.group_key_created', {
postProcess: 'capitalizeFirst',
})}
</p>
);
const additionalFields = const additionalFields =
item?.data === 'NDAwMQ==' item?.data === 'NDAwMQ==' // TODO put magic string somewhere in a file
? { ? {
text: '<p>First group key created.</p>', text: message,
} }
: {}; : {};
return { return {
@ -362,7 +370,9 @@ export const ChatGroup = ({
!newTimestamp !newTimestamp
) { ) {
console.warn( console.warn(
'Invalid content, sender, or timestamp in reaction data', t('group:message.generic.invalid_content', {
postProcess: 'capitalizeFirst',
}),
item item
); );
return; return;
@ -435,10 +445,17 @@ export const ChatGroup = ({
const formatted = combineUIAndExtensionMsgs const formatted = combineUIAndExtensionMsgs
.filter((rawItem) => !rawItem?.chatReference) .filter((rawItem) => !rawItem?.chatReference)
.map((item) => { .map((item) => {
const message = (
<p>
{t('group:message.generic.group_key_created', {
postProcess: 'capitalizeFirst',
})}
</p>
);
const additionalFields = const additionalFields =
item?.data === 'NDAwMQ==' item?.data === 'NDAwMQ=='
? { ? {
text: '<p>First group key created.</p>', text: message,
} }
: {}; : {};
const divide = const divide =
@ -510,7 +527,9 @@ export const ChatGroup = ({
!newTimestamp !newTimestamp
) { ) {
console.warn( console.warn(
'Invalid content, sender, or timestamp in reaction data', t('group:message.generic.invalid_content', {
postProcess: 'capitalizeFirst',
}),
item item
); );
return; return;
@ -583,7 +602,12 @@ export const ChatGroup = ({
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || 'An error occurred'); rej(
error.message ||
t('core:message.error.generic', {
postProcess: 'capitalizeFirst',
})
);
}); });
}); });
} catch (error) { } catch (error) {
@ -615,6 +639,7 @@ export const ChatGroup = ({
console.error('Error during ping:', error); console.error('Error during ping:', error);
} }
}; };
const initWebsocketMessageGroup = () => { const initWebsocketMessageGroup = () => {
let socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?txGroupId=${selectedGroup}&encoding=BASE64&limit=100`; let socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?txGroupId=${selectedGroup}&encoding=BASE64&limit=100`;
socketRef.current = new WebSocket(socketLink); socketRef.current = new WebSocket(socketLink);
@ -709,7 +734,12 @@ export const ChatGroup = ({
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || 'An error occurred'); rej(
error.message ||
t('core:message.error.generic', {
postProcess: 'capitalizeFirst',
})
);
}); });
}); });
} catch (error) { } catch (error) {
@ -744,7 +774,12 @@ export const ChatGroup = ({
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || 'An error occurred'); rej(
error.message ||
t('core:message.error.generic', {
postProcess: 'capitalizeFirst',
})
);
}); });
}); });
} catch (error) { } catch (error) {
@ -760,12 +795,22 @@ export const ChatGroup = ({
const sendMessage = async () => { const sendMessage = async () => {
try { try {
if (messageSize > 4000) return; if (messageSize > 4000) return; // TODO magic number
if (isPrivate === null) if (isPrivate === null)
throw new Error('Unable to determine if group is private'); throw new Error(
t('group:message.error.unable_determine_group_private', {
postProcess: 'capitalizeFirst',
})
);
if (isSending) return; if (isSending) return;
if (+balance < 4) if (+balance < 4)
throw new Error('You need at least 4 QORT to send a message'); // TODO magic number
throw new Error(
t('group:message.error.qortals_required', {
quantity: 4,
postProcess: 'capitalizeFirst',
})
);
pauseAllQueues(); pauseAllQueues();
if (editorRef.current) { if (editorRef.current) {
const htmlContent = editorRef.current.getHTML(); const htmlContent = editorRef.current.getHTML();
@ -792,13 +837,16 @@ export const ChatGroup = ({
const imagesToPublish = []; const imagesToPublish = [];
const deleteImage = const deleteImage =
onEditMessage && isDeleteImage && messageHasImage(onEditMessage); onEditMessage && isDeleteImage && messageHasImage(onEditMessage);
if (deleteImage) { if (deleteImage) {
const fee = await getFee('ARBITRARY'); const fee = await getFee('ARBITRARY');
// TODO translate
await show({ await show({
publishFee: fee.fee + ' QORT', publishFee: fee.fee + ' QORT',
message: 'Would you like to delete your previous chat image?', message: 'Would you like to delete your previous chat image?',
}); });
// TODO magic string
await window.sendMessage('publishOnQDN', { await window.sendMessage('publishOnQDN', {
data: 'RA==', data: 'RA==',
identifier: onEditMessage?.images[0]?.identifier, identifier: onEditMessage?.images[0]?.identifier,
@ -811,6 +859,7 @@ export const ChatGroup = ({
const base64ToSave = isPrivate const base64ToSave = isPrivate
? await encryptChatMessage(imageToSave, secretKeyObject) ? await encryptChatMessage(imageToSave, secretKeyObject)
: imageToSave; : imageToSave;
// 1 represents public group, 0 is private // 1 represents public group, 0 is private
const identifier = `grp-q-manager_${isPrivate ? 0 : 1}_group_${selectedGroup}_${uidImages.rnd()}`; const identifier = `grp-q-manager_${isPrivate ? 0 : 1}_group_${selectedGroup}_${uidImages.rnd()}`;
imagesToPublish.push({ imagesToPublish.push({
@ -822,7 +871,6 @@ export const ChatGroup = ({
const res = await window.sendMessage( const res = await window.sendMessage(
'PUBLISH_MULTIPLE_QDN_RESOURCES', 'PUBLISH_MULTIPLE_QDN_RESOURCES',
{ {
resources: imagesToPublish, resources: imagesToPublish,
}, },
@ -979,6 +1027,7 @@ export const ChatGroup = ({
.setContent(message?.messageText || message?.text) .setContent(message?.messageText || message?.text)
.run(); .run();
}, []); }, []);
const handleReaction = useCallback( const handleReaction = useCallback(
async (reaction, chatMessage, reactionState = true) => { async (reaction, chatMessage, reactionState = true) => {
try { try {
@ -1033,12 +1082,6 @@ export const ChatGroup = ({
chatReference: chatMessage.signature, chatReference: chatMessage.signature,
}; };
addToQueue(sendMessageFunc, messageObj, 'chat-reaction', selectedGroup); addToQueue(sendMessageFunc, messageObj, 'chat-reaction', selectedGroup);
// setTimeout(() => {
// executeEvent("sent-new-message-group", {})
// }, 150);
// clearEditorContent()
// setReplyMessage(null)
// send chat message // send chat message
} catch (error) { } catch (error) {
const errorMsg = error?.message || error; const errorMsg = error?.message || error;
@ -1417,6 +1460,7 @@ export const ChatGroup = ({
}} }}
> >
<Typography>Q-Manager</Typography> <Typography>Q-Manager</Typography>
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
setIsOpenQManager(false); setIsOpenQManager(false);
@ -1429,7 +1473,9 @@ export const ChatGroup = ({
/> />
</ButtonBase> </ButtonBase>
</Box> </Box>
<Divider /> <Divider />
<AppViewerContainer <AppViewerContainer
customHeight="560px" customHeight="560px"
app={{ app={{

View File

@ -202,11 +202,11 @@ export const ChatList = ({
ref={parentRef} ref={parentRef}
className="List" className="List"
style={{ style={{
display: 'flex',
flexGrow: 1, flexGrow: 1,
height: '0px',
overflow: 'auto', overflow: 'auto',
position: 'relative', position: 'relative',
display: 'flex',
height: '0px',
}} }}
> >
<div <div
@ -391,6 +391,7 @@ export const ChatList = ({
</div> </div>
</div> </div>
</div> </div>
{showScrollButton && ( {showScrollButton && (
<button <button
onClick={() => scrollToBottom()} onClick={() => scrollToBottom()}

View File

@ -387,6 +387,7 @@ export const ChatOptions = ({
}} }}
/> />
</Box> </Box>
<Box <Box
sx={{ sx={{
alignItems: 'center', alignItems: 'center',

View File

@ -1,4 +1,4 @@
import React, { useContext } from 'react'; import { useContext, useState } from 'react';
import { Box, Button, Typography, useTheme } from '@mui/material'; import { Box, Button, Typography, useTheme } from '@mui/material';
import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
@ -34,9 +34,9 @@ export const CreateCommonSecret = ({
const { show } = useContext(MyContext); const { show } = useContext(MyContext);
const setTxList = useSetAtom(txListAtom); const setTxList = useSetAtom(txListAtom);
const [openSnack, setOpenSnack] = React.useState(false); const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = React.useState(null); const [infoSnack, setInfoSnack] = useState(null);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = useState(false);
const theme = useTheme(); const theme = useTheme();

View File

@ -1,7 +1,9 @@
import React, { import {
useCallback, useCallback,
useContext,
useEffect, useEffect,
useMemo, useMemo,
useReducer,
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
@ -140,9 +142,9 @@ export const GroupAnnouncements = ({
const [selectedAnnouncement, setSelectedAnnouncement] = useState(null); const [selectedAnnouncement, setSelectedAnnouncement] = useState(null);
const [isFocusedParent, setIsFocusedParent] = useState(false); const [isFocusedParent, setIsFocusedParent] = useState(false);
const { show } = React.useContext(MyContext); const { show } = useContext(MyContext);
const [openSnack, setOpenSnack] = React.useState(false); const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = React.useState(null); const [infoSnack, setInfoSnack] = useState(null);
const hasInitialized = useRef(false); const hasInitialized = useRef(false);
const hasInitializedWebsocket = useRef(false); const hasInitializedWebsocket = useRef(false);
const editorRef = useRef(null); const editorRef = useRef(null);
@ -150,12 +152,13 @@ export const GroupAnnouncements = ({
const setEditorRef = (editorInstance) => { const setEditorRef = (editorInstance) => {
editorRef.current = editorInstance; editorRef.current = editorInstance;
}; };
const [, forceUpdate] = React.useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const { t } = useTranslation(['core', 'group']); const { t } = useTranslation(['auth', 'core', 'group']);
const triggerRerender = () => { const triggerRerender = () => {
forceUpdate(); // Trigger re-render by updating the state forceUpdate(); // Trigger re-render by updating the state
}; };
useEffect(() => { useEffect(() => {
if (!selectedGroup) return; if (!selectedGroup) return;
(async () => { (async () => {

View File

@ -77,8 +77,10 @@ export const GroupAvatar = ({
try { try {
if (!groupId) return; if (!groupId) return;
const fee = await getFee('ARBITRARY'); const fee = await getFee('ARBITRARY');
if (+balance < +fee.fee) if (+balance < +fee.fee)
throw new Error(`Publishing an Avatar requires ${fee.fee}`); throw new Error(`Publishing an Avatar requires ${fee.fee}`);
await show({ await show({
message: 'Would you like to publish an avatar?', message: 'Would you like to publish an avatar?',
publishFee: fee.fee + ' QORT', publishFee: fee.fee + ' QORT',
@ -132,6 +134,7 @@ export const GroupAvatar = ({
> >
{myName?.charAt(0)} {myName?.charAt(0)}
</Avatar> </Avatar>
<ButtonBase onClick={handleChildClick}> <ButtonBase onClick={handleChildClick}>
<Typography <Typography
sx={{ sx={{
@ -142,6 +145,7 @@ export const GroupAvatar = ({
change avatar change avatar
</Typography> </Typography>
</ButtonBase> </ButtonBase>
<PopoverComp <PopoverComp
myName={myName} myName={myName}
avatarFile={avatarFile} avatarFile={avatarFile}
@ -170,6 +174,7 @@ export const GroupAvatar = ({
> >
{myName?.charAt(0)} {myName?.charAt(0)}
</Avatar> </Avatar>
<ButtonBase onClick={handleChildClick}> <ButtonBase onClick={handleChildClick}>
<Typography <Typography
sx={{ sx={{
@ -180,6 +185,7 @@ export const GroupAvatar = ({
change avatar change avatar
</Typography> </Typography>
</ButtonBase> </ButtonBase>
<PopoverComp <PopoverComp
myName={myName} myName={myName}
avatarFile={avatarFile} avatarFile={avatarFile}
@ -208,6 +214,7 @@ export const GroupAvatar = ({
set avatar set avatar
</Typography> </Typography>
</ButtonBase> </ButtonBase>
<PopoverComp <PopoverComp
myName={myName} myName={myName}
avatarFile={avatarFile} avatarFile={avatarFile}
@ -258,11 +265,15 @@ const PopoverComp = ({
> >
(500 KB max. for GIFS){' '} (500 KB max. for GIFS){' '}
</Typography> </Typography>
<ImageUploader onPick={(file) => setAvatarFile(file)}> <ImageUploader onPick={(file) => setAvatarFile(file)}>
<Button variant="contained">Choose Image</Button> <Button variant="contained">Choose Image</Button>
</ImageUploader> </ImageUploader>
{avatarFile?.name} {avatarFile?.name}
<Spacer height="25px" /> <Spacer height="25px" />
{!myName && ( {!myName && (
<Box <Box
sx={{ sx={{
@ -283,6 +294,7 @@ const PopoverComp = ({
)} )}
<Spacer height="25px" /> <Spacer height="25px" />
<LoadingButton <LoadingButton
loading={isLoading} loading={isLoading}
disabled={!avatarFile || !myName} disabled={!avatarFile || !myName}

View File

@ -171,8 +171,9 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
} }
} }
} catch (error) { } catch (error) {
//error console.log(error);
} }
const res = extractComponents(url); const res = extractComponents(url);
if (res) { if (res) {
const { service, name, identifier, path } = res; const { service, name, identifier, path } = res;

View File

@ -1,4 +1,5 @@
import React, { import {
memo,
useCallback, useCallback,
useContext, useContext,
useEffect, useEffect,
@ -77,7 +78,7 @@ const getBadgeImg = (level) => {
} }
}; };
const UserBadge = React.memo(({ userInfo }) => { const UserBadge = memo(({ userInfo }) => {
return ( return (
<Tooltip disableFocusListener title={`level ${userInfo ?? 0}`}> <Tooltip disableFocusListener title={`level ${userInfo ?? 0}`}>
<img <img
@ -92,7 +93,7 @@ const UserBadge = React.memo(({ userInfo }) => {
); );
}); });
export const MessageItem = React.memo( export const MessageItem = memo(
({ ({
message, message,
onSeen, onSeen,

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { EditorProvider, useCurrentEditor } from '@tiptap/react'; import { EditorProvider, useCurrentEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit'; import StarterKit from '@tiptap/starter-kit';
import { Color } from '@tiptap/extension-color'; import { Color } from '@tiptap/extension-color';
@ -42,7 +42,7 @@ function textMatcher(doc, from) {
return { start, query }; return { start, query };
} }
const MenuBar = React.memo( const MenuBar = memo(
({ ({
setEditorRef, setEditorRef,
isChat, isChat,

View File

@ -61,6 +61,8 @@
"descrypt_wallet": "decrypting wallet...", "descrypt_wallet": "decrypting wallet...",
"encryption_key": "the group's first common encryption key is in the process of creation. Please wait a few minutes for it to be retrieved by the network. Checking every 2 minutes...", "encryption_key": "the group's first common encryption key is in the process of creation. Please wait a few minutes for it to be retrieved by the network. Checking every 2 minutes...",
"group_invited_you": "{{group}} has invited you", "group_invited_you": "{{group}} has invited you",
"group_key_created": "first group key created.",
"invalid_content": "invalid content, sender, or timestamp in reaction data",
"invalid_data": "error loading content: Invalid Data", "invalid_data": "error loading content: Invalid Data",
"latest_promotion": "only the latest promotion from the week will be shown for your group.", "latest_promotion": "only the latest promotion from the week will be shown for your group.",
"loading_members": "loading member list with names... please wait.", "loading_members": "loading member list with names... please wait.",
@ -97,6 +99,7 @@
"qortals_required": "you need at least {{ quantity }} QORT to send a message", "qortals_required": "you need at least {{ quantity }} QORT to send a message",
"timeout_reward": "timeout waiting for reward share confirmation", "timeout_reward": "timeout waiting for reward share confirmation",
"thread_id": "unable to locate thread Id", "thread_id": "unable to locate thread Id",
"unable_determine_group_private": "unable to determine if group is private",
"unable_minting": "unable to start minting" "unable_minting": "unable to start minting"
}, },
"success": { "success": {