test: 单个会话订阅. 发送和接收

dev/chat
Lei OT 1 year ago
parent 7bdd2a6eac
commit e13bc7b096

@ -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",

@ -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;
}
}

@ -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: {

@ -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',

@ -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;

@ -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()

@ -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;
};

@ -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 (
<Layout style={{ minHeight: '100vh' }}>
<Sider width={200}>
<Menu mode="inline" style={{ height: '100%', borderRight: 0 }}>
{channels.map(channel => (
<Menu.Item key={channel}>{channel}</Menu.Item>
))}
</Menu>
</Sider>
<Layout>
<Header style={{ background: '#fff', padding: 0 }} />
<Content style={{ margin: '24px 16px 0', overflow: 'initial' }}>
<div style={{ padding: 24, background: '#fff', textAlign: 'center' }}>
{messages.map((message, index) => (
<p key={index}>
<strong>{message.user}</strong>: {message.text}
</p>
))}
</div>
</Content>
</Layout>
</Layout>
);
}
export default App;

@ -0,0 +1,44 @@
import { Layout, Menu, List, Timeline, Input } from 'antd';
const { Sider, Content } = Layout;
function ChatApp() {
return (
<Layout>
<Sider theme="light" width={300}>
<Menu>
<Menu.Item>Unread</Menu.Item>
<Menu.Item>Mentions</Menu.Item>
<Menu.Item>Favorites</Menu.Item>
<Menu.Item>Channel List</Menu.Item>
<Menu.Item>Direct Messages</Menu.Item>
</Menu>
<List>
{/* Show channels and DMs */}
</List>
</Sider>
<Content>
<Layout>
<Sider theme="light">
<List>
{/* Show user profile cards */}
</List>
</Sider>
<Content>
<Timeline>
{/* Show messages */}
</Timeline>
<Input.Search
enterButton="Send"
/>
</Content>
</Layout>
</Content>
</Layout>
);
}
export default ChatApp;

@ -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 (
<Layout style={{maxHeight: 'calc(100% - 150px)', height: 'calc(100% - 150px)'}}>
<Sider width={300} theme={'light'} style={{ height: '70vh' }}>
<Conversations conversations={CList} />
</Sider>
<Content className='h70' style={{maxHeight: '70vh', height: '70vh'}}>
<Layout style={{ height: '100%' }}>
<Messages />
<InputBox onSend={(v) => sendMessage(v)} />
</Layout>
</Content>
<Sider width={300} theme={'light'}>
<CustomerProfile customer={{}} />
</Sider>
</Layout>
);
});
export default ChatWindow;

@ -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 (
<Layout>
<Sider width={300} theme={'light'}>
<Conversations conversations={CList} />
</Sider>
<Content>
<Layout>
<Messages />
<InputBox />
</Layout>
</Content>
<Sider width={300} theme={'light'}>
<CustomerProfile customer={{}}/>
</Sider>
</Layout>
);
}
export default ChatWindow;

@ -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 (
<List
dataSource={conversations}
renderItem={(item, ii) => (
<List.Item>
<List.Item actions={[<a key='list-loadmore-edit'>mark</a>]}>
<List.Item.Meta
avatar={
<Avatar
style={{
backgroundColor: ColorList[ii],
backgroundColor: stringToColour(item.name), // ColorList[ii],
verticalAlign: 'middle',
}}>
{item.name}
</Avatar>
}
title={item.name}
description='{最近的消息}'
/>
</List.Item>
)}
/>
);
}
Conversations.propTypes = {
conversations: PropTypes.string.isRequired,
};
});
export default Conversations;

@ -1,8 +1,7 @@
import { observer } from 'mobx-react';
import { Card } from 'antd';
function CustomerProfile({ customer }) {
const CustomerProfile = observer(({ customer }) => {
return <Card title={customer.name}>{/* other profile details */}</Card>;
}
CustomerProfile.propTypes = {
};
});
export default CustomerProfile;

@ -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 (
<div>
<Input.Search
placeholder="Type message here"
enterButton="Send"
size="large"
onSearch={handleSend}
/>
{/* <Input.TextArea rows={4} />
<Button type="primary" className="chat-button">
Send
</Button> */}
<Input.Search placeholder='Type message here' enterButton='Send' size='large' onSearch={handleSend} defaultValue={message} />
</div>
);
}
});
export default InputBox;

@ -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 (
<List
dataSource={messages}
renderItem={message => (
<List.Item>
<List.Item.Meta
avatar={<Avatar>{message.sender[0]}</Avatar>}
title={message.sender}
/>
<div>{message.text}</div>
</List.Item>
)}
/>
<>
<List
dataSource={conversationsStore.messages}
style={{ flex: '1 1' }}
renderItem={(message) => (
<List.Item>
<List.Item.Meta
// avatar={<Avatar>{message.sender[0]}</Avatar>}
title={message.sender !== 'me' ? message.sender : ''}
description={message.sender !== 'me' ? `(${message.id}) ${message.content}` : ''}
/>
{message.sender === 'me' && <div>{message.content} ({message.id})</div>}
</List.Item>
)}
/>
</>
);
}
});
export default Messages;

Loading…
Cancel
Save