├── .gitignore ├── LICENSE ├── README.md ├── electron-builder.json ├── electron ├── electron-env.d.js ├── main │ ├── index.js │ ├── menu.js │ └── update.js └── preload │ └── index.js ├── index.html ├── jsconfig.json ├── jsconfig.node.json ├── package.json ├── playwright.config.js ├── public ├── icons │ ├── dock.png │ ├── icon.icns │ └── icon.ico ├── images │ ├── icon-message.png │ └── icon-new-message.png └── xchat.svg ├── src ├── App.jsx ├── assets │ └── images │ │ ├── broken.png │ │ ├── crash.png │ │ ├── emoji-map.png │ │ ├── filetypes │ │ ├── ai.png │ │ ├── apk.png │ │ ├── archive.png │ │ ├── audio.png │ │ ├── excel.png │ │ ├── exe.png │ │ ├── image.png │ │ ├── ipa.png │ │ ├── pdf.png │ │ ├── ppt.png │ │ ├── psd.png │ │ ├── unknow.png │ │ ├── video.png │ │ └── word.png │ │ ├── messageGreen.png │ │ ├── messageRed.png │ │ ├── noselected.png │ │ ├── offline.png │ │ ├── qqemoji-map.png │ │ └── user-fallback.png ├── components │ ├── Avatar │ │ ├── index.jsx │ │ └── style.global.scss │ ├── Loader │ │ ├── index.jsx │ │ └── style.global.scss │ ├── MessageInput │ │ ├── Emoji │ │ │ ├── index.jsx │ │ │ └── style.module.scss │ │ ├── Suggestion │ │ │ ├── index.jsx │ │ │ └── style.global.scss │ │ ├── index.jsx │ │ └── style.module.scss │ ├── Modal │ │ ├── index.jsx │ │ └── style.scss │ ├── Offline │ │ ├── index.jsx │ │ └── style.module.scss │ ├── Snackbar │ │ ├── index.jsx │ │ └── style.global.scss │ ├── Switch │ │ ├── index.jsx │ │ └── style.global.css │ ├── TransitionPortal │ │ └── index.jsx │ └── UserList │ │ ├── index.jsx │ │ └── style.module.scss ├── global.css ├── hooks │ └── useStore.jsx ├── main.jsx ├── pages │ ├── AddFriend │ │ ├── index.jsx │ │ └── style.module.scss │ ├── AddMember │ │ ├── index.jsx │ │ └── style.module.scss │ ├── BatchSend │ │ ├── index.jsx │ │ └── style.module.scss │ ├── ConfirmImagePaste │ │ ├── index.jsx │ │ └── style.module.scss │ ├── Contacts │ │ ├── index.jsx │ │ └── style.module.scss │ ├── Footer │ │ ├── Contacts.jsx │ │ ├── Home.jsx │ │ ├── Settings.jsx │ │ ├── index.jsx │ │ └── style.module.scss │ ├── Forward │ │ ├── index.jsx │ │ └── style.module.scss │ ├── Home │ │ ├── ChatContent │ │ │ ├── index.jsx │ │ │ └── style.module.scss │ │ ├── Chats │ │ │ ├── index.jsx │ │ │ └── style.module.scss │ │ ├── SearchBar │ │ │ ├── index.jsx │ │ │ └── style.module.scss │ │ ├── index.jsx │ │ └── style.module.scss │ ├── Layout.jsx │ ├── Layout.module.scss │ ├── Login │ │ ├── index.jsx │ │ └── style.module.scss │ ├── Members │ │ ├── index.jsx │ │ └── style.module.scss │ ├── NewChat │ │ ├── index.jsx │ │ └── style.module.scss │ ├── Settings │ │ ├── index.jsx │ │ └── style.module.scss │ ├── UserInfo │ │ ├── index.jsx │ │ └── style.module.scss │ └── index.jsx ├── routes.jsx ├── stores │ ├── addfriend.js │ ├── addmember.js │ ├── batchsend.js │ ├── chat.js │ ├── confirmImagePaste.js │ ├── contacts.js │ ├── forward.js │ ├── index.js │ ├── members.js │ ├── newchat.js │ ├── search.js │ ├── session.js │ ├── settings.js │ ├── snackbar.js │ └── userinfo.js ├── utils │ ├── albumcolors.js │ ├── blacklist.js │ ├── emoji.js │ ├── event.js │ ├── helper.js │ ├── indexdb.js │ └── storage.js └── vite-env.d.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | **/node_modules 11 | dist 12 | dist-ssr 13 | dist-electron 14 | release 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/.debug.env 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | #lockfile 28 | package-lock.json 29 | pnpm-lock.yaml 30 | yarn.lock 31 | /test-results/ 32 | /playwright-report/ 33 | /playwright/.cache/ 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 草鞋没号 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xchat 2 | 3 | ## 概述 4 | 根据[@trazyn](https://github.com/trazyn/weweChat)使用React18+Vite重构 5 | 6 | ## 快速开始 7 | ```sh 8 | yarn && yarn dev 9 | ``` 10 | ## 构建 11 | ```sh 12 | yarn build 13 | ``` 14 | -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "org.xYx.xchat", 3 | "productName":"xchat", 4 | "asar": true, 5 | "directories": { 6 | "output": "release/${version}" 7 | }, 8 | "files": [ 9 | "dist-electron", 10 | "dist" 11 | ], 12 | "linux": { 13 | "executableName": "xchat", 14 | "icon": "public/icons/icon.icns", 15 | "artifactName": "${productName}_${version}.${ext}", 16 | "desktop": { 17 | "Icon": "/usr/share/icons/hicolor/256x256/apps/xchat.png" 18 | }, 19 | "target": [ 20 | "deb", 21 | "rpm", 22 | "AppImage" 23 | ] 24 | }, 25 | "mac": { 26 | "artifactName": "${productName}_${version}.${ext}", 27 | "icon": "public/icons/icon.icns", 28 | "target": [ 29 | "dmg", 30 | "zip" 31 | ] 32 | }, 33 | "win": { 34 | "icon": "public/icons/icon.ico", 35 | "target": [ 36 | { 37 | "target": "nsis", 38 | "arch": [ 39 | "x64" 40 | ] 41 | } 42 | ], 43 | "artifactName": "${productName}_${version}.${ext}" 44 | }, 45 | "nsis": { 46 | "oneClick": false, 47 | "perMachine": false, 48 | "allowToChangeInstallationDirectory": true, 49 | "deleteAppDataOnUninstall": false 50 | }, 51 | "publish": { 52 | "provider": "generic", 53 | "channel": "latest", 54 | "url": "https://github.com/xYx-c/xchat/releases/download/${version}" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /electron/electron-env.d.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | // declare namespace NodeJS { 4 | // interface ProcessEnv { 5 | // VSCODE_DEBUG?: 'true' 6 | // DIST_ELECTRON: string 7 | // DIST: string 8 | // /** /dist/ or /public/ */ 9 | // PUBLIC: string 10 | // } 11 | // } 12 | -------------------------------------------------------------------------------- /electron/main/menu.js: -------------------------------------------------------------------------------- 1 | import pkg from '../../package.json'; 2 | import { shell } from 'electron'; 3 | 4 | // let isWin = process.platform === 'win32'; 5 | let isOsx = process.platform === 'darwin'; 6 | let isFullScreen = false; 7 | 8 | export const menu = (mainWindow, app) => [ 9 | { 10 | label: pkg.name, 11 | submenu: [ 12 | { 13 | label: `About ${pkg.name}`, 14 | selector: 'orderFrontStandardAboutPanel:', 15 | }, 16 | { 17 | label: 'Preferences...', 18 | accelerator: !isOsx ? 'Ctrl+,' : 'Cmd+,', 19 | click() { 20 | mainWindow.show(); 21 | mainWindow.webContents.send('show-settings'); 22 | }, 23 | }, 24 | // { 25 | // label: 'Check for updates', 26 | // accelerator: !isOsx ? 'Ctrl+U' : 'Cmd+U', 27 | // click() { 28 | // checkForUpdates(); 29 | // } 30 | // }, 31 | { 32 | type: 'separator', 33 | }, 34 | { 35 | label: 'Quit xChat', 36 | accelerator: !isOsx ? 'Alt+Q' : 'Command+Q', 37 | selector: 'terminate:', 38 | click() { 39 | mainWindow = null; 40 | app.quit(); 41 | }, 42 | }, 43 | ], 44 | }, 45 | { 46 | label: 'File', 47 | submenu: [ 48 | { 49 | label: 'New Chat', 50 | accelerator: !isOsx ? 'Ctrl+N' : 'Cmd+N', 51 | click() { 52 | mainWindow.show(); 53 | mainWindow.webContents.send('show-newchat'); 54 | }, 55 | }, 56 | { 57 | label: 'Search...', 58 | accelerator: !isOsx ? 'Ctrl+F' : 'Cmd+F', 59 | click() { 60 | mainWindow.show(); 61 | mainWindow.webContents.send('show-search'); 62 | }, 63 | }, 64 | { 65 | label: 'Batch Send Message', 66 | accelerator: !isOsx ? 'Ctrl+B' : 'Cmd+B', 67 | click() { 68 | mainWindow.show(); 69 | mainWindow.webContents.send('show-batchsend'); 70 | }, 71 | }, 72 | // { 73 | // type: 'separator', 74 | // }, 75 | // { 76 | // label: 'Insert emoji', 77 | // accelerator: !isOsx ? 'Ctrl+I' : 'Cmd+I', 78 | // click() { 79 | // mainWindow.show(); 80 | // mainWindow.webContents.send('show-emoji'); 81 | // }, 82 | // }, 83 | // { 84 | // type: 'separator', 85 | // }, 86 | // { 87 | // label: 'Next conversation', 88 | // accelerator: !isOsx ? 'Ctrl+J' : 'Cmd+J', 89 | // click() { 90 | // mainWindow.show(); 91 | // mainWindow.webContents.send('show-next'); 92 | // }, 93 | // }, 94 | // { 95 | // label: 'Previous conversation', 96 | // accelerator: !isOsx ? 'Ctrl+K' : 'Cmd+K', 97 | // click() { 98 | // mainWindow.show(); 99 | // mainWindow.webContents.send('show-previous'); 100 | // }, 101 | // }, 102 | ], 103 | }, 104 | // { 105 | // label: 'Conversations', 106 | // submenu: [ 107 | // { 108 | // label: 'Loading...', 109 | // }, 110 | // ], 111 | // }, 112 | // { 113 | // label: 'Contacts', 114 | // submenu: [ 115 | // { 116 | // label: 'Loading...', 117 | // }, 118 | // ], 119 | // }, 120 | { 121 | label: 'Edit', 122 | submenu: [ 123 | { 124 | role: 'undo', 125 | }, 126 | { 127 | role: 'redo', 128 | }, 129 | { 130 | type: 'separator', 131 | }, 132 | { 133 | role: 'cut', 134 | }, 135 | { 136 | role: 'copy', 137 | }, 138 | { 139 | role: 'paste', 140 | }, 141 | { 142 | role: 'pasteandmatchstyle', 143 | }, 144 | { 145 | role: 'delete', 146 | }, 147 | { 148 | role: 'selectall', 149 | }, 150 | ], 151 | }, 152 | { 153 | label: 'View', 154 | submenu: [ 155 | { 156 | label: isFullScreen ? 'Exit Full Screen' : 'Enter Full Screen', 157 | accelerator: !isOsx ? 'Ctrl+Shift+F' : 'Shift+Cmd+F', 158 | click() { 159 | isFullScreen = !isFullScreen; 160 | 161 | mainWindow.show(); 162 | mainWindow.setFullScreen(isFullScreen); 163 | }, 164 | }, 165 | // { 166 | // label: 'Toggle Conversations', 167 | // accelerator: !isOsx ? 'Ctrl+Shift+M' : 'Shift+Cmd+M', 168 | // click() { 169 | // mainWindow.show(); 170 | // mainWindow.webContents.send('show-conversations'); 171 | // }, 172 | // }, 173 | { 174 | type: 'separator', 175 | }, 176 | { 177 | role: 'toggledevtools', 178 | }, 179 | ], 180 | }, 181 | { 182 | role: 'window', 183 | submenu: [ 184 | { 185 | role: 'minimize', 186 | }, 187 | { 188 | role: 'close', 189 | }, 190 | ], 191 | }, 192 | // { 193 | // role: 'help', 194 | // submenu: [ 195 | // { 196 | // label: '反馈(不一定解决)', 197 | // click() { 198 | // shell.openExternal('https://github.com/xYx-c/xchat/issues'); 199 | // }, 200 | // }, 201 | // { 202 | // label: 'Fork me on Github', 203 | // click() { 204 | // shell.openExternal('https://github.com/xYx-c/xchat'); 205 | // }, 206 | // }, 207 | // ], 208 | // }, 209 | ]; 210 | export const tMenu = (mainWindow, app) => [ 211 | { 212 | label: `You have 0 messages`, 213 | click() { 214 | mainWindow.show(); 215 | mainWindow.webContents.send('show-messages'); 216 | }, 217 | }, 218 | { 219 | label: 'Toggle main window', 220 | click() { 221 | let isVisible = mainWindow.isVisible(); 222 | isVisible ? mainWindow.hide() : mainWindow.show(); 223 | }, 224 | }, 225 | { 226 | type: 'separator', 227 | }, 228 | { 229 | label: 'Preferences...', 230 | accelerator: !isOsx ? 'Ctrl+,' : 'Cmd+,', 231 | click() { 232 | mainWindow.show(); 233 | mainWindow.webContents.send('show-settings'); 234 | }, 235 | }, 236 | { 237 | label: 'Fork me on Github', 238 | click() { 239 | shell.openExternal('https://github.com/xYx-c/xchat'); 240 | }, 241 | }, 242 | { 243 | type: 'separator', 244 | }, 245 | { 246 | label: 'Toggle DevTools', 247 | accelerator: !isOsx ? 'Ctrl+Alt+I' : 'Alt+Command+I', 248 | click() { 249 | mainWindow.show(); 250 | mainWindow.toggleDevTools(); 251 | }, 252 | }, 253 | // { 254 | // label: 'Hide menu bar icon', 255 | // click() { 256 | // mainWindow.webContents.send('hide-tray'); 257 | // }, 258 | // }, 259 | { 260 | type: 'separator', 261 | }, 262 | { 263 | label: 'Quit xChat', 264 | accelerator: !isOsx ? 'Alt+Q' : 'Command+Q', 265 | selector: 'terminate:', 266 | click() { 267 | mainWindow = null; 268 | app.quit(); 269 | }, 270 | }, 271 | ]; 272 | -------------------------------------------------------------------------------- /electron/main/update.js: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from 'electron'; 2 | import { autoUpdater } from 'electron-updater'; 3 | 4 | export function update(win) { 5 | // When set to false, the update download will be triggered through the API 6 | autoUpdater.autoDownload = false; 7 | autoUpdater.disableWebInstaller = false; 8 | autoUpdater.allowDowngrade = false; 9 | 10 | // start check 11 | autoUpdater.on('checking-for-update', function() { }); 12 | // update available 13 | autoUpdater.on('update-available', arg => { 14 | win.webContents.send('update-can-available', { update: true, version: app.getVersion(), newVersion: arg?.version }); 15 | }); 16 | // update not available 17 | autoUpdater.on('update-not-available', arg => { 18 | win.webContents.send('update-can-available', { 19 | update: false, 20 | version: app.getVersion(), 21 | newVersion: arg?.version, 22 | }); 23 | }); 24 | 25 | // Checking for updates 26 | ipcMain.handle('check-update', async () => { 27 | if (!app.isPackaged) { 28 | const error = new Error('The update feature is only available after the package.'); 29 | return { message: error.message, error }; 30 | } 31 | 32 | try { 33 | return await autoUpdater.checkForUpdatesAndNotify(); 34 | } catch (error) { 35 | return { message: 'Network error', error }; 36 | } 37 | }); 38 | 39 | // Start downloading and feedback on progress 40 | ipcMain.handle('start-download', event => { 41 | startDownload( 42 | (error, progressInfo) => { 43 | if (error) { 44 | // feedback download error message 45 | event.sender.send('update-error', { message: error.message, error }); 46 | } else { 47 | // feedback update progress message 48 | event.sender.send('download-progress', progressInfo); 49 | } 50 | }, 51 | () => { 52 | // feedback update downloaded message 53 | event.sender.send('update-downloaded'); 54 | }, 55 | ); 56 | }); 57 | 58 | // Install now 59 | ipcMain.handle('quit-and-install', () => { 60 | autoUpdater.quitAndInstall(false, true); 61 | }); 62 | } 63 | 64 | function startDownload(callback, complete) { 65 | autoUpdater.on('download-progress', info => callback(null, info)); 66 | autoUpdater.on('error', error => callback(error, null)); 67 | autoUpdater.on('update-downloaded', complete); 68 | autoUpdater.downloadUpdate(); 69 | } 70 | -------------------------------------------------------------------------------- /electron/preload/index.js: -------------------------------------------------------------------------------- 1 | function domReady(condition = ['complete', 'interactive']) { 2 | return new Promise(resolve => { 3 | if (condition.includes(document.readyState)) { 4 | resolve(true) 5 | } else { 6 | document.addEventListener('readystatechange', () => { 7 | if (condition.includes(document.readyState)) { 8 | resolve(true) 9 | } 10 | }) 11 | } 12 | }) 13 | } 14 | 15 | const safeDOM = { 16 | append(parent, child) { 17 | if (!Array.from(parent.children).find(e => e === child)) { 18 | return parent.appendChild(child) 19 | } 20 | }, 21 | remove(parent, child) { 22 | if (Array.from(parent.children).find(e => e === child)) { 23 | return parent.removeChild(child) 24 | } 25 | }, 26 | } 27 | 28 | /** 29 | * https://tobiasahlin.com/spinkit 30 | * https://connoratherton.com/loaders 31 | * https://projects.lukehaas.me/css-loaders 32 | * https://matejkustec.github.io/SpinThatShit 33 | */ 34 | function useLoading() { 35 | const className = `loaders-css__square-spin` 36 | const styleContent = ` 37 | @keyframes square-spin { 38 | 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); } 39 | 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); } 40 | 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); } 41 | 100% { transform: perspective(100px) rotateX(0) rotateY(0); } 42 | } 43 | .${className} > div { 44 | animation-fill-mode: both; 45 | width: 50px; 46 | height: 50px; 47 | background: #fff; 48 | animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite; 49 | } 50 | .app-loading-wrap { 51 | position: fixed; 52 | top: 0; 53 | left: 0; 54 | width: 100vw; 55 | height: 100vh; 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | background: #282c34; 60 | z-index: 9; 61 | } 62 | ` 63 | const oStyle = document.createElement('style') 64 | const oDiv = document.createElement('div') 65 | 66 | oStyle.id = 'app-loading-style' 67 | oStyle.innerHTML = styleContent 68 | oDiv.className = 'app-loading-wrap' 69 | oDiv.innerHTML = `
` 70 | 71 | return { 72 | appendLoading() { 73 | safeDOM.append(document.head, oStyle) 74 | safeDOM.append(document.body, oDiv) 75 | }, 76 | removeLoading() { 77 | safeDOM.remove(document.head, oStyle) 78 | safeDOM.remove(document.body, oDiv) 79 | }, 80 | } 81 | } 82 | 83 | // ---------------------------------------------------------------------- 84 | 85 | const { appendLoading, removeLoading } = useLoading() 86 | domReady().then(appendLoading) 87 | 88 | window.onmessage = (ev) => { 89 | ev.data.payload === 'removeLoading' && removeLoading() 90 | } 91 | 92 | setTimeout(removeLoading, 4999) 93 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | xchat 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "skipLibCheck": true, 8 | "allowJs": true, 9 | "esModuleInterop": false, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "baseUrl": "./", 19 | "paths": { 20 | "@/*": ["src/*"], 21 | "utils/*": ["src/utils/*"], 22 | "components/*": ["src/components/*"] 23 | } 24 | }, 25 | "include": ["src", "electron"], 26 | "exclude": ["node_modules", "build", "dist", "electron", "src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.tsx", "src/**/*.test.tsx"], 27 | "references": [{ "path": "./jsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /jsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts", "package.json"] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xchat", 3 | "version": "1.0.1", 4 | "main": "dist-electron/main/index.js", 5 | "description": "Electron Vite React boilerplate.", 6 | "author": "uiox ", 7 | "license": "MIT", 8 | "private": true, 9 | "debug": { 10 | "env": { 11 | "VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/" 12 | } 13 | }, 14 | "scripts": { 15 | "dev": "vite", 16 | "build": "vite build && electron-builder", 17 | "preview": "vite preview" 18 | }, 19 | "dependencies": { 20 | "@electron/remote": "^2.1.0", 21 | "@icon-park/react": "^1.4.2", 22 | "axios": "^1.6.1", 23 | "browser-md5-file": "^1.1.1", 24 | "classnames": "^2.3.2", 25 | "delegate": "^3.2.0", 26 | "electron-store": "^8.1.0", 27 | "electron-updater": "^6.1.4", 28 | "mobx": "^6.10.2", 29 | "mobx-react": "^9.0.2", 30 | "moment": "^2.29.4", 31 | "pinyin-pro": "^3.17.0", 32 | "randomcolor": "^0.6.2", 33 | "react-transition-group": "^4.4.5", 34 | "tmp": "^0.2.1" 35 | }, 36 | "devDependencies": { 37 | "@babel/plugin-proposal-class-properties": "^7.18.6", 38 | "@babel/plugin-proposal-decorators": "^7.23.2", 39 | "@playwright/test": "^1.39.0", 40 | "@types/react": "^18.2.37", 41 | "@types/react-dom": "^18.2.15", 42 | "@vitejs/plugin-react": "^4.1.1", 43 | "electron": "^27.0.4", 44 | "electron-builder": "^24.6.4", 45 | "prop-types": "^15.8.1", 46 | "react": "^18.2.0", 47 | "react-dom": "^18.2.0", 48 | "react-router-dom": "^6.18.0", 49 | "sass": "^1.69.4", 50 | "vite": "^4.5.0", 51 | "vite-plugin-electron": "^0.15.4", 52 | "vite-plugin-electron-renderer": "^0.14.5" 53 | }, 54 | "engines": { 55 | "node": "^14.18.0 || >=16.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from "@playwright/test"; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | const config: PlaywrightTestConfig = { 13 | testDir: "./e2e", 14 | /* Maximum time one test can run for. */ 15 | timeout: 30 * 1000, 16 | expect: { 17 | /** 18 | * Maximum time expect() should wait for the condition to be met. 19 | * For example in `await expect(locator).toHaveText();` 20 | */ 21 | timeout: 5000, 22 | }, 23 | /* Run tests in files in parallel */ 24 | fullyParallel: true, 25 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 26 | forbidOnly: !!process.env.CI, 27 | /* Retry on CI only */ 28 | retries: process.env.CI ? 2 : 0, 29 | /* Opt out of parallel tests on CI. */ 30 | workers: process.env.CI ? 1 : undefined, 31 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 32 | reporter: "html", 33 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 34 | use: { 35 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 36 | actionTimeout: 0, 37 | /* Base URL to use in actions like `await page.goto('/')`. */ 38 | // baseURL: 'http://localhost:3000', 39 | 40 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 41 | trace: "on-first-retry", 42 | }, 43 | 44 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 45 | // outputDir: 'test-results/', 46 | 47 | /* Run your local dev server before starting the tests */ 48 | // webServer: { 49 | // command: 'npm run start', 50 | // port: 3000, 51 | // }, 52 | }; 53 | 54 | export default config; 55 | -------------------------------------------------------------------------------- /public/icons/dock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/public/icons/dock.png -------------------------------------------------------------------------------- /public/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/public/icons/icon.icns -------------------------------------------------------------------------------- /public/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/public/icons/icon.ico -------------------------------------------------------------------------------- /public/images/icon-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/public/images/icon-message.png -------------------------------------------------------------------------------- /public/images/icon-new-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/public/images/icon-new-message.png -------------------------------------------------------------------------------- /public/xchat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { ipcRenderer } from 'electron'; 4 | import Router from './routes'; 5 | 6 | import { useLocation, useNavigate } from 'react-router-dom'; 7 | import './global.css'; 8 | import './utils/albumcolors'; 9 | import { useStores } from './hooks/useStore'; 10 | 11 | const App = () => { 12 | const navigator = useNavigate(); 13 | const canisend = () => useLocation().pathname === '/' && stores.chat.user; 14 | const stores = useStores(); 15 | useEffect(() => { 16 | if (window.navigator.onLine) { 17 | stores.session.hasLogin(); 18 | stores.settings.init(); 19 | // stores.search.getHistory(); 20 | } 21 | }, []); 22 | useEffect(() => { 23 | ipcRenderer.on('hide-tray', () => { 24 | stores.settings.setShowOnTray(false); 25 | }); 26 | ipcRenderer.on('message-chatto', (event, args) => { 27 | const user = stores.contacts.memberList.find(e => e.UserName === args.id); 28 | navigator('/'); 29 | setTimeout(stores.chat.chatTo(user)); 30 | }); 31 | ipcRenderer.on('show-userinfo', (event, args) => { 32 | const user = stores.contacts.memberList.find(e => e.UserName === args.id); 33 | stores.userinfo.toggle(true, user); 34 | }); 35 | ipcRenderer.on('show-settings', () => { 36 | navigator('/settings'); 37 | }); 38 | ipcRenderer.on('show-newchat', () => { 39 | navigator('/'); 40 | stores.newchat.toggle(true); 41 | }); 42 | ipcRenderer.on('show-conversations', () => { 43 | if (canisend()) { 44 | stores.chat.toggleConversation(); 45 | } 46 | }); 47 | ipcRenderer.on('show-search', () => { 48 | navigator('/'); 49 | stores.chat.toggleConversation(true); 50 | setTimeout(() => document.querySelector('#search').focus()); 51 | }); 52 | ipcRenderer.on('show-messages', () => { 53 | navigator('/'); 54 | stores.chat.toggleConversation(true); 55 | }); 56 | ipcRenderer.on('show-batchsend', () => { 57 | stores.batchsend.toggle(true); 58 | }); 59 | ipcRenderer.on('show-emoji', () => { 60 | if (canisend()) { 61 | document.querySelector('#showEmoji').click(); 62 | } 63 | }); 64 | ipcRenderer.on('show-messageInput', () => { 65 | if (canisend()) { 66 | document.querySelector('#messageInput').focus(); 67 | } 68 | }); 69 | ipcRenderer.on('show-contacts', () => { 70 | navigator.current.history.push('/contacts'); 71 | }); 72 | ipcRenderer.on('show-next', () => { 73 | navigator('/'); 74 | stores.chat.toggleConversation(true); 75 | setTimeout(stores.chat.chatToNext); 76 | }); 77 | ipcRenderer.on('show-previous', () => { 78 | navigator.current.history.push('/'); 79 | stores.chat.toggleConversation(true); 80 | setTimeout(stores.chat.chatToPrev); 81 | }); 82 | 83 | ipcRenderer.on('os-resume', async () => { 84 | const session = stores.session; 85 | console.log('os-resume' + new Date()); 86 | setTimeout(() => { 87 | session.checkTimeout(true); 88 | }, 3000); 89 | }); 90 | ipcRenderer.on('show-errors', (event, args) => { 91 | stores.snackbar.showMessage(args.message); 92 | }); 93 | }, []); 94 | return ; 95 | }; 96 | 97 | export default observer(App); 98 | -------------------------------------------------------------------------------- /src/assets/images/broken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/broken.png -------------------------------------------------------------------------------- /src/assets/images/crash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/crash.png -------------------------------------------------------------------------------- /src/assets/images/emoji-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/emoji-map.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/ai.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/apk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/apk.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/archive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/archive.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/audio.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/excel.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/exe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/exe.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/image.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/ipa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/ipa.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/pdf.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/ppt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/ppt.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/psd.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/unknow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/unknow.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/video.png -------------------------------------------------------------------------------- /src/assets/images/filetypes/word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/filetypes/word.png -------------------------------------------------------------------------------- /src/assets/images/messageGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/messageGreen.png -------------------------------------------------------------------------------- /src/assets/images/messageRed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/messageRed.png -------------------------------------------------------------------------------- /src/assets/images/noselected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/noselected.png -------------------------------------------------------------------------------- /src/assets/images/offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/offline.png -------------------------------------------------------------------------------- /src/assets/images/qqemoji-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/qqemoji-map.png -------------------------------------------------------------------------------- /src/assets/images/user-fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xYx-c/xchat/13663661b7ac403af37bbc33588cdbaeba0f0db8/src/assets/images/user-fallback.png -------------------------------------------------------------------------------- /src/components/Avatar/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import './style.global.scss'; 6 | import helper from '@/utils/helper'; 7 | 8 | export default class Avatar extends Component { 9 | static propTypes = { 10 | src: PropTypes.string, 11 | fallback: PropTypes.string, 12 | }; 13 | 14 | static defaultProps = { 15 | fallback: helper.getImageUrl('user-fallback.png'), 16 | }; 17 | 18 | handleError(e) { 19 | e.target.src = this.props.fallback; 20 | } 21 | 22 | handleLoad(e) { 23 | e.target.classList.remove('fadein'); 24 | } 25 | 26 | render() { 27 | if (!this.props.src) return false; 28 | 29 | return ( 30 | this.handleLoad(e)} 34 | // onError={e => this.handleError(e)} 35 | src={this.props.src} 36 | /> 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Avatar/style.global.scss: -------------------------------------------------------------------------------- 1 | 2 | .Avatar { 3 | width: 48px; 4 | height: 48px; 5 | border-radius: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Loader/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { CSSTransition } from 'react-transition-group'; 4 | import clazz from 'classnames'; 5 | 6 | import './style.global.scss'; 7 | 8 | export default class Button extends Component { 9 | static propTypes = { 10 | show: PropTypes.bool, 11 | fullscreen: PropTypes.bool, 12 | }; 13 | 14 | static defaultProps = { 15 | show: false, 16 | fullscreen: false, 17 | }; 18 | 19 | renderContent() { 20 | if (!this.props.show) { 21 | return; 22 | } 23 | return ( 24 |
25 | 26 | 27 | 28 |
29 | ); 30 | } 31 | 32 | render() { 33 | return ( 34 | 35 | {() => this.renderContent()} 36 | 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Loader/style.global.scss: -------------------------------------------------------------------------------- 1 | .Loader { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | border-radius: 1px; 8 | background: rgba(255, 255, 255, 0.7); 9 | z-index: 999; 10 | 11 | &.Loader--fullscreen { 12 | position: fixed; 13 | } 14 | } 15 | 16 | .Loader-enter { 17 | opacity: 0; 18 | visibility: hidden; 19 | transition: 0.2s; 20 | 21 | &.Loader-enter-active { 22 | opacity: 1; 23 | visibility: visible; 24 | } 25 | } 26 | 27 | .Loader-leave { 28 | opacity: 1; 29 | visibility: visible; 30 | transition: 0.2s; 31 | 32 | &.Loader-leave-active { 33 | opacity: 0; 34 | visibility: hidden; 35 | } 36 | } 37 | 38 | .Loader-circular { 39 | position: absolute; 40 | top: 50%; 41 | left: 50%; 42 | display: block; 43 | height: 100px; 44 | width: 100px; 45 | margin-top: -50px; 46 | margin-left: -50px; 47 | animation: Loader-rotate 2s linear infinite; 48 | } 49 | 50 | .Loader-path { 51 | stroke-dasharray: 1, 200; 52 | stroke-dashoffset: 0; 53 | stroke: #039be5; 54 | stroke-width: 3; 55 | animation: Loader-dash 1.5s ease-in-out infinite; 56 | stroke-linecap: round; 57 | } 58 | 59 | @keyframes Loader-rotate { 60 | 100% { 61 | transform: rotate(360deg); 62 | } 63 | } 64 | 65 | @keyframes Loader-dash { 66 | 0% { 67 | stroke-dasharray: 1, 200; 68 | stroke-dashoffset: 0; 69 | } 70 | 71 | 50% { 72 | stroke-dasharray: 89, 200; 73 | stroke-dashoffset: -35; 74 | } 75 | 76 | 100% { 77 | stroke-dasharray: 89, 200; 78 | stroke-dashoffset: -124; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/components/MessageInput/Emoji/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clazz from 'classnames'; 4 | import delegate from 'delegate'; 5 | 6 | import classes from './style.module.scss'; 7 | import { emoji } from 'utils/emoji'; 8 | 9 | export default class Emoji extends Component { 10 | static propTypes = { 11 | output: PropTypes.func.isRequired, 12 | show: PropTypes.bool.isRequired, 13 | close: PropTypes.func.isRequired, 14 | }; 15 | 16 | componentDidMount() { 17 | delegate(this.refs.container, 'a.qqemoji', 'click', e => { 18 | e.preventDefault(); 19 | e.stopPropagation(); 20 | 21 | this.props.output(e.target.title); 22 | this.props.close(); 23 | }); 24 | } 25 | 26 | componentDidUpdate() { 27 | if (this.props.show) { 28 | this.refs.container.focus(); 29 | } 30 | } 31 | 32 | renderEmoji(emoji) { 33 | return emoji.map((e, index) => { 34 | var { key, className } = e; 35 | return ; 36 | }); 37 | } 38 | 39 | render() { 40 | return ( 41 |
this.props.close()} 48 | > 49 |
{this.renderEmoji(emoji.slice(0, 15))}
50 | 51 |
{this.renderEmoji(emoji.slice(15, 30))}
52 | 53 |
{this.renderEmoji(emoji.slice(30, 45))}
54 | 55 |
{this.renderEmoji(emoji.slice(45, 60))}
56 | 57 |
{this.renderEmoji(emoji.slice(60, 75))}
58 | 59 |
{this.renderEmoji(emoji.slice(75, 90))}
60 | 61 |
{this.renderEmoji(emoji.slice(90, 105))}
62 |
63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/MessageInput/Emoji/style.module.scss: -------------------------------------------------------------------------------- 1 | 2 | .container { 3 | position: absolute; 4 | bottom: 60px; 5 | right: 0; 6 | padding: 8px 12px; 7 | background: #fff; 8 | box-shadow: 0 6px 28px 0 rgba(230, 230, 230, 1); 9 | z-index: 99; 10 | outline: 0; 11 | opacity: 0; 12 | visibility: hidden; 13 | 14 | & a { 15 | margin: 4px; 16 | cursor: pointer; 17 | zoom: 1.1; 18 | transition: .2s; 19 | 20 | &:hover { 21 | transform: scale(1.2); 22 | } 23 | } 24 | 25 | &.show { 26 | opacity: 1; 27 | visibility: visible; 28 | } 29 | } 30 | 31 | .row { 32 | display: flex; 33 | justify-content: space-between; 34 | align-items: center; 35 | } 36 | 37 | @media (width <= 800px) { 38 | .container { 39 | bottom: 46px; 40 | padding: 6px 10px; 41 | 42 | & a { 43 | margin: 3px; 44 | zoom: .9; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/MessageInput/Suggestion/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clazz from 'classname'; 4 | 5 | import './style.global.scss'; 6 | import TransitionPortal from 'components/TransitionPortal'; 7 | 8 | export default class Suggestion extends Component { 9 | static propTypes = { 10 | show: PropTypes.bool.isRequired, 11 | list: PropTypes.array.isRequired, 12 | }; 13 | 14 | renderContent() { 15 | var { show, list, selected } = this.props; 16 | 17 | if (!show) { 18 | return false; 19 | } 20 | 21 | console.log(window.event); 22 | 23 | return ( 24 |
25 | {list.map((e, index) => { 26 | return ( 27 |
33 | 34 | 35 |
36 |

37 |

38 |
39 | ); 40 | })} 41 |
42 | ); 43 | } 44 | 45 | render() { 46 | return ( 47 | 48 | {this.renderContent()} 49 | 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/MessageInput/Suggestion/style.global.scss: -------------------------------------------------------------------------------- 1 | 2 | .Suggestion { 3 | position: fixed; 4 | bottom: 60px; 5 | left: 311px; 6 | height: calc(100vh - 200px); 7 | background: #fff; 8 | box-shadow: 0 0 24px 0 rgba(0, 0, 0, .2); 9 | border-radius: 1px; 10 | overflow: hidden; 11 | overflow-y: auto; 12 | } 13 | 14 | .Suggestion-item { 15 | display: flex; 16 | padding: 6px 12px; 17 | justify-content: flex-start; 18 | align-items: center; 19 | cursor: pointer; 20 | } 21 | 22 | .Suggestion--selected, 23 | .Suggestion-item:hover { 24 | background: #405de6; 25 | } 26 | 27 | .Suggestion--selected .Suggestion-username, 28 | .Suggestion-item:hover .Suggestion-username { 29 | color: #fff; 30 | } 31 | 32 | .Suggestion-item img { 33 | width: 24px; 34 | height: 24px; 35 | margin-right: 12px; 36 | } 37 | 38 | .Suggestion-username { 39 | color: #777; 40 | margin-bottom: 2px; 41 | } 42 | 43 | .Suggestion-enter { 44 | transform: translateY(24px); 45 | opacity: 0; 46 | transition: .2s cubic-bezier(.5, -.55, .4, 1.55); 47 | } 48 | 49 | .Suggestion-enter.Suggestion-enter-active { 50 | transform: translateY(0); 51 | opacity: 1; 52 | } 53 | 54 | .Suggestion-leave { 55 | opacity: 1; 56 | transition: .14s; 57 | } 58 | 59 | .Suggestion-leave.Suggestion-leave-active { 60 | transform: translateY(-24px); 61 | opacity: 0; 62 | } 63 | 64 | .Suggestion-input-user { 65 | display: flex; 66 | padding: 0 23px; 67 | margin: 0 2px; 68 | height: 32px; 69 | align-items: center; 70 | background: rgba(230, 230, 230, 1); 71 | color: #777; 72 | border-radius: 32px; 73 | white-space: nowrap; 74 | } 75 | 76 | .Suggestion-input-user img { 77 | height: 20px; 78 | width: 20px; 79 | margin-right: 2px; 80 | } 81 | 82 | .Suggestion-input-user * { 83 | display: inline-block; 84 | white-space: nowrap; 85 | } 86 | 87 | @media (width <= 800px) { 88 | .Suggestion { 89 | bottom: 48px; 90 | left: 280px; 91 | } 92 | 93 | .Suggestion-item { 94 | display: flex; 95 | padding: 6px 12px; 96 | } 97 | 98 | .Suggestion-item img { 99 | width: 24px; 100 | height: 24px; 101 | margin-right: 12px; 102 | } 103 | 104 | .Suggestion-username { 105 | margin-bottom: 2px; 106 | } 107 | 108 | .Suggestion-input-user { 109 | padding: 0 23px; 110 | margin: 0 2px; 111 | height: 32px; 112 | border-radius: 32px; 113 | } 114 | 115 | .Suggestion-input-user img { 116 | height: 20px; 117 | width: 20px; 118 | margin-right: 2px; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/components/MessageInput/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ipcRenderer } from 'electron'; 4 | import clazz from 'classnames'; 5 | import { basename } from 'path'; 6 | 7 | import classes from './style.module.scss'; 8 | import Emoji from './Emoji'; 9 | import { Link,GrinningFace } from '@icon-park/react'; 10 | 11 | export default class MessageInput extends Component { 12 | static propTypes = { 13 | me: PropTypes.object, 14 | sendMessage: PropTypes.func.isRequired, 15 | showMessage: PropTypes.func.isRequired, 16 | user: PropTypes.array.isRequired, 17 | confirmSendImage: PropTypes.func.isRequired, 18 | process: PropTypes.func.isRequired, 19 | }; 20 | 21 | static defaultProps = { 22 | me: {}, 23 | }; 24 | 25 | canisend() { 26 | var user = this.props.user; 27 | 28 | if (true && user.length === 1 && user.slice(-1).pop().UserName === this.props.me.UserName) { 29 | this.props.showMessage("Can't send messages to yourself."); 30 | return false; 31 | } 32 | 33 | return true; 34 | } 35 | 36 | async handleEnter(e) { 37 | e.preventDefault(); 38 | const textarea = this.refs.input; 39 | textarea.addEventListener('keydown', function (event) { 40 | if (event.key === 'Enter') { 41 | event.preventDefault(); // 阻止默认的 Enter 换行行为 42 | } 43 | }); 44 | let message = textarea.value.trim(); 45 | if (e.ctrlKey && e.code == 'Enter') { 46 | const cursorPosition = textarea.selectionStart; 47 | const newMessage = message.substring(0, cursorPosition) + '\n' + message.substring(cursorPosition); 48 | textarea.value = newMessage; 49 | // textarea.setSelectionRange(cursorPosition + 1, cursorPosition + 1); 50 | textarea.scrollTop = textarea.scrollHeight; 51 | return; 52 | } 53 | var user = this.props.user; 54 | var batch = user.length > 1; 55 | if (false || !this.canisend() || !message || e.code !== 'Enter') return; 56 | // You can not send message to yourself 57 | Promise.all( 58 | user 59 | .filter(e => e.UserName !== this.props.me.UserName) 60 | .map(async e => { 61 | let res = await this.props.sendMessage(e, { content: message, type: 1 }, true); 62 | if (!res) { 63 | await this.props.showMessage( 64 | batch ? `Sending message to ${e.NickName} has failed!` : 'Failed to send message.', 65 | ); 66 | } 67 | return true; 68 | }), 69 | ); 70 | this.refs.input.value = ''; 71 | } 72 | 73 | state = { 74 | showEmoji: false, 75 | }; 76 | 77 | toggleEmoji(show = !this.state.showEmoji) { 78 | this.setState({ showEmoji: show }); 79 | } 80 | 81 | writeEmoji(emoji) { 82 | var input = this.refs.input; 83 | 84 | input.value += `[${emoji}]`; 85 | input.focus(); 86 | } 87 | 88 | async batchProcess(file) { 89 | var message; 90 | var batch = this.props.user.length > 1; 91 | var receiver = this.props.user.filter(e => e.UserName !== this.props.me.UserName); 92 | var showMessage = this.props.showMessage; 93 | 94 | if (this.canisend() === false) { 95 | return; 96 | } 97 | for (let user of receiver) { 98 | if (message) { 99 | await this.props 100 | .sendMessage(user, message, true) 101 | .catch(ex => showMessage(`Sending message to ${user.NickName} has failed!`)); 102 | continue; 103 | } 104 | // Do not repeat upload file, forward the message to another user 105 | message = await this.props.process(file, user); 106 | if (message === false) { 107 | if (batch) { 108 | showMessage(`Send message to ${user.NickName} is failed!`); 109 | continue; 110 | } 111 | // In batch mode just show the failed message 112 | showMessage('Failed to send image.'); 113 | } 114 | } 115 | } 116 | 117 | async handlePaste(e) { 118 | var args = ipcRenderer.sendSync('file-paste'); 119 | if (args.hasImage && this.canisend()) { 120 | e.preventDefault(); 121 | if ((await this.props.confirmSendImage(args.filename)) === false) { 122 | return; 123 | } 124 | let parts = [new Blob([new Uint8Array(args.raw)], { type: 'image/png' })]; 125 | let file = new File(parts, basename(args.filename), { 126 | lastModified: new Date(), 127 | type: 'image/png', 128 | }); 129 | Object.defineProperty(file, 'path', { value: args.filename }); 130 | this.batchProcess(file); 131 | } 132 | } 133 | 134 | componentWillReceiveProps(nextProps) { 135 | var input = this.refs.input; 136 | 137 | // When user has changed clear the input 138 | if ( 139 | true && 140 | input && 141 | input.value && 142 | this.props.user.map(e => e.UserName).join() !== nextProps.user.map(e => e.UserName).join() 143 | ) { 144 | input.value = ''; 145 | } 146 | } 147 | 148 | render() { 149 | var canisend = !!this.props.user.length; 150 | 151 | return ( 152 |
157 |
You should choose a contact first.
158 | 159 |