Compare commits
4 commits
main
...
remove-cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8286c18020 | ||
|
|
275b323765 | ||
|
|
a6d3d1d8a8 | ||
|
|
fa931f35b2 |
7 changed files with 197 additions and 163 deletions
|
|
@ -0,0 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { Box, BoxProps } from "@mui/material";
|
||||
|
||||
interface MessageProps extends BoxProps {
|
||||
message: string;
|
||||
sentTime: string;
|
||||
senderName: string;
|
||||
direction: "outgoing" | "incoming";
|
||||
}
|
||||
|
||||
export const Message: FC<MessageProps> = ({ message, sentTime, senderName, direction, ...props }) => {
|
||||
return (
|
||||
<Box {...props}>
|
||||
<Box className="cs-message__time">{sentTime}{senderName ? ` - ${senderName}` : ''}</Box>
|
||||
<Box className="cs-message__content" dangerouslySetInnerHTML={{ __html: message }} />
|
||||
</Box>
|
||||
)
|
||||
};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useEffect, useRef, useCallback } from "react";
|
||||
import { Box, styled, BoxProps } from "@mui/material";
|
||||
|
||||
interface MessageListProps extends BoxProps {
|
||||
ticketId: string;
|
||||
}
|
||||
|
||||
export const MessageList: FC<MessageListProps> = ({
|
||||
ticketId,
|
||||
children,
|
||||
...props
|
||||
}: MessageListProps) => {
|
||||
const messageListRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.removeItem("messageListScroll");
|
||||
}, [ticketId]);
|
||||
|
||||
const onRefChange = useCallback(node => {
|
||||
if (node !== null) {
|
||||
// DOM node referenced by ref has changed and exists
|
||||
const scrollTop = parseFloat(sessionStorage.getItem("messageListScroll") ?? "1000000");
|
||||
node.scrollTop = Math.min(scrollTop, node.scrollHeight - node.clientHeight);
|
||||
node.onscroll = () => {
|
||||
sessionStorage.setItem("messageListScroll", node.scrollTop.toPrecision(6));
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
node.scrollTo({
|
||||
top: node.scrollHeight - node.clientHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const InternalMessageList = styled((props: any) => (
|
||||
<Box key="messageList" ref={onRefChange} {...props} />
|
||||
))(({ theme }) => ({
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
padding: "20px",
|
||||
}));
|
||||
|
||||
return (
|
||||
<InternalMessageList {...props}>
|
||||
{children}
|
||||
</InternalMessageList>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,18 +1,27 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState, useEffect } from "react";
|
||||
import { FC, useState, useEffect, useRef, memo } from "react";
|
||||
import { getTicketAction, getTicketArticlesAction } from "app/_actions/tickets";
|
||||
import { Grid, Box, Typography } from "@mui/material";
|
||||
import { Grid, Box, Typography, styled, BoxProps } from "@mui/material";
|
||||
import { Button, fonts, colors } from "@link-stack/ui";
|
||||
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
||||
import {
|
||||
MainContainer,
|
||||
ChatContainer,
|
||||
MessageList,
|
||||
Message,
|
||||
ConversationHeader,
|
||||
} from "@chatscope/chat-ui-kit-react";
|
||||
import { ArticleCreateDialog } from "./ArticleCreateDialog";
|
||||
import { Message } from "./Message";
|
||||
import { MessageList } from "./MessageList";
|
||||
|
||||
const ChatContainer = styled((props: BoxProps) => <Box {...props} />)(
|
||||
({ theme }) => ({
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}),
|
||||
);
|
||||
|
||||
const ChatHeader = styled((props: any) => <Box {...props} />)(({ theme }) => ({
|
||||
padding: "20px",
|
||||
textAlign: "center",
|
||||
}));
|
||||
|
||||
interface TicketDetailProps {
|
||||
id: string;
|
||||
|
|
@ -26,9 +35,22 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
|||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [articleKind, setArticleKind] = useState("note");
|
||||
|
||||
const messageListRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset remembered state
|
||||
sessionStorage.setItem("ticketDetailTicket", "");
|
||||
sessionStorage.setItem("ticketDetailArticles", "");
|
||||
}, [id])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTicket = async () => {
|
||||
const result = await getTicketAction(id);
|
||||
const resultRaw = JSON.stringify(result);
|
||||
if (resultRaw == sessionStorage.getItem("ticketDetailTicket")) {
|
||||
return; // No change
|
||||
}
|
||||
sessionStorage.setItem("ticketDetailTicket", resultRaw);
|
||||
setTicket(result);
|
||||
};
|
||||
|
||||
|
|
@ -42,6 +64,11 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
|||
useEffect(() => {
|
||||
const fetchTicketArticles = async () => {
|
||||
const result = await getTicketArticlesAction(id);
|
||||
const resultRaw = JSON.stringify(result);
|
||||
if (resultRaw == sessionStorage.getItem("ticketDetailArticles")) {
|
||||
return; // No change
|
||||
}
|
||||
sessionStorage.setItem("ticketDetailArticles", resultRaw);
|
||||
setTicketArticles(result);
|
||||
};
|
||||
|
||||
|
|
@ -64,69 +91,58 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
|||
<Box sx={{ height: "100%", width: "100%", background: veryLightGray }}>
|
||||
{shouldRender && (
|
||||
<>
|
||||
<MainContainer>
|
||||
<ChatContainer>
|
||||
<ConversationHeader>
|
||||
<ConversationHeader.Content>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontFamily: poppins.style.fontFamily,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{ticket.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontFamily: roboto.style.fontFamily,
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>{`Ticket #${ticket.number} (created ${new Date(
|
||||
ticket.createdAt,
|
||||
).toLocaleDateString()})`}</Typography>
|
||||
</Box>
|
||||
</ConversationHeader.Content>
|
||||
</ConversationHeader>
|
||||
<MessageList style={{ marginBottom: 80 }}>
|
||||
{ticketArticles.edges.map(({ node: article }: any) => (
|
||||
<Message
|
||||
key={article.id}
|
||||
className={
|
||||
article.internal
|
||||
? "internal-note"
|
||||
: article?.sender?.name === "Agent"
|
||||
? "outgoing-message"
|
||||
: "incoming-message"
|
||||
}
|
||||
model={{
|
||||
message: article.bodyWithUrls,
|
||||
sentTime: article.updated_at,
|
||||
sender: article.from,
|
||||
direction:
|
||||
article.sender === "Agent" ? "outgoing" : "incoming",
|
||||
position: "single",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</MessageList>
|
||||
</ChatContainer>
|
||||
<ChatContainer className="cs-main-container">
|
||||
<ChatHeader className="cs-conversation-header">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontFamily: poppins.style.fontFamily,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{ticket.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontFamily: roboto.style.fontFamily,
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>{`Ticket #${ticket.number} (created ${new Date(
|
||||
ticket.createdAt,
|
||||
).toLocaleDateString()})`}</Typography>
|
||||
</ChatHeader>
|
||||
<MessageList ticketId={id}>
|
||||
{ticketArticles.edges.map(({ node: article }: any, index: number) => {
|
||||
const thisDate = new Date(ticketArticles.edges[index].node.updatedAt).toLocaleDateString();
|
||||
const lastDate = index > 0 ? new Date(ticketArticles.edges[index - 1].node.updatedAt).toLocaleDateString() : ""
|
||||
return (
|
||||
<>
|
||||
{thisDate !== lastDate ? (<Box className="cs-day-marker"><Box className="cs-day-marker__line"></Box><Box className="cs-day-marker__text" sx={{background: veryLightGray}}>{thisDate}</Box></Box>) : (null)}
|
||||
<Message
|
||||
key={article.id}
|
||||
className={
|
||||
article.internal
|
||||
? "internal-note"
|
||||
: article?.sender?.name === "Agent"
|
||||
? "outgoing-message"
|
||||
: "incoming-message"
|
||||
}
|
||||
message={article.bodyWithUrls}
|
||||
sentTime={new Date(article.updatedAt).toLocaleString()}
|
||||
senderName={article.sender?.name}
|
||||
direction={
|
||||
article.sender === "Agent" ? "outgoing" : "incoming"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)})}
|
||||
</MessageList>
|
||||
<Box
|
||||
sx={{
|
||||
height: 80,
|
||||
flex: "0 0 80",
|
||||
background: veryLightGray,
|
||||
borderTop: `1px solid ${lightGray}`,
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
|
|
@ -160,7 +176,7 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
|||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</MainContainer>
|
||||
</ChatContainer>
|
||||
<ArticleCreateDialog
|
||||
ticketID={ticket.internalId}
|
||||
open={dialogOpen}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ query getTicketArticles($ticketId: ID!) {
|
|||
emailAddress
|
||||
}
|
||||
}
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@ body {
|
|||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
.cs-message__time {
|
||||
color: #888;
|
||||
font-family: Roboto;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
left: 12px;
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
.internal-note .cs-message__content {
|
||||
background-color: #FFB62088 !important;
|
||||
border: 2px solid #FFB620 !important;
|
||||
|
|
@ -75,8 +84,6 @@ body {
|
|||
|
||||
.cs-conversation-header {
|
||||
background-color: #ddd !important;
|
||||
border: 0 !important;
|
||||
padding: 20px !important;
|
||||
border-bottom: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +96,34 @@ body {
|
|||
}
|
||||
|
||||
.cs-main-container {
|
||||
border: 0 !important;
|
||||
border-right: 1px solid #ccc !important;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.cs-day-marker {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
height: 30px;
|
||||
color: #aaa;
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cs-day-marker__line {
|
||||
position: absolute;
|
||||
content: " ";
|
||||
height: 1px;
|
||||
top: 15px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.cs-day-marker__text {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
@ -10,8 +10,6 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chatscope/chat-ui-kit-react": "^2.0.3",
|
||||
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
||||
"@emotion/cache": "^11.13.1",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/server": "^11.11.0",
|
||||
|
|
|
|||
87
package-lock.json
generated
87
package-lock.json
generated
|
|
@ -234,8 +234,6 @@
|
|||
"name": "@link-stack/link",
|
||||
"version": "2.2.0",
|
||||
"dependencies": {
|
||||
"@chatscope/chat-ui-kit-react": "^2.0.3",
|
||||
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
||||
"@emotion/cache": "^11.13.1",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/server": "^11.11.0",
|
||||
|
|
@ -1001,32 +999,6 @@
|
|||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@chatscope/chat-ui-kit-react": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chatscope/chat-ui-kit-react/-/chat-ui-kit-react-2.0.3.tgz",
|
||||
"integrity": "sha512-0IkjFskRec7SHrFivOQPiZMie5GLQL+ZnROiIbj4yptbC3aMEMFdHRAZrfqlid3uQx9kYhdtn34wMLh1vVNMLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@chatscope/chat-ui-kit-styles": "^1.2.0",
|
||||
"@fortawesome/fontawesome-free": "^5.12.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
||||
"@fortawesome/react-fontawesome": "^0.1.8",
|
||||
"classnames": "^2.2.6",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.12.0 || ^17.0.0 || ^18.2.0",
|
||||
"react-dom": "^16.12.0 || ^17.0.0 || ^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@chatscope/chat-ui-kit-styles": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@chatscope/chat-ui-kit-styles/-/chat-ui-kit-styles-1.4.0.tgz",
|
||||
"integrity": "sha512-016mBJD3DESw7Nh+lkKcPd22xG92ghA0VpIXIbjQtmXhC7Ve6wRazTy8z1Ahut+Tbv179+JxrftuMngsj/yV8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
|
||||
|
|
@ -1655,65 +1627,6 @@
|
|||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "0.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
|
||||
"integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
|
||||
"integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "1.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
|
||||
"integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz",
|
||||
"integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||
"react": ">=16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@graphile/logger": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphile/logger/-/logger-0.2.0.tgz",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue