diff --git a/components/common/chat/bubble_message/bubble_message.tsx b/components/common/chat/bubble_message/bubble_message.tsx index 952a0955..768cabcb 100644 --- a/components/common/chat/bubble_message/bubble_message.tsx +++ b/components/common/chat/bubble_message/bubble_message.tsx @@ -1,95 +1,77 @@ import * as React from 'react' import moment from 'moment' +import { DirectMessageResponse } from '@/types/graphql' export const BubbleMessage = ({ - message, + messages, currentUserID, }: BubbleMessageProps) => { return ( -
-
-
-
-
-
    - {message.length > 0 && - message.map( - ( - { message, user, timestamp }, - index - ) => ( - <> -
  • -
    - User profile picture -
    -
    - - {message} +
    +
    +
      + {messages.length > 0 && + messages.map( + ({ body, author, recipient, createdAt }, index) => ( + <> +
    • +
      + User profile picture +
      +
      + + {body} + +
      +
      + + {author.id === currentUserID ? ( + <> + {moment(createdAt) + .utcOffset(-480) + .format('hh:mm A')} + + // -
      -
      - - // - {moment( - timestamp - ).format('hh:mm A')} - -
      -
    • - {message[index + 1] && ( -
    • -
      - - {/* {moment */} - {/* .unix(timestamp1) */} - {/* .format( */} - {/* 'dddd, MMMM Do, hh:mm a' */} - {/* )} */} - -
      -
    • + + ) : ( + <> + // + {moment(createdAt) + .utcOffset(-480) + .format('hh:mm A')} + )} - - ) - )} -
    -
    -
    -
    + +
+ + + ) + )} +
) } export type BubbleMessageProps = { - message: MessageProps - currentUserID: string | number + messages: DirectMessageResponse[] + currentUserID: string } -export type MessageProps = { - user: { - id: string | number - image: string - } - message: string - timestamp: number -}[] diff --git a/components/common/chat/chat_history/chat_history.tsx b/components/common/chat/chat_history/chat_history.tsx index 67f328f4..44ac496f 100644 --- a/components/common/chat/chat_history/chat_history.tsx +++ b/components/common/chat/chat_history/chat_history.tsx @@ -2,77 +2,173 @@ import * as React from 'react' import moment from 'moment' import { FaTrash, FaArchive } from 'react-icons/fa' +import { DirectMessageResponse } from '@/types/graphql' -export const ChatHistory = ({ messages, handle }: ChatHistoryProps) => { - const [isChecked, setIsChecked] = React.useState(0 ?? null) - // const [isNotified, setIsNotified] = React.useState(0 ?? null) - +export const ChatHistory = ({ + messages, + handle, + handleSelect, + selected, +}: ChatHistoryProps) => { return (
{messages?.length && messages.map((message, messageIndex) => (
setIsChecked(messageIndex)} + onClick={() => handleSelect(message.recipient.id)} > -
-
- + ) : ( + -
-
-
-
- {message.name} -
- - {moment(message.timestamp).fromNow()} - -
-
-
- - -
+ )}
))}
) } -type ChatHistoryProps = { - messages: HistoryProps[] +const GroupChatListItem = ({ + messages, + messageIndex, + name, + timestamp, + handle, +}: { + messages: DirectMessageResponse[] + messageIndex: number + name: string + timestamp: Date handle: () => void +}) => { + return ( + <> +
+
+
+ + {name.split(' ')[0][0]} + +
+
+
+
+
{name}
+ + {moment(timestamp).utcOffset(-480).format('hh:mm A')} + +
+
+
+ + +
+ + ) } -type HistoryProps = { - name: string - image: string - selected: boolean - timestamp: number - newNotification: boolean + +const UserChatListItem = ({ + messages, + messageIndex, + firstName, + lastName, + picURL, + handle, + timestamp, +}) => { + return ( + <> +
+
+ The profile picture of the user you are sending a message to. +
+
+
+
+ {firstName} {lastName} +
+ + {moment(timestamp).utcOffset(-480).format('hh:mm A')} + +
+
+
+ + +
+ + ) +} + +type ChatHistoryProps = { + messages: DirectMessageResponse[] + handle: () => void + handleSelect: (id: string) => void + selected: string } diff --git a/components/common/message_input/message_input.tsx b/components/common/message_input/message_input.tsx index dd8479ba..6581b664 100644 --- a/components/common/message_input/message_input.tsx +++ b/components/common/message_input/message_input.tsx @@ -4,9 +4,14 @@ import { MdAttachFile, MdKeyboardVoice } from 'react-icons/md' import { BiImageAdd } from 'react-icons/bi' import { RiFileGifFill } from 'react-icons/ri' import { IoSendSharp } from 'react-icons/io5' +import { Input } from '@/common/forms/inputs/input/input' export const MessageInput: React.FC = ({ message, + handleSubmit, + recipientID, + userInput, + handleUserInput, }): React.ReactElement => { const [isClicked, setIsClicked] = React.useState(null) @@ -29,7 +34,7 @@ export const MessageInput: React.FC = ({
-
+
-
- - -
-
- +
{ + event.preventDefault() + handleSubmit(userInput, recipientID) + }} + className="w-full flex" + > +
+ handleUserInput(e)} + type={'text'} + length={'full'} + /> +
+
+ - -
+ +
+
) @@ -152,6 +166,10 @@ export const MessageInput: React.FC = ({ type MessageInputProps = { message: hintMessage[] + handleSubmit: (message: string, recipientID: string) => void + recipientID: string + userInput: string + handleUserInput: (value: ((prevState: string) => string) | string) => void } type hintMessage = { messages: string diff --git a/components/common/pages/sidebar/sidebar.tsx b/components/common/pages/sidebar/sidebar.tsx index 6b4a3440..dc5c55c0 100644 --- a/components/common/pages/sidebar/sidebar.tsx +++ b/components/common/pages/sidebar/sidebar.tsx @@ -5,6 +5,7 @@ import { PlanOfStudy } from '../../svg/plan_of_study' import { Modules } from '../../svg/modules' import { Communities } from '../../svg/communities' import { GradeBook } from '../../svg/gradebook' +import { ChatBubble } from '@/common/svg/chat_bubble' type IconType = React.ReactNode @@ -54,6 +55,13 @@ export const Sidebar: React.FC = ({ href="/communities" icon={''} /> + } + collapsed={!open} + value="MESSAGES" + href="/messages" + icon={''} + /> } collapsed={!open} diff --git a/package-lock.json b/package-lock.json index 522c17b6..27b51662 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "google-auth-library": "^8.5.1", "graphql": "^16.6.0", "graphql-request": "^5.0.0", + "graphql-ws": "^5.12.1", "import": "^0.0.6", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", @@ -35,6 +36,8 @@ "rehype-katex": "^6.0.2", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", + "socket.io": "^4.6.1", + "socket.io-client": "^4.6.1", "swr": "^1.3.0" }, "devDependencies": { @@ -4126,6 +4129,11 @@ "@sinonjs/commons": "^2.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, "node_modules/@storybook/addon-actions": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.0.5.tgz", @@ -8690,6 +8698,19 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.4.tgz", @@ -9778,7 +9799,6 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -10894,6 +10914,14 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/better-opn": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz", @@ -12520,6 +12548,18 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -13637,6 +13677,94 @@ "objectorarray": "^1.0.5" } }, + "node_modules/engine.io": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", + "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz", + "integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", @@ -16543,6 +16671,17 @@ "graphql": "14 - 16" } }, + "node_modules/graphql-ws": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.1.tgz", + "integrity": "sha512-umt4f5NnMK46ChM2coO36PTFhHouBrK9stWWBczERguwYrGnPNxJ9dimU6IyOBfOkC6Izhkg4H8+F51W/8CYDg==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, "node_modules/gtoken": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", @@ -21987,7 +22126,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -26246,6 +26384,76 @@ "node": ">=0.10.0" } }, + "node_modules/socket.io": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", + "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.4.1", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dependencies": { + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", + "integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.4.0", + "socket.io-parser": "~4.2.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", + "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -28628,7 +28836,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, "engines": { "node": ">= 0.8" } @@ -29506,6 +29713,14 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -32349,6 +32564,11 @@ "@sinonjs/commons": "^2.0.0" } }, + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, "@storybook/addon-actions": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.0.5.tgz", @@ -35739,6 +35959,19 @@ "@types/node": "*" } }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "requires": { + "@types/node": "*" + } + }, "@types/d3-array": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.4.tgz", @@ -36710,7 +36943,6 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, "requires": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -37546,6 +37778,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "better-opn": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz", @@ -38820,6 +39057,15 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -39706,6 +39952,61 @@ "objectorarray": "^1.0.5" } }, + "engine.io": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", + "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.11.0" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "requires": {} + } + } + }, + "engine.io-client": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz", + "integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + }, + "dependencies": { + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "requires": {} + } + } + }, + "engine.io-parser": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==" + }, "enhanced-resolve": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", @@ -41970,6 +42271,12 @@ "form-data": "^3.0.0" } }, + "graphql-ws": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.1.tgz", + "integrity": "sha512-umt4f5NnMK46ChM2coO36PTFhHouBrK9stWWBczERguwYrGnPNxJ9dimU6IyOBfOkC6Izhkg4H8+F51W/8CYDg==", + "requires": {} + }, "gtoken": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", @@ -45895,8 +46202,7 @@ "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, "neo-async": { "version": "2.6.2", @@ -49151,6 +49457,55 @@ } } }, + "socket.io": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", + "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.4.1", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" + } + }, + "socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "requires": { + "ws": "~8.11.0" + }, + "dependencies": { + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "requires": {} + } + } + }, + "socket.io-client": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", + "integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.4.0", + "socket.io-parser": "~4.2.1" + } + }, + "socket.io-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", + "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + } + }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -51000,8 +51355,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "vfile": { "version": "5.3.7", @@ -51699,6 +52053,11 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 315c2534..99e177a1 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "google-auth-library": "^8.5.1", "graphql": "^16.6.0", "graphql-request": "^5.0.0", + "graphql-ws": "^5.12.1", "import": "^0.0.6", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", @@ -43,6 +44,8 @@ "rehype-katex": "^6.0.2", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", + "socket.io": "^4.6.1", + "socket.io-client": "^4.6.1", "swr": "^1.3.0" }, "devDependencies": { diff --git a/pages/api/messages/socket.ts b/pages/api/messages/socket.ts new file mode 100644 index 00000000..d13edb5c --- /dev/null +++ b/pages/api/messages/socket.ts @@ -0,0 +1,23 @@ +import { Server } from 'socket.io' + +const SocketHandler = (req, res) => { + if (res.socket.server.io) { + console.log('Socket is already running') + } else { + console.log('Socket is initializing') + const io = new Server(res.socket.server) + res.socket.server.io = io + + io.on('connection', (socket) => { + socket.on('input-change', (msg) => { + socket.broadcast.emit('update-input', msg) + }) + socket.on('message', () => { + socket.broadcast.emit('update-input', 'Hello from the server') + }) + }) + } + res.end() +} + +export default SocketHandler diff --git a/pages/messages/hotline.tsx b/pages/messages/hotline.tsx new file mode 100644 index 00000000..3e3bad8e --- /dev/null +++ b/pages/messages/hotline.tsx @@ -0,0 +1,51 @@ +import React, { useEffect, useState } from 'react' +import io from 'socket.io-client' +import { Button } from '@/common/button/button' +let socket + +function HotlineHomePage() { + useEffect(() => { + socketInitializer() + }, []) + + const [input, setInput] = useState('') + + const onChangeHandler = (e) => { + setInput(e.target.value) + // socket.emit('input-change', e.target.value) + } + + const socketInitializer = async () => { + await fetch('/api/messages/socket') + socket = io() + + socket.on('connect', () => { + console.log('connected') + }) + + socket.on('update-input', (msg) => { + setInput(msg) + }) + } + + return ( +
{ + e.preventDefault() + if (input) { + socket.emit('message', input) + setInput('') + } + }} + > + + +
+ ) +} + +export default HotlineHomePage diff --git a/pages/messages/index.tsx b/pages/messages/index.tsx index 2d0034c6..fc1ff7c2 100644 --- a/pages/messages/index.tsx +++ b/pages/messages/index.tsx @@ -1,108 +1,214 @@ import { BubbleMessage } from '@/components/common/chat/bubble_message/bubble_message' import { ChatHistory } from '@/components/common/chat/chat_history/chat_history' import Head from 'next/head' +import { Layout } from '@/common/pages/layouts/layout/layout' +import useSWR from 'swr' +import gqlFetcher, { client } from '@/utils/gql_fetcher' +import { gql } from 'graphql-request' +import { useContext, useState } from 'react' +import GlobalUserContext from '@/contexts/global_user_context' +import Loading from '@/common/loader/loader' +import RequestFailed from '@/pages/errors/request_failed/request_failed' +import { DirectMessageResponse } from '@/types/graphql' +import { MessageInput } from '@/common/message_input/message_input' +import { useRouter } from 'next/router' + +const DirectMessageHomePage = () => { + const { user } = useContext(GlobalUserContext) + const router = useRouter() + const { selected: selectedID } = router.query + const [selected, setSelected] = useState( + (selectedID as string) ?? null + ) + const [message, setMessage] = useState(null) + const { data, error } = useSWR( + { + query: gql` + query GetSentMessages($userID: ID!) { + sentMessages(senderID: $userID) { + id + body + createdAt + author { + id + firstName + lastName + picURL + } + recipient { + ... on User { + id + firstName + lastName + picURL + __typename + } + ... on Group { + id + name + public + __typename + } + } + } + } + `, + variables: { + userID: user.id, + }, + }, + gqlFetcher + ) as { + data: { + sentMessages: Array + } + error: Error + } + + const { data: conversation, error: conversationError } = useSWR( + () => + selected !== null + ? { + query: gql` + query GetConversation( + $senderID: ID! + $recipientID: ID! + ) { + directMessages( + receiverID: $recipientID + senderID: $senderID + ) { + createdAt + author { + firstName + lastName + id + picURL + } + recipient { + ... on User { + id + firstName + lastName + picURL + __typename + } + ... on Group { + id + name + public + __typename + } + } + body + } + } + `, + variables: { + senderID: user.id, + recipientID: selected, + }, + } + : null, + gqlFetcher + ) as { + data: { + directMessages: Array + } + error: Error + } + + const { mutate } = useSWR({}, gqlFetcher) + + const handleSendMessage = (message: string, recipientID: string) => { + console.log(message, recipientID) + mutate(async () => { + await client.request( + gql` + mutation SendMessage( + $receiverID: ID! + $message: String! + $senderID: ID! + ) { + createDirectMessage( + receiverID: $receiverID + message: $message + senderID: $senderID + ) + } + `, + { + receiverID: recipientID, + senderID: user.id, + message, + } + ) + }) + .then(() => { + setMessage(null) + }) + .finally(() => router.reload()) + .catch((error) => { + console.error('Error while sending message:', error) + }) + } + + if (error || conversationError) + return ( + + ) + + if (!data) return -const Index = () => { return ( <> Messages | GLANCE
-
+
{}} - messages={[ - { - image: 'https://www.creative-tim.com/learning-lab/tailwind-starter-kit/img/team-4-470x470.png', - name: 'AVantika', - newNotification: false, - selected: false, - timestamp: 1664376815, - }, - { - image: 'https://www.creative-tim.com/learning-lab/tailwind-starter-kit/img/team-4-470x470.png', - name: 'AVantika', - newNotification: true, - selected: false, - timestamp: 1748347589, - }, - { - image: 'https://www.creative-tim.com/learning-lab/tailwind-starter-kit/img/team-4-470x470.png', - name: 'AVantika', - newNotification: false, - selected: false, - timestamp: 1231211842, - }, - { - image: 'https://www.creative-tim.com/learning-lab/tailwind-starter-kit/img/team-4-470x470.png', - name: 'AVantika', - newNotification: false, - selected: false, - timestamp: 1748347589, - }, - { - image: 'https://www.creative-tim.com/learning-lab/tailwind-starter-kit/img/team-4-470x470.png', - name: 'AVantika', - newNotification: false, - selected: false, - timestamp: 1341465382, - }, - { - image: 'https://www.creative-tim.com/learning-lab/tailwind-starter-kit/img/team-4-470x470.png', - name: 'AVantika', - newNotification: false, - selected: false, - timestamp: 1034500193, - }, - ]} + handle={() => null} + messages={data.sentMessages} + handleSelect={(id: string) => setSelected(id)} + selected={selected} />
-
- - {/* - FIXME: Replace this with a component - ALMP-559 - */} +
+ {selected !== null && conversation ? ( + <> + + + + ) : ( +
+

+ Select a conversation to view +

+
+ )}
) } -export default Index + +DirectMessageHomePage.getLayout = (page: any) => { + return {page} +} + +export default DirectMessageHomePage diff --git a/tailwind.config.js b/tailwind.config.js index 2aaa0c99..bfb21758 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -38,6 +38,9 @@ module.exports = { borderRadius: { DEFAULT: '1px', }, + scale: { + '-1': '-1', + }, }, }, plugins: [require('flowbite/plugin')], diff --git a/types/graphql.d.ts b/types/graphql.d.ts index ff41eb3f..74ad746a 100644 --- a/types/graphql.d.ts +++ b/types/graphql.d.ts @@ -96,19 +96,16 @@ export interface CoursePathInput { export interface SectionPathInput { id: string - name: string collections: CollectionPathInput[] } export interface CollectionPathInput { id: string - name: string modules: ModulePathInput[] } export interface ModulePathInput { id: string - enrollmentID?: Nullable } export interface CreateContentArgs { @@ -540,6 +537,11 @@ export interface IMutation { message: string, senderID: string ): boolean | Promise + createGroup( + name: string, + members: string[], + publicGroup?: Nullable + ): Group | Promise addPlan(input?: Nullable): PlanOfStudy | Promise updatePlan( id: string, @@ -724,12 +726,16 @@ export interface IQuery { ): Nullable | Promise> thread(input?: Nullable): Thread[] | Promise directMessages( - receiverID: string + receiverID: string, + senderID: string ): DirectMessageResponse[] | Promise groups(userID: string): Group[] | Promise groupMessages( groupID: string ): DirectMessageResponse[] | Promise + sentMessages( + senderID: string + ): DirectMessageResponse[] | Promise plan( studentID: string ): Nullable | Promise> @@ -856,6 +862,7 @@ export interface Group { members: User[] public: boolean messages: DirectMessageResponse[] + __typename?: string } export interface PlanOfStudy { @@ -995,6 +1002,16 @@ export interface ModuleFlow { currentSection?: Nullable
} +export interface SimpleModuleFlow { + previousModule?: Nullable + previousCollection?: Nullable + nextModule?: Nullable + nextCollection?: Nullable + currentModule?: Nullable + currentCollection?: Nullable + currentSection?: Nullable +} + export interface SimpleLearningPath { id: string createdAt: Date @@ -1062,8 +1079,8 @@ export interface Path { export interface CoursePath { id: string name: string - prefix?: string - number?: number + prefix?: Nullable + number?: Nullable required: boolean carnegieHours: number sections: SectionPath[] @@ -1230,6 +1247,7 @@ export interface User { watchedThreads?: Nullable watchedThreadIDs?: Nullable createdThreads?: Nullable + __typename?: Nullable } export interface Token { diff --git a/utils/gql_fetcher.ts b/utils/gql_fetcher.ts index cef41077..03aafc57 100644 --- a/utils/gql_fetcher.ts +++ b/utils/gql_fetcher.ts @@ -1,4 +1,5 @@ import { GraphQLClient } from 'graphql-request' +import { createClient } from 'graphql-ws' const gqlFetcher = (args) => { const client = new GraphQLClient(process.env.NEXT_PUBLIC_API_URL, { @@ -14,4 +15,11 @@ export const client = new GraphQLClient(process.env.NEXT_PUBLIC_API_URL, { headers: {}, }) +export const wsClient = + typeof window !== 'undefined' + ? createClient({ + url: 'ws://localhost:5000/graphql', + }) + : null + export default gqlFetcher