diff --git a/package.json b/package.json index e1eccd3..5c841f4 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "ahooks": "^3.7.8", "antd": "^5.12.8", + "crypto-js": "^4.2.0", "mobx": "^6.12.0", "mobx-react": "^9.1.0", "react": "^18.2.0", diff --git a/src/lib/realTimeAPI.js b/src/lib/realTimeAPI.js new file mode 100644 index 0000000..d29f14b --- /dev/null +++ b/src/lib/realTimeAPI.js @@ -0,0 +1,177 @@ +import { + webSocket, +} from "rxjs/webSocket"; +import { filter, buffer, flatMap, merge, map, tap } from "rxjs/operators"; +// import { v4 as uuid } from "uuid"; +import { SHA256 } from "crypto-js"; + +export class RealTimeAPI { + constructor(param) { + this.webSocket = webSocket(param); + } + + getObservable() { + return this.webSocket; + } + + disconnect() { + return this.webSocket.unsubscribe(); + } + + onMessage(messageHandler) { + this.subscribe(messageHandler, undefined, undefined); + } + + onError(errorHandler) { + this.subscribe(undefined, errorHandler, undefined); + } + + onCompletion(completionHandler) { + this.subscribe(undefined, undefined, completionHandler); + } + + subscribe(messageHandler, errorHandler, completionHandler) { + // this.getObservable().subscribe( + // messageHandler, + // errorHandler, + // completionHandler + // ); + this.getObservable().subscribe({ + next: messageHandler, + error: errorHandler, + complete: completionHandler + }); + } + + sendMessage(messageObject) { + this.webSocket.next(messageObject); + } + + getObservableFilteredByMessageType(messageType) { + return this.getObservable().pipe( + filter((message) => message.msg === messageType) + ); + } + + getObservableFilteredByID(id) { + return this.getObservable().pipe( + filter((message) => message.id === id) + ); + } + + connectToServer() { + this.sendMessage({ + msg: "connect", + version: "1", + support: ["1", "pre2", "pre1"] + }); + return this.getObservableFilteredByMessageType("connected"); + } + + keepAlive() { + return this.getObservableFilteredByMessageType("ping").pipe( + tap(() => this.sendMessage({ msg: "pong" })) + ); + } + + login(username, password) { + let id = 'uuid()'; + let usernameType = username.indexOf("@") !== -1 ? "email" : "username"; + this.sendMessage({ + msg: "method", + method: "login", + id: id, + params: [ + { + user: { [usernameType]: username }, + password: { + digest: SHA256(password).toString(), + algorithm: "sha-256" + } + } + ] + }); + return this.getLoginObservable(id); + } + + loginWithAuthToken(authToken) { + let id = 'uuid()'; + this.sendMessage({ + msg: "method", + method: "login", + id: id, + params: [{ resume: authToken }] + }); + return this.getLoginObservable(id); + } + + loginWithOAuth(credToken, credSecret) { + let id = 'uuid()'; + this.sendMessage({ + msg: "method", + method: "login", + id: id, + params: [ + { + oauth: { + credentialToken: credToken, + credentialSecret: credSecret + } + } + ] + }); + return this.getLoginObservable(id); + } + + getLoginObservable(id) { + let resultObservable = this.getObservableFilteredByID(id); + let resultId; + + let addedObservable = this.getObservable().pipe( + buffer( + resultObservable.pipe( + map(({ msg, error, result }) => { + if (msg === "result" && !error) return (resultId = result.id); + }) + ) + ), + flatMap(x => x), + filter(({ id: msgId }) => resultId !== undefined && msgId === resultId), + merge(resultObservable) + ); + + return addedObservable; + } + + callMethod(method, ...params) { + let id = 'uuid()'; + this.sendMessage({ + msg: "method", + method, + id, + params + }); + return this.getObservableFilteredByID(id); + } + + getSubscription(streamName, streamParam, addEvent) { + let id = 'uuid()'; + let subscription = this.webSocket.multiplex( + () => ({ + msg: "sub", + id: id, + name: streamName, + params: [streamParam, addEvent] + }), + () => ({ + msg: "unsub", + id: id + }), + (message) => + typeof message.collection === "string" && + message.collection === streamName && + message.fields.eventName === streamParam + ); + return subscription; + } +} diff --git a/src/lib/websocketLib.js b/src/lib/websocketLib.js index c9d1b3e..7f1db5b 100644 --- a/src/lib/websocketLib.js +++ b/src/lib/websocketLib.js @@ -17,7 +17,7 @@ class WebSocketLib { next: () => { console.log('Connection established'); // Send authentication message as soon as connection is established - this.socket$.next({ type: 'authenticate', token: this.authToken }); + this.socket$.next({ event: 'authenticate', token: this.authToken }); }, }, closeObserver: { diff --git a/src/main.jsx b/src/main.jsx index 49bc1b2..fcf04e0 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -16,7 +16,8 @@ import DingdingQRCode from '@/views/DingdingQRCode' import DingdingCallbak from '@/views/DingdingCallbak' import ErrorPage from '@/views/ErrorPage' -import Conversations from '@/views/Conversations/Components/ChatWindow'; +import Conversations from '@/views/Conversations/ChatWindow'; +// import Conversations from '@/views/Conversations/ChatApp'; configure({ useProxies: 'ifavailable', diff --git a/src/stores/Conversations.js b/src/stores/Conversations.js new file mode 100644 index 0000000..ebb5bcb --- /dev/null +++ b/src/stores/Conversations.js @@ -0,0 +1,64 @@ +import { makeAutoObservable, runInAction, toJS } from 'mobx'; +import { RealTimeAPI } from '@/lib/realTimeAPI'; + +const URL = 'ws://202.103.68.144:8888/whatever/'; +let realtimeAPI = new RealTimeAPI({ url: URL, protocol: 'aaa' }); + +class Conversations { + constructor() { + makeAutoObservable(this, { rootStore: false }); + // this.sendMessage = this.sendMessage.bind(this); + + realtimeAPI.onError(this.addError.bind(this, 'Error')); + realtimeAPI.onMessage(this.handleMessage.bind(this)); + realtimeAPI.onCompletion(this.addError.bind(this, 'Not Connected to Server')); + realtimeAPI.keepAlive(); // Ping Server + // realtimeAPI.onMessage().subscribe(message => { + // this.addMessage(message); + // }); + } + + addError = (reason) => { + // this.errors.push({ reason }); + this.errors = [...this.errors, { reason }]; + } + + addMessage = (message) => { + // this.messages.push(msg.message); // Mobx will not work + this.messages = [...this.messages, message]; + } + + handleMessage = (data) => { + const msg = data.result; + if (!msg) { + return false; + } + if (typeof msg.type === 'string' && msg.type === 'error') { + this.addError('Error Connecting to Server'); + } + // todo: handle message + runInAction(() => { + this.addMessage({ ...msg.message, sender: 'other', id: Date.now().toString(16), }); + console.log(toJS(this.messages), 'messages'); + }); + } + + sendMessage = (msg) => { + const msgObj = { + type: 'message', + message: { + sender: 'me', + content: msg, + readState: false, + id: Date.now().toString(16), + }, + }; + realtimeAPI.sendMessage(msgObj); + this.addMessage(msgObj.message); + } + + errors = []; + messages = []; +} + +export default Conversations; diff --git a/src/stores/Root.js b/src/stores/Root.js index 0046fa5..3fe81cf 100644 --- a/src/stores/Root.js +++ b/src/stores/Root.js @@ -1,15 +1,17 @@ import { makeAutoObservable } from 'mobx' import Auth from './Auth' import Order from './Order' +import Conversations from './Conversations' class Root { constructor() { this.orderStore = new Order(this) this.authStore = new Auth(this) + this.conversationsStore = new Conversations(this) makeAutoObservable(this) } - clearSession() { + clearSession() { if (window.sessionStorage) { const sessionStorage = window.sessionStorage sessionStorage.clear() diff --git a/src/utils/utils.js b/src/utils/utils.js new file mode 100644 index 0000000..6f41bf8 --- /dev/null +++ b/src/utils/utils.js @@ -0,0 +1,300 @@ +/** + * ! 不支持计算 Set 或 Map + * @param {*} val + * @example + * true if: 0, [], {}, null, '', undefined + * false if: 'false', 'undefined' + */ +export function isEmpty(val) { + // return val === undefined || val === null || val === ""; + return [Object, Array].includes((val || {}).constructor) && !Object.entries(val || {}).length; +} +/** + * 数组排序 + */ +export const sortBy = (key) => { + return (a, b) => (a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0); +}; + +/** + * Object排序keys + */ +export const sortKeys = (obj) => + Object.keys(obj) + .sort() + .reduce((a, k2) => ({ ...a, [k2]: obj[k2] }), {}); + +/** + * 数组排序, 给定排序数组 + * @param {array} items 需要排序的数组 + * @param {array} keyName 排序的key + * @param {array} keyOrder 给定排序 + * @returns + */ +export const sortArrayByOrder = (items, keyName, keyOrder) => { + return items.sort((a, b) => { + return keyOrder.indexOf(a[keyName]) - keyOrder.indexOf(b[keyName]); + }); +}; +/** + * 合并Object, 递归地 + */ +export function merge(...objects) { + const isDeep = objects.some((obj) => obj !== null && typeof obj === 'object'); + + const result = objects[0] || (isDeep ? {} : objects[0]); + + for (let i = 1; i < objects.length; i++) { + const obj = objects[i]; + + if (!obj) continue; + + Object.keys(obj).forEach((key) => { + const val = obj[key]; + + if (isDeep) { + if (Array.isArray(val)) { + result[key] = [].concat(Array.isArray(result[key]) ? result[key] : [result[key]], val); + } else if (typeof val === 'object') { + result[key] = merge(result[key], val); + } else { + result[key] = val; + } + } else { + result[key] = typeof val === 'boolean' ? val : result[key]; + } + }); + } + + return result; +} + +/** + * 数组分组 + * - 相当于 lodash 的 _.groupBy + * @see https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity + */ +export function groupBy(array, callback) { + return array.reduce((groups, item) => { + const key = typeof callback === 'function' ? callback(item) : item[callback]; + + if (!groups[key]) { + groups[key] = []; + } + + groups[key].push(item); + return groups; + }, {}); +} + +/** + * 创建一个从 object 中选中的属性的对象。 + * @param {*} object + * @param {array} keys + */ +export function pick(object, keys) { + return keys.reduce((obj, key) => { + if (object && Object.prototype.hasOwnProperty.call(object, key)) { + obj[key] = object[key]; + } + return obj; + }, {}); +} + +/** + * 返回对象的副本,经过筛选以省略指定的键。 + * @param {*} object + * @param {string[]} keysToOmit + * @returns + */ +export function omit(object, keysToOmit) { + return Object.fromEntries(Object.entries(object).filter(([key]) => !keysToOmit.includes(key))); +} + +/** + * 深拷贝 + */ +export function cloneDeep(value) { + if (typeof value !== 'object' || value === null) { + return value; + } + + const result = Array.isArray(value) ? [] : {}; + + for (const key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) { + result[key] = cloneDeep(value[key]); + } + } + + return result; +} + +/** + * 向零四舍五入, 固定精度设置 + */ +function curriedFix(precision = 0) { + return function (number) { + // Shift number by precision places + const shift = Math.pow(10, precision); + const shiftedNumber = number * shift; + + // Round to nearest integer + const roundedNumber = Math.round(shiftedNumber); + + // Shift back decimal place + return roundedNumber / shift; + }; +} +/** + * 向零四舍五入, 保留2位小数 + */ +export const fixTo2Decimals = curriedFix(2); +/** + * 向零四舍五入, 保留4位小数 + */ +export const fixTo4Decimals = curriedFix(4); + +export const fixTo1Decimals = curriedFix(1); +export const fixToInt = curriedFix(0); + +/** + * 映射 + * @example + * const keyMap = { + a: [{key: 'a1'}, {key: 'a2', transform: v => v * 2}], + b: {key: 'b1'} + }; + const result = objectMapper({a: 1, b: 3}, keyMap); + // result = {a1: 1, a2: 2, b1: 3} + * + */ +export function objectMapper(input, keyMap) { + // Loop through array mapping + if (Array.isArray(input)) { + return input.map((obj) => objectMapper(obj, keyMap)); + } + + if (typeof input === 'object') { + const mappedObj = {}; + + Object.keys(input).forEach((key) => { + // Keep original keys not in keyMap + if (!keyMap[key]) { + mappedObj[key] = input[key]; + } + // Handle array of maps + if (Array.isArray(keyMap[key])) { + keyMap[key].forEach((map) => { + let value = input[key]; + if (map.transform) value = map.transform(value); + mappedObj[map.key] = value; + }); + + // Handle single map + } else { + const map = keyMap[key]; + if (map) { + let value = input[key]; + if (map.transform) value = map.transform(value); + mappedObj[map.key || key] = value; + } + } + }); + + return mappedObj; + } + + return input; +} + +/** + * 创建一个对应于对象路径的值数组 + */ +export function at(obj, path) { + let result; + if (Array.isArray(obj)) { + // array case + const indexes = path.split('.').map((i) => parseInt(i)); + result = []; + for (let i = 0; i < indexes.length; i++) { + result.push(obj[indexes[i]]); + } + } else { + // object case + const indexes = path.split('.').map((i) => i); + result = [obj]; + for (let i = 0; i < indexes.length; i++) { + result = [result[0][indexes[i]]]; + } + } + return result; +} +/** + * 删除 null/undefined + */ +export function flush(collection) { + let result, len, i; + if (!collection) { + return undefined; + } + if (Array.isArray(collection)) { + result = []; + len = collection.length; + for (i = 0; i < len; i++) { + const elem = collection[i]; + if (elem != null) { + result.push(elem); + } + } + return result; + } + if (typeof collection === 'object') { + result = {}; + const keys = Object.keys(collection); + len = keys.length; + for (i = 0; i < len; i++) { + const key = keys[i]; + const value = collection[key]; + if (value != null) { + result[key] = value; + } + } + return result; + } + return undefined; +} + +/** + * 千分位 格式化数字 + */ +export const numberFormatter = (number) => { + return new Intl.NumberFormat().format(number); +}; + +/** + * @example + * const obj = { a: { b: 'c' } }; + * const keyArr = ['a', 'b']; + * getNestedValue(obj, keyArr); // Returns: 'c' + */ +export const getNestedValue = (obj, keyArr) => { + return keyArr.reduce((acc, curr) => { + return acc && Object.prototype.hasOwnProperty.call(acc, curr) ? acc[curr] : undefined; + // return acc && acc[curr]; + }, obj); +}; + +/** + * 计算笛卡尔积 + */ +export const cartesianProductArray = (arr, sep = '_', index = 0, prefix = '') => { + let result = []; + if (index === arr.length) { + return [prefix]; + } + arr[index].forEach((item) => { + result = result.concat(cartesianProductArray(arr, sep, index + 1, prefix ? `${prefix}${sep}${item}` : `${item}`)); + }); + return result; +}; diff --git a/src/views/Conversations/ChatApp.jsx b/src/views/Conversations/ChatApp.jsx new file mode 100644 index 0000000..73d043f --- /dev/null +++ b/src/views/Conversations/ChatApp.jsx @@ -0,0 +1,41 @@ +// App.js + +import React from 'react'; +import { Layout, Menu } from 'antd'; +// import './App.css'; + +const { Header, Sider, Content } = Layout; + +function App() { + const channels = ['General', 'Random']; + const messages = [ + { user: 'User1', text: 'Hello!' }, + { user: 'User2', text: 'Hi!' }, + ]; + + return ( + + + + {channels.map(channel => ( + {channel} + ))} + + + + + + + {messages.map((message, index) => ( + + {message.user}: {message.text} + + ))} + + + + + ); +} + +export default App; diff --git a/src/views/Conversations/ChatApp2.jsx b/src/views/Conversations/ChatApp2.jsx new file mode 100644 index 0000000..e3e8114 --- /dev/null +++ b/src/views/Conversations/ChatApp2.jsx @@ -0,0 +1,44 @@ +import { Layout, Menu, List, Timeline, Input } from 'antd'; + +const { Sider, Content } = Layout; +function ChatApp() { + return ( + + + + Unread + Mentions + Favorites + Channel List + Direct Messages + + + + {/* Show channels and DMs */} + + + + + + + + {/* Show user profile cards */} + + + + + + {/* Show messages */} + + + + + + + + ); +} + +export default ChatApp; diff --git a/src/views/Conversations/ChatWindow.jsx b/src/views/Conversations/ChatWindow.jsx new file mode 100644 index 0000000..1a1fe6f --- /dev/null +++ b/src/views/Conversations/ChatWindow.jsx @@ -0,0 +1,59 @@ +import { useEffect, useContext } from 'react'; +import { observer } from 'mobx-react'; +import { Layout } from 'antd'; +import Messages from './Components/Messages'; +import InputBox from './Components/InputBox'; +import Conversations from './Components/Conversations'; +import CustomerProfile from './Components/CustomerProfile'; + +import WebSocketLib from '@/lib/websocketLib'; +import { useStore } from '@/stores/StoreContext.js'; + +const customer = { url: 'ws://202.103.68.144:8888/whatever/', authToken: 'customer1Token' }; +// Create a WebSocketLib instance for each customer +// const wsConnect = new WebSocketLib(customer.url, customer.authToken, 'WhatApp'); +// const wsConnect = new WebSocketLib(customer.url, customer.authToken, 'aaa'); + +const { Sider, Content } = Layout; + +const CList = [ + { name: 'Customer_1', label: 'Customer_1', key: 'Customer_1', value: 'Customer_1' }, + { name: 'Customer_2', label: 'Customer_2', key: 'Customer_2', value: 'Customer_2' }, + { name: 'Customer_3', label: 'Customer_3', key: 'Customer_3', value: 'Customer_3' }, + { name: 'Customer_4', label: 'Customer_4', key: 'Customer_4', value: 'Customer_4' }, +]; +const messages = [ + { sender: 'Customer_1', text: 'Hello, how can I help you today?' }, + { sender: 'Customer_2', text: 'Hello, how can I help you today?' }, + { sender: 'Customer_3', text: 'Hello, how can I help you today?' }, + { sender: 'Customer_4', text: 'Hello, how can I help you today?' }, +]; +const ChatWindow = observer(() => { + const { conversationsStore } = useStore(); + const { sendMessage, messages } = conversationsStore; + useEffect(() => { + + return () => {}; + }, []); + + return ( + + + + + + + + + sendMessage(v)} /> + + + + + + + + ); +}); + +export default ChatWindow; diff --git a/src/views/Conversations/Components/ChatWindow.jsx b/src/views/Conversations/Components/ChatWindow.jsx deleted file mode 100644 index 201f6ad..0000000 --- a/src/views/Conversations/Components/ChatWindow.jsx +++ /dev/null @@ -1,38 +0,0 @@ - -import { Layout } from 'antd'; -import Messages from './Messages'; -import InputBox from './InputBox'; -import Conversations from './Conversations'; -import CustomerProfile from './CustomerProfile'; - -const {Sider, Content } = Layout; - -const CList = [ - { name: 'Customer_1', label: 'Customer_1', key: 'Customer_1', value: 'Customer_1' }, - { name: 'Customer_2', label: 'Customer_2', key: 'Customer_2', value: 'Customer_2' }, - { name: 'Customer_3', label: 'Customer_3', key: 'Customer_3', value: 'Customer_3' }, - { name: 'Customer_4', label: 'Customer_4', key: 'Customer_4', value: 'Customer_4' }, -]; -function ChatWindow() { - - return ( - - - - - - - - - - - - - - - - - ); -} - -export default ChatWindow; diff --git a/src/views/Conversations/Components/Conversations.jsx b/src/views/Conversations/Components/Conversations.jsx index 020bc15..a617c0c 100644 --- a/src/views/Conversations/Components/Conversations.jsx +++ b/src/views/Conversations/Components/Conversations.jsx @@ -1,31 +1,55 @@ +import { observer } from 'mobx-react'; import { List, Avatar } from 'antd'; -import PropTypes from 'prop-types'; +import crypto from 'crypto-js'; -const ColorList = ['#f56a00', '#7265e6', '#ffbf00', '#00a2ae']; -function Conversations({ conversations }) { +const ColorList = []; // ['#f56a00', '#7265e6', '#ffbf00', '#00a2ae']; +// const colors = []; +for (let i = 0; i < 10; i++) { + const red = Math.floor(Math.random() * 256).toString(16); + const green = Math.floor(Math.random() * 256).toString(16); + const blue = Math.floor(Math.random() * 256).toString(16); + // ColorList.push(`rgb(${red}, ${green}, ${blue})`); + ColorList.push(`#${red}${green}${blue}`); +} +console.log(ColorList); +const stringToColour = (str) => { + // Hash the username using SHA256 + const hash = crypto.SHA256(str); + + // Convert the hash to a hexadecimal string + const hexString = hash.toString(crypto.enc.Hex); + + // Use the first 6 characters of the hex string as a color + const color = '#' + hexString.substring(0, 6); + + return color; +}; + +/** + * [] + */ +const Conversations = observer(({ conversations }) => { return ( ( - + mark]}> {item.name} } title={item.name} + description='{最近的消息}' /> )} /> ); -} -Conversations.propTypes = { - conversations: PropTypes.string.isRequired, -}; +}); export default Conversations; diff --git a/src/views/Conversations/Components/CustomerProfile.jsx b/src/views/Conversations/Components/CustomerProfile.jsx index 961b524..9d49e2d 100644 --- a/src/views/Conversations/Components/CustomerProfile.jsx +++ b/src/views/Conversations/Components/CustomerProfile.jsx @@ -1,8 +1,7 @@ +import { observer } from 'mobx-react'; import { Card } from 'antd'; -function CustomerProfile({ customer }) { +const CustomerProfile = observer(({ customer }) => { return {/* other profile details */}; -} -CustomerProfile.propTypes = { -}; +}); export default CustomerProfile; diff --git a/src/views/Conversations/Components/InputBox.jsx b/src/views/Conversations/Components/InputBox.jsx index a665e8b..9149a14 100644 --- a/src/views/Conversations/Components/InputBox.jsx +++ b/src/views/Conversations/Components/InputBox.jsx @@ -1,22 +1,34 @@ - +import { useEffect, useState } from 'react'; +import { observer } from 'mobx-react'; import { Input, Button } from 'antd'; -function InputBox({ onSend }) { - - function handleSend() { - // Logic to get message and call onSend +const InputBox = observer(({ onSend }) => { + const [message, setMessage] = useState(''); + const handleSend = (v) => { + // console.log(v); + if (typeof onSend === 'function' && v.trim() !== '') { + onSend(v); + setMessage(''); + } } + const sendMessage = async () => { + if (message.trim() !== '') { + // const api = new RealTimeAPI('wss://your_rocket_chat_server_url/websocket'); + // await api.login('your_username', 'your_password'); // replace with your actual username and password + // await api.sendMessage({ roomId: 'ROOM_ID', msg: message }); // replace 'ROOM_ID' with your actual room id + setMessage(''); + } + }; return ( - + {/* + + Send + */} + ); -} +}); export default InputBox; diff --git a/src/views/Conversations/Components/Messages.jsx b/src/views/Conversations/Components/Messages.jsx index 49d071c..a04b6c9 100644 --- a/src/views/Conversations/Components/Messages.jsx +++ b/src/views/Conversations/Components/Messages.jsx @@ -1,21 +1,36 @@ +import { useEffect } from 'react'; +import { List, Avatar, Timeline } from 'antd'; +import { observer } from 'mobx-react'; +import { useStore } from '@/stores/StoreContext.js'; -import { List, Avatar } from 'antd'; +const Messages = observer(() => { + const { conversationsStore } = useStore(); + const { messages: newMessages } = conversationsStore; + + useEffect(() => { + // Load your data here + // For example: + // conversationsStore.loadMessages(); + }, [newMessages]); -function Messages({ messages }) { return ( - ( - - {message.sender[0]}} - title={message.sender} - /> - {message.text} - - )} - /> + <> + ( + + {message.sender[0]}} + title={message.sender !== 'me' ? message.sender : ''} + description={message.sender !== 'me' ? `(${message.id}) ${message.content}` : ''} + /> + {message.sender === 'me' && {message.content} ({message.id})} + + )} + /> + > ); -} +}); export default Messages;
+ {message.user}: {message.text} +