diff --git a/client/src/components/Anonymous.jsx b/client/src/components/Anonymous.jsx
index f8673a83..4bc9fe6b 100644
--- a/client/src/components/Anonymous.jsx
+++ b/client/src/components/Anonymous.jsx
@@ -262,20 +262,19 @@ const Anonymous = ({ onChatClosed }) => {
- handleClose()} className='sm:w-[200px]'>
-
- Close Chat
- Ctrl + Shift + X
-
-
-
- handleClose(true)} className='sm:w-[200px]'>
-
- Find a new buddy
- Ctrl + Alt + N
-
-
-
+ handleClose()} className='sm:w-[200px]'>
+
+ Close Chat
+ Ctrl + Shift + X
+
+
+ handleClose(true)} className='sm:w-[200px]'>
+
+ Find a new buddy
+ Ctrl + Alt + N
+
+
+
{
isediting: false,
messageID: null,
});
- const [isQuoteReply, setIsQuoteReply] = useState(false);
const [message, setMessage] = useState('');
- const [quoteMessage, setQuoteMessage] = useState(null);
// use the id so we can track what message's previousMessage is open
const [openPreviousMessages, setOpenPreviousMessages] = useState(null);
const [badwordChoices, setBadwordChoices] = useState({});
- const { messages: state, addMessage, updateMessage, removeMessage, receiveMessage } = useChat();
+ const { messages: state, addMessage, updateMessage, removeMessage, receiveMessage, startReply, currentReplyMessageId, cancelReply } = useChat();
const { authState, dispatchAuth } = useAuth();
const { logout } = useKindeAuth();
const socket = useContext(SocketContext);
const { sendMessage, editMessage } = useChatUtils(socket);
- const { getMessage, handleResend } = chatHelper(state, app)
+ const { getMessage, handleResend, scrollToMessage } = chatHelper(state, app)
const inputRef = useRef('');
@@ -105,14 +105,15 @@ const Chat = () => {
[state, app.currentChatId]
);
- const doSend = async ({ senderId, room, message, time, containsBadword }) => {
+ const doSend = async ({ senderId, room, message, time, containsBadword, replyTo = null }) => {
try {
const sentMessage = await sendMessage({
senderId,
message,
time,
chatId: room,
- containsBadword
+ containsBadword,
+ replyTo
});
addMessage({
@@ -122,7 +123,8 @@ const Chat = () => {
message,
time,
status: 'pending',
- containsBadword
+ containsBadword,
+ replyTo
});
try {
@@ -140,7 +142,8 @@ const Chat = () => {
message,
time,
status: 'failed',
- containsBadword
+ containsBadword,
+ replyTo
});
} catch {
logOut();
@@ -158,25 +161,12 @@ const Chat = () => {
socket.emit(NEW_EVENT_TYPING, { chatId: app.currentChatId, isTyping: false });
const d = new Date();
- let message = inputRef.current.value.trim(); // Trim the message to remove the extra spaces
-
- if (!isQuoteReply) {
- const cleanedText = message.replace(/>+/g, '');
- message = cleanedText;
- }
+ const message = inputRef.current.value.trim(); // Trim the message to remove the extra spaces
if (message === '' || senderId === undefined || senderId === '123456') {
return;
}
- if (isQuoteReply && message.trim() === quoteMessage.trim()) {
- return;
- }
-
- setIsQuoteReply(false);
- setQuoteMessage(null);
-
-
if (editing.isediting === true) {
try {
const messageObject = getMessage(editing.messageID, state, app)
@@ -200,7 +190,8 @@ const Chat = () => {
room: app.currentChatId,
message,
time: d.getTime(),
- containsBadword: badwords.check(message)
+ containsBadword: badwords.check(message),
+ replyTo: currentReplyMessageId
});
}
@@ -209,6 +200,7 @@ const Chat = () => {
setMessage('');
inputRef.current.focus();
}
+ cancelReply()
};
const handleTypingStatus = throttle((e) => {
@@ -311,6 +303,10 @@ const Chat = () => {
}
}, [sortedMessages]);
+ useEffect(() => {
+ inputRef.current.focus()
+ }, [currentReplyMessageId])
+
return (
@@ -323,89 +319,143 @@ const Chat = () => {
className="h-[100%] md:max-h-full overflow-y-auto w-full scroll-smooth"
>
{sortedMessages.map(
- ({ senderId: sender, id, message, time, status, isEdited, oldMessages, containsBadword, isRead }) => {
+ ({ senderId: sender, id, message, time, status, isEdited, oldMessages, containsBadword, isRead, replyTo }) => {
const isSender = sender.toString() === senderId.toString();
message = decryptMessage(message)
+ // original message this message is a reply to
+ const repliedMessage = replyTo ? (() => {
+ const messageObj = getMessage(replyTo)
+ if (!messageObj) {
+ return null
+ }
+
+ return {
+ ...messageObj,
+ message: decryptMessage(messageObj.message)
+ }
+ })() : null
+
+ // is this message currently being replied?
+ const hasActiveReply = currentReplyMessageId === id
return (
-
+
+ {replyTo && (
scrollToMessage(replyTo)}
>
- {containsBadword && !isSender && !badwordChoices[id] ? (
-
-
Your buddy is trying to send you a bad word
-
- showBadword(id)} className='text-sm cursor-pointer'>See
- hideBadword(id)} className='text-red text-sm cursor-pointer'>Hide
-
-
- )
- :
- <>
-
- {typeof message === 'string' ? (
-
- ) : (
- badwordChoices[id] === 'hide' ? badwords.filter(message) : badwordChoices[id] === 'show' ? message : message
- )}
-
-
+ {repliedMessage ? (
+ typeof repliedMessage.message === 'string' ? (
+
+ ) : (
+ repliedMessage.message
+ )
+ ) : (
+
+ Original Message Deleted
+
+ )}
+
+
+ {sender.toString() === senderId.toString() ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+
+ {containsBadword && !isSender && !badwordChoices[id] ? (
+
+
Your buddy is trying to send you a bad word
+
+ showBadword(id)} className='text-sm cursor-pointer'>See
+ hideBadword(id)} className='text-red text-sm cursor-pointer'>Hide
+
-
+ )
+ :
+ <>
- handleResend(id, doSend, state, app)}
+ {typeof message === 'string' ? (
+
+ ) : (
+ badwordChoices[id] === 'hide' ? badwords.filter(message) : badwordChoices[id] === 'show' ? message : message
+ )}
+
+
-
-
-
- >}
+
+
+ handleResend(id, doSend, state, app)}
+ />
+
+
+
+
+ >}
+
+
-
);
}
)}
diff --git a/client/src/components/Chat/DropDownOption.jsx b/client/src/components/Chat/DropDownOption.jsx
index 195a17ad..f84567b3 100644
--- a/client/src/components/Chat/DropDownOption.jsx
+++ b/client/src/components/Chat/DropDownOption.jsx
@@ -19,8 +19,7 @@ const DropDownOptions = ({
inputRef,
cancelEdit,
setEditing,
- setIsQuoteReply,
- setQuoteMessage
+ setReplyId
}) => {
const { app } = useApp();
const socket = useContext(SocketContext);
@@ -50,23 +49,6 @@ const DropDownOptions = ({
setEditing({ isediting: true, messageID: id });
};
- const handleQuoteReply = (id) => {
- inputRef.current.focus();
-
- const { message } = getMessage(id, state, app);
- if (message.includes('Warning Message')) {
- cancelEdit();
- return;
- }
-
- const quotedMessage = `> ${message}
-
-`;
- inputRef.current.value = quotedMessage;
- setIsQuoteReply(true);
- setQuoteMessage(quotedMessage);
- };
-
const handleDelete = async (id) => {
if (!messageExists(id)) {
return;
@@ -126,8 +108,8 @@ const DropDownOptions = ({
onClick={() => handleCopyToClipBoard(id, state, app)}>
Copy
-
handleQuoteReply(id)}>
- Quote Reply
+ setReplyId(id)}>
+ Reply
handleDelete(id)}>Delete
@@ -145,8 +127,8 @@ const DropDownOptions = ({
onClick={() => handleCopyToClipBoard(id, state, app)}>
Copy
-
handleQuoteReply(id)}>
- Quote Reply
+ setReplyId(id)}>
+ Reply
} else {
@@ -162,6 +144,5 @@ DropDownOptions.propTypes = {
isSender: PropTypes.bool.isRequired,
cancelEdit: PropTypes.func.isRequired,
setEditing: PropTypes.func.isRequired,
- setIsQuoteReply: PropTypes.func.isRequired,
- setQuoteMessage: PropTypes.func.isRequired
+ setReplyId: PropTypes.func.isRequired
};
diff --git a/client/src/components/Chat/MessageInput.jsx b/client/src/components/Chat/MessageInput.jsx
index c84d7071..a6c434a1 100644
--- a/client/src/components/Chat/MessageInput.jsx
+++ b/client/src/components/Chat/MessageInput.jsx
@@ -2,12 +2,15 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { ImCancelCircle } from 'react-icons/im';
import { IoSend } from 'react-icons/io5';
+import { IoIosArrowDropright } from 'react-icons/io'
import EmojiPicker from '../EmojiPicker';
import useKeyPress, { ShortcutFlags } from 'src/hooks/useKeyPress';
import { socket } from 'context/Context';
import {
NEW_EVENT_SEND_FAILED
} from '../../../../constants.json';
+import { useChat } from 'src/context/ChatContext';
+import { useAuth } from 'src/context/AuthContext';
const MessageInput = ({
inputRef,
@@ -18,7 +21,10 @@ const MessageInput = ({
cancelEdit,
handleSubmit,
}) => {
- const [isTextAreaDisabled, setTextAreaDisabled] = useState(false);
+ const [isTextAreaDisabled, setTextAreaDisabled] = useState(false);
+ const { currentReplyMessage, currentReplyMessageId, scrollToMessage, cancelReply } = useChat()
+ const { authState } = useAuth()
+ const senderId = authState.loginId
// Define the limitMessageHandler function
function limitMessageHandler() {
@@ -54,30 +60,55 @@ const MessageInput = ({
return (
<>
-
>
);
diff --git a/client/src/context/ChatContext.jsx b/client/src/context/ChatContext.jsx
index c27fd6f3..d9e2ebe1 100644
--- a/client/src/context/ChatContext.jsx
+++ b/client/src/context/ChatContext.jsx
@@ -1,7 +1,9 @@
-import { createContext, useContext, useReducer } from 'react';
+import { createContext, useContext, useMemo, useReducer, useState } from 'react';
import PropTypes from 'prop-types';
import chatReducer from './reducers/chatReducer';
+import { useApp } from './AppContext';
+import useChatHelper from 'src/lib/chatHelper';
/**
* @typedef {'pending' | 'sent' | 'failed'} MessageStatus
@@ -18,7 +20,7 @@ import chatReducer from './reducers/chatReducer';
const initialState = {};
-const ChatContext = createContext({
+export const ChatContext = createContext({
...initialState,
deleteMessage: () => undefined,
addMessage: () => undefined,
@@ -33,6 +35,7 @@ export const useChat = () => {
};
export const ChatProvider = ({ children }) => {
+ const {app} = useApp()
const [state, dispatch] = useReducer(chatReducer, initialState, (defaultState) => {
try {
const persistedState = JSON.parse(localStorage.getItem('chats'));
@@ -46,6 +49,11 @@ export const ChatProvider = ({ children }) => {
return defaultState;
}
});
+ const { getMessage } = useChatHelper(state, app)
+
+ const [currentReplyMessageId, setCurrentReplyMessageId] = useState(null)
+ // eslint-disable-next-line no-use-before-define
+ const currentReplyMessage = useMemo(() => getMessage(currentReplyMessageId), [currentReplyMessageId]);
/**
*
@@ -111,17 +119,29 @@ export const ChatProvider = ({ children }) => {
});
}
+ function startReply(messageId) {
+ setCurrentReplyMessageId(messageId)
+ }
+
+ function cancelReply() {
+ setCurrentReplyMessageId(null)
+ }
+
return (
{children}
diff --git a/client/src/context/reducers/chatReducer.js b/client/src/context/reducers/chatReducer.js
index 9c5e8956..a6f7814e 100644
--- a/client/src/context/reducers/chatReducer.js
+++ b/client/src/context/reducers/chatReducer.js
@@ -53,7 +53,7 @@ export default function chatReducer(state, action) {
}
case 'ADD_MESSAGE': {
- const { senderId, room, id, message, time, status, containsBadword } = action.payload;
+ const { senderId, room, id, message, time, status, containsBadword, replyTo } = action.payload;
if (!clonedState[room]) {
throw new Error('Room not found!');
@@ -67,6 +67,7 @@ export default function chatReducer(state, action) {
time,
status,
containsBadword,
+ replyTo
};
break;
}
diff --git a/client/src/index.css b/client/src/index.css
index 6d52f745..792afd7c 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -55,3 +55,7 @@ h6 {
.chat blockquote {
@apply px-2 my-4 border-l-4 border-gray-600;
}
+
+.message-reply-container * {
+ @apply m-0 p-0;
+}
diff --git a/client/src/lib/chatHelper.js b/client/src/lib/chatHelper.js
index 658369cf..94de0eea 100644
--- a/client/src/lib/chatHelper.js
+++ b/client/src/lib/chatHelper.js
@@ -41,7 +41,45 @@ export default (state, app) => {
}
};
- return { getMessage, messageExists, handleCopyToClipBoard, handleResend };
+ function scrollToMessage(messageId, animate = true) {
+ const element = document.getElementById(`message-${messageId}`);
+
+ if (!element) {
+ return;
+ }
+
+ const alreadyHighlighted = element.classList.contains('bg-[#FF9F1C]/25');
+
+ element.scrollIntoView({
+ behavior: 'auto',
+ });
+
+ if (!animate) {
+ return
+ }
+
+ if (alreadyHighlighted) {
+ element.classList.replace('bg-[#FF9F1C]/25', 'bg-[#FF9F1C]/50');
+ } else {
+ element.classList.add('bg-[#FF9F1C]/50');
+ }
+
+ element.addEventListener(
+ 'transitionend',
+ () => {
+ if (alreadyHighlighted) {
+ element.classList.replace('bg-[#FF9F1C]/50', 'bg-[#FF9F1C]/25');
+ } else {
+ element.classList.remove('bg-[#FF9F1C]/50');
+ }
+ },
+ {
+ once: true,
+ }
+ );
+ }
+
+ return { getMessage, messageExists, handleCopyToClipBoard, handleResend, scrollToMessage };
};
export const adjustTextareaHeight = (inputRef) => {
diff --git a/server/.env_sample b/server/.env_sample
index 73711ae5..45916004 100644
--- a/server/.env_sample
+++ b/server/.env_sample
@@ -2,4 +2,5 @@ THIS IS JUST A FILE TO HELP YOU KNOW WHAT IS NEEDED IN THE .env FILE.
YOU HAVE TO CREATE ANOTHER FILE AND NAME IT .env
MongoDB_URL=mongodb://username:password@localhost:27018/
-GOOGLE_APPLICATION_CREDENTIALS=''
\ No newline at end of file
+GOOGLE_APPLICATION_CREDENTIALS=''
+SECRET_KEY=decryption101
diff --git a/server/controllers/userController.js b/server/controllers/userController.js
index fd3082e7..387667c6 100644
--- a/server/controllers/userController.js
+++ b/server/controllers/userController.js
@@ -98,6 +98,7 @@ const getAccessToken = async () => {
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
+ audience: `${domain}/api`,
}),
});
@@ -112,23 +113,46 @@ const getAccessToken = async () => {
}
};
-const checkIfUserExistsInKinde = async (email) => {
- const headers = {
- Accept: 'application/json',
- Authorization: `Bearer ${accessToken}`,
- };
-
+const getKindeUser = async (email) => {
const response = await fetch(`${domain}/api/v1/users?email=${email}`, {
method: 'GET',
- headers: headers,
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ Authorization: `Bearer ${accessToken}`,
+ },
});
- const data = await response.json();
- return data.users ? true : false;
+ let data;
+ if (!response.ok) {
+ const errorText = await response.json(); // Capture the error response text
+ if (errorText.errors[1].code === 'TOKEN_INVALID') {
+ const newAccessToken = await getAccessToken();
+ accessToken = newAccessToken;
+
+ const response = await fetch(`${domain}/api/v1/users?email=${email}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ Authorization: `Bearer ${newAccessToken}`,
+ },
+ });
+
+ data = await response.json();
+ } else {
+ console.log(errorText);
+ throw new Error(`Couldn't get user from kinde`);
+ }
+ } else {
+ data = await response.json();
+ }
+ return data;
};
const createUserWithId = async (email, id) => {
// Logic to create a new user with a provided ID
- const doesUserExist = await checkIfUserExistsInKinde(email);
+ const getUser = await getKindeUser(email);
+ const doesUserExist = getUser.users ? true : false;
if (doesUserExist) {
return User.create({ _id: id, email });
@@ -291,6 +315,22 @@ const deleteUser = async (req, res) => {
return res.status(NOT_FOUND).json({ error: 'User not found' });
}
+ const kindeUser = await getKindeUser(email);
+ const kindeUserId = kindeUser.users[0].id;
+
+ // delte user from kinde
+ const response = await fetch(`${domain}/api/v1/user?id=${kindeUserId}`, {
+ method: 'DELETE',
+
+ headers: {
+ Accept: 'application/json',
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(response.text());
+ }
// Delete the user
await user.deleteOne();
diff --git a/server/models/MessageModel.js b/server/models/MessageModel.js
index 42c39640..4430299d 100644
--- a/server/models/MessageModel.js
+++ b/server/models/MessageModel.js
@@ -33,6 +33,10 @@ const MessageSchema = new Schema(
type: Boolean,
default: false,
},
+ replyTo: {
+ type: Schema.Types.ObjectId,
+ ref: 'Message'
+ }
},
{
timestamps: true,
@@ -49,6 +53,7 @@ const MessageSchema = new Schema(
containsBadword: this.containsBadword,
oldMessages: this.oldMessages,
isRead: this.isRead,
+ replyTo: this.replyTo?.toString() || null
};
},
},
diff --git a/server/sockets/sendMessage.js b/server/sockets/sendMessage.js
index 0c6a3fd2..c102a5c4 100644
--- a/server/sockets/sendMessage.js
+++ b/server/sockets/sendMessage.js
@@ -11,7 +11,7 @@ const messageCounts = {};
module.exports = (socket) => {
socket.on(
NEW_EVENT_SEND_MESSAGE,
- async ({ senderId, message, time, chatId, containsBadword }, returnMessageToSender) => {
+ async ({ senderId, message, time, chatId, containsBadword, replyTo }, returnMessageToSender) => {
// Below line is just a failed message simulator for testing purposes.
// const rndInt = Math.floor(Math.random() * 6) + 1;
@@ -51,7 +51,8 @@ module.exports = (socket) => {
time,
senderId,
type: 'message',
- containsBadword
+ containsBadword,
+ replyTo
});
const messageDetails = {
diff --git a/server/utils/lib.js b/server/utils/lib.js
index 47e52c2f..68f53e1f 100644
--- a/server/utils/lib.js
+++ b/server/utils/lib.js
@@ -296,7 +296,7 @@ function chatExists(chatId) {
*/
async function addMessage(
chatId,
- { message, time, senderId, type = 'message', containsBadword }
+ { message, time, senderId, type = 'message', containsBadword, replyTo }
) {
const sender = getActiveUser(senderId);
@@ -318,6 +318,7 @@ async function addMessage(
type,
createdAt: new Date(time),
containsBadword,
+ replyTo
})
).optimizedVersion,
senderId,