diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index d4dd50f..b1980c2 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -4,6 +4,7 @@ import { getIO } from "../libs/socket"; import CreateTicketService from "../services/TicketServices/CreateTicketService"; import DeleteTicketService from "../services/TicketServices/DeleteTicketService"; import ListTicketsService from "../services/TicketServices/ListTicketsService"; +import ShowTicketService from "../services/TicketServices/ShowTicketService"; import UpdateTicketService from "../services/TicketServices/UpdateTicketService"; type IndexQuery = { @@ -60,6 +61,14 @@ export const store = async (req: Request, res: Response): Promise => { return res.status(200).json(ticket); }; +export const show = async (req: Request, res: Response): Promise => { + const { ticketId } = req.params; + + const contact = await ShowTicketService(ticketId); + + return res.status(200).json(contact); +}; + export const update = async ( req: Request, res: Response diff --git a/backend/src/routes/ticketRoutes.ts b/backend/src/routes/ticketRoutes.ts index 1627be2..41f41e7 100644 --- a/backend/src/routes/ticketRoutes.ts +++ b/backend/src/routes/ticketRoutes.ts @@ -7,6 +7,8 @@ const ticketRoutes = express.Router(); ticketRoutes.get("/tickets", isAuth, TicketController.index); +ticketRoutes.get("/tickets/:ticketId", isAuth, TicketController.show); + ticketRoutes.post("/tickets", isAuth, TicketController.store); ticketRoutes.put("/tickets/:ticketId", isAuth, TicketController.update); diff --git a/backend/src/services/ContactServices/UpdateContactService.ts b/backend/src/services/ContactServices/UpdateContactService.ts index cc39bc7..d802e9e 100644 --- a/backend/src/services/ContactServices/UpdateContactService.ts +++ b/backend/src/services/ContactServices/UpdateContactService.ts @@ -56,8 +56,12 @@ const UpdateContactService = async ({ await contact.update({ name, number, - email, - extraInfo + email + }); + + await contact.reload({ + attributes: ["id", "name", "number", "email", "profilePicUrl"], + include: ["extraInfo"] }); return contact; diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 4dd28fb..9b957b8 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -27,19 +27,26 @@ const verifyContact = async ( msgContact: WbotContact, profilePicUrl: string ): Promise => { + const io = getIO(); + let contact = await Contact.findOne({ where: { number: msgContact.id.user } }); if (contact) { await contact.update({ profilePicUrl }); + + io.emit("contact", { + action: "update", + contact + }); } else { contact = await Contact.create({ name: msgContact.name || msgContact.pushname || msgContact.id.user, number: msgContact.id.user, profilePicUrl }); - const io = getIO(); + io.emit("contact", { action: "create", contact diff --git a/frontend/src/components/MessageListItem/index.js b/frontend/src/components/MessageListItem/index.js new file mode 100644 index 0000000..067ed08 --- /dev/null +++ b/frontend/src/components/MessageListItem/index.js @@ -0,0 +1,7 @@ +import React from "react"; + +const MessageListItem = () => { + return
; +}; + +export default MessageListItem; diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js new file mode 100644 index 0000000..f0d6dad --- /dev/null +++ b/frontend/src/components/MessagesList/index.js @@ -0,0 +1,559 @@ +import React, { useState, useEffect, useReducer, useRef } from "react"; + +import { isSameDay, parseISO, format } from "date-fns"; +import openSocket from "socket.io-client"; +import clsx from "clsx"; + +import { green } from "@material-ui/core/colors"; +import { + Button, + CircularProgress, + Divider, + IconButton, + makeStyles, +} from "@material-ui/core"; +import { + AccessTime, + Block, + Done, + DoneAll, + ExpandMore, + GetApp, +} from "@material-ui/icons"; + +import LinkifyWithTargetBlank from "../LinkifyWithTargetBlank"; +import ModalImageCors from "../ModalImageCors"; +import MessageOptionsMenu from "../MessageOptionsMenu"; +import whatsBackground from "../../assets/wa-background.png"; + +import api from "../../services/api"; +import { toast } from "react-toastify"; + +const useStyles = makeStyles(theme => ({ + messagesListWrapper: { + overflow: "hidden", + position: "relative", + display: "flex", + flexDirection: "column", + flexGrow: 1, + }, + + messagesList: { + backgroundImage: `url(${whatsBackground})`, + display: "flex", + flexDirection: "column", + flexGrow: 1, + padding: "20px 20px 20px 20px", + overflowY: "scroll", + ...theme.scrollbarStyles, + }, + + circleLoading: { + color: green[500], + position: "absolute", + opacity: "70%", + top: 0, + left: "50%", + marginTop: 12, + }, + + messageLeft: { + marginRight: 20, + marginTop: 2, + minWidth: 100, + maxWidth: 600, + height: "auto", + display: "block", + position: "relative", + + whiteSpace: "pre-wrap", + backgroundColor: "#ffffff", + color: "#303030", + alignSelf: "flex-start", + borderTopLeftRadius: 0, + borderTopRightRadius: 8, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + paddingLeft: 5, + paddingRight: 5, + paddingTop: 5, + paddingBottom: 0, + boxShadow: "0 1px 1px #b3b3b3", + }, + + messageRight: { + marginLeft: 20, + marginTop: 2, + minWidth: 100, + maxWidth: 600, + height: "auto", + display: "block", + position: "relative", + + "&:hover #messageActionsButton": { + display: "flex", + position: "absolute", + top: 0, + right: 0, + }, + + whiteSpace: "pre-wrap", + backgroundColor: "#dcf8c6", + color: "#303030", + alignSelf: "flex-end", + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 0, + paddingLeft: 5, + paddingRight: 5, + paddingTop: 5, + paddingBottom: 0, + boxShadow: "0 1px 1px #b3b3b3", + }, + + messageActionsButton: { + display: "none", + position: "relative", + color: "#999", + zIndex: 1, + backgroundColor: "#dcf8c6", + "&:hover, &.Mui-focusVisible": { backgroundColor: "#dcf8c6" }, + }, + + messageContactName: { + display: "flex", + paddingLeft: 6, + color: "#6bcbef", + fontWeight: 500, + }, + + textContentItem: { + overflowWrap: "break-word", + padding: "3px 80px 6px 6px", + }, + + textContentItemDeleted: { + fontStyle: "italic", + color: "rgba(0, 0, 0, 0.36)", + overflowWrap: "break-word", + padding: "3px 80px 6px 6px", + }, + + messageMedia: { + objectFit: "cover", + width: 250, + height: 200, + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + }, + + timestamp: { + fontSize: 11, + position: "absolute", + bottom: 0, + right: 5, + color: "#999", + }, + + dailyTimestamp: { + alignItems: "center", + textAlign: "center", + alignSelf: "center", + width: "110px", + backgroundColor: "#e1f3fb", + margin: "10px", + borderRadius: "10px", + boxShadow: "0 1px 1px #b3b3b3", + }, + + dailyTimestampText: { + color: "#808888", + padding: 8, + alignSelf: "center", + marginLeft: "0px", + }, + + ackIcons: { + fontSize: 18, + verticalAlign: "middle", + marginLeft: 4, + }, + + deletedIcon: { + fontSize: 18, + verticalAlign: "middle", + marginRight: 4, + }, + + ackDoneAllIcon: { + color: green[500], + fontSize: 18, + verticalAlign: "middle", + marginLeft: 4, + }, + + downloadMedia: { + display: "flex", + alignItems: "center", + justifyContent: "center", + backgroundColor: "inherit", + padding: 10, + }, +})); + +const reducer = (state, action) => { + if (action.type === "LOAD_MESSAGES") { + const messages = action.payload; + const newMessages = []; + + messages.forEach(message => { + const messageIndex = state.findIndex(m => m.id === message.id); + if (messageIndex !== -1) { + state[messageIndex] = message; + } else { + newMessages.push(message); + } + }); + + return [...newMessages, ...state]; + } + + if (action.type === "ADD_MESSAGE") { + const newMessage = action.payload; + const messageIndex = state.findIndex(m => m.id === newMessage.id); + + if (messageIndex !== -1) { + state[messageIndex] = newMessage; + } else { + state.push(newMessage); + } + + return [...state]; + } + + if (action.type === "UPDATE_MESSAGE") { + const messageToUpdate = action.payload; + const messageIndex = state.findIndex(m => m.id === messageToUpdate.id); + + if (messageIndex !== -1) { + state[messageIndex] = messageToUpdate; + } + + return [...state]; + } + + if (action.type === "RESET") { + return []; + } +}; + +const MessagesList = ({ ticketId }) => { + const classes = useStyles(); + + const [messagesList, dispatch] = useReducer(reducer, []); + const [pageNumber, setPageNumber] = useState(1); + const [hasMore, setHasMore] = useState(false); + const [loading, setLoading] = useState(false); + const lastMessageRef = useRef(); + + const [selectedMessageId, setSelectedMessageId] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); + const messageOptionsMenuOpen = Boolean(anchorEl); + + useEffect(() => { + dispatch({ type: "RESET" }); + setPageNumber(1); + }, [ticketId]); + + useEffect(() => { + setLoading(true); + const delayDebounceFn = setTimeout(() => { + const fetchMessages = async () => { + try { + const { data } = await api.get("/messages/" + ticketId, { + params: { pageNumber }, + }); + + dispatch({ type: "LOAD_MESSAGES", payload: data.messages }); + setHasMore(data.hasMore); + + if (pageNumber === 1 && data.messages.length > 1) { + scrollToBottom(); + } + } catch (err) { + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } + } + }; + + setLoading(false); + fetchMessages(); + }, 500); + return () => clearTimeout(delayDebounceFn); + }, [pageNumber, ticketId]); + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + socket.emit("joinChatBox", ticketId); + + socket.on("appMessage", data => { + if (data.action === "create") { + dispatch({ type: "ADD_MESSAGE", payload: data.message }); + scrollToBottom(); + } + + if (data.action === "update") { + dispatch({ type: "UPDATE_MESSAGE", payload: data.message }); + } + }); + + return () => { + socket.disconnect(); + }; + }, [ticketId]); + + const loadMore = () => { + setPageNumber(prevPageNumber => prevPageNumber + 1); + }; + + const scrollToBottom = () => { + if (lastMessageRef.current) { + lastMessageRef.current.scrollIntoView({}); + } + }; + + const handleScroll = e => { + if (!hasMore) return; + const { scrollTop } = e.currentTarget; + + if (scrollTop === 0) { + document.getElementById("messagesList").scrollTop = 1; + } + + if (loading) { + return; + } + + if (scrollTop < 50) { + loadMore(); + } + }; + + const handleOpenMessageOptionsMenu = (e, messageId) => { + setAnchorEl(e.currentTarget); + setSelectedMessageId(messageId); + }; + + const handleCloseMessageOptionsMenu = e => { + setAnchorEl(null); + }; + + const checkMessageMedia = message => { + if (message.mediaType === "image") { + return ; + } + if (message.mediaType === "audio") { + return ( + + ); + } + + if (message.mediaType === "video") { + return ( +