mirror of
https://github.com/Qortal/chrome-extension.git
synced 2025-03-28 08:15:55 +00:00
added group chat reactions
This commit is contained in:
parent
6f2787a0d4
commit
0810381455
20
package-lock.json
generated
20
package-lock.json
generated
@ -32,6 +32,7 @@
|
|||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
"compressorjs": "^1.2.1",
|
"compressorjs": "^1.2.1",
|
||||||
"dompurify": "^3.1.6",
|
"dompurify": "^3.1.6",
|
||||||
|
"emoji-picker-react": "^4.12.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jssha": "3.3.1",
|
"jssha": "3.3.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@ -4128,6 +4129,20 @@
|
|||||||
"integrity": "sha512-w+9yAVHoHhysCa+gln7AzbO9CdjFcL/wN/5dd+XW/Msl2d/4+WisEaCF1nty0xbAKaxdaJfgLB2296U7zZB7BA==",
|
"integrity": "sha512-w+9yAVHoHhysCa+gln7AzbO9CdjFcL/wN/5dd+XW/Msl2d/4+WisEaCF1nty0xbAKaxdaJfgLB2296U7zZB7BA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-picker-react": {
|
||||||
|
"version": "4.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.12.0.tgz",
|
||||||
|
"integrity": "sha512-q2c8UcZH0eRIMj41bj0k1akTjk69tsu+E7EzkW7giN66iltF6H9LQvQvw6ugscsxdC+1lmt3WZpQkkY65J95tg==",
|
||||||
|
"dependencies": {
|
||||||
|
"flairup": "1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
@ -4725,6 +4740,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/flairup": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA=="
|
||||||
|
},
|
||||||
"node_modules/flat-cache": {
|
"node_modules/flat-cache": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
"compressorjs": "^1.2.1",
|
"compressorjs": "^1.2.1",
|
||||||
"dompurify": "^3.1.6",
|
"dompurify": "^3.1.6",
|
||||||
|
"emoji-picker-react": "^4.12.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jssha": "3.3.1",
|
"jssha": "3.3.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
@ -1541,6 +1541,7 @@ async function sendChatGroup({
|
|||||||
chatReference,
|
chatReference,
|
||||||
messageText,
|
messageText,
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
let _reference = new Uint8Array(64);
|
let _reference = new Uint8Array(64);
|
||||||
self.crypto.getRandomValues(_reference);
|
self.crypto.getRandomValues(_reference);
|
||||||
|
|
||||||
@ -1557,18 +1558,23 @@ async function sendChatGroup({
|
|||||||
// const hasEnoughBalance = +balance < 4 ? false : true;
|
// const hasEnoughBalance = +balance < 4 ? false : true;
|
||||||
const difficulty = 8;
|
const difficulty = 8;
|
||||||
|
|
||||||
const tx = await createTransaction(181, keyPair, {
|
const txBody = {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
groupID: Number(groupId),
|
groupID: Number(groupId),
|
||||||
hasReceipient: 0,
|
hasReceipient: 0,
|
||||||
hasChatReference: typeMessage === "edit" ? 1 : 0,
|
hasChatReference: chatReference ? 1 : 0,
|
||||||
// chatReference: chatReference,
|
|
||||||
message: messageText,
|
message: messageText,
|
||||||
lastReference: reference,
|
lastReference: reference,
|
||||||
proofOfWorkNonce: 0,
|
proofOfWorkNonce: 0,
|
||||||
isEncrypted: 0, // Set default to not encrypted for groups
|
isEncrypted: 0, // Set default to not encrypted for groups
|
||||||
isText: 1,
|
isText: 1,
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if(chatReference){
|
||||||
|
txBody['chatReference'] = chatReference
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = await createTransaction(181, keyPair, txBody);
|
||||||
|
|
||||||
// if (!hasEnoughBalance) {
|
// if (!hasEnoughBalance) {
|
||||||
// throw new Error("Must have at least 4 QORT to send a chat message");
|
// throw new Error("Must have at least 4 QORT to send a chat message");
|
||||||
|
@ -29,6 +29,7 @@ const uid = new ShortUniqueId({ length: 5 });
|
|||||||
|
|
||||||
export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance}) => {
|
export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance}) => {
|
||||||
const [messages, setMessages] = useState([])
|
const [messages, setMessages] = useState([])
|
||||||
|
const [chatReferences, setChatReferences] = useState({})
|
||||||
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);
|
||||||
@ -56,7 +57,14 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
const tempMessages = useMemo(()=> {
|
const tempMessages = useMemo(()=> {
|
||||||
if(!selectedGroup) return []
|
if(!selectedGroup) return []
|
||||||
if(queueChats[selectedGroup]){
|
if(queueChats[selectedGroup]){
|
||||||
return queueChats[selectedGroup]
|
return queueChats[selectedGroup]?.filter((item)=> !item?.chatReference)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}, [selectedGroup, queueChats])
|
||||||
|
const tempChatReferences = useMemo(()=> {
|
||||||
|
if(!selectedGroup) return []
|
||||||
|
if(queueChats[selectedGroup]){
|
||||||
|
return queueChats[selectedGroup]?.filter((item)=> !!item?.chatReference)
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}, [selectedGroup, queueChats])
|
}, [selectedGroup, queueChats])
|
||||||
@ -91,6 +99,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const decryptMessages = (encryptedMessages: any[])=> {
|
const decryptMessages = (encryptedMessages: any[])=> {
|
||||||
try {
|
try {
|
||||||
@ -113,7 +122,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
res(response)
|
res(response)
|
||||||
if(hasInitialized.current){
|
if(hasInitialized.current){
|
||||||
|
|
||||||
const formatted = response.map((item: any)=> {
|
const formatted = response.filter((rawItem)=> !rawItem?.chatReference).map((item: any)=> {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
id: item.signature,
|
id: item.signature,
|
||||||
@ -123,8 +132,66 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
}
|
}
|
||||||
} )
|
} )
|
||||||
setMessages((prev)=> [...prev, ...formatted])
|
setMessages((prev)=> [...prev, ...formatted])
|
||||||
|
|
||||||
|
|
||||||
|
setChatReferences((prev) => {
|
||||||
|
let organizedChatReferences = { ...prev };
|
||||||
|
|
||||||
|
response
|
||||||
|
.filter((rawItem) => rawItem && rawItem.chatReference && rawItem.decryptedData?.type === 'reaction')
|
||||||
|
.forEach((item) => {
|
||||||
|
try {
|
||||||
|
const content = item.decryptedData?.content;
|
||||||
|
const sender = item.sender;
|
||||||
|
const newTimestamp = item.timestamp;
|
||||||
|
const contentState = item.decryptedData?.contentState;
|
||||||
|
|
||||||
|
if (!content || typeof content !== 'string' || !sender || typeof sender !== 'string' || !newTimestamp) {
|
||||||
|
console.warn("Invalid content, sender, or timestamp in reaction data", item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize chat reference and reactions if not present
|
||||||
|
organizedChatReferences[item.chatReference] = {
|
||||||
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
|
reactions: organizedChatReferences[item.chatReference]?.reactions || {}
|
||||||
|
};
|
||||||
|
|
||||||
|
organizedChatReferences[item.chatReference].reactions[content] =
|
||||||
|
organizedChatReferences[item.chatReference].reactions[content] || [];
|
||||||
|
|
||||||
|
const existingReactionIndex = organizedChatReferences[item.chatReference].reactions[content]
|
||||||
|
.findIndex(reaction => reaction.sender === sender);
|
||||||
|
|
||||||
|
// Handle contentState: if false, remove the reaction
|
||||||
|
if (contentState === false) {
|
||||||
|
if (existingReactionIndex !== -1) {
|
||||||
|
organizedChatReferences[item.chatReference].reactions[content].splice(existingReactionIndex, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add or update reaction
|
||||||
|
if (existingReactionIndex !== -1) {
|
||||||
|
const existingReaction = organizedChatReferences[item.chatReference].reactions[content][existingReactionIndex];
|
||||||
|
const existingTimestamp = existingReaction.timestamp;
|
||||||
|
|
||||||
|
if (newTimestamp > existingTimestamp) {
|
||||||
|
organizedChatReferences[item.chatReference].reactions[content][existingReactionIndex] = item;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
organizedChatReferences[item.chatReference].reactions[content].push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing reaction item:", error, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return organizedChatReferences;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const formatted = response.map((item: any)=> {
|
const formatted = response.filter((rawItem)=> !rawItem?.chatReference).map((item: any)=> {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
id: item.signature,
|
id: item.signature,
|
||||||
@ -135,7 +202,64 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
} )
|
} )
|
||||||
setMessages(formatted)
|
setMessages(formatted)
|
||||||
hasInitialized.current = true
|
hasInitialized.current = true
|
||||||
|
setChatReferences((prev) => {
|
||||||
|
let organizedChatReferences = { ...prev };
|
||||||
|
|
||||||
|
response
|
||||||
|
.filter((rawItem) => rawItem && rawItem.chatReference && rawItem.decryptedData?.type === 'reaction')
|
||||||
|
.forEach((item) => {
|
||||||
|
try {
|
||||||
|
const content = item.decryptedData?.content;
|
||||||
|
const sender = item.sender;
|
||||||
|
const newTimestamp = item.timestamp;
|
||||||
|
const contentState = item.decryptedData?.contentState;
|
||||||
|
|
||||||
|
if (!content || typeof content !== 'string' || !sender || typeof sender !== 'string' || !newTimestamp) {
|
||||||
|
console.warn("Invalid content, sender, or timestamp in reaction data", item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize chat reference and reactions if not present
|
||||||
|
organizedChatReferences[item.chatReference] = {
|
||||||
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
|
reactions: organizedChatReferences[item.chatReference]?.reactions || {}
|
||||||
|
};
|
||||||
|
|
||||||
|
organizedChatReferences[item.chatReference].reactions[content] =
|
||||||
|
organizedChatReferences[item.chatReference].reactions[content] || [];
|
||||||
|
|
||||||
|
const existingReactionIndex = organizedChatReferences[item.chatReference].reactions[content]
|
||||||
|
.findIndex(reaction => reaction.sender === sender);
|
||||||
|
|
||||||
|
// Handle contentState: if false, remove the reaction
|
||||||
|
if (contentState === false) {
|
||||||
|
if (existingReactionIndex !== -1) {
|
||||||
|
organizedChatReferences[item.chatReference].reactions[content].splice(existingReactionIndex, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add or update reaction
|
||||||
|
if (existingReactionIndex !== -1) {
|
||||||
|
const existingReaction = organizedChatReferences[item.chatReference].reactions[content][existingReactionIndex];
|
||||||
|
const existingTimestamp = existingReaction.timestamp;
|
||||||
|
|
||||||
|
if (newTimestamp > existingTimestamp) {
|
||||||
|
organizedChatReferences[item.chatReference].reactions[content][existingReactionIndex] = item;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
organizedChatReferences[item.chatReference].reactions[content].push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing reaction item:", error, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return organizedChatReferences;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rej(response.error)
|
rej(response.error)
|
||||||
@ -386,6 +510,73 @@ const clearEditorContent = () => {
|
|||||||
const onReply = useCallback((message)=> {
|
const onReply = useCallback((message)=> {
|
||||||
setReplyMessage(message)
|
setReplyMessage(message)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleReaction = useCallback(async (reaction, chatMessage, reactionState = true)=> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
if(isSending) return
|
||||||
|
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message')
|
||||||
|
pauseAllQueues()
|
||||||
|
|
||||||
|
|
||||||
|
setIsSending(true)
|
||||||
|
const message = ''
|
||||||
|
const secretKeyObject = await getSecretKey(false, true)
|
||||||
|
|
||||||
|
|
||||||
|
const otherData = {
|
||||||
|
specialId: uid.rnd(),
|
||||||
|
type: 'reaction',
|
||||||
|
content: reaction,
|
||||||
|
contentState: reactionState
|
||||||
|
}
|
||||||
|
const objectMessage = {
|
||||||
|
message,
|
||||||
|
...(otherData || {})
|
||||||
|
}
|
||||||
|
const message64: any = await objectToBase64(objectMessage)
|
||||||
|
|
||||||
|
const encryptSingle = await encryptChatMessage(message64, secretKeyObject)
|
||||||
|
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
||||||
|
|
||||||
|
const sendMessageFunc = async () => {
|
||||||
|
await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference: chatMessage.signature})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the function to the queue
|
||||||
|
const messageObj = {
|
||||||
|
message: {
|
||||||
|
text: message,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
senderName: myName,
|
||||||
|
sender: myAddress,
|
||||||
|
...(otherData || {})
|
||||||
|
},
|
||||||
|
chatReference: chatMessage.signature
|
||||||
|
}
|
||||||
|
addToQueue(sendMessageFunc, messageObj, 'chat-reaction',
|
||||||
|
selectedGroup );
|
||||||
|
// setTimeout(() => {
|
||||||
|
// executeEvent("sent-new-message-group", {})
|
||||||
|
// }, 150);
|
||||||
|
// clearEditorContent()
|
||||||
|
// setReplyMessage(null)
|
||||||
|
|
||||||
|
// send chat message
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error?.message || error
|
||||||
|
setInfoSnack({
|
||||||
|
type: "error",
|
||||||
|
message: errorMsg,
|
||||||
|
});
|
||||||
|
setOpenSnack(true);
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setIsSending(false)
|
||||||
|
resumeAllQueues()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@ -398,7 +589,7 @@ const clearEditorContent = () => {
|
|||||||
left: hide && '-100000px',
|
left: hide && '-100000px',
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
<ChatList onReply={onReply} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages}/>
|
<ChatList onReply={onReply} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences}/>
|
||||||
|
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
|
@ -3,7 +3,7 @@ import { Virtuoso } from 'react-virtuoso';
|
|||||||
import { MessageItem } from './MessageItem';
|
import { MessageItem } from './MessageItem';
|
||||||
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
|
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
|
||||||
|
|
||||||
export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply }) => {
|
export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences }) => {
|
||||||
const virtuosoRef = useRef();
|
const virtuosoRef = useRef();
|
||||||
const [messages, setMessages] = useState(initialMessages);
|
const [messages, setMessages] = useState(initialMessages);
|
||||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||||
@ -70,10 +70,7 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const scrollToBottom = (initialMsgs) => {
|
const scrollToBottom = (initialMsgs) => {
|
||||||
console.log('initialMsgs', {
|
|
||||||
initialMsgs,
|
|
||||||
messages
|
|
||||||
})
|
|
||||||
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1
|
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1
|
||||||
if (virtuosoRef.current) {
|
if (virtuosoRef.current) {
|
||||||
virtuosoRef.current.scrollToIndex({ index});
|
virtuosoRef.current.scrollToIndex({ index});
|
||||||
@ -109,6 +106,7 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
|||||||
|
|
||||||
let replyIndex = messages.findIndex((msg)=> msg?.signature === message?.repliedTo)
|
let replyIndex = messages.findIndex((msg)=> msg?.signature === message?.repliedTo)
|
||||||
let reply
|
let reply
|
||||||
|
let reactions = null
|
||||||
if(message?.repliedTo && replyIndex !== -1){
|
if(message?.repliedTo && replyIndex !== -1){
|
||||||
reply = messages[replyIndex]
|
reply = messages[replyIndex]
|
||||||
}
|
}
|
||||||
@ -125,6 +123,16 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(chatReferences && chatReferences[message?.signature]){
|
||||||
|
if(chatReferences[message.signature]?.reactions){
|
||||||
|
reactions = chatReferences[message.signature]?.reactions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let isUpdating = false
|
||||||
|
if(tempChatReferences && tempChatReferences?.find((item)=> item?.chatReference === message?.signature)){
|
||||||
|
isUpdating = true
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '10px 0', display: 'flex', justifyContent: 'center', width: '100%', minHeight: '50px' , overscrollBehavior: "none"}}>
|
<div style={{ padding: '10px 0', display: 'flex', justifyContent: 'center', width: '100%', minHeight: '50px' , overscrollBehavior: "none"}}>
|
||||||
<MessageItem
|
<MessageItem
|
||||||
@ -137,6 +145,9 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
|||||||
reply={reply}
|
reply={reply}
|
||||||
replyIndex={replyIndex}
|
replyIndex={replyIndex}
|
||||||
scrollToItem={scrollToItem}
|
scrollToItem={scrollToItem}
|
||||||
|
handleReaction={handleReaction}
|
||||||
|
reactions={reactions}
|
||||||
|
isUpdating={isUpdating}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -14,6 +14,7 @@ import { executeEvent } from "../../utils/events";
|
|||||||
import { WrapperUserAction } from "../WrapperUserAction";
|
import { WrapperUserAction } from "../WrapperUserAction";
|
||||||
import ReplyIcon from "@mui/icons-material/Reply";
|
import ReplyIcon from "@mui/icons-material/Reply";
|
||||||
import { Spacer } from "../../common/Spacer";
|
import { Spacer } from "../../common/Spacer";
|
||||||
|
import { ReactionPicker } from "../ReactionPicker";
|
||||||
|
|
||||||
export const MessageItem = ({
|
export const MessageItem = ({
|
||||||
message,
|
message,
|
||||||
@ -25,7 +26,10 @@ export const MessageItem = ({
|
|||||||
isShowingAsReply,
|
isShowingAsReply,
|
||||||
reply,
|
reply,
|
||||||
replyIndex,
|
replyIndex,
|
||||||
scrollToItem
|
scrollToItem,
|
||||||
|
handleReaction,
|
||||||
|
reactions,
|
||||||
|
isUpdating
|
||||||
}) => {
|
}) => {
|
||||||
const { ref, inView } = useInView({
|
const { ref, inView } = useInView({
|
||||||
threshold: 0.7, // Fully visible
|
threshold: 0.7, // Fully visible
|
||||||
@ -48,7 +52,7 @@ export const MessageItem = ({
|
|||||||
width: "95%",
|
width: "95%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: "7px",
|
gap: "7px",
|
||||||
opacity: isTemp ? 0.5 : 1,
|
opacity: (isTemp || isUpdating) ? 0.5 : 1,
|
||||||
}}
|
}}
|
||||||
id={message?.signature}
|
id={message?.signature}
|
||||||
>
|
>
|
||||||
@ -110,6 +114,11 @@ export const MessageItem = ({
|
|||||||
{message?.senderName || message?.sender}
|
{message?.senderName || message?.sender}
|
||||||
</Typography>
|
</Typography>
|
||||||
</WrapperUserAction>
|
</WrapperUserAction>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
{!isShowingAsReply && (
|
{!isShowingAsReply && (
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -119,6 +128,18 @@ export const MessageItem = ({
|
|||||||
<ReplyIcon />
|
<ReplyIcon />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
)}
|
)}
|
||||||
|
{!isShowingAsReply && handleReaction && (
|
||||||
|
<ReactionPicker onReaction={(val)=> {
|
||||||
|
|
||||||
|
if(reactions && reactions[val] && reactions[val]?.find((item)=> item?.sender === myAddress)){
|
||||||
|
handleReaction(val, message, false)
|
||||||
|
} else {
|
||||||
|
handleReaction(val, message, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{reply && (
|
{reply && (
|
||||||
<>
|
<>
|
||||||
@ -187,11 +208,49 @@ export const MessageItem = ({
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "space-between",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isTemp ? (
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '5px'
|
||||||
|
}}>
|
||||||
|
{reactions && Object.keys(reactions).map((reaction)=> {
|
||||||
|
const numberOfReactions = reactions[reaction]?.length
|
||||||
|
// const myReaction = reactions
|
||||||
|
if(numberOfReactions === 0) return null
|
||||||
|
return (
|
||||||
|
<ButtonBase sx={{
|
||||||
|
height: '30px',
|
||||||
|
minWidth: '45px',
|
||||||
|
background: 'var(--bg-2)',
|
||||||
|
borderRadius: '7px'
|
||||||
|
}} onClick={()=> {
|
||||||
|
if(reactions[reaction] && reactions[reaction]?.find((item)=> item?.sender === myAddress)){
|
||||||
|
handleReaction(reaction, message, false)
|
||||||
|
} else {
|
||||||
|
handleReaction(reaction, message, true)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<div>{reaction}</div>
|
||||||
|
</ButtonBase>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{isUpdating ? (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "gray",
|
||||||
|
fontFamily: "Inter",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Updating...
|
||||||
|
</Typography>
|
||||||
|
) : isTemp ? (
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
|
14
src/components/ReactionPicker.css
Normal file
14
src/components/ReactionPicker.css
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
.reaction-container {
|
||||||
|
position: relative; /* Parent must be positioned relatively */
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-picker {
|
||||||
|
position: absolute; /* Picker positioned absolutely relative to the parent */
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000; /* Ensure picker appears above other content */
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-container {
|
||||||
|
overflow: visible; /* Ensure the message container doesn't cut off the picker */
|
||||||
|
}
|
||||||
|
|
64
src/components/ReactionPicker.tsx
Normal file
64
src/components/ReactionPicker.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import Picker, { Theme } from 'emoji-picker-react';
|
||||||
|
import './ReactionPicker.css'; // CSS for proper positioning
|
||||||
|
import { ButtonBase } from '@mui/material';
|
||||||
|
|
||||||
|
export const ReactionPicker = ({ onReaction }) => {
|
||||||
|
const [showPicker, setShowPicker] = useState(false); // Manage picker visibility
|
||||||
|
const pickerRef = useRef(null); // Reference to the picker
|
||||||
|
|
||||||
|
const handleReaction = (emojiObject) => {
|
||||||
|
onReaction(emojiObject.emoji); // Handle the selected emoji reaction
|
||||||
|
setShowPicker(false); // Close picker after selection
|
||||||
|
};
|
||||||
|
const handlePicker = (emojiObject) => {
|
||||||
|
|
||||||
|
onReaction(emojiObject.emoji); // Handle the selected emoji reaction
|
||||||
|
setShowPicker(false); // Close picker after selection
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close picker if clicked outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (pickerRef.current && !pickerRef.current.contains(event.target)) {
|
||||||
|
setShowPicker(false); // Close picker
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listener when picker is shown
|
||||||
|
if (showPicker) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the event listener on unmount
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [showPicker]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="reaction-container">
|
||||||
|
{/* Emoji CTA */}
|
||||||
|
<ButtonBase sx={{
|
||||||
|
fontSize: '22px'
|
||||||
|
}} onClick={() => setShowPicker(!showPicker)}>
|
||||||
|
😃
|
||||||
|
</ButtonBase>
|
||||||
|
|
||||||
|
{/* Emoji Picker with dark theme */}
|
||||||
|
{showPicker && (
|
||||||
|
<div className="emoji-picker" ref={pickerRef}>
|
||||||
|
<Picker
|
||||||
|
reactionsDefaultOpen={true}
|
||||||
|
onReactionClick={handleReaction}
|
||||||
|
onEmojiClick={handlePicker}
|
||||||
|
allowExpandReactions={true}
|
||||||
|
theme={Theme.DARK}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user