├── .env.dev ├── .env.prod ├── .github └── workflows │ ├── env-test.yml │ └── main.yml ├── .gitignore ├── .prettierrc ├── .release-it.json ├── .vscode └── launch.json ├── README.md ├── chat.png ├── config-overrides.js ├── index.ts ├── package-lock.json ├── package.json ├── phone.png ├── pnpm-lock.yaml ├── public ├── app_bg.jpg ├── build-date.log ├── chat_bg.jpg ├── favicon.ico ├── index.html ├── logo.png ├── manifest.json ├── robots.txt ├── system.png └── world_channel.png ├── src ├── App.css ├── App.tsx ├── api │ ├── api.ts │ ├── axios.ts │ ├── model.ts │ ├── response.ts │ └── rxios.ts ├── bg_login.jpg ├── component │ ├── AppMainPanel.tsx │ ├── EventBus.ts │ ├── Profile.tsx │ ├── auth │ │ ├── Auth.tsx │ │ ├── Guest.tsx │ │ ├── Register.tsx │ │ └── SettingsDialog.tsx │ ├── chat │ │ ├── ChatRoom.tsx │ │ ├── GroupMemberList.tsx │ │ ├── Message.tsx │ │ ├── MessageInput.tsx │ │ ├── MessageList.tsx │ │ ├── MessagePopup.tsx │ │ ├── components │ │ │ └── AddBlackList.tsx │ │ └── context │ │ │ └── ChatContext.ts │ ├── friends │ │ ├── AddContactDialog.tsx │ │ ├── ContactsList.tsx │ │ └── CreateGroupDialog.tsx │ ├── hooks │ │ └── useSession.ts │ ├── session │ │ ├── SessionListItem.tsx │ │ ├── SessionListView.tsx │ │ └── UserInfoHeader.tsx │ ├── square │ │ └── Square.tsx │ ├── webrtc │ │ ├── VideoChatDialog.tsx │ │ └── WebRTC.tsx │ └── widget │ │ ├── ImageViewer.tsx │ │ ├── Loading.tsx │ │ ├── Markdown.tsx │ │ ├── MarkdownRender.tsx │ │ ├── OnlineStatus.tsx │ │ ├── PopupMenu.tsx │ │ └── SnackBar.tsx ├── im │ ├── account.ts │ ├── cache.ts │ ├── channel.ts │ ├── chat_message.ts │ ├── contacts.ts │ ├── contacts_list.ts │ ├── db.ts │ ├── def.ts │ ├── im_ws_client.ts │ ├── message.ts │ ├── relative_list.ts │ ├── session.ts │ ├── session_list.ts │ └── ws_client.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts ├── rx │ └── next.ts ├── t.json ├── utils │ ├── Cookies.ts │ ├── Logger.ts │ ├── TimeUtils.ts │ └── Utils.ts └── webrtc │ ├── dialing.ts │ ├── log.ts │ ├── peer.ts │ ├── signaling.ts │ ├── test_rtc.ts │ └── webrtc.ts ├── tailwind.config.js └── tsconfig.json /.env.dev: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL=http://localhost:8081/api/ 2 | REACT_APP_WS_URL=ws://localhost:8080/ws 3 | REACT_APP_ENV=dev -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL=https://intercom.ink/api/ 2 | REACT_APP_WS_URL=wss://intercom.ink/ws 3 | REACT_APP_ENV=production -------------------------------------------------------------------------------- /.github/workflows/env-test.yml: -------------------------------------------------------------------------------- 1 | name: Publish & Deploy Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-and-deploy: 7 | name: GlideIM 8 | runs-on: ubuntu-latest 9 | steps: 10 | 11 | - name: Checkout 12 | uses: actions/checkout@master 13 | 14 | - name: Get Tag 15 | id: get_tag 16 | run: | 17 | echo ::set-output name=TAG::${GITHUB_REF/refs\/tags\//} 18 | 19 | - name: NodeJS 14.x 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 14.15.4 23 | 24 | - name: npm install build 25 | run: | 26 | npm install 27 | npm run build:prod 28 | cd build 29 | date >> build-date.log 30 | 31 | - name: Deploy 32 | uses: easingthemes/ssh-deploy@main 33 | env: 34 | SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }} 35 | ARGS: "-rltgoDzvO --delete" 36 | SOURCE: "build/" 37 | REMOTE_HOST: ${{ secrets.HOST }} 38 | REMOTE_USER: ${{ secrets.USER }} 39 | TARGET: ${{ secrets.UPLOAD_DIR_PRE }} 40 | EXCLUDE: "/dist/, /node_modules/" -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish & Deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | build-and-deploy: 10 | name: GlideIM 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: Checkout 15 | uses: actions/checkout@master 16 | 17 | - name: Get Tag 18 | id: get_tag 19 | run: | 20 | echo ::set-output name=TAG::${GITHUB_REF/refs\/tags\//} 21 | 22 | - name: NodeJS 14.x 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: 14.15.4 26 | 27 | - name: npm install build 28 | run: | 29 | npm install 30 | npm run build:prod 31 | 32 | - name: build TAR PACKAGE 33 | run: | 34 | cd build 35 | date >> build-date.log 36 | tar -czvf glide-im-web.tar.gz * 37 | 38 | - name: Create Release 39 | id: create_release 40 | uses: actions/create-release@master 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GitAction }} 43 | with: 44 | tag_name: ${{ github.ref }} 45 | release_name: release_${{ steps.get_tag.outputs.TAG }} 46 | draft: false 47 | prerelease: false 48 | 49 | - name: Upload Release 50 | id: upload-release-asset 51 | uses: actions/upload-release-asset@master 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GitAction }} 54 | with: 55 | tag_name: ${{ github.ref }} 56 | upload_url: ${{ steps.create_release.outputs.upload_url }} 57 | asset_path: glide-im-web.tar.gz 58 | asset_name: glide-im-web-${{ steps.get_tag.outputs.TAG }}.tar.gz 59 | asset_content_type: application/gzip 60 | 61 | - name: Deploy 62 | uses: easingthemes/ssh-deploy@main 63 | env: 64 | SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }} 65 | ARGS: "-rltgoDzvO --delete" 66 | SOURCE: "build/" 67 | REMOTE_HOST: ${{ secrets.HOST }} 68 | REMOTE_USER: ${{ secrets.USER }} 69 | TARGET: ${{ secrets.UPLOAD_DIR_RELEASE }} 70 | EXCLUDE: "/dist/, /node_modules/" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | /.idea 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "jsxSingleQuote": true, 8 | "trailingComma": "es5", 9 | "bracketSpacing": true, 10 | "jsxBracketSameLine": true, 11 | "arrowParens": "always", 12 | "requirePragma": false, 13 | "insertPragma": false 14 | } -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | }, 5 | "git": { 6 | "commitMessage": "chore: release ${version} [skip ci]" 7 | }, 8 | "npm": { 9 | "publish": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "command": "npm start", 9 | "name": "Run npm start", 10 | "request": "launch", 11 | "type": "node-terminal" 12 | }, 13 | { 14 | "name": "Launch via NPM", 15 | "request": "launch", 16 | "runtimeArgs": [ 17 | "run-script", 18 | "debug" 19 | ], 20 | "runtimeExecutable": "npm", 21 | "skipFiles": [ 22 | "/**" 23 | ], 24 | "type": "node" 25 | }, 26 | { 27 | "type": "pwa-chrome", 28 | "request": "launch", 29 | "name": "Launch Chrome against localhost", 30 | "url": "http://localhost:8080", 31 | "webRoot": "${workspaceFolder}" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GlideIM - Web 2 | 3 | 示例项目: [GlideIM](http://im.dengzii.com/) 4 | 5 | chat 6 | 7 | home -------------------------------------------------------------------------------- /chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glide-im/glide_ts_sdk/7901c46800371b53eedcfa9e003ac3b0f72dd026/chat.png -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | module.exports = function override(config, env){ 2 | return config; 3 | } -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/im/message' 2 | export * from './src/im/account' 3 | export * from './src/im/cache' 4 | export * from './src/im/chat_message' 5 | export * from './src/im/def' 6 | export * from './src/im/im_ws_client' 7 | export * from './src/im/message' 8 | export * from './src/im/session' 9 | export * from './src/im/session_list' 10 | export * from './src/im/ws_client' 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glide_ts_sdk", 3 | "version": "1.0.2", 4 | "description": "Glide IM TypeScript SDK", 5 | "author": { 6 | "name": "dengzi" 7 | }, 8 | "license": "Apache-2.0", 9 | "dependencies": { 10 | "@emotion/react": "^11.8.2", 11 | "@emotion/styled": "^11.8.1", 12 | "@mui/icons-material": "^5.5.1", 13 | "@mui/material": "^5.5.2", 14 | "@mui/system": "^5.13.2", 15 | "@testing-library/jest-dom": "^5.14.1", 16 | "@testing-library/react": "^11.2.7", 17 | "@testing-library/user-event": "^12.8.3", 18 | "@types/jest": "^26.0.24", 19 | "@types/react": "^17.0.14", 20 | "@types/react-dom": "^17.0.9", 21 | "axios": "^1.3.4", 22 | "idb": "^7.1.1", 23 | "notistack": "^3.0.1", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2", 26 | "react-draggable": "^4.4.5", 27 | "react-markdown": "^8.0.6", 28 | "react-router-dom": "^5.2.0", 29 | "react-scripts": "^5.0.1", 30 | "react-syntax-highlighter": "^15.5.0", 31 | "rxjs": "^7.5.5" 32 | }, 33 | "scripts": { 34 | "start": "react-scripts start", 35 | "start:prod": "dotenv -e .env.prod react-app-rewired start", 36 | "build:prod": "dotenv -e .env.prod react-app-rewired build", 37 | "build": "react-scripts build", 38 | "test": "react-scripts test", 39 | "eject": "react-scripts eject", 40 | "release": "release-it" 41 | }, 42 | "eslintConfig": { 43 | "extends": [ 44 | "react-app" 45 | ] 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "devDependencies": { 60 | "@types/node": "^12.20.47", 61 | "@types/react-router-dom": "^5.1.8", 62 | "dotenv-cli": "^5.1.0", 63 | "prettier": "^2.8.8", 64 | "react-app-rewired": "^2.2.1", 65 | "release-it": "^15.11.0", 66 | "tailwindcss": "^3.3.1", 67 | "typescript": "^4.6.3" 68 | }, 69 | "publishConfig": { 70 | "registry": "https://registry.npmjs.org/" 71 | }, 72 | "main": "index.ts", 73 | "keywords": [ 74 | "glide", 75 | "im", 76 | "sdk", 77 | "typescript" 78 | ], 79 | "bugs": { 80 | "url": "https://github.com/glide-im/glide_ts_sdk/issues" 81 | }, 82 | "homepage": "https://github.com/glide-im", 83 | "repository": { 84 | "type": "git", 85 | "url": "git+https://github.com/glide-im/glide_ts_sdk" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glide-im/glide_ts_sdk/7901c46800371b53eedcfa9e003ac3b0f72dd026/phone.png -------------------------------------------------------------------------------- /public/app_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glide-im/glide_ts_sdk/7901c46800371b53eedcfa9e003ac3b0f72dd026/public/app_bg.jpg -------------------------------------------------------------------------------- /public/build-date.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glide-im/glide_ts_sdk/7901c46800371b53eedcfa9e003ac3b0f72dd026/public/build-date.log -------------------------------------------------------------------------------- /public/chat_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glide-im/glide_ts_sdk/7901c46800371b53eedcfa9e003ac3b0f72dd026/public/chat_bg.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glide-im/glide_ts_sdk/7901c46800371b53eedcfa9e003ac3b0f72dd026/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | GlideIM 28 | 33 | 34 | 35 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glide-im/glide_ts_sdk/7901c46800371b53eedcfa9e003ac3b0f72dd026/public/logo.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "GlideIM", 3 | "name": "GlideIM", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo.png", 12 | "type": "image/png", 13 | "sizes": "128x128" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glide-im/glide_ts_sdk/7901c46800371b53eedcfa9e003ac3b0f72dd026/public/system.png -------------------------------------------------------------------------------- /public/world_channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glide-im/glide_ts_sdk/7901c46800371b53eedcfa9e003ac3b0f72dd026/public/world_channel.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | } 3 | 4 | .BeautyScrollBar::-webkit-scrollbar { 5 | width: 6px; 6 | height: 1px; 7 | } 8 | 9 | .BeautyScrollBar::-webkit-scrollbar-thumb { 10 | border-radius: 10px; 11 | box-shadow: inset 0 0 5px rgba(164, 164, 164, 0.54); 12 | background: rgba(0, 0, 0, 0.2); 13 | } 14 | 15 | .BeautyScrollBar::-webkit-scrollbar-track { 16 | box-shadow: inset 0 0 5px rgba(227, 227, 227, 0.1); 17 | border-radius: 10px; 18 | background: rgba(255, 255, 255, 0.2); 19 | } 20 | 21 | .App-logo { 22 | height: 40vmin; 23 | pointer-events: none; 24 | } 25 | 26 | @media (prefers-reduced-motion: no-preference) { 27 | .App-logo { 28 | animation: App-logo-spin infinite 20s linear; 29 | } 30 | } 31 | 32 | .app-code { 33 | overflow-x: auto; 34 | font-family: Consolas, Monaco, 'Source Code Pro', 'Courier New', monospace, 35 | serif; 36 | background-color: gainsboro; 37 | padding: 8px; 38 | border-radius: 4px; 39 | display: block; 40 | } 41 | 42 | .app-pre { 43 | margin: 8px 0; 44 | border-radius: 4px; 45 | overflow-x: auto; 46 | } 47 | 48 | .app-blockquote { 49 | border-left: 4px solid #dfe2e5; 50 | padding: 8px 1em; 51 | color: #6a737d; 52 | background-color: #f6f8fa; 53 | margin: 8px 0; 54 | } 55 | 56 | .App-header { 57 | background-color: #282c34; 58 | min-height: 100vh; 59 | display: flex; 60 | flex-direction: column; 61 | align-items: center; 62 | justify-content: center; 63 | font-size: calc(10px + 2vmin); 64 | color: white; 65 | } 66 | 67 | .App-link { 68 | color: #61dafb; 69 | } 70 | 71 | @keyframes App-logo-spin { 72 | from { 73 | transform: rotate(0deg); 74 | } 75 | to { 76 | transform: rotate(360deg); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/material'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { 4 | BrowserRouter as Router, 5 | Redirect, 6 | Route, 7 | Switch, 8 | } from 'react-router-dom'; 9 | import { Api } from './api/api'; 10 | import './App.css'; 11 | import { Register } from './component/auth/Register'; 12 | import { AppMainPanel } from './component/AppMainPanel'; 13 | import { showSnack, SnackBar } from './component/widget/SnackBar'; 14 | import { Account } from './im/account'; 15 | import { getCookie } from './utils/Cookies'; 16 | import { Guest } from './component/auth/Guest'; 17 | import { Loading } from './component/widget/Loading'; 18 | import { Subscription } from 'rxjs'; 19 | import { Auth } from './component/auth/Auth'; 20 | import { IMWsClient } from './im/im_ws_client'; 21 | 22 | function App() { 23 | const authed = Account.getInstance().isAuthenticated(); 24 | const [state, setState] = useState({ 25 | isAuthenticated: authed, 26 | isLoading: authed, 27 | }); 28 | 29 | useEffect(() => { 30 | const base = getCookie('baseUrl'); 31 | const ws = getCookie('wsUrl'); 32 | if (base) { 33 | Api.setBaseUrl(base); 34 | } 35 | if (ws) { 36 | Account.getInstance().server = ws; 37 | } 38 | 39 | return () => { 40 | IMWsClient.close(); 41 | }; 42 | }, []); 43 | 44 | useEffect(() => { 45 | let subscription: Subscription | null = null; 46 | if (authed) { 47 | subscription = Account.getInstance() 48 | .auth() 49 | .subscribe({ 50 | error: (e) => { 51 | setState({ 52 | isAuthenticated: false, 53 | isLoading: false, 54 | }); 55 | showSnack(e.message); 56 | }, 57 | complete: () => { 58 | setState({ 59 | isAuthenticated: true, 60 | isLoading: false, 61 | }); 62 | }, 63 | }); 64 | } 65 | return () => subscription?.unsubscribe(); 66 | }, [authed]); 67 | 68 | return ( 69 |
70 | 71 | 75 | 86 | 87 | 92 | {state.isLoading ? ( 93 | 94 | ) : ( 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | {state.isAuthenticated ? ( 113 | 114 | ) : ( 115 | 116 | )} 117 | 118 | 119 | )} 120 | 121 | 122 | 123 |
124 | ); 125 | } 126 | 127 | export default App; 128 | -------------------------------------------------------------------------------- /src/api/api.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from "rxjs"; 2 | import { getBaseUrl, post, setBaseUrl } from "./axios"; 3 | import { 4 | AuthBean, 5 | ContactsBean, 6 | MessageBean, 7 | MidBean, 8 | ServerInfoBean, 9 | SessionBean, TicketBean, 10 | UserInfoBean 11 | } from "./model"; 12 | import { rxios } from "./rxios"; 13 | 14 | function login(account: string, password: string, v2: boolean): Observable { 15 | const param = { 16 | Email: account, 17 | Device: 2, 18 | Password: password 19 | }; 20 | return rxios.post("auth/signin" + (v2 ? "_v2" : ""), param); 21 | } 22 | 23 | function guest(nickname: string, avatar: string): Observable { 24 | const param = { 25 | avatar: avatar, 26 | nickname: nickname 27 | }; 28 | return rxios.post("auth/guest", param); 29 | } 30 | 31 | function verifyCode(email: string): Observable { 32 | return rxios.post("auth/verifyCode", { email: email, mode: "register" }); 33 | } 34 | 35 | function auth(token: string): Observable { 36 | const param = { 37 | Token: token 38 | }; 39 | return rxios.post("auth/token", param); 40 | } 41 | 42 | function register( 43 | email: string, 44 | nickname: string, 45 | captcha: string, 46 | password: string 47 | ): Promise { 48 | const param = { 49 | nickname: nickname, 50 | email: email, 51 | captcha: captcha, 52 | password: password 53 | }; 54 | return post("auth/register", param); 55 | } 56 | 57 | function getContacts(): Observable { 58 | return rxios.post("contacts/list"); 59 | } 60 | 61 | function addContacts(uid: string): Promise { 62 | const param = { 63 | Uid: uid, 64 | Remark: "" 65 | }; 66 | return post("contacts/add", param); 67 | } 68 | 69 | function getProfile(): Promise { 70 | return post("user/profile"); 71 | } 72 | 73 | function getUserInfo(...uids: string[]): Promise { 74 | return post("user/info", { Uid: uids.map((uid) => parseInt(uid)) }); 75 | } 76 | 77 | function getRecentSession(): Observable { 78 | return rxios.post("session/recent"); 79 | } 80 | 81 | function getMessageHistry( 82 | uid: string, 83 | beforeMid: number 84 | ): Observable { 85 | return rxios.post("msg/chat/history", { 86 | Uid: parseInt(uid), 87 | Before: beforeMid 88 | }); 89 | } 90 | 91 | function getOrCreateSession(to: number): Promise { 92 | return post("session/get", { To: to }); 93 | } 94 | 95 | function getMid(): Observable { 96 | return rxios.post("msg/id"); 97 | } 98 | 99 | function getServerInfo(): Observable { 100 | return rxios.get("app/info"); 101 | } 102 | 103 | function updateProfile(avatar: string, nickname: string): Observable { 104 | return rxios.post("profile/update", { 105 | avatar: avatar, 106 | nick_name: nickname 107 | }); 108 | } 109 | 110 | function addToBlackList(ids: Array): Observable { 111 | return rxios.post("session/blacklist/add", { 112 | relative_ids: ids.map((id) => id) 113 | }); 114 | } 115 | 116 | function getBlacklistList(): Observable { 117 | return rxios.get("session/blacklist"); 118 | } 119 | 120 | function removeFromBlackList(ids: Array): Observable { 121 | return rxios.post("session/blacklist/remove", { 122 | relative_ids: ids.map((id) => id) 123 | }); 124 | } 125 | 126 | function getTicket(to: string): Observable { 127 | return rxios.post("session/ticket", { To: to }); 128 | } 129 | 130 | export const Api = { 131 | setBaseUrl, 132 | getBaseUrl, 133 | getUserInfo, 134 | getProfile, 135 | getRecentSession, 136 | getOrCreateSession, 137 | addContacts, 138 | getContacts, 139 | register, 140 | updateProfile, 141 | verifyCode, 142 | auth, 143 | login, 144 | getMid, 145 | getMessageHistry, 146 | getServerInfo, 147 | guest, 148 | addToBlackList, 149 | removeFromBlackList, 150 | getBlacklistList, 151 | getTicket 152 | } as const; 153 | -------------------------------------------------------------------------------- /src/api/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosPromise } from 'axios'; 2 | import { Response } from './response'; 3 | 4 | export const axiosInstance: AxiosInstance = axios.create({ 5 | timeout: 3000, 6 | baseURL: process.env.REACT_APP_BASE_URL, 7 | }); 8 | 9 | const setAuthHeader = (key: string, value: string) => { 10 | axiosInstance.defaults.headers.common[key] = value; 11 | }; 12 | 13 | export function setBaseUrl(url: string) { 14 | axiosInstance.defaults.baseURL = url; 15 | } 16 | 17 | export function getBaseUrl(): string { 18 | return axiosInstance.defaults.baseURL; 19 | } 20 | 21 | export function setApiToken(token: string) { 22 | setAuthHeader('Authorization', 'Bearer ' + token); 23 | } 24 | 25 | export function get(path: string): Promise { 26 | const req = get_(path); 27 | return resolve(req); 28 | } 29 | 30 | export function post(path: string, data?: any): Promise { 31 | const req = post_(path, data); 32 | return resolve(req); 33 | } 34 | 35 | function resolve(axiosPromise: AxiosPromise): Promise { 36 | const exec = ( 37 | resolve: (r: T) => void, 38 | reject: (reason: string) => void 39 | ) => { 40 | axiosPromise 41 | .then((r) => { 42 | if (r.status !== 200) { 43 | reject(`HTTP${r.status} ${r.statusText}`); 44 | return; 45 | } 46 | const data = r.data as Response; 47 | 48 | if (data.Code !== 100) { 49 | console.log(`${data.Code}, ${data.Msg}`); 50 | return; 51 | } 52 | resolve(data.Data); 53 | }) 54 | .catch((reason) => { 55 | reject('Server Error'); 56 | // reject(reason) 57 | }); 58 | }; 59 | return new Promise(exec); 60 | } 61 | 62 | function post_(path: string, data?: any): AxiosPromise { 63 | return axiosInstance.post(path, data); 64 | } 65 | 66 | function get_(path: string): AxiosPromise { 67 | return axiosInstance.get(path); 68 | } 69 | -------------------------------------------------------------------------------- /src/api/model.ts: -------------------------------------------------------------------------------- 1 | export interface AuthBean { 2 | token: string; 3 | uid: number; 4 | server: string[]; 5 | credential: CredentialBean; 6 | } 7 | 8 | export interface CredentialBean { 9 | credential: string; 10 | } 11 | 12 | export interface ContactsBean { 13 | Id: string; 14 | Type: number; 15 | Remark: string; 16 | } 17 | 18 | export interface UserInfoBean { 19 | uid: number; 20 | account: string; 21 | nick_name: string; 22 | avatar: string; 23 | } 24 | 25 | export interface TicketBean { 26 | Ticket: string; 27 | } 28 | 29 | export interface SessionBean { 30 | Uid1: number; 31 | Uid2: number; 32 | To: number; 33 | Unread: number; 34 | LastMid: number; 35 | UpdateAt: number; 36 | CreateAt: number; 37 | Type: number; 38 | } 39 | 40 | export interface MessageBean { 41 | Mid: number; 42 | From: string; 43 | To: string; 44 | Type: number; 45 | Content: string; 46 | CreateAt: number; 47 | SendAt: number; 48 | Status: number; 49 | } 50 | 51 | export interface MidBean { 52 | Mid: number; 53 | } 54 | 55 | export interface OnlineUserInfoBean { 56 | ID: string; 57 | AliveAt: number; 58 | ConnectionAt: number; 59 | Device: number; 60 | } 61 | 62 | export interface ServerInfoBean { 63 | Online: number; 64 | MaxOnline: number; 65 | MessageSent: number; 66 | StartAt: number; 67 | OnlineCli: OnlineUserInfoBean[]; 68 | } 69 | -------------------------------------------------------------------------------- /src/api/response.ts: -------------------------------------------------------------------------------- 1 | export interface Response { 2 | Msg: string; 3 | Code: number; 4 | Data: T; 5 | } 6 | 7 | export function isResponse(data: any): data is Response { 8 | return ( 9 | data.hasOwnProperty('Code') && 10 | data.hasOwnProperty('Data') && 11 | data.hasOwnProperty('Msg') 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/api/rxios.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance, AxiosPromise } from 'axios'; 2 | import { map, Observable, OperatorFunction } from 'rxjs'; 3 | import { axiosInstance } from './axios'; 4 | import { Response } from './response'; 5 | 6 | interface HttpResponse { 7 | status: number; 8 | msg: string; 9 | headers: any; 10 | data: any; 11 | } 12 | 13 | export class Rxios { 14 | private _axios: AxiosInstance; 15 | 16 | constructor(a: AxiosInstance) { 17 | this._axios = a; 18 | } 19 | 20 | public post(url: string, data?: any): Observable { 21 | return this.fromAxios(() => this._axios.post(url, data)).pipe( 22 | this.mapResponse() 23 | ); 24 | } 25 | 26 | public put(url: string, data?: any): Observable { 27 | return this.fromAxios(() => this._axios.put(url, data)).pipe( 28 | this.mapResponse() 29 | ); 30 | } 31 | 32 | public patch(url: string, data?: any): Observable { 33 | return this.fromAxios(() => this._axios.patch(url, data)).pipe( 34 | this.mapResponse() 35 | ); 36 | } 37 | 38 | public get(url: string): Observable { 39 | return this.fromAxios(() => this._axios.get(url)).pipe( 40 | this.mapResponse() 41 | ); 42 | } 43 | 44 | private mapResponse(): OperatorFunction { 45 | return map((response) => { 46 | if (response.status !== 200) { 47 | throw new Error(response.msg); 48 | } 49 | const s = response.data as Response; 50 | if (s.Code !== 100) { 51 | throw s.Msg; 52 | } 53 | return s.Data; 54 | }); 55 | } 56 | 57 | private fromAxios(fn: () => AxiosPromise): Observable { 58 | return new Observable((observer) => { 59 | fn() 60 | .then((response) => { 61 | observer.next({ 62 | status: response.status, 63 | msg: response.statusText, 64 | headers: response.headers, 65 | data: response.data, 66 | }); 67 | observer.complete(); 68 | }) 69 | .catch((error) => { 70 | observer.error(error); 71 | observer.complete(); 72 | }); 73 | }); 74 | } 75 | } 76 | 77 | export const rxios = new Rxios(axiosInstance); 78 | -------------------------------------------------------------------------------- /src/bg_login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glide-im/glide_ts_sdk/7901c46800371b53eedcfa9e003ac3b0f72dd026/src/bg_login.jpg -------------------------------------------------------------------------------- /src/component/AppMainPanel.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AppBar, 3 | BottomNavigation, 4 | BottomNavigationAction, 5 | Box, 6 | Divider, 7 | Hidden, 8 | IconButton, 9 | Toolbar, 10 | Typography, 11 | } from '@mui/material'; 12 | import { grey } from '@mui/material/colors'; 13 | import React from 'react'; 14 | import { 15 | Redirect, 16 | Route, 17 | RouteComponentProps, 18 | Switch, 19 | useRouteMatch, 20 | withRouter, 21 | } from 'react-router-dom'; 22 | import { Account } from '../im/account'; 23 | import { ContactsList } from './friends/ContactsList'; 24 | import { Square } from './square/Square'; 25 | import { SessionListView } from './session/SessionListView'; 26 | import { ManageAccountsOutlined, MessageOutlined } from '@mui/icons-material'; 27 | import { ChatRoomContainer, ChatRoomContainerMobile } from './chat/ChatRoom'; 28 | import { Profile } from './Profile'; 29 | import { UserInfoHeader } from './session/UserInfoHeader'; 30 | import VideoChat from './webrtc/VideoChatDialog'; 31 | 32 | export const AppMainPanel: any = withRouter((props: RouteComponentProps) => { 33 | if (!Account.getInstance().isAuthenticated()) { 34 | props.history.push('/auth'); 35 | return <>; 36 | } 37 | 38 | const match = useRouteMatch(); 39 | 40 | return ( 41 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 59 | 60 | 61 | 62 | 63 | 64 | 68 | 69 | 70 | 71 | 72 | } 75 | /> 76 | } 79 | /> 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
88 |
89 | 90 | 91 | 92 |
93 | ); 94 | }); 95 | 96 | const MobileMain = withRouter((props: RouteComponentProps) => { 97 | const match = useRouteMatch(); 98 | const selected = 99 | window.location.href.match(/\/im\/(session\/?)$/g) != null ? 0 : 1; 100 | const isMainPage = 101 | window.location.href.match(/\/im\/(session\/?|profile\/?)$/g) != null; 102 | 103 | return ( 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 会话 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | {isMainPage ? ( 132 | 133 | } 136 | onClick={() => { 137 | props.history.replace(`/im/session`); 138 | }} 139 | /> 140 | } 143 | onClick={() => { 144 | props.history.replace(`/im/profile`); 145 | }} 146 | /> 147 | 148 | ) : ( 149 | <> 150 | )} 151 | 152 | ); 153 | }); 154 | -------------------------------------------------------------------------------- /src/component/EventBus.ts: -------------------------------------------------------------------------------- 1 | import { filter, map, Observable, Subject } from 'rxjs'; 2 | import { Logger } from '../utils/Logger'; 3 | 4 | export class EventBus { 5 | private static readonly TAG = 'EventBus'; 6 | 7 | private _subject = new Subject<{ event: Event; data: any }>(); 8 | 9 | private static instance: EventBus = new EventBus(); 10 | 11 | private constructor() { 12 | this._subject.subscribe({ 13 | next: (v) => Logger.info(EventBus.TAG, v.event, v.data), 14 | }); 15 | } 16 | 17 | public static post(event: Event, data: any) { 18 | this.instance._subject.next({ 19 | event: event, 20 | data: data, 21 | }); 22 | } 23 | 24 | public static event(event: Event): Observable { 25 | return this.instance._subject.pipe( 26 | filter((e) => e.event === event), 27 | map((e) => e.data) 28 | ); 29 | } 30 | } 31 | 32 | export enum Event { 33 | ReplyMessage = 'ReplyMessage', 34 | } 35 | -------------------------------------------------------------------------------- /src/component/Profile.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | Box, 4 | Button, 5 | Grid, 6 | IconButton, 7 | Typography, 8 | } from '@mui/material'; 9 | import React from 'react'; 10 | import { Account } from '../im/account'; 11 | import { RouteComponentProps, withRouter } from 'react-router-dom'; 12 | 13 | export const Profile: any = withRouter((props: RouteComponentProps) => { 14 | let u = Account.getInstance().getUserInfo(); 15 | if (u === null) { 16 | u = { 17 | avatar: '-', 18 | name: '-', 19 | id: '-', 20 | isChannel: false, 21 | }; 22 | } 23 | 24 | const logout = () => { 25 | Account.getInstance().logout(); 26 | props.history.replace('/auth'); 27 | }; 28 | 29 | return ( 30 | 31 | 32 | 33 | {}}> 34 | 35 | 36 | 37 | 38 | 39 | 43 | {u.name} 44 | 45 | 46 | uid: {u.id} 47 | 48 | 49 | 55 | 62 | 63 | 64 | ); 65 | }); 66 | -------------------------------------------------------------------------------- /src/component/auth/Auth.tsx: -------------------------------------------------------------------------------- 1 | import GitHubIcon from '@mui/icons-material/GitHub'; 2 | import SettingsIcon from '@mui/icons-material/Settings'; 3 | import { 4 | Avatar, 5 | Box, 6 | Button, 7 | Grid, 8 | IconButton, 9 | Paper, 10 | TextField, 11 | Typography, 12 | } from '@mui/material'; 13 | import React, { useRef, useState } from 'react'; 14 | import { RouteComponentProps, withRouter } from 'react-router-dom'; 15 | import { Account } from '../../im/account'; 16 | import { SettingDialog } from './SettingsDialog'; 17 | import { showSnack } from '../widget/SnackBar'; 18 | 19 | export const Auth: any = withRouter((props: RouteComponentProps) => { 20 | const accountInput = useRef(null); 21 | const passwordInput = useRef(null); 22 | 23 | const token = Account.getInstance().token; 24 | const [open, setOpen] = useState(false); 25 | 26 | if (token) { 27 | props.history.replace('/im'); 28 | return <>; 29 | } 30 | 31 | const submit = () => { 32 | const account = accountInput.current.value; 33 | const password = passwordInput.current.value; 34 | 35 | Account.getInstance() 36 | .login(account, password) 37 | .subscribe({ 38 | error: (e) => { 39 | console.log(e); 40 | showSnack('登录失败:' + e.message ?? e); 41 | }, 42 | complete: () => { 43 | props.history.replace('/im'); 44 | }, 45 | }); 46 | }; 47 | 48 | const onSettingClick = () => { 49 | setOpen(true); 50 | }; 51 | 52 | const onGithubClick = () => { 53 | window.open('https://github.com/glide-im/glide_ts_sdk'); 54 | }; 55 | 56 | const onGuestClick = () => { 57 | props.history.replace('/auth/guest'); 58 | }; 59 | 60 | return ( 61 | 62 | { 65 | setOpen(false); 66 | }} 67 | /> 68 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 99 | 108 | 109 | 110 | 111 | 112 | 118 | 119 | 125 | 131 | 132 | 133 | 134 | 135 | ); 136 | }); 137 | -------------------------------------------------------------------------------- /src/component/auth/Guest.tsx: -------------------------------------------------------------------------------- 1 | import { GitHub, Settings } from '@mui/icons-material'; 2 | import { 3 | Avatar, 4 | Box, 5 | Button, 6 | Grid, 7 | Hidden, 8 | IconButton, 9 | Paper, 10 | TextField, 11 | Typography, 12 | } from '@mui/material'; 13 | import { useRef, useState } from 'react'; 14 | import { RouteComponentProps, withRouter } from 'react-router-dom'; 15 | import { Account } from '../../im/account'; 16 | import { SettingDialog } from './SettingsDialog'; 17 | import { showSnack } from '../widget/SnackBar'; 18 | 19 | export const Guest: any = withRouter((props: RouteComponentProps) => { 20 | const accountInput = useRef(null); 21 | 22 | const [open, setOpen] = useState(false); 23 | const onSubmit = () => { 24 | let n = accountInput.current.value; 25 | if (n === '') { 26 | n = generateWuxiaName(); 27 | } 28 | 29 | Account.getInstance() 30 | .guest(n, `https://api.dicebear.com/6.x/adventurer/svg?seed=${n}`) 31 | .subscribe({ 32 | error: (e) => { 33 | showSnack(e.message); 34 | }, 35 | complete: () => { 36 | props.history.replace('/im'); 37 | }, 38 | }); 39 | }; 40 | const onSettingClick = () => { 41 | setOpen(true); 42 | }; 43 | const onGithubClick = () => { 44 | window.open('https://github.com/glide-im/glide_ts_sdk'); 45 | }; 46 | return ( 47 | <> 48 | { 51 | setOpen(false); 52 | }} 53 | /> 54 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 78 | 82 | 83 | 84 | 89 | 98 | 99 | 100 | 104 | * 您的数据将不会被保存, 105 | 部分功能可能访问受限 106 | 107 | 108 | 109 | 110 | 111 | 117 | 118 | 123 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 143 | 147 | 148 | 149 | 150 | 159 | 160 | 161 | 162 | 163 | 170 | 171 | 172 | 179 | 180 | 181 | 182 | 183 | 184 | ); 185 | }); 186 | 187 | const surnames: string[] = [ 188 | '张', 189 | '李', 190 | '赵', 191 | '钱', 192 | '孙', 193 | '周', 194 | '吴', 195 | '郑', 196 | '王', 197 | '冯', 198 | '陈', 199 | '褚', 200 | '卫', 201 | '沈', 202 | '韩', 203 | '杨', 204 | '朱', 205 | '秦', 206 | '尤', 207 | '许', 208 | '何', 209 | '吕', 210 | '施', 211 | '桂', 212 | '袁', 213 | '夏', 214 | '殷', 215 | '崔', 216 | '侯', 217 | '邓', 218 | '龚', 219 | '苏', 220 | '梁', 221 | '魏', 222 | '忻', 223 | '唐', 224 | '董', 225 | '于', 226 | '祝', 227 | '鲁', 228 | '薛', 229 | '雷', 230 | '贺', 231 | '倪', 232 | '汤', 233 | '滕', 234 | '殳', 235 | '牛', 236 | ]; 237 | const names: string[] = [ 238 | '衣', 239 | '燕', 240 | '心', 241 | '天', 242 | '罡', 243 | '马', 244 | '刀', 245 | '影', 246 | '漠', 247 | '魂', 248 | '剑', 249 | '飞', 250 | '云', 251 | '雪', 252 | '岳', 253 | '华', 254 | '青', 255 | '枫', 256 | '波', 257 | '霜', 258 | '明', 259 | '良', 260 | '俊', 261 | '忠', 262 | '信', 263 | '义', 264 | '勇', 265 | '虎', 266 | '龙', 267 | '豹', 268 | '猛', 269 | '辉', 270 | '杰', 271 | '晨', 272 | '昊', 273 | '博', 274 | '翔', 275 | '萧', 276 | '瑾', 277 | '琦', 278 | '雯', 279 | '婧', 280 | '嘉', 281 | '慧', 282 | '思', 283 | '娜', 284 | '欣', 285 | '峰', 286 | '升', 287 | '强', 288 | '川', 289 | '群', 290 | '爽', 291 | ]; 292 | 293 | function generateWuxiaName(): string { 294 | let name: string; 295 | 296 | const randomSurname = surnames[Math.floor(Math.random() * surnames.length)]; 297 | const randomName = names[Math.floor(Math.random() * names.length)]; 298 | 299 | name = `${randomSurname}${randomName}`; 300 | 301 | return name; 302 | } 303 | -------------------------------------------------------------------------------- /src/component/auth/Register.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | Box, 4 | Button, 5 | Grid, 6 | Paper, 7 | TextField, 8 | Typography, 9 | } from '@mui/material'; 10 | import React, { useRef } from 'react'; 11 | import { RouteComponentProps, withRouter } from 'react-router-dom'; 12 | import { showSnack } from '../widget/SnackBar'; 13 | import { Api } from '../../api/api'; 14 | 15 | export const Register: any = withRouter((props: RouteComponentProps) => { 16 | const accountInput = useRef(null); 17 | const passwordInput = useRef(null); 18 | const codeInput = useRef(null); 19 | const nicknameInput = useRef(null); 20 | 21 | const sendCode = () => { 22 | Api.verifyCode(accountInput.current.value).subscribe({ 23 | error: (e) => { 24 | showSnack(e.toString()); 25 | }, 26 | complete: () => { 27 | showSnack('发送成功'); 28 | }, 29 | }); 30 | }; 31 | 32 | const submit = () => { 33 | const account = accountInput.current.value; 34 | const password = passwordInput.current.value; 35 | const nickname = nicknameInput.current.value; 36 | 37 | Api.register(account, nickname, codeInput.current.value, password) 38 | .then((resp) => { 39 | showSnack('注册成功'); 40 | props.history.replace('/auth'); 41 | }) 42 | .catch((reason) => { 43 | alert(reason); 44 | }); 45 | }; 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 | 54 | 注册账号 55 | 56 | 66 | 67 | 68 | 77 | 78 | 85 | 88 | 89 | 90 | 99 | 108 | 109 | 110 | 111 | 112 | 118 | 124 | 125 | 126 | 127 | 128 | 129 | ); 130 | }); 131 | -------------------------------------------------------------------------------- /src/component/auth/SettingsDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogTitle, 7 | FormControlLabel, 8 | Switch, 9 | TextField, 10 | } from '@mui/material'; 11 | import { useRef } from 'react'; 12 | import { Account } from '../../im/account'; 13 | import { 14 | isEnableCookie, 15 | setCookie, 16 | setEnableCookie, 17 | } from '../../utils/Cookies'; 18 | import { Api } from '../../api/api'; 19 | 20 | export function SettingDialog(props: { show: boolean; onClose: () => void }) { 21 | const baseUrl = useRef(null); 22 | const wsUrl = useRef(null); 23 | const ackNotify = useRef(null); 24 | const enableCache = useRef(null); 25 | const local = useRef(null); 26 | 27 | const url = Api.getBaseUrl(); 28 | const ws = Account.getInstance().server; 29 | const enableCookie = isEnableCookie(); 30 | 31 | const onApply = () => { 32 | const u = baseUrl.current.value; 33 | const w = wsUrl.current.value; 34 | Api.setBaseUrl(u); 35 | Account.getInstance().server = w; 36 | setCookie('baseUrl', u, 100); 37 | setCookie('wsUrl', w, 100); 38 | setEnableCookie(enableCache.current.checked); 39 | props.onClose(); 40 | }; 41 | 42 | const onLocal = () => { 43 | if (local.current.checked) { 44 | baseUrl.current.value = 'http://localhost:8081/api'; 45 | wsUrl.current.value = 'ws://localhost:8083/ws'; 46 | }else{ 47 | baseUrl.current.value = 'https://intercom.ink/api/'; 48 | wsUrl.current.value = 'wss://intercom.ink/ws'; 49 | } 50 | } 51 | 52 | return ( 53 | <> 54 | 55 | 应用设置 56 | 57 | 67 | 77 | } 80 | label='启用接收者确认收到' 81 | /> 82 | 88 | } 89 | label='启用 Cookie' 90 | /> 91 | 98 | } 99 | label='本地调试' 100 | /> 101 | 102 | 103 | 106 | 109 | 110 | 111 | 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /src/component/chat/GroupMemberList.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | Box, 4 | Button, 5 | Dialog, 6 | DialogActions, 7 | DialogContent, 8 | DialogContentText, 9 | DialogTitle, 10 | TextField, 11 | } from '@mui/material'; 12 | import { useEffect, useState } from 'react'; 13 | import { ChannelList } from '../../im/channel'; 14 | 15 | export function GroupMemberList(props: { id: string }) { 16 | const [showAddMember, setShowAddMember] = useState(false); 17 | 18 | const style = { 19 | margin: '4px 1px', 20 | display: 'inline-block', 21 | justifyContent: 'center', 22 | }; 23 | 24 | const ch = ChannelList.getChannel(props.id); 25 | 26 | useEffect(() => {}, [props.id]); 27 | 28 | if (ch === null) { 29 | return <>; 30 | } 31 | 32 | const avatars = ch.getMembers().map((value) => { 33 | return ( 34 |
  • 35 | 44 |
  • 45 | ); 46 | }); 47 | const addMember = (id: number) => { 48 | if (id > 0) { 49 | // group.inviteToGroup(group.Gid, [id]).then() 50 | } 51 | setShowAddMember(false); 52 | }; 53 | 54 | return ( 55 | 56 | 57 |
      69 | {avatars} 70 | {/*
    • */} 71 | {/* */} 72 | {/* */} 73 | {/* */} 74 | {/*
    • */} 75 |
    76 |
    77 | ); 78 | } 79 | 80 | function AddMemberDialog(props: { 81 | open: boolean; 82 | callback: (s: number) => void; 83 | }) { 84 | const [id, setId] = useState(-1); 85 | 86 | return ( 87 | <> 88 | { 91 | props.callback(-1); 92 | }} 93 | aria-labelledby='form-dialog-title'> 94 | Add Member 95 | 96 | 97 | Invite member to group 98 | 99 | { 102 | const id = parseInt(e.target.value); 103 | if (isNaN(id)) { 104 | return; 105 | } 106 | setId(id); 107 | }} 108 | margin='dense' 109 | id='number' 110 | label='ID' 111 | type='text' 112 | fullWidth 113 | /> 114 | 115 | 116 | 123 | 124 | 125 | 126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /src/component/chat/MessageList.tsx: -------------------------------------------------------------------------------- 1 | import { Box, List, ListItem, Typography } from '@mui/material'; 2 | import React, { useEffect, useRef, useState } from 'react'; 3 | import { ChatMessageItem } from './Message'; 4 | import { ChatMessage } from '../../im/chat_message'; 5 | import { useParams } from 'react-router-dom'; 6 | import { Session, SessionEventType } from '../../im/session'; 7 | import { Account } from '../../im/account'; 8 | import { ChatContext } from './context/ChatContext'; 9 | import { filter } from 'rxjs'; 10 | 11 | export function SessionMessageList() { 12 | const { sid } = useParams<{ sid: string }>(); 13 | const [session, setSession] = React.useState(null); 14 | const [messages, setMessages] = useState( 15 | session?.getMessages() ?? [] 16 | ); 17 | const scrollRef = useRef() as React.MutableRefObject; 18 | const messageListEle = useRef(); 19 | 20 | const scrollToBottom = async () => { 21 | if (scrollRef.current) 22 | scrollRef.current.scrollTop = scrollRef.current.scrollHeight; 23 | }; 24 | 25 | useEffect(() => { 26 | scrollToBottom().then(); 27 | }, [messages]); 28 | 29 | useEffect(() => { 30 | setSession(Account.session().get(sid)); 31 | }, [sid]); 32 | 33 | useEffect(() => { 34 | const sp = session?.event 35 | .pipe( 36 | filter((e) => e.type === SessionEventType.ReloadMessageHistory) 37 | ) 38 | .subscribe((e) => { 39 | setMessages([...session.getMessages()]); 40 | }); 41 | return () => sp?.unsubscribe(); 42 | }, [session]); 43 | 44 | useEffect(() => { 45 | if (session === null) { 46 | return; 47 | } 48 | setMessages(session.getMessages()); 49 | const sp = session.messageSubject.subscribe((msg) => { 50 | setMessages([...session.getMessages()]); 51 | }); 52 | return () => sp.unsubscribe(); 53 | }, [session]); 54 | 55 | if (sid === '1') { 56 | // loadHistory() 57 | } 58 | 59 | if (session == null) { 60 | return ( 61 | 62 | 63 | 选择一个会话开始聊天 64 | 65 | 66 | ); 67 | } 68 | 69 | const list = messages.map((value) => { 70 | if (typeof value === 'string') { 71 | return ( 72 | 73 | 74 | 78 | {value} 79 | 80 | 81 | 82 | ); 83 | } 84 | return ( 85 | 86 | 87 | 88 | ); 89 | }); 90 | 91 | return ( 92 | 93 | 98 | 99 | {list} 100 | 101 | 102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/component/chat/MessagePopup.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Menu, MenuItem } from '@mui/material'; 2 | import React, { useRef } from 'react'; 3 | import { ChatMessage } from '../../im/chat_message'; 4 | import { Event, EventBus } from '../EventBus'; 5 | 6 | export function MessagePopup(props: { 7 | children: JSX.Element; 8 | msg: ChatMessage; 9 | }) { 10 | const [location, setLocation] = React.useState<{ 11 | mouseX: number; 12 | mouseY: number; 13 | } | null>(null); 14 | 15 | const anchorEl = useRef(); 16 | const menuRef = useRef(); 17 | 18 | const handleClose = () => { 19 | setLocation(null); 20 | }; 21 | 22 | const handleContextMenu = (e: React.MouseEvent) => { 23 | e.preventDefault(); 24 | setLocation( 25 | location === null 26 | ? { 27 | mouseX: e.clientX - 2, 28 | mouseY: e.clientY - 4, 29 | } 30 | : null 31 | ); 32 | }; 33 | 34 | const handleReply = () => { 35 | EventBus.post(Event.ReplyMessage, props.msg); 36 | handleClose(); 37 | }; 38 | 39 | const handleForward = () => { 40 | // todo forward message 41 | }; 42 | 43 | const handleDelete = () => { 44 | // todo delete local message 45 | }; 46 | 47 | const handleRevoke = () => { 48 | // todo revoke message 49 | }; 50 | 51 | menuRef?.current?.addEventListener('contextmenu', (e) => { 52 | setLocation(null); 53 | e.preventDefault(); 54 | }); 55 | 56 | return ( 57 | <> 58 | 71 | {props.msg.FromMe ? ( 72 | 73 | 撤回 74 | 75 | ) : null} 76 | 77 | 转发 78 | 79 | 80 | 回复 81 | 82 | 83 | 删除 84 | 85 | 86 | 90 | {props.children} 91 | 92 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/component/chat/components/AddBlackList.tsx: -------------------------------------------------------------------------------- 1 | import { Box, MenuItem } from '@mui/material'; 2 | import GroupRemoveIcon from '@mui/icons-material/GroupRemove'; 3 | import GroupAddIcon from '@mui/icons-material/GroupAdd'; 4 | import React, { useEffect, useState } from 'react'; 5 | import { showSnack } from '../../widget/SnackBar'; 6 | import { Event, RelativeList } from '../../../im/relative_list'; 7 | import { Logger } from '../../../utils/Logger'; 8 | 9 | const AddBlackList = (props) => { 10 | const relativeList: RelativeList = props.relativeList; 11 | const relativeListByBlackIds = relativeList.getBlackRelativeListID(); 12 | const [isBlackList, setIsBlackList] = useState(false); 13 | const uid: string = props.uid; 14 | 15 | const add = () => { 16 | relativeList.addBlackRelativeList(uid).subscribe({ 17 | error: (e) => { 18 | showSnack(e.toString()); 19 | }, 20 | next: (e) => { 21 | Logger.log('AddBlackList:', e); 22 | }, 23 | complete: () => { 24 | showSnack('添加成功'); 25 | // setIsBlackList(true) 26 | console.log(relativeList.getBlackRelativeListID()); 27 | }, 28 | }); 29 | }; 30 | 31 | const remove = () => { 32 | relativeList.removeBlackRelativeList(uid).subscribe({ 33 | error: (e) => { 34 | showSnack(e.toString()); 35 | }, 36 | complete: () => { 37 | showSnack('移出成功'); 38 | console.log(relativeList.getBlackRelativeListID()); 39 | }, 40 | next: (e) => { 41 | Logger.log('RemoveBlackList:', e); 42 | }, 43 | }); 44 | }; 45 | 46 | const toggleBlackList = () => { 47 | if (isBlackList) { 48 | remove(); 49 | } else { 50 | add(); 51 | } 52 | }; 53 | 54 | useEffect(() => { 55 | props.updateIsBlackList(isBlackList); 56 | }, [isBlackList]); 57 | 58 | useEffect(() => { 59 | setIsBlackList(false); 60 | console.log( 61 | 'relativeList', 62 | relativeListByBlackIds, 63 | props.uid, 64 | relativeListByBlackIds.includes(props.uid) 65 | ); 66 | setIsBlackList(relativeList.inUserBlackList(props.uid)); 67 | }, [props.uid]); 68 | 69 | useEffect(() => { 70 | const sb = relativeList.event(Event.BlackListUpdate).subscribe({ 71 | next: (e) => { 72 | console.log(relativeList.inUserBlackList(props.uid)); 73 | setIsBlackList(relativeList.inUserBlackList(props.uid)); 74 | }, 75 | }); 76 | 77 | return () => sb.unsubscribe(); 78 | }, []); 79 | 80 | return ( 81 | toggleBlackList()}> 82 | {isBlackList ? ( 83 | <> 84 | 85 | 移出黑名单 86 | 87 | ) : ( 88 | <> 89 | 90 | 加入黑名单 91 | 92 | )} 93 | 94 | ); 95 | }; 96 | 97 | export default AddBlackList; 98 | -------------------------------------------------------------------------------- /src/component/chat/context/ChatContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | interface ChatContextValue { 4 | scrollToBottom: Function; 5 | } 6 | 7 | export const ChatContext = createContext({ 8 | scrollToBottom: Function, 9 | } as ChatContextValue); 10 | -------------------------------------------------------------------------------- /src/component/friends/AddContactDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogTitle, 7 | TextField, 8 | } from '@mui/material'; 9 | import { useState } from 'react'; 10 | 11 | interface AddContactDialogProp { 12 | open: boolean; 13 | onClose: () => void; 14 | onSubmit: (isGroup: boolean, id: string) => void; 15 | } 16 | 17 | export function AddContactDialog(props: AddContactDialogProp) { 18 | const [id, setId] = useState(''); 19 | 20 | return ( 21 | <> 22 | 26 | Add Contact 27 | 28 | { 31 | setId(e.target.value); 32 | }} 33 | margin='dense' 34 | id='number' 35 | label='ID' 36 | type='text' 37 | fullWidth 38 | /> 39 | 40 | 41 | 44 | 51 | 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/component/friends/ContactsList.tsx: -------------------------------------------------------------------------------- 1 | import { GroupAdd, Refresh } from '@mui/icons-material'; 2 | import { 3 | Avatar, 4 | Box, 5 | Divider, 6 | Grid, 7 | IconButton, 8 | List, 9 | ListItem, 10 | ListItemIcon, 11 | ListItemText, 12 | Typography, 13 | } from '@mui/material'; 14 | import { grey } from '@mui/material/colors'; 15 | import React, { useEffect, useState } from 'react'; 16 | import { RouteComponentProps, withRouter } from 'react-router-dom'; 17 | import { Contacts } from '../../im/contacts'; 18 | import { AddContactDialog } from './AddContactDialog'; 19 | import { Account } from '../../im/account'; 20 | 21 | export function ContactsList() { 22 | const [contacts, setContacts] = useState([]); 23 | 24 | const [showAddContact, setShowAddContact] = useState(false); 25 | // const [showCreateGroup, setShowCreateGroup] = useState(false) 26 | 27 | const c = Account.getInstance().getContactList(); 28 | 29 | useEffect(() => { 30 | c.getOrInitContactList().subscribe({ 31 | next: (list: Contacts[]) => { 32 | setContacts(list); 33 | }, 34 | error: (err) => { 35 | console.log(err); 36 | }, 37 | }); 38 | c.setContactsUpdateListener(() => { 39 | setContacts([...c.getAllContacts()]); 40 | }); 41 | return () => c.setContactsUpdateListener(null); 42 | }, [c]); 43 | 44 | const list = contacts.flatMap((value) => { 45 | return ; 46 | }); 47 | 48 | const refresh = () => {}; 49 | 50 | // const createGroup = (name: string) => { 51 | // client.createGroup(name) 52 | // .then() 53 | // setShowCreateGroup(false) 54 | // } 55 | 56 | const addContactHandler = (isGroup: boolean, id: string) => { 57 | if (!isGroup) { 58 | c.addFriend(id, '') 59 | .then((r) => { 60 | setContacts([...c.getContacts()]); 61 | console.log(r); 62 | }) 63 | .catch((e) => { 64 | alert(e); 65 | console.log(e); 66 | }); 67 | } else { 68 | // client.joinGroup(id).then() 69 | } 70 | setShowAddContact(false); 71 | }; 72 | 73 | return ( 74 | 75 | 76 | 77 | {/* setShowAddContact(false)} 78 | onSubmit={createGroup} /> */} 79 | setShowAddContact(false)} 82 | onSubmit={addContactHandler} 83 | /> 84 | 联系人 85 | 86 | 90 | 91 | 92 | 93 | setShowAddContact(true)} 96 | style={{ float: 'right' }}> 97 | 98 | 99 | 100 | 101 | 102 | {list} 103 | 104 | 105 | 106 | 107 | 108 | {' '} 109 | 110 | 111 | 112 | ); 113 | } 114 | 115 | interface Props extends RouteComponentProps { 116 | contact: Contacts; 117 | onClick?: (id: number) => void; 118 | } 119 | 120 | export const ContactsItem = withRouter((props: Props) => { 121 | const c = props.contact; 122 | const handleClick = () => { 123 | props.history.push(`./session/${c.id}`); 124 | }; 125 | 126 | return ( 127 | <> 128 | 129 | 130 | 135 | 136 | 137 | 138 | 139 | ); 140 | }); 141 | -------------------------------------------------------------------------------- /src/component/friends/CreateGroupDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogContentText, 7 | DialogTitle, 8 | TextField, 9 | } from '@mui/material'; 10 | import { useState } from 'react'; 11 | 12 | interface CreateGroupDialogProp { 13 | open: boolean; 14 | onClose: () => void; 15 | onSubmit: (name: string) => void; 16 | } 17 | 18 | export function CreateGroupDialog(props: CreateGroupDialogProp) { 19 | const [name, setName] = useState(''); 20 | 21 | return ( 22 | <> 23 | 27 | 28 | Create Group Chat 29 | 30 | 31 | Group Name 32 | { 35 | setName(e.target.value); 36 | }} 37 | margin='dense' 38 | id='text' 39 | label='ID' 40 | type='text' 41 | fullWidth 42 | /> 43 | 44 | 45 | 48 | 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/component/hooks/useSession.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { Account } from '../../im/account'; 4 | 5 | function useSession() { 6 | const { sid } = useParams<{ sid: string }>(); 7 | const [session, setSession] = useState(Account.session().get(sid)); 8 | 9 | useEffect(() => { 10 | Account.session().setSelectedSession(sid); 11 | setSession(Account.session().get(sid)); 12 | }, [sid]); 13 | 14 | return session; 15 | } 16 | 17 | export default useSession; 18 | -------------------------------------------------------------------------------- /src/component/session/SessionListItem.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | Badge, 4 | ListItemButton, 5 | ListItemIcon, 6 | ListItemText, 7 | ListSubheader, 8 | } from '@mui/material'; 9 | import { green } from '@mui/material/colors'; 10 | import { useEffect, useState } from 'react'; 11 | import { showSnack } from '../widget/SnackBar'; 12 | import { useParams } from 'react-router-dom'; 13 | import { Session, SessionBaseInfo, SessionType } from '../../im/session'; 14 | import { Logger } from '../../utils/Logger'; 15 | import { time2Str } from '../../utils/TimeUtils'; 16 | 17 | export function SessionListItem(props: { 18 | chat: Session; 19 | onSelect: (c: Session) => void; 20 | }) { 21 | const { sid } = useParams<{ sid: string }>(); 22 | 23 | const [sessionInfo, setSessionInfo] = useState({ 24 | ...props.chat, 25 | }); 26 | 27 | useEffect(() => { 28 | const sp = props.chat.messageSubject.subscribe((msg) => { 29 | if (!msg.FromMe && props.chat.ID !== sid) { 30 | if (props.chat.Type === SessionType.Channel) { 31 | showSnack( 32 | `[${props.chat.Title}] ${msg.getSenderName()}: ${ 33 | props.chat.LastMessage 34 | }` 35 | ); 36 | } else { 37 | showSnack(`${props.chat.Title}: ${props.chat.LastMessage}`); 38 | } 39 | } 40 | }); 41 | return () => sp.unsubscribe(); 42 | }, [props.chat, sid]); 43 | 44 | useEffect(() => { 45 | Logger.log('SessionListItem', 'init', [props.chat]); 46 | const sp = props.chat.event.subscribe({ 47 | next: (s) => { 48 | Logger.log('SessionListItem', 'chat updated', [props.chat]); 49 | setSessionInfo({ ...props.chat }); 50 | }, 51 | }); 52 | return () => sp.unsubscribe(); 53 | }, [props.chat]); 54 | 55 | const onItemClick = () => { 56 | props.onSelect(props.chat); 57 | }; 58 | 59 | let updateAt = ''; 60 | if (sessionInfo.UpdateAt > 0) { 61 | updateAt = time2Str(sessionInfo.UpdateAt); 62 | } 63 | 64 | let lastMessage = `${sessionInfo.LastMessageSender}: ${sessionInfo.LastMessage}`; 65 | if ( 66 | sessionInfo.LastMessageSender === '' || 67 | sessionInfo.LastMessage === '' 68 | ) { 69 | lastMessage = '[还没有消息]'; 70 | } 71 | 72 | return ( 73 | <> 74 | 79 | 80 | 85 | 90 | 91 | 92 | 107 | 116 | {updateAt} 117 | 118 | 119 | 120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /src/component/session/SessionListView.tsx: -------------------------------------------------------------------------------- 1 | import {Box, CircularProgress, List, Typography} from '@mui/material'; 2 | import React, {useEffect, useState} from 'react'; 3 | import {RouteComponentProps, useParams, withRouter} from 'react-router-dom'; 4 | import {Account} from '../../im/account'; 5 | import {Session} from '../../im/session'; 6 | import {SessionListEventType} from '../../im/session_list'; 7 | import {SessionListItem} from './SessionListItem'; 8 | 9 | export const SessionListView: any = withRouter((props: RouteComponentProps) => { 10 | const {sid} = useParams<{ sid: string }>(); 11 | const sessionList = Account.getInstance().getSessionList(); 12 | const relativeList = Account.getInstance().getRelativeList(); 13 | const [sessions, setSessions] = useState(sessionList.getSessionsTemped()); 14 | const [loadSate, setLoadSate] = useState(true); 15 | const [loadError, setLoadError] = useState(''); 16 | 17 | useEffect(() => { 18 | Account.session().setSelectedSession(sid); 19 | }); 20 | 21 | useEffect(() => { 22 | const sp = sessionList.event().subscribe({ 23 | next: (e) => { 24 | switch (e.event) { 25 | case SessionListEventType.create: 26 | setSessions(sessionList.getSessionsTemped()); 27 | break; 28 | case SessionListEventType.update: 29 | setSessions(sessionList.getSessionsTemped()); 30 | break; 31 | case SessionListEventType.deleted: 32 | setSessions(sessionList.getSessionsTemped()); 33 | break; 34 | case SessionListEventType.init: 35 | break; 36 | } 37 | }, 38 | }); 39 | return () => sp.unsubscribe(); 40 | }, [sessionList, sid]); 41 | 42 | useEffect(() => { 43 | sessionList.getSessions().subscribe({ 44 | next: (res: Session[]) => { 45 | setSessions(res); 46 | }, 47 | error: (err) => { 48 | setLoadSate(false); 49 | setLoadError(err.toString()); 50 | }, 51 | complete: () => { 52 | setLoadSate(false); 53 | }, 54 | }); 55 | }, [sessionList]); 56 | 57 | const onSelect = (s: Session) => { 58 | s.clearUnread(); 59 | sessionList.setSelectedSession(s.ID); 60 | props.history.push(`/im/session/${s.ID}`); 61 | }; 62 | 63 | let content: JSX.Element; 64 | 65 | if (loadSate) { 66 | content = ; 67 | } else if (loadError) { 68 | content = ; 69 | } else if (sessions.length === 0) { 70 | content = 71 | 还没有人找你聊天哦 72 | 73 | } else { 74 | content = ( 75 | 76 | {sessions?.map((value: Session) => ( 77 | 82 | ))} 83 | 84 | ); 85 | } 86 | 87 | return <>{content}; 88 | }); 89 | 90 | function Progress(props: { showProgress?: boolean; msg?: string }) { 91 | return ( 92 | 93 | {props.showProgress !== false ? ( 94 | 95 | ) : ( 96 | <> 97 | )} 98 | {props.msg ? ( 99 | 100 | {props.msg} 101 | 102 | ) : ( 103 | <> 104 | )} 105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/component/square/Square.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | Box, 4 | Card, 5 | CircularProgress, 6 | Divider, 7 | Grid, 8 | Typography, 9 | } from '@mui/material'; 10 | import { useEffect, useState } from 'react'; 11 | import { OnlineUserInfoBean, ServerInfoBean } from '../../api/model'; 12 | import { Account } from '../../im/account'; 13 | import { GlideBaseInfo } from '../../im/def'; 14 | import { Api } from '../../api/api'; 15 | import { Cache } from '../../im/cache'; 16 | 17 | export function Square() { 18 | const [serverInfo, setServerInfo] = useState(null); 19 | const [error, setError] = useState(null); 20 | 21 | useEffect(() => { 22 | Api.getServerInfo().subscribe({ 23 | next: (data) => { 24 | setServerInfo(data); 25 | }, 26 | error: (err) => { 27 | setError(err.message); 28 | }, 29 | }); 30 | }, []); 31 | 32 | let content = <>; 33 | 34 | let servInfo = <>; 35 | 36 | if (serverInfo == null) { 37 | content = ( 38 | 44 | {error != null ? ( 45 | 46 | {' '} 47 | {error} 48 | 49 | ) : ( 50 | 51 | 52 | 53 | )} 54 | 55 | ); 56 | } else { 57 | const users = serverInfo.OnlineCli.map((u) => { 58 | return ( 59 | 60 | 61 | 62 | ); 63 | }); 64 | 65 | const now = Date.parse(new Date().toString()) / 1000; 66 | const runHours = ((now - serverInfo.StartAt) / 60 / 60).toFixed(1); 67 | 68 | servInfo = ( 69 | 75 | 76 | Online: {serverInfo.Online} 77 | 78 | 79 | MaxOnline: {serverInfo.MaxOnline} 80 | 81 | 82 | 83 | MessageCount: {serverInfo.MessageSent} 84 | 85 | 86 | 87 | ServerRunning: {runHours} Hour 88 | 89 | 90 | ); 91 | 92 | content = ( 93 | 98 | {users} 99 | 100 | ); 101 | } 102 | 103 | return ( 104 | 105 | 106 | 107 | 在线 108 | 109 | 110 | {servInfo} 111 | 112 | 113 | 114 | 115 | {content} 116 | 117 | 118 | 119 | ); 120 | } 121 | 122 | function UserCard(props: { u: OnlineUserInfoBean }) { 123 | const [user, setUser] = useState(null); 124 | 125 | useEffect(() => { 126 | Cache.loadUserInfo(props.u.ID).subscribe({ 127 | next: (data) => { 128 | setUser(data[0]); 129 | }, 130 | }); 131 | }, [props.u.ID]); 132 | 133 | const onClick = () => { 134 | Account.getInstance().getSessionList(); 135 | }; 136 | 137 | return ( 138 | 139 | 140 | 141 | 142 | 143 | Name: {user?.name} 144 | 145 | 146 | ID: {props.u.ID} 147 | 148 | 149 | ); 150 | } 151 | -------------------------------------------------------------------------------- /src/component/webrtc/VideoChatDialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useEffect } from 'react'; 3 | import Dialog from '@mui/material/Dialog'; 4 | import DialogContent from '@mui/material/DialogContent'; 5 | import DialogTitle from '@mui/material/DialogTitle'; 6 | import Paper, { PaperProps } from '@mui/material/Paper'; 7 | import Draggable from 'react-draggable'; 8 | import { IconButton } from '@mui/material'; 9 | import { VideoCallRounded } from '@mui/icons-material'; 10 | import { WebRTC } from '../../webrtc/webrtc'; 11 | import { WebRtcView } from './WebRTC'; 12 | import { Subscription } from 'rxjs'; 13 | import { SessionType } from '../../im/session'; 14 | import { Account } from '../../im/account'; 15 | 16 | export function PaperComponent(props: PaperProps) { 17 | return ( 18 | 21 | 22 | 23 | ); 24 | } 25 | 26 | export default function VideoChat(props: { 27 | session: string; 28 | showIcon: boolean; 29 | }) { 30 | const [open, setOpen] = React.useState(false); 31 | 32 | useEffect(() => { 33 | let sp: Subscription | null = null; 34 | if (!props.showIcon) { 35 | WebRTC.onIncoming((peerInfo, incoming) => { 36 | sp = incoming.cancelEvent.subscribe(() => { 37 | setOpen(false); 38 | sp.unsubscribe(); 39 | }); 40 | setOpen(true); 41 | }); 42 | } 43 | return () => sp?.unsubscribe(); 44 | }, [props.showIcon]); 45 | 46 | const session = Account.session().get(props.session); 47 | if (session?.Type !== SessionType.Single) { 48 | return <>; 49 | } 50 | 51 | const handleClickOpen = () => { 52 | setOpen(true); 53 | }; 54 | 55 | const handleClose = () => { 56 | setOpen(false); 57 | }; 58 | 59 | return ( 60 |
    61 | {props.showIcon ? ( 62 | 63 | 64 | 65 | ) : ( 66 | <> 67 | )} 68 | 73 | 76 | 视频通话 77 | 78 | 79 | 80 | 81 | 82 |
    83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/component/webrtc/WebRTC.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { 3 | rtcConfig, 4 | setRtcConfig, 5 | WebRTC, 6 | WebRtcSessionState, 7 | } from '../../webrtc/webrtc'; 8 | import { Dialing, Incoming, RtcDialog } from '../../webrtc/dialing'; 9 | import { Box, Button, Dialog, IconButton, Typography } from '@mui/material'; 10 | import { 11 | CheckRounded, 12 | CloseRounded, 13 | PhoneRounded, 14 | SettingsRounded, 15 | } from '@mui/icons-material'; 16 | import DialogTitle from '@mui/material/DialogTitle'; 17 | import DialogContent from '@mui/material/DialogContent'; 18 | import { showSnack } from '../widget/SnackBar'; 19 | 20 | function ConfigureDialog() { 21 | const textRef = useRef(null); 22 | const [show, setShow] = React.useState(false); 23 | 24 | const onApply = () => { 25 | if (show) { 26 | const config = JSON.parse( 27 | textRef.current!.value 28 | ) as RTCConfiguration; 29 | setRtcConfig(config); 30 | } 31 | setShow(!show); 32 | }; 33 | 34 | const handleClose = () => { 35 | setShow(false); 36 | }; 37 | 38 | return ( 39 | <> 40 | 44 | 47 | ICE Server 配置 48 | 49 | 50 | 51 |