├── .env ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── .debug.script.mjs ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── README.zh-CN.md ├── commitlint.config.js ├── docs └── images │ ├── logo.jpg │ ├── preveiw.zh-CN.png │ └── preview.png ├── e2e ├── example.spec.ts └── screenshots │ └── example.png ├── electron-builder.json5 ├── electron ├── constants │ └── index.ts ├── electron-env.d.ts ├── i18n │ ├── index.ts │ └── resources │ │ ├── en-US.ts │ │ └── zh-CN.ts ├── main │ ├── appManage.ts │ ├── index.ts │ ├── ipcHandlerManage.ts │ ├── menuManage.ts │ ├── shortcutManage.ts │ ├── storeManage.ts │ ├── trayManage.ts │ └── windowManage.ts ├── preload │ └── index.ts └── utils │ ├── imsdk.ts │ ├── index.ts │ └── log.ts ├── index.html ├── package.json ├── package_dev.json ├── package_electron.json ├── patches └── @ckeditor+ckeditor5-ui+43.0.0.patch ├── playwright.config.ts ├── postcss.config.js ├── public ├── emojis.json ├── favicon.ico ├── font │ └── twemoji.woff2 ├── icons │ ├── empty_tray.png │ ├── icon.ico │ ├── icon.png │ ├── mac_icon.png │ ├── tray.png │ └── tray@2x.png ├── openIM.wasm ├── splash.html ├── sql-wasm.wasm └── wasm_exec.js ├── src ├── AntdGlobalComp.tsx ├── App.tsx ├── api │ ├── errorHandle.ts │ ├── imApi.ts │ ├── login.ts │ └── typings.d.ts ├── assets │ ├── audio │ │ └── newMsg.mp3 │ ├── avatar │ │ ├── ic_avatar_01.png │ │ ├── ic_avatar_02.png │ │ ├── ic_avatar_03.png │ │ ├── ic_avatar_04.png │ │ ├── ic_avatar_05.png │ │ └── ic_avatar_06.png │ ├── images │ │ ├── chatFooter │ │ │ ├── call_audio.png │ │ │ ├── call_video.png │ │ │ ├── cancel.png │ │ │ ├── card.png │ │ │ ├── cricle_cancel.png │ │ │ ├── cut.png │ │ │ ├── emoji.png │ │ │ ├── emoji_pop.png │ │ │ ├── emoji_pop_active.png │ │ │ ├── favorite.png │ │ │ ├── favorite_active.png │ │ │ ├── favorite_add.png │ │ │ ├── file.png │ │ │ ├── forward.png │ │ │ ├── image.png │ │ │ ├── remove.png │ │ │ ├── rtc.png │ │ │ └── video.png │ │ ├── chatHeader │ │ │ ├── cancel.png │ │ │ ├── file_manage.png │ │ │ ├── group_member.png │ │ │ ├── group_notice.png │ │ │ ├── launch_group.png │ │ │ ├── search_history.png │ │ │ ├── settings.png │ │ │ └── speaker.png │ │ ├── chatSetting │ │ │ ├── copy.png │ │ │ ├── edit_avatar.png │ │ │ ├── edit_name.png │ │ │ ├── empty_announcement.png │ │ │ ├── invite.png │ │ │ ├── invite_header.png │ │ │ ├── kick.png │ │ │ ├── member_admin.png │ │ │ ├── member_admin_active.png │ │ │ ├── member_delete.png │ │ │ ├── member_mute.png │ │ │ ├── member_mute_active.png │ │ │ └── search.png │ │ ├── chooseModal │ │ │ ├── friend.png │ │ │ ├── group.png │ │ │ └── recently.png │ │ ├── common │ │ │ ├── bench.png │ │ │ ├── bench_active.png │ │ │ ├── call.png │ │ │ ├── call_active.png │ │ │ ├── cancel.png │ │ │ ├── cancel_active.png │ │ │ ├── card.png │ │ │ ├── card_active.png │ │ │ ├── card_bg.png │ │ │ ├── clock.png │ │ │ ├── member_etc.png │ │ │ ├── sync.png │ │ │ └── sync_error.png │ │ ├── contact │ │ │ ├── arrowTopRight.png │ │ │ ├── frequent_contacts.png │ │ │ ├── group.png │ │ │ ├── group_notifications.png │ │ │ ├── label.png │ │ │ ├── my_friends.png │ │ │ ├── my_groups.png │ │ │ ├── new_friends.png │ │ │ ├── tuoyun.png │ │ │ └── union.png │ │ ├── disturb.png │ │ ├── empty_chat_bg.png │ │ ├── login │ │ │ ├── login_bg.png │ │ │ ├── login_pc.png │ │ │ └── login_qr.png │ │ ├── messageItem │ │ │ ├── file_download.png │ │ │ ├── file_downloaded.png │ │ │ ├── file_icon.png │ │ │ ├── location.png │ │ │ └── play_video.png │ │ ├── messageMenu │ │ │ ├── check.png │ │ │ ├── copy.png │ │ │ ├── emoji.png │ │ │ ├── forward.png │ │ │ ├── remove.png │ │ │ ├── reply.png │ │ │ └── revoke.png │ │ ├── moments │ │ │ └── background.png │ │ ├── nav │ │ │ ├── nav_bar_contact.png │ │ │ ├── nav_bar_contact_active.png │ │ │ ├── nav_bar_message.png │ │ │ ├── nav_bar_message_active.png │ │ │ ├── nav_bar_moments.png │ │ │ ├── nav_bar_moments_active.png │ │ │ ├── nav_bar_workbench.png │ │ │ └── nav_bar_workbench_active.png │ │ ├── profile │ │ │ ├── change_avatar.png │ │ │ └── logo.png │ │ ├── rtc │ │ │ ├── rtc_accept.png │ │ │ ├── rtc_camera.png │ │ │ ├── rtc_camera_off.png │ │ │ ├── rtc_hungup.png │ │ │ ├── rtc_mic.png │ │ │ └── rtc_mic_off.png │ │ ├── searchModal │ │ │ └── empty.png │ │ └── topSearchBar │ │ │ ├── add_friend.png │ │ │ ├── add_group.png │ │ │ ├── create_group.png │ │ │ ├── meeting.png │ │ │ ├── search.png │ │ │ ├── show_more.png │ │ │ ├── win_close.png │ │ │ ├── win_max.png │ │ │ └── win_min.png │ └── node.svg ├── components │ ├── ApplicationItem │ │ └── index.tsx │ ├── CKEditor │ │ ├── index.scss │ │ ├── index.tsx │ │ └── utils.ts │ ├── DraggableModalWrap │ │ └── index.tsx │ ├── EditableContent │ │ └── index.tsx │ ├── ErrorBoundary │ │ └── index.tsx │ ├── FlexibleSider │ │ ├── flexible-sider.module.scss │ │ └── index.tsx │ ├── OIMAvatar │ │ └── index.tsx │ ├── SettingRow │ │ └── index.tsx │ └── WindowControlBar │ │ └── index.tsx ├── config │ └── index.ts ├── constants │ ├── errcode.ts │ ├── im.ts │ └── index.ts ├── hooks │ ├── useConversationToggle.ts │ ├── useCurrentMemberRole.ts │ ├── useGroupMembers.ts │ └── useOverlayVisible.ts ├── i18n │ ├── index.ts │ └── resources │ │ ├── en.json │ │ └── zh.json ├── index.scss ├── layout │ ├── LeftNavBar │ │ ├── About.tsx │ │ ├── BlackList.tsx │ │ ├── PersonalSettings.tsx │ │ ├── index.tsx │ │ └── left-nav-bar.module.scss │ ├── MainContentLayout.tsx │ ├── MainContentWrap.tsx │ ├── TopSearchBar │ │ ├── SearchUserOrGroup.tsx │ │ └── index.tsx │ └── useGlobalEvents.tsx ├── main.tsx ├── pages │ ├── chat │ │ ├── ConversationSider │ │ │ ├── ConversationItem.tsx │ │ │ ├── conversation-item.module.scss │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── EmptyChat.tsx │ │ ├── index.tsx │ │ └── queryChat │ │ │ ├── ChatContent.tsx │ │ │ ├── ChatFooter │ │ │ ├── SendActionBar │ │ │ │ ├── CallPopContent.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── useFileMessage.ts │ │ │ ├── index.tsx │ │ │ └── useSendMessage.ts │ │ │ ├── ChatHeader │ │ │ └── index.tsx │ │ │ ├── GroupSetting │ │ │ ├── GroupMemberList.tsx │ │ │ ├── GroupMemberListHeader.tsx │ │ │ ├── GroupMemberRow.tsx │ │ │ ├── GroupSettings.tsx │ │ │ ├── group-setting.module.scss │ │ │ ├── index.tsx │ │ │ └── useGroupSettings.tsx │ │ │ ├── MessageItem │ │ │ ├── CatchMsgRenderer.tsx │ │ │ ├── MediaMessageRender.tsx │ │ │ ├── MessageItemErrorBoundary.tsx │ │ │ ├── MessageSuffix.tsx │ │ │ ├── TextMessageRender.tsx │ │ │ ├── index.tsx │ │ │ └── message-item.module.scss │ │ │ ├── NotificationMessage.tsx │ │ │ ├── SingleSetting │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── useConversationState.ts │ │ │ └── useHistoryMessageList.tsx │ ├── common │ │ ├── ChooseModal │ │ │ ├── ChooseBox │ │ │ │ ├── CheckItem.tsx │ │ │ │ ├── MenuItem.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── GroupCardModal │ │ │ └── index.tsx │ │ ├── RtcCallModal │ │ │ ├── Counter.tsx │ │ │ ├── RtcControl.tsx │ │ │ ├── RtcLayout.tsx │ │ │ ├── data.ts │ │ │ └── index.tsx │ │ └── UserCardModal │ │ │ ├── EditSelfInfo.tsx │ │ │ ├── SendRequest.tsx │ │ │ └── index.tsx │ ├── contact │ │ ├── ContactSider.tsx │ │ ├── groupNotifications │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── myFriends │ │ │ ├── AlphabetIndex.tsx │ │ │ ├── FriendListItem.tsx │ │ │ └── index.tsx │ │ ├── myGroups │ │ │ ├── GroupListItem.tsx │ │ │ └── index.tsx │ │ └── newFriends │ │ │ └── index.tsx │ └── login │ │ ├── LoginForm.tsx │ │ ├── ModifyForm.tsx │ │ ├── RegisterForm.tsx │ │ ├── areaCode.ts │ │ ├── index.module.scss │ │ └── index.tsx ├── routes │ ├── ContactRoutes.ts │ ├── GlobalErrorElement.tsx │ └── index.tsx ├── store │ ├── contact.ts │ ├── conversation.ts │ ├── index.ts │ ├── type.d.ts │ └── user.ts ├── styles │ ├── antd.scss │ ├── global.scss │ └── svg.scss ├── types │ ├── common.d.ts │ └── globalExpose.d.ts ├── utils │ ├── avatar.ts │ ├── common.ts │ ├── contactsFormat.ts │ ├── events.ts │ ├── imCommon.ts │ ├── pinyin.ts │ ├── request.ts │ └── storage.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── vite.legacy.config.ts /.env: -------------------------------------------------------------------------------- 1 | VITE_BASE_HOST=your-server-ip 2 | 3 | VITE_WS_URL=ws://$VITE_BASE_HOST:10001 4 | VITE_API_URL=http://$VITE_BASE_HOST:10002 5 | VITE_CHAT_URL=http://$VITE_BASE_HOST:10008 6 | 7 | 8 | # VITE_BASE_DOMAIN=your-server-domain 9 | 10 | # VITE_WS_URL=wss://$VITE_BASE_DOMAIN/msg_gateway 11 | # VITE_API_URL=https://$VITE_BASE_DOMAIN/api 12 | # VITE_CHAT_URL=https://$VITE_BASE_DOMAIN/chat -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/utils -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:react-hooks/recommended", 10 | "plugin:react/jsx-runtime", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 13 | "plugin:prettier/recommended", 14 | ], 15 | overrides: [], 16 | parser: "@typescript-eslint/parser", 17 | parserOptions: { 18 | ecmaVersion: "latest", 19 | sourceType: "module", 20 | project: ["./tsconfig.json"], 21 | }, 22 | plugins: [ 23 | "react", 24 | "react-hooks", 25 | "@typescript-eslint", 26 | "prettier", 27 | "simple-import-sort", 28 | ], 29 | rules: { 30 | eqeqeq: "error", 31 | "no-else-return": "error", 32 | "no-implicit-coercion": ["error", { disallowTemplateShorthand: true }], 33 | "no-unneeded-ternary": "error", 34 | "no-useless-call": "error", 35 | "no-useless-computed-key": "error", 36 | "no-useless-concat": "error", 37 | "prefer-arrow-callback": "error", 38 | "prefer-const": "error", 39 | "prefer-rest-params": "error", 40 | "prefer-spread": "error", 41 | "prefer-template": "error", 42 | radix: ["error", "always"], 43 | "simple-import-sort/imports": "error", 44 | "simple-import-sort/exports": "error", 45 | "prettier/prettier": "error", 46 | "react-hooks/exhaustive-deps": "warn", 47 | "react/display-name": "off", 48 | "@typescript-eslint/restrict-template-expressions": "off", 49 | "@typescript-eslint/ban-ts-comment": "off", 50 | "@typescript-eslint/no-floating-promises": "off", 51 | "@typescript-eslint/no-unsafe-assignment": "warn", 52 | "@typescript-eslint/no-unsafe-member-access": "warn", 53 | "@typescript-eslint/no-unsafe-call": "warn", 54 | "react-hooks/exhaustive-deps": "warn", 55 | "react/no-danger-with-children": "warn", 56 | "@typescript-eslint/no-misused-promises": "warn", 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /.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 | package 15 | release 16 | *.local 17 | 18 | # Editor directories and files 19 | .vscode/.debug.env 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | #lockfile 29 | package-lock.json 30 | pnpm-lock.yaml 31 | yarn.lock 32 | /test-results/ 33 | /playwright-report/ 34 | /playwright/.cache/ 35 | 36 | # sdk core 37 | open-im-sdk-core.* 38 | OpenIM_* -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/" -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | public -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "printWidth": 88, 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "singleQuote": false, 8 | "endOfLine": "lf", 9 | "arrowParens": "always", 10 | "plugins": ["prettier-plugin-tailwindcss"] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/.debug.script.mjs: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { createRequire } from "node:module"; 5 | import { spawn } from "node:child_process"; 6 | 7 | const pkg = createRequire(import.meta.url)("../package.json"); 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 | 10 | // write .debug.env 11 | const envContent = Object.entries(pkg.debug.env).map(([key, val]) => `${key}=${val}`); 12 | fs.writeFileSync(path.join(__dirname, ".debug.env"), envContent.join("\n")); 13 | 14 | // bootstrap 15 | spawn( 16 | // TODO: terminate `npm run dev` when Debug exits. 17 | process.platform === "win32" ? "npm.cmd" : "npm", 18 | ["run", "dev"], 19 | { 20 | stdio: "inherit", 21 | env: Object.assign(process.env, { VSCODE_DEBUG: "true" }), 22 | }, 23 | ); 24 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["mrmlnc.vscode-json5"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "compounds": [ 7 | { 8 | "name": "Debug App", 9 | "preLaunchTask": "Before Debug", 10 | "configurations": ["Debug Main Process", "Debug Renderer Process"], 11 | "presentation": { 12 | "hidden": false, 13 | "group": "", 14 | "order": 1 15 | }, 16 | "stopAll": true 17 | } 18 | ], 19 | "configurations": [ 20 | { 21 | "name": "Debug Main Process", 22 | "type": "node", 23 | "request": "launch", 24 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 25 | "windows": { 26 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 27 | }, 28 | "runtimeArgs": ["--no-sandbox", "--remote-debugging-port=9229", "."], 29 | "envFile": "${workspaceFolder}/.vscode/.debug.env", 30 | "console": "integratedTerminal" 31 | }, 32 | { 33 | "name": "Debug Renderer Process", 34 | "port": 9229, 35 | "request": "attach", 36 | "type": "chrome", 37 | "timeout": 60000, 38 | "skipFiles": [ 39 | "/**", 40 | "${workspaceRoot}/node_modules/**", 41 | "${workspaceRoot}/dist-electron/**", 42 | // Skip files in host(VITE_DEV_SERVER_URL) 43 | "http://127.0.0.1:7777/**" 44 | ] 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.tsc.autoDetect": "off", 4 | "json.schemas": [ 5 | { 6 | "fileMatch": ["/*electron-builder.json5", "/*electron-builder.json"], 7 | "url": "https://json.schemastore.org/electron-builder" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Before Debug", 8 | "type": "shell", 9 | "command": "node .vscode/.debug.script.mjs", 10 | "isBackground": true, 11 | "problemMatcher": { 12 | "owner": "typescript", 13 | "fileLocation": "relative", 14 | "pattern": { 15 | // TODO: correct "regexp" 16 | "regexp": "^([a-zA-Z]\\:/?([\\w\\-]/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$", 17 | "file": 1, 18 | "line": 3, 19 | "column": 4, 20 | "code": 5, 21 | "message": 6 22 | }, 23 | "background": { 24 | "activeOnStart": true, 25 | "beginsPattern": "^.*VITE v.* ready in \\d* ms.*$", 26 | "endsPattern": "^.*\\[startup\\] Electron App.*$" 27 | } 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /docs/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/docs/images/logo.jpg -------------------------------------------------------------------------------- /docs/images/preveiw.zh-CN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/docs/images/preveiw.zh-CN.png -------------------------------------------------------------------------------- /docs/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/docs/images/preview.png -------------------------------------------------------------------------------- /e2e/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, _electron as electron } from "@playwright/test"; 2 | 3 | test("homepage has title and links to intro page", async () => { 4 | const app = await electron.launch({ args: [".", "--no-sandbox"] }); 5 | const page = await app.firstWindow(); 6 | expect(await page.title()).toBe("OpenCorp"); 7 | await page.screenshot({ path: "e2e/screenshots/example.png" }); 8 | }); 9 | -------------------------------------------------------------------------------- /e2e/screenshots/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/e2e/screenshots/example.png -------------------------------------------------------------------------------- /electron-builder.json5: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://www.electron.build/configuration/configuration 3 | */ 4 | { 5 | appId: "io.opencorp.desktop.base", 6 | productName: "OpenCorp-Base", 7 | asar: true, 8 | asarUnpack: ["**/*.node"], 9 | extends: null, 10 | directories: { 11 | output: "release/Base/${version}", 12 | }, 13 | files: [ 14 | "dist", 15 | "!dist/*.wasm", 16 | "dist-electron", 17 | "!node_modules/rxjs/**/*", 18 | "!node_modules/koffi/src/**/*", 19 | "!node_modules/koffi/build/**/*", 20 | "!node_modules/koffi/vendor/**/*", 21 | "!node_modules/@openim/wasm-client-sdk/assets/**/*", 22 | "!node_modules/@openim/electron-client-sdk/assets/**/*", 23 | ], 24 | extraResources: [ 25 | { 26 | from: "extraResources", 27 | to: "extraResources", 28 | }, 29 | { 30 | from: "node_modules/rxjs", 31 | to: "app.asar.unpacked/node_modules/rxjs", 32 | }, 33 | ], 34 | mac: { 35 | artifactName: "${productName}_${version}_${arch}.${ext}", 36 | target: ["dmg"], 37 | icon: "./dist/icons/mac_icon.png", 38 | extraResources: [ 39 | { 40 | from: "node_modules/@openim/electron-client-sdk/assets/mac_${arch}/", 41 | to: "app.asar.unpacked/node_modules/@openim/electron-client-sdk/assets/mac_${arch}", 42 | }, 43 | { 44 | from: "node_modules/koffi/build/koffi/darwin_${arch}/", 45 | to: "koffi/darwin_${arch}", 46 | }, 47 | ], 48 | }, 49 | win: { 50 | target: [ 51 | { 52 | target: "nsis", 53 | }, 54 | ], 55 | artifactName: "${productName}_${version}.${ext}", 56 | icon: "./dist/icons/icon.ico", 57 | extraResources: [ 58 | { 59 | from: "node_modules/@openim/electron-client-sdk/assets/win_${arch}/", 60 | to: "app.asar.unpacked/node_modules/@openim/electron-client-sdk/assets/win_${arch}", 61 | }, 62 | { 63 | from: "node_modules/koffi/build/koffi/win32_${arch}/", 64 | to: "koffi/win32_${arch}", 65 | }, 66 | ], 67 | }, 68 | linux: { 69 | icon: "./dist/icons/icon.png", 70 | target: "deb", 71 | maintainer: "opencorp-base", 72 | artifactName: "${productName}_${version}_${arch}.${ext}", 73 | extraResources: [ 74 | { 75 | from: "node_modules/@openim/electron-client-sdk/assets/linux_${arch}/", 76 | to: "app.asar.unpacked/node_modules/@openim/electron-client-sdk/assets/linux_${arch}", 77 | }, 78 | { 79 | from: "node_modules/koffi/build/koffi/linux_${arch}/", 80 | to: "koffi/linux_${arch}", 81 | }, 82 | ], 83 | }, 84 | nsis: { 85 | oneClick: false, 86 | perMachine: true, 87 | allowElevation: true, 88 | allowToChangeInstallationDirectory: true, 89 | createDesktopShortcut: true, 90 | createStartMenuShortcut: true, 91 | deleteAppDataOnUninstall: true, 92 | shortcutName: "OpenCorp-Base", 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /electron/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const IpcMainToRender = { 2 | appResume: "appResume", 3 | }; 4 | 5 | export const IpcRenderToMain = { 6 | showMainWindow: "showMainWindow", 7 | clearSession: "clearSession", 8 | minimizeWindow: "minimizeWindow", 9 | maxmizeWindow: "maxmizeWindow", 10 | closeWindow: "closeWindow", 11 | showMessageBox: "showMessageBox", 12 | setKeyStore: "setKeyStore", 13 | getKeyStore: "getKeyStore", 14 | getKeyStoreSync: "getKeyStoreSync", 15 | showInputContextMenu: "showInputContextMenu", 16 | getDataPath: "getDataPath", 17 | }; 18 | -------------------------------------------------------------------------------- /electron/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | VSCODE_DEBUG?: "true"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /electron/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | 3 | import { getStore } from "../main/storeManage"; 4 | import { app } from "electron"; 5 | 6 | import translation_en from "./resources/en-US"; 7 | import translation_zh from "./resources/zh-CN"; 8 | 9 | const store = getStore(); 10 | 11 | export const initI18n = () => { 12 | const systemLanguage = app.getLocale(); 13 | const language = store.get("language", systemLanguage) as string; 14 | 15 | const resources = { 16 | "en-US": { 17 | translation: translation_en, 18 | }, 19 | "zh-CN": { 20 | translation: translation_zh, 21 | }, 22 | zh: { 23 | translation: translation_zh, 24 | }, 25 | }; 26 | 27 | i18n.init( 28 | { 29 | resources, 30 | lng: language, 31 | fallbackLng: "en-US", 32 | }, 33 | (err) => { 34 | if (err) return console.error("Error loading i18n resources:", err); 35 | console.log("i18n resources loaded successfully"); 36 | }, 37 | ); 38 | }; 39 | 40 | export const changeLanguage = i18n.changeLanguage; 41 | -------------------------------------------------------------------------------- /electron/i18n/resources/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | system: { 3 | showWindow: "ShowWindow", 4 | hideWindow: "HideWindow", 5 | hide: "Hide", 6 | about: "About", 7 | quit: "Quit", 8 | window: "Window", 9 | toggleDevTools: "ToggleDevTools", 10 | minimize: "Minimize", 11 | close: "Close", 12 | copy: "Copy", 13 | paste: "Paste", 14 | cut: "Cut", 15 | undo: "Undo", 16 | redo: "Redo", 17 | selectAll: "SelectAll", 18 | fastKeys: "FastKeys", 19 | magnifier_position_label:"Position" 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /electron/i18n/resources/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | system: { 3 | showWindow: "显示", 4 | hideWindow: "隐藏", 5 | hide: "隐藏", 6 | about: "关于", 7 | quit: "退出", 8 | window: "窗口", 9 | toggleDevTools: "调试", 10 | minimize: "最小化", 11 | close: "关闭", 12 | copy: "复制", 13 | paste: "粘贴", 14 | cut: "剪切", 15 | undo: "撤销", 16 | redo: "重做", 17 | selectAll: "全选", 18 | fastKeys: "快键键", 19 | magnifier_position_label: "坐标", 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /electron/main/appManage.ts: -------------------------------------------------------------------------------- 1 | import { app, powerMonitor } from "electron"; 2 | import { isExistMainWindow, sendEvent, showWindow } from "./windowManage"; 3 | import { join } from "node:path"; 4 | import fs from "fs"; 5 | import { isMac, isProd, isWin } from "../utils"; 6 | import { getStore } from "./storeManage"; 7 | import { IpcMainToRender } from "../constants"; 8 | import { logger } from "."; 9 | 10 | const store = getStore(); 11 | 12 | export const setSingleInstance = () => { 13 | if (!app.requestSingleInstanceLock()) { 14 | app.quit(); 15 | process.exit(0); 16 | } 17 | 18 | app.on("second-instance", () => { 19 | showWindow(); 20 | }); 21 | }; 22 | 23 | export const setAppListener = (startApp: () => void) => { 24 | app.on("activate", () => { 25 | if (isExistMainWindow()) { 26 | showWindow(); 27 | } else { 28 | startApp(); 29 | } 30 | }); 31 | 32 | app.on("window-all-closed", () => { 33 | if (isMac && !getIsForceQuit()) return; 34 | 35 | app.quit(); 36 | }); 37 | 38 | powerMonitor.on("suspend", () => { 39 | logger.debug("app suspend"); 40 | }); 41 | 42 | powerMonitor.on("resume", () => { 43 | logger.debug("app resume"); 44 | sendEvent(IpcMainToRender.appResume); 45 | }); 46 | }; 47 | 48 | export const setAppGlobalData = () => { 49 | const electronDistPath = join(__dirname, "../"); 50 | const distPath = join(electronDistPath, "../dist"); 51 | const publicPath = isProd ? distPath : join(electronDistPath, "../public"); 52 | const asarPath = process.resourcesPath; 53 | 54 | global.pathConfig = { 55 | electronDistPath, 56 | distPath, 57 | publicPath, 58 | asarPath, 59 | logsPath: join(app.getPath("userData"), `/OpenIMData/logs`), 60 | sdkResourcesPath: join(app.getPath("userData"), `/OpenIMData/sdkResources`), 61 | imsdkLibPath: isProd 62 | ? join( 63 | asarPath, 64 | "/app.asar.unpacked/node_modules/@openim/electron-client-sdk/assets", 65 | ) 66 | : join(__dirname, "../../node_modules/@openim/electron-client-sdk/assets"), 67 | trayIcon: join(publicPath, `/icons/${isWin ? "icon.ico" : "tray.png"}`), 68 | emptyTrayIcon: join(publicPath, `/icons/${"empty_tray.png"}`), 69 | indexHtml: join(distPath, "index.html"), 70 | splashHtml: join(distPath, "splash.html"), 71 | preload: join(__dirname, "../preload/index.js"), 72 | }; 73 | 74 | if (isProd) { 75 | fs.promises 76 | .readdir(global.pathConfig.logsPath) 77 | .catch( 78 | (err) => 79 | err.code === "ENOENT" && 80 | fs.promises.mkdir(global.pathConfig.logsPath, { recursive: true }), 81 | ); 82 | fs.promises 83 | .readdir(global.pathConfig.sdkResourcesPath) 84 | .catch( 85 | (err) => 86 | err.code === "ENOENT" && 87 | fs.promises.mkdir(global.pathConfig.sdkResourcesPath, { recursive: true }), 88 | ); 89 | } 90 | }; 91 | 92 | export const getIsForceQuit = () => 93 | store.get("closeAction") === "quit" || global.forceQuit; 94 | -------------------------------------------------------------------------------- /electron/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from "electron"; 2 | import { join } from "node:path"; 3 | import { createMainWindow } from "./windowManage"; 4 | import { createTray } from "./trayManage"; 5 | import { setIpcMainListener } from "./ipcHandlerManage"; 6 | import { setAppGlobalData, setAppListener, setSingleInstance } from "./appManage"; 7 | import createAppMenu from "./menuManage"; 8 | import { isLinux } from "../utils"; 9 | import { getLogger } from "../utils/log"; 10 | import { initI18n } from "../i18n"; 11 | 12 | export const logger = getLogger(join(app.getPath("userData"), `/OpenIMData/logs`)); 13 | 14 | const init = () => { 15 | initI18n(); 16 | createMainWindow(); 17 | createAppMenu(); 18 | createTray(); 19 | }; 20 | 21 | setAppGlobalData(); 22 | setIpcMainListener(); 23 | setSingleInstance(); 24 | setAppListener(init); 25 | 26 | app.whenReady().then(() => { 27 | isLinux ? setTimeout(init, 300) : init(); 28 | }); 29 | -------------------------------------------------------------------------------- /electron/main/ipcHandlerManage.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, Menu, app, dialog, ipcMain } from "electron"; 2 | import { 3 | clearCache, 4 | closeWindow, 5 | minimize, 6 | showWindow, 7 | splashEnd, 8 | updateMaximize, 9 | } from "./windowManage"; 10 | import { t } from "i18next"; 11 | import { IpcRenderToMain } from "../constants"; 12 | import { getStore } from "./storeManage"; 13 | import { changeLanguage } from "../i18n"; 14 | 15 | const store = getStore(); 16 | 17 | export const setIpcMainListener = () => { 18 | ipcMain.handle(IpcRenderToMain.clearSession, () => { 19 | clearCache(); 20 | }); 21 | 22 | // window manage 23 | ipcMain.handle("changeLanguage", (_, locale) => { 24 | store.set("language", locale); 25 | changeLanguage(locale).then(() => { 26 | app.relaunch(); 27 | app.exit(0); 28 | }); 29 | }); 30 | ipcMain.handle("main-win-ready", () => { 31 | splashEnd(); 32 | }); 33 | ipcMain.handle(IpcRenderToMain.showMainWindow, () => { 34 | showWindow(); 35 | }); 36 | ipcMain.handle(IpcRenderToMain.minimizeWindow, () => { 37 | minimize(); 38 | }); 39 | ipcMain.handle(IpcRenderToMain.maxmizeWindow, () => { 40 | updateMaximize(); 41 | }); 42 | ipcMain.handle(IpcRenderToMain.closeWindow, () => { 43 | closeWindow(); 44 | }); 45 | ipcMain.handle(IpcRenderToMain.showMessageBox, (_, options) => { 46 | return dialog 47 | .showMessageBox(BrowserWindow.getFocusedWindow(), options) 48 | .then((res) => res.response); 49 | }); 50 | 51 | // data transfer 52 | ipcMain.handle(IpcRenderToMain.setKeyStore, (_, { key, data }) => { 53 | store.set(key, data); 54 | }); 55 | ipcMain.handle(IpcRenderToMain.getKeyStore, (_, { key }) => { 56 | return store.get(key); 57 | }); 58 | ipcMain.on(IpcRenderToMain.getKeyStoreSync, (e, { key }) => { 59 | e.returnValue = store.get(key); 60 | }); 61 | ipcMain.handle(IpcRenderToMain.showInputContextMenu, () => { 62 | const menu = Menu.buildFromTemplate([ 63 | { 64 | label: t("system.copy"), 65 | type: "normal", 66 | role: "copy", 67 | accelerator: "CommandOrControl+c", 68 | }, 69 | { 70 | label: t("system.paste"), 71 | type: "normal", 72 | role: "paste", 73 | accelerator: "CommandOrControl+v", 74 | }, 75 | { 76 | label: t("system.selectAll"), 77 | type: "normal", 78 | role: "selectAll", 79 | accelerator: "CommandOrControl+a", 80 | }, 81 | ]); 82 | menu.popup({ 83 | window: BrowserWindow.getFocusedWindow()!, 84 | }); 85 | }); 86 | ipcMain.on(IpcRenderToMain.getDataPath, (e, key: string) => { 87 | switch (key) { 88 | case "public": 89 | e.returnValue = global.pathConfig.publicPath; 90 | break; 91 | case "sdkResources": 92 | e.returnValue = global.pathConfig.sdkResourcesPath; 93 | break; 94 | case "logsPath": 95 | e.returnValue = global.pathConfig.logsPath; 96 | break; 97 | default: 98 | e.returnValue = global.pathConfig.publicPath; 99 | break; 100 | } 101 | }); 102 | }; 103 | -------------------------------------------------------------------------------- /electron/main/menuManage.ts: -------------------------------------------------------------------------------- 1 | import { app, Menu } from "electron"; 2 | import { t } from "i18next"; 3 | import { isMac } from "../utils"; 4 | 5 | const createAppMenu = () => { 6 | if (isMac) { 7 | const template: Electron.MenuItemConstructorOptions[] = [ 8 | { 9 | label: app.getName(), 10 | submenu: [ 11 | { label: t("system.about"), role: "about" }, 12 | { type: "separator" }, 13 | { label: t("system.hide"), role: "hide" }, 14 | { type: "separator" }, 15 | { 16 | label: t("system.quit"), 17 | click: () => { 18 | global.forceQuit = true; 19 | app.quit(); 20 | }, 21 | }, 22 | ], 23 | }, 24 | { 25 | label: t("system.fastKeys"), 26 | submenu: [ 27 | { label: t("system.copy"), role: "copy", accelerator: "CmdOrCtrl+C" }, 28 | { label: t("system.paste"), role: "paste", accelerator: "CmdOrCtrl+V" }, 29 | { label: t("system.cut"), role: "cut", accelerator: "CmdOrCtrl+X" }, 30 | { label: t("system.undo"), role: "undo", accelerator: "CmdOrCtrl+Z" }, 31 | { label: t("system.redo"), role: "redo", accelerator: "CmdOrCtrl+Y" }, 32 | { 33 | label: t("system.selectAll"), 34 | role: "selectAll", 35 | accelerator: "CmdOrCtrl+A", 36 | }, 37 | ], 38 | }, 39 | { 40 | label: t("system.window"), 41 | role: "window", 42 | submenu: [ 43 | { label: t("system.minimize"), role: "minimize", accelerator: "CmdOrCtrl+W" }, 44 | { label: t("system.close"), role: "close" }, 45 | ], 46 | }, 47 | ]; 48 | 49 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)); 50 | } else { 51 | Menu.setApplicationMenu(null); 52 | } 53 | }; 54 | 55 | export default createAppMenu; 56 | -------------------------------------------------------------------------------- /electron/main/shortcutManage.ts: -------------------------------------------------------------------------------- 1 | import { globalShortcut } from "electron"; 2 | import { toggleDevTools } from "./windowManage"; 3 | 4 | export const registerShortcuts = () => { 5 | globalShortcut.register("CmdOrCtrl+F12", toggleDevTools); 6 | }; 7 | 8 | export const unregisterShortcuts = () => { 9 | globalShortcut.unregisterAll(); 10 | }; 11 | -------------------------------------------------------------------------------- /electron/main/storeManage.ts: -------------------------------------------------------------------------------- 1 | import Store from "electron-store"; 2 | 3 | let store: Store; 4 | 5 | export const getStore = () => { 6 | if (!store) { 7 | store = new Store(); 8 | } 9 | return store; 10 | }; 11 | 12 | export { Store }; 13 | -------------------------------------------------------------------------------- /electron/main/trayManage.ts: -------------------------------------------------------------------------------- 1 | import { app, Menu, Tray } from "electron"; 2 | import { t } from "i18next"; 3 | import { hideWindow, showWindow } from "./windowManage"; 4 | 5 | let appTray: Tray; 6 | 7 | export const createTray = () => { 8 | const trayMenu = Menu.buildFromTemplate([ 9 | { 10 | label: t("system.showWindow"), 11 | click: showWindow, 12 | }, 13 | { 14 | label: t("system.hideWindow"), 15 | click: hideWindow, 16 | }, 17 | { 18 | label: t("system.toggleDevTools"), 19 | role: "toggleDevTools", 20 | }, 21 | { 22 | label: t("system.quit"), 23 | click: () => { 24 | global.forceQuit = true; 25 | app.quit(); 26 | }, 27 | }, 28 | ]); 29 | appTray = new Tray(global.pathConfig.trayIcon); 30 | appTray.setToolTip(app.getName()); 31 | appTray.setIgnoreDoubleClickEvents(true); 32 | appTray.on("click", showWindow); 33 | 34 | appTray.setContextMenu(trayMenu); 35 | }; 36 | 37 | export const destroyTray = () => { 38 | if (!appTray || appTray.isDestroyed()) return; 39 | appTray.destroy(); 40 | appTray = null; 41 | }; 42 | -------------------------------------------------------------------------------- /electron/preload/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { DataPath, IElectronAPI } from "./../../src/types/globalExpose.d"; 4 | import { contextBridge, ipcRenderer } from "electron"; 5 | import { isProd } from "../utils"; 6 | import "@openim/electron-client-sdk/lib/preload"; 7 | import { Platform } from "@openim/wasm-client-sdk"; 8 | 9 | const getPlatform = () => { 10 | if (process.platform === "darwin") { 11 | return Platform.MacOSX; 12 | } 13 | if (process.platform === "win32") { 14 | return Platform.Windows; 15 | } 16 | return Platform.Linux; 17 | }; 18 | 19 | const getDataPath = (key: DataPath) => { 20 | switch (key) { 21 | case "public": 22 | return isProd ? ipcRenderer.sendSync("getDataPath", "public") : ""; 23 | case "sdkResources": 24 | return isProd ? ipcRenderer.sendSync("getDataPath", "sdkResources") : ""; 25 | case "logsPath": 26 | return isProd ? ipcRenderer.sendSync("getDataPath", "logsPath") : ""; 27 | default: 28 | return ""; 29 | } 30 | }; 31 | 32 | const subscribe = (channel: string, callback: (...args: any[]) => void) => { 33 | const subscription = (_, ...args) => callback(...args); 34 | ipcRenderer.on(channel, subscription); 35 | return () => ipcRenderer.removeListener(channel, subscription); 36 | }; 37 | 38 | const subscribeOnce = (channel: string, callback: (...args: any[]) => void) => { 39 | ipcRenderer.once(channel, (_, ...args) => callback(...args)); 40 | }; 41 | 42 | const unsubscribeAll = (channel: string) => { 43 | ipcRenderer.removeAllListeners(channel); 44 | }; 45 | 46 | const ipcInvoke = (channel: string, ...arg: any) => { 47 | return ipcRenderer.invoke(channel, ...arg); 48 | }; 49 | 50 | const ipcSendSync = (channel: string, ...arg: any) => { 51 | return ipcRenderer.sendSync(channel, ...arg); 52 | }; 53 | 54 | const getUniqueSavePath = (originalPath: string) => { 55 | let counter = 0; 56 | let savePath = originalPath; 57 | let fileDir = path.dirname(originalPath); 58 | let fileName = path.basename(originalPath); 59 | let fileExt = path.extname(originalPath); 60 | let baseName = path.basename(fileName, fileExt); 61 | 62 | while (fs.existsSync(savePath)) { 63 | counter++; 64 | fileName = `${baseName}(${counter})${fileExt}`; 65 | savePath = path.join(fileDir, fileName); 66 | } 67 | 68 | return savePath; 69 | }; 70 | 71 | const getFileByPath = async (filePath: string) => { 72 | try { 73 | const filename = path.basename(filePath); 74 | const data = await fs.promises.readFile(filePath); 75 | return new File([data], filename); 76 | } catch (error) { 77 | console.log(error); 78 | return null; 79 | } 80 | }; 81 | 82 | const saveFileToDisk = async ({ 83 | file, 84 | sync, 85 | }: { 86 | file: File; 87 | sync?: boolean; 88 | }): Promise => { 89 | const arrayBuffer = await file.arrayBuffer(); 90 | const saveDir = ipcRenderer.sendSync("getDataPath", "sdkResources"); 91 | const savePath = path.join(saveDir, file.name); 92 | const uniqueSavePath = getUniqueSavePath(savePath); 93 | if (!fs.existsSync(saveDir)) { 94 | fs.mkdirSync(saveDir, { recursive: true }); 95 | } 96 | if (sync) { 97 | await fs.promises.writeFile(uniqueSavePath, Buffer.from(arrayBuffer)); 98 | } else { 99 | fs.promises.writeFile(uniqueSavePath, Buffer.from(arrayBuffer)); 100 | } 101 | return uniqueSavePath; 102 | }; 103 | 104 | const Api: IElectronAPI = { 105 | getDataPath, 106 | getVersion: () => process.version, 107 | getPlatform, 108 | getSystemVersion: process.getSystemVersion, 109 | subscribe, 110 | subscribeOnce, 111 | unsubscribeAll, 112 | ipcInvoke, 113 | ipcSendSync, 114 | getFileByPath, 115 | saveFileToDisk, 116 | }; 117 | 118 | contextBridge.exposeInMainWorld("electronAPI", Api); 119 | -------------------------------------------------------------------------------- /electron/utils/imsdk.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import os from "os"; 3 | import OpenIMSDKMain from "@openim/electron-client-sdk"; 4 | import { WebContents } from "electron"; 5 | 6 | export const getLibSuffix = () => { 7 | const platform = process.platform; 8 | const arch = os.arch(); 9 | if (platform === "darwin") { 10 | return path.join(`mac_${arch === "arm64" ? "arm64" : "x64"}`, "libopenimsdk.dylib"); 11 | } 12 | if (platform === "win32") { 13 | return path.join(`win_${arch === "ia32" ? "ia32" : "x64"}`, "libopenimsdk.dll"); 14 | } 15 | return path.join(`linux_${arch === "arm64" ? "arm64" : "x64"}`, "libopenimsdk.so"); 16 | }; 17 | 18 | export const initIMSDK = (webContents: WebContents) => 19 | new OpenIMSDKMain( 20 | path.join(global.pathConfig.imsdkLibPath, getLibSuffix()), 21 | webContents, 22 | ); 23 | -------------------------------------------------------------------------------- /electron/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const isLinux = process.platform == "linux"; 2 | export const isWin = process.platform == "win32"; 3 | export const isMac = process.platform == "darwin"; 4 | export const isProd = !process.env.VITE_DEV_SERVER_URL; 5 | -------------------------------------------------------------------------------- /electron/utils/log.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log/main"; 2 | import { join } from "node:path"; 3 | import fs from "fs"; 4 | 5 | const getLogger = (logsPath: string) => { 6 | log.transports.file.level = "debug"; 7 | log.transports.file.maxSize = 104857600; // max size 100M 8 | log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}"; 9 | let date = new Date(); 10 | // let dateStr = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); 11 | // log.transports.file.resolvePathFn = () => join(logsPath, `log${dateStr}.log`); 12 | log.transports.file.resolvePathFn = () => join(logsPath, `OpenIM.log`); 13 | log.initialize({ preload: true }); 14 | return log.scope("ipcMain"); 15 | }; 16 | 17 | export { getLogger }; 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | OpenCorp-Base 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package_electron.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenCorp-Base", 3 | "version": "3.8.3", 4 | "main": "dist-electron/main/index.js", 5 | "description": "OpenIM PC Client.", 6 | "author": "blooming", 7 | "private": true, 8 | "debug": { 9 | "env": { 10 | "VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/" 11 | } 12 | }, 13 | "scripts": { 14 | "dev": "vite --host", 15 | "build": "vite build", 16 | "preview": "vite preview", 17 | "build:mac": "vite build && electron-builder --macos --x64", 18 | "build:mac-arm": "vite build && electron-builder --macos --arm64", 19 | "build:win": "vite build && electron-builder --win --x64", 20 | "build:win-arm": "vite build && electron-builder --win --arm64", 21 | "build:linux": "vite build && electron-builder --linux --x64", 22 | "build:linux-arm": "vite build && electron-builder --linux --arm64", 23 | "build:all": "vite build && electron-builder --macos --x64 && electron-builder --macos --arm64 && electron-builder --win --x64 && electron-builder --linux --x64 && electron-builder --linux --arm64", 24 | "pree2e": "vite build --mode=test", 25 | "e2e": "playwright test", 26 | "format": "prettier --write .", 27 | "lint": "eslint --ext .js,.jsx,.ts,.tsx src", 28 | "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx --fix --quiet src", 29 | "prepare": "husky install" 30 | }, 31 | "engines": { 32 | "node": "^14.18.0 || >=16.0.0" 33 | }, 34 | "lint-staged": { 35 | "src/**/*.{tsx,ts}": ["prettier --write", "eslint --fix"], 36 | "*.{json,html,css,scss,xml,md}": ["prettier --write"] 37 | }, 38 | "dependencies": { 39 | "@openim/electron-client-sdk": "^3.8.3-patch.3", 40 | "@openim/wasm-client-sdk": "^3.8.3-patch.3", 41 | "electron-log": "^5.0.0", 42 | "electron-screenshots": "^0.5.23", 43 | "electron-store": "^8.1.0", 44 | "adm-zip": "^0.5.10", 45 | "i18next": "^22.5.0", 46 | "sudo-prompt": "^9.2.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /patches/@ckeditor+ckeditor5-ui+43.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@ckeditor/ckeditor5-ui/src/editorui/poweredby.js b/node_modules/@ckeditor/ckeditor5-ui/src/editorui/poweredby.js 2 | index 191aba1..324f6d2 100644 3 | --- a/node_modules/@ckeditor/ckeditor5-ui/src/editorui/poweredby.js 4 | +++ b/node_modules/@ckeditor/ckeditor5-ui/src/editorui/poweredby.js 5 | @@ -112,12 +112,9 @@ export default class PoweredBy extends /* #__PURE__ */ DomEmitterMixin() { 6 | if (!this._balloonView) { 7 | this._createBalloonView(); 8 | } 9 | - this._balloonView.pin(attachOptions); 10 | + // this._balloonView.pin(attachOptions); 11 | } 12 | } 13 | - /** 14 | - * Hides the "powered by" balloon if already visible. 15 | - */ 16 | _hideBalloon() { 17 | if (this._balloonView) { 18 | this._balloonView.unpin(); 19 | diff --git a/node_modules/@ckeditor/ckeditor5-ui/src/icon/iconview.js b/node_modules/@ckeditor/ckeditor5-ui/src/icon/iconview.js 20 | index dd6d2ed..7af8e61 100644 21 | --- a/node_modules/@ckeditor/ckeditor5-ui/src/icon/iconview.js 22 | +++ b/node_modules/@ckeditor/ckeditor5-ui/src/icon/iconview.js 23 | @@ -42,17 +42,14 @@ class IconView extends View { 24 | } 25 | }); 26 | } 27 | - /** 28 | - * @inheritDoc 29 | - */ 30 | render() { 31 | super.render(); 32 | - this._updateXMLContent(); 33 | + // this._updateXMLContent(); 34 | this._colorFillPaths(); 35 | // This is a hack for lack of innerHTML binding. 36 | // See: https://github.com/ckeditor/ckeditor5-ui/issues/99. 37 | this.on('change:content', () => { 38 | - this._updateXMLContent(); 39 | + // this._updateXMLContent(); 40 | this._colorFillPaths(); 41 | }); 42 | this.on('change:fillColor', () => { 43 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'tailwindcss/nesting': 'postcss-nesting', 4 | tailwindcss: {}, 5 | autoprefixer: {} 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/public/favicon.ico -------------------------------------------------------------------------------- /public/font/twemoji.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/public/font/twemoji.woff2 -------------------------------------------------------------------------------- /public/icons/empty_tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/public/icons/empty_tray.png -------------------------------------------------------------------------------- /public/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/public/icons/icon.ico -------------------------------------------------------------------------------- /public/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/public/icons/icon.png -------------------------------------------------------------------------------- /public/icons/mac_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/public/icons/mac_icon.png -------------------------------------------------------------------------------- /public/icons/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/public/icons/tray.png -------------------------------------------------------------------------------- /public/icons/tray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/public/icons/tray@2x.png -------------------------------------------------------------------------------- /public/openIM.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/public/openIM.wasm -------------------------------------------------------------------------------- /public/splash.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 45 | 46 | 47 | 55 | 56 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /public/sql-wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/public/sql-wasm.wasm -------------------------------------------------------------------------------- /src/AntdGlobalComp.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "antd"; 2 | import type { MessageInstance } from "antd/es/message/interface"; 3 | import type { ModalStaticFunctions } from "antd/es/modal/confirm"; 4 | import type { NotificationInstance } from "antd/es/notification/interface"; 5 | 6 | let message: MessageInstance; 7 | let notification: NotificationInstance; 8 | let modal: Omit; 9 | 10 | export default () => { 11 | const staticFunction = App.useApp(); 12 | message = staticFunction.message; 13 | modal = staticFunction.modal; 14 | notification = staticFunction.notification; 15 | return null; 16 | }; 17 | 18 | export { message, modal, notification }; 19 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { App as AntdApp, ConfigProvider, theme } from "antd"; 2 | import enUS from "antd/locale/en_US"; 3 | import zhCN from "antd/locale/zh_CN"; 4 | import { Suspense, useEffect } from "react"; 5 | import { QueryClient, QueryClientProvider } from "react-query"; 6 | import { ReactQueryDevtools } from "react-query/devtools"; 7 | import { RouterProvider } from "react-router-dom"; 8 | 9 | import AntdGlobalComp from "./AntdGlobalComp"; 10 | import router from "./routes"; 11 | import { useUserStore } from "./store"; 12 | 13 | function App() { 14 | const locale = useUserStore((state) => state.appSettings.locale); 15 | const queryClient = new QueryClient({ 16 | defaultOptions: { 17 | queries: { 18 | refetchOnWindowFocus: false, 19 | }, 20 | }, 21 | }); 22 | 23 | return ( 24 | 31 | 32 | loading...}> 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /src/api/errorHandle.ts: -------------------------------------------------------------------------------- 1 | import { message } from "@/AntdGlobalComp"; 2 | import { ErrCodeMap } from "@/constants"; 3 | 4 | interface ErrorData { 5 | errCode: number; 6 | errMsg?: string; 7 | } 8 | 9 | export const errorHandle = (err: unknown) => { 10 | const errData = err as ErrorData; 11 | if (errData.errMsg) { 12 | message.error(ErrCodeMap[errData.errCode] || errData.errMsg); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/imApi.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | 3 | import createAxiosInstance from "@/utils/request"; 4 | import { getChatToken } from "@/utils/storage"; 5 | 6 | const request = createAxiosInstance(import.meta.env.VITE_CHAT_URL as string); 7 | 8 | export const getRtcConnectData = async (room: string, identity: string) => { 9 | const token = (await getChatToken()) as string; 10 | return request.post<{ serverUrl: string; token: string }>( 11 | "/user/rtc/get_token", 12 | { 13 | room, 14 | identity, 15 | }, 16 | { 17 | headers: { 18 | token, 19 | operationID: uuidv4(), 20 | }, 21 | }, 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/api/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | declare namespace Login { 3 | enum UsedFor { 4 | Register = 1, 5 | Modify = 2, 6 | Login = 3, 7 | } 8 | type RegisterUserInfo = { 9 | nickname: string; 10 | faceURL: string; 11 | birth?: number; 12 | gender?: number; 13 | email?: string; 14 | account?: string; 15 | areaCode: string; 16 | phoneNumber?: string; 17 | password: string; 18 | }; 19 | type DemoRegisterType = { 20 | invitationCode?: string; 21 | verifyCode: string; 22 | deviceID?: string; 23 | autoLogin?: boolean; 24 | user: RegisterUserInfo; 25 | }; 26 | type LoginParams = { 27 | email?: string; 28 | verifyCode: string; 29 | deviceID?: string; 30 | phoneNumber?: string; 31 | areaCode: string; 32 | account?: string; 33 | password: string; 34 | }; 35 | type ModifyParams = { 36 | userID: string; 37 | currentPassword: string; 38 | newPassword: string; 39 | }; 40 | type ResetParams = { 41 | email?: string; 42 | phoneNumber?: string; 43 | areaCode: string; 44 | verifyCode: string; 45 | password: string; 46 | }; 47 | type VerifyCodeParams = { 48 | email?: string; 49 | phoneNumber?: string; 50 | areaCode: string; 51 | verifyCode: string; 52 | usedFor: UsedFor; 53 | }; 54 | type SendSmsParams = { 55 | email?: string; 56 | phoneNumber?: string; 57 | areaCode: string; 58 | deviceID?: string; 59 | usedFor: UsedFor; 60 | invitationCode?: string; 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/assets/audio/newMsg.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/audio/newMsg.mp3 -------------------------------------------------------------------------------- /src/assets/avatar/ic_avatar_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/avatar/ic_avatar_01.png -------------------------------------------------------------------------------- /src/assets/avatar/ic_avatar_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/avatar/ic_avatar_02.png -------------------------------------------------------------------------------- /src/assets/avatar/ic_avatar_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/avatar/ic_avatar_03.png -------------------------------------------------------------------------------- /src/assets/avatar/ic_avatar_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/avatar/ic_avatar_04.png -------------------------------------------------------------------------------- /src/assets/avatar/ic_avatar_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/avatar/ic_avatar_05.png -------------------------------------------------------------------------------- /src/assets/avatar/ic_avatar_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/avatar/ic_avatar_06.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/call_audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/call_audio.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/call_video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/call_video.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/cancel.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/card.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/cricle_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/cricle_cancel.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/cut.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/emoji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/emoji.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/emoji_pop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/emoji_pop.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/emoji_pop_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/emoji_pop_active.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/favorite.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/favorite_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/favorite_active.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/favorite_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/favorite_add.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/file.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/forward.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/image.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/remove.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/rtc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/rtc.png -------------------------------------------------------------------------------- /src/assets/images/chatFooter/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatFooter/video.png -------------------------------------------------------------------------------- /src/assets/images/chatHeader/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatHeader/cancel.png -------------------------------------------------------------------------------- /src/assets/images/chatHeader/file_manage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatHeader/file_manage.png -------------------------------------------------------------------------------- /src/assets/images/chatHeader/group_member.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatHeader/group_member.png -------------------------------------------------------------------------------- /src/assets/images/chatHeader/group_notice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatHeader/group_notice.png -------------------------------------------------------------------------------- /src/assets/images/chatHeader/launch_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatHeader/launch_group.png -------------------------------------------------------------------------------- /src/assets/images/chatHeader/search_history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatHeader/search_history.png -------------------------------------------------------------------------------- /src/assets/images/chatHeader/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatHeader/settings.png -------------------------------------------------------------------------------- /src/assets/images/chatHeader/speaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatHeader/speaker.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/copy.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/edit_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/edit_avatar.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/edit_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/edit_name.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/empty_announcement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/empty_announcement.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/invite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/invite.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/invite_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/invite_header.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/kick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/kick.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/member_admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/member_admin.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/member_admin_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/member_admin_active.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/member_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/member_delete.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/member_mute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/member_mute.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/member_mute_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/member_mute_active.png -------------------------------------------------------------------------------- /src/assets/images/chatSetting/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chatSetting/search.png -------------------------------------------------------------------------------- /src/assets/images/chooseModal/friend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chooseModal/friend.png -------------------------------------------------------------------------------- /src/assets/images/chooseModal/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chooseModal/group.png -------------------------------------------------------------------------------- /src/assets/images/chooseModal/recently.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/chooseModal/recently.png -------------------------------------------------------------------------------- /src/assets/images/common/bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/bench.png -------------------------------------------------------------------------------- /src/assets/images/common/bench_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/bench_active.png -------------------------------------------------------------------------------- /src/assets/images/common/call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/call.png -------------------------------------------------------------------------------- /src/assets/images/common/call_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/call_active.png -------------------------------------------------------------------------------- /src/assets/images/common/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/cancel.png -------------------------------------------------------------------------------- /src/assets/images/common/cancel_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/cancel_active.png -------------------------------------------------------------------------------- /src/assets/images/common/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/card.png -------------------------------------------------------------------------------- /src/assets/images/common/card_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/card_active.png -------------------------------------------------------------------------------- /src/assets/images/common/card_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/card_bg.png -------------------------------------------------------------------------------- /src/assets/images/common/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/clock.png -------------------------------------------------------------------------------- /src/assets/images/common/member_etc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/member_etc.png -------------------------------------------------------------------------------- /src/assets/images/common/sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/sync.png -------------------------------------------------------------------------------- /src/assets/images/common/sync_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/common/sync_error.png -------------------------------------------------------------------------------- /src/assets/images/contact/arrowTopRight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/contact/arrowTopRight.png -------------------------------------------------------------------------------- /src/assets/images/contact/frequent_contacts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/contact/frequent_contacts.png -------------------------------------------------------------------------------- /src/assets/images/contact/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/contact/group.png -------------------------------------------------------------------------------- /src/assets/images/contact/group_notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/contact/group_notifications.png -------------------------------------------------------------------------------- /src/assets/images/contact/label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/contact/label.png -------------------------------------------------------------------------------- /src/assets/images/contact/my_friends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/contact/my_friends.png -------------------------------------------------------------------------------- /src/assets/images/contact/my_groups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/contact/my_groups.png -------------------------------------------------------------------------------- /src/assets/images/contact/new_friends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/contact/new_friends.png -------------------------------------------------------------------------------- /src/assets/images/contact/tuoyun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/contact/tuoyun.png -------------------------------------------------------------------------------- /src/assets/images/contact/union.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/contact/union.png -------------------------------------------------------------------------------- /src/assets/images/disturb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/disturb.png -------------------------------------------------------------------------------- /src/assets/images/empty_chat_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/empty_chat_bg.png -------------------------------------------------------------------------------- /src/assets/images/login/login_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/login/login_bg.png -------------------------------------------------------------------------------- /src/assets/images/login/login_pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/login/login_pc.png -------------------------------------------------------------------------------- /src/assets/images/login/login_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/login/login_qr.png -------------------------------------------------------------------------------- /src/assets/images/messageItem/file_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageItem/file_download.png -------------------------------------------------------------------------------- /src/assets/images/messageItem/file_downloaded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageItem/file_downloaded.png -------------------------------------------------------------------------------- /src/assets/images/messageItem/file_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageItem/file_icon.png -------------------------------------------------------------------------------- /src/assets/images/messageItem/location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageItem/location.png -------------------------------------------------------------------------------- /src/assets/images/messageItem/play_video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageItem/play_video.png -------------------------------------------------------------------------------- /src/assets/images/messageMenu/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageMenu/check.png -------------------------------------------------------------------------------- /src/assets/images/messageMenu/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageMenu/copy.png -------------------------------------------------------------------------------- /src/assets/images/messageMenu/emoji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageMenu/emoji.png -------------------------------------------------------------------------------- /src/assets/images/messageMenu/forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageMenu/forward.png -------------------------------------------------------------------------------- /src/assets/images/messageMenu/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageMenu/remove.png -------------------------------------------------------------------------------- /src/assets/images/messageMenu/reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageMenu/reply.png -------------------------------------------------------------------------------- /src/assets/images/messageMenu/revoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/messageMenu/revoke.png -------------------------------------------------------------------------------- /src/assets/images/moments/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/moments/background.png -------------------------------------------------------------------------------- /src/assets/images/nav/nav_bar_contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/nav/nav_bar_contact.png -------------------------------------------------------------------------------- /src/assets/images/nav/nav_bar_contact_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/nav/nav_bar_contact_active.png -------------------------------------------------------------------------------- /src/assets/images/nav/nav_bar_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/nav/nav_bar_message.png -------------------------------------------------------------------------------- /src/assets/images/nav/nav_bar_message_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/nav/nav_bar_message_active.png -------------------------------------------------------------------------------- /src/assets/images/nav/nav_bar_moments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/nav/nav_bar_moments.png -------------------------------------------------------------------------------- /src/assets/images/nav/nav_bar_moments_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/nav/nav_bar_moments_active.png -------------------------------------------------------------------------------- /src/assets/images/nav/nav_bar_workbench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/nav/nav_bar_workbench.png -------------------------------------------------------------------------------- /src/assets/images/nav/nav_bar_workbench_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/nav/nav_bar_workbench_active.png -------------------------------------------------------------------------------- /src/assets/images/profile/change_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/profile/change_avatar.png -------------------------------------------------------------------------------- /src/assets/images/profile/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/profile/logo.png -------------------------------------------------------------------------------- /src/assets/images/rtc/rtc_accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/rtc/rtc_accept.png -------------------------------------------------------------------------------- /src/assets/images/rtc/rtc_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/rtc/rtc_camera.png -------------------------------------------------------------------------------- /src/assets/images/rtc/rtc_camera_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/rtc/rtc_camera_off.png -------------------------------------------------------------------------------- /src/assets/images/rtc/rtc_hungup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/rtc/rtc_hungup.png -------------------------------------------------------------------------------- /src/assets/images/rtc/rtc_mic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/rtc/rtc_mic.png -------------------------------------------------------------------------------- /src/assets/images/rtc/rtc_mic_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/rtc/rtc_mic_off.png -------------------------------------------------------------------------------- /src/assets/images/searchModal/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/searchModal/empty.png -------------------------------------------------------------------------------- /src/assets/images/topSearchBar/add_friend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/topSearchBar/add_friend.png -------------------------------------------------------------------------------- /src/assets/images/topSearchBar/add_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/topSearchBar/add_group.png -------------------------------------------------------------------------------- /src/assets/images/topSearchBar/create_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/topSearchBar/create_group.png -------------------------------------------------------------------------------- /src/assets/images/topSearchBar/meeting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/topSearchBar/meeting.png -------------------------------------------------------------------------------- /src/assets/images/topSearchBar/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/topSearchBar/search.png -------------------------------------------------------------------------------- /src/assets/images/topSearchBar/show_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/topSearchBar/show_more.png -------------------------------------------------------------------------------- /src/assets/images/topSearchBar/win_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/topSearchBar/win_close.png -------------------------------------------------------------------------------- /src/assets/images/topSearchBar/win_max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/topSearchBar/win_max.png -------------------------------------------------------------------------------- /src/assets/images/topSearchBar/win_min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openimsdk/openim-electron-demo/d1f60da66003d18be8286b58b57fc46504b6eae8/src/assets/images/topSearchBar/win_min.png -------------------------------------------------------------------------------- /src/assets/node.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/CKEditor/index.scss: -------------------------------------------------------------------------------- 1 | .image-inline { 2 | margin: 2px; 3 | img { 4 | aspect-ratio: auto !important; 5 | max-width: 30vw !important; 6 | max-height: 15vh !important; 7 | width: auto !important; 8 | object-fit: contain; 9 | cursor: pointer; 10 | } 11 | } 12 | 13 | .ck-content { 14 | padding: 0 18px !important; 15 | border: none !important; 16 | word-break: break-all; 17 | } 18 | 19 | .ck-content [draggable="true"] { 20 | pointer-events: none; 21 | } 22 | 23 | .ck-content > p { 24 | margin: 10px 0 !important; 25 | } 26 | 27 | .ck.ck-toolbar { 28 | border: none !important; 29 | } 30 | 31 | .ck-editor__editable { 32 | --ck-inner-shadow: none !important; 33 | --ck-drop-shadow: none !important; 34 | --ck-drop-shadow-active: none !important; 35 | outline: none !important; 36 | } 37 | 38 | .ck-editor__editable:focus { 39 | box-shadow: none !important; 40 | } 41 | 42 | .ck.ck-editor__editable[role="textbox"]:focus { 43 | border: none; 44 | box-shadow: none !important; 45 | } 46 | 47 | .ck-powered-by { 48 | display: none !important; 49 | } 50 | 51 | .ck-widget__type-around__button, 52 | .ck-widget__type-around__button_after { 53 | display: none !important; 54 | } 55 | 56 | .ck-editor { 57 | flex: 1; 58 | overflow-y: auto; 59 | } 60 | 61 | .ck-editor__main, 62 | .ck-content { 63 | height: 100% !important; 64 | } 65 | 66 | .ck-image-upload-complete-icon { 67 | display: none !important; 68 | } 69 | 70 | .ck-image-upload-complete-icon:after { 71 | display: none !important; 72 | } 73 | 74 | .ck-balloon-panel_visible { 75 | border-radius: 10px !important; 76 | border: none !important; 77 | box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 78 | 0 9px 28px 8px rgba(0, 0, 0, 0.05) !important; 79 | overflow: hidden !important; 80 | } 81 | 82 | .ck-list__item .ck-on { 83 | background-color: #f0f0f0 !important; 84 | } 85 | 86 | .ck .ck-widget { 87 | --ck-widget-outline-thickness: 2px !important; 88 | --ck-color-widget-hover-border: transparent !important; 89 | } 90 | 91 | .ck .ck-widget.ck-widget_selected { 92 | --ck-widget-outline-thickness: 2px !important; 93 | } 94 | 95 | .ck.ck-clipboard-drop-target-line { 96 | --ck-clipboard-drop-target-color: transparent !important; 97 | } 98 | 99 | .ck-powered-by-balloon { 100 | z-index: -99; 101 | } 102 | 103 | .ck-editor__top { 104 | display: none; 105 | } 106 | -------------------------------------------------------------------------------- /src/components/CKEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.scss"; 2 | import "ckeditor5/ckeditor5.css"; 3 | 4 | import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic"; 5 | import { Essentials } from "@ckeditor/ckeditor5-essentials"; 6 | import { Paragraph } from "@ckeditor/ckeditor5-paragraph"; 7 | import { CKEditor } from "@ckeditor/ckeditor5-react"; 8 | import { 9 | forwardRef, 10 | ForwardRefRenderFunction, 11 | memo, 12 | useImperativeHandle, 13 | useRef, 14 | } from "react"; 15 | 16 | export type CKEditorRef = { 17 | focus: (moveToEnd?: boolean) => void; 18 | }; 19 | 20 | interface CKEditorProps { 21 | value: string; 22 | placeholder?: string; 23 | onChange?: (value: string) => void; 24 | onEnter?: () => void; 25 | } 26 | 27 | export interface EmojiData { 28 | src: string; 29 | alt: string; 30 | } 31 | 32 | const keyCodes = { 33 | delete: 46, 34 | backspace: 8, 35 | }; 36 | 37 | const Index: ForwardRefRenderFunction = ( 38 | { value, placeholder, onChange, onEnter }, 39 | ref, 40 | ) => { 41 | const ckEditor = useRef(null); 42 | 43 | const focus = (moveToEnd = false) => { 44 | const editor = ckEditor.current; 45 | 46 | if (editor) { 47 | const model = editor.model; 48 | const view = editor.editing.view; 49 | const root = model.document.getRoot(); 50 | if (moveToEnd && root) { 51 | const range = model.createRange(model.createPositionAt(root, "end")); 52 | 53 | model.change((writer) => { 54 | writer.setSelection(range); 55 | }); 56 | } 57 | view.focus(); 58 | } 59 | }; 60 | 61 | const listenKeydown = (editor: ClassicEditor) => { 62 | editor.editing.view.document.on( 63 | "keydown", 64 | (evt, data) => { 65 | if (data.keyCode === 13 && !data.shiftKey) { 66 | data.preventDefault(); 67 | evt.stop(); 68 | onEnter?.(); 69 | return; 70 | } 71 | if (data.keyCode === keyCodes.backspace || data.keyCode === keyCodes.delete) { 72 | const selection = editor.model.document.selection; 73 | const hasSelectContent = !editor.model.getSelectedContent(selection).isEmpty; 74 | const hasEditorContent = Boolean(editor.getData()); 75 | 76 | if (!hasEditorContent) { 77 | return; 78 | } 79 | 80 | if (hasSelectContent) return; 81 | } 82 | }, 83 | { priority: "high" }, 84 | ); 85 | }; 86 | 87 | useImperativeHandle( 88 | ref, 89 | () => ({ 90 | focus, 91 | }), 92 | [], 93 | ); 94 | 95 | return ( 96 | { 111 | ckEditor.current = editor; 112 | listenKeydown(editor); 113 | focus(true); 114 | }} 115 | onChange={(event, editor) => { 116 | const data = editor.getData(); 117 | onChange?.(data); 118 | }} 119 | /> 120 | ); 121 | }; 122 | 123 | export default memo(forwardRef(Index)); 124 | -------------------------------------------------------------------------------- /src/components/CKEditor/utils.ts: -------------------------------------------------------------------------------- 1 | export const replaceEmoji2Str = (text: string) => { 2 | const parser = new DOMParser(); 3 | const doc = parser.parseFromString(text, "text/html"); 4 | 5 | const emojiEls: HTMLImageElement[] = Array.from(doc.querySelectorAll(".emojione")); 6 | emojiEls.map((face) => { 7 | // @ts-ignore 8 | const escapedOut = face.outerHTML.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); 9 | text = text.replace(new RegExp(escapedOut, "g"), face.alt); 10 | }); 11 | return text; 12 | }; 13 | 14 | export const getCleanText = (html: string) => { 15 | let text = replaceEmoji2Str(html); 16 | text = text.replace(/<\/p>

/g, "\n"); 17 | text = text.replace(//gi, "\n"); 18 | text = text.replace(/<[^>]+>/g, ""); 19 | text = convertChar(text); 20 | text = decodeHtmlEntities(text); 21 | return text.trim(); 22 | }; 23 | 24 | let textAreaDom: HTMLTextAreaElement | null = null; 25 | const decodeHtmlEntities = (text: string) => { 26 | if (!textAreaDom) { 27 | textAreaDom = document.createElement("textarea"); 28 | } 29 | textAreaDom.innerHTML = text; 30 | return textAreaDom.value; 31 | }; 32 | 33 | export const convertChar = (text: string) => text.replace(/ /gi, " "); 34 | 35 | export const getCleanTextExceptImg = (html: string) => { 36 | html = replaceEmoji2Str(html); 37 | 38 | const regP = /<\/p>

/g; 39 | html = html.replace(regP, "


"); 40 | 41 | const regBr = //gi; 42 | html = html.replace(regBr, "\n"); 43 | 44 | const regWithoutHtmlExceptImg = /<(?!img\s*\/?)[^>]+>/gi; 45 | return html.replace(regWithoutHtmlExceptImg, ""); 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/DraggableModalWrap/index.tsx: -------------------------------------------------------------------------------- 1 | import { Modal, ModalProps } from "antd"; 2 | import { FC, memo, useRef, useState } from "react"; 3 | import type { DraggableData, DraggableEvent } from "react-draggable"; 4 | import Draggable from "react-draggable"; 5 | 6 | interface IDraggableModalWrapProps extends ModalProps { 7 | ignoreClasses?: string; 8 | } 9 | 10 | const DraggableModalWrap: FC = (props) => { 11 | const [bounds, setBounds] = useState({ left: 0, top: 0, bottom: 0, right: 0 }); 12 | const draggleRef = useRef(null); 13 | 14 | const onStart = (_event: DraggableEvent, uiData: DraggableData) => { 15 | const { clientWidth, clientHeight } = window.document.documentElement; 16 | const targetRect = draggleRef.current?.getBoundingClientRect(); 17 | if (!targetRect) { 18 | return; 19 | } 20 | setBounds({ 21 | left: -targetRect.left + uiData.x, 22 | right: clientWidth - (targetRect.right - uiData.x), 23 | top: -targetRect.top + uiData.y, 24 | bottom: clientHeight - (targetRect.bottom - uiData.y), 25 | }); 26 | }; 27 | 28 | return ( 29 | ( 32 | onStart(event, uiData)} 37 | > 38 |

{modal}
39 | 40 | )} 41 | > 42 | {props.children} 43 | 44 | ); 45 | }; 46 | 47 | export default memo(DraggableModalWrap); 48 | -------------------------------------------------------------------------------- /src/components/EditableContent/index.tsx: -------------------------------------------------------------------------------- 1 | import { EnterOutlined } from "@ant-design/icons"; 2 | import { useClickAway } from "ahooks"; 3 | import { Input, InputRef } from "antd"; 4 | import clsx from "clsx"; 5 | import { FC, useRef, useState } from "react"; 6 | 7 | import edit_name from "@/assets/images/chatSetting/edit_name.png"; 8 | 9 | interface IEditableContentProps { 10 | editable?: boolean; 11 | value?: string; 12 | placeholder?: string; 13 | className?: string; 14 | textClassName?: string; 15 | onChange?: (value: string) => Promise; 16 | } 17 | 18 | const EditableContent: FC = ({ 19 | editable, 20 | value, 21 | placeholder, 22 | className, 23 | textClassName, 24 | onChange, 25 | }) => { 26 | const wrapRef = useRef(null); 27 | const inputRef = useRef(null); 28 | const [editState, setEditState] = useState({ 29 | isEdit: false, 30 | loading: false, 31 | innerValue: value, 32 | }); 33 | 34 | useClickAway(() => { 35 | if (editState.isEdit) { 36 | setEditState({ 37 | isEdit: false, 38 | loading: false, 39 | innerValue: value, 40 | }); 41 | } 42 | }, [wrapRef]); 43 | 44 | const toggleEdit = (e: React.MouseEvent) => { 45 | e.stopPropagation(); 46 | setEditState({ 47 | isEdit: true, 48 | loading: false, 49 | innerValue: value === "-" ? "" : value, 50 | }); 51 | setTimeout(() => inputRef.current?.focus()); 52 | }; 53 | 54 | const onPressEnter = async ( 55 | e: React.KeyboardEvent & { target: { value: string } }, 56 | ) => { 57 | setEditState((state) => ({ ...state, loading: true })); 58 | await onChange?.(e.target.value); 59 | setEditState({ 60 | isEdit: false, 61 | loading: false, 62 | innerValue: e.target.value, 63 | }); 64 | }; 65 | 66 | return ( 67 |
68 | {editState.isEdit ? ( 69 | 75 | setEditState((state) => ({ ...state, innerValue: e.target.value })) 76 | } 77 | ref={inputRef} 78 | onPressEnter={onPressEnter} 79 | suffix={} 80 | /> 81 | ) : ( 82 | <> 83 |
84 | {value} 85 |
86 | {editable && ( 87 | edit name 94 | )} 95 | 96 | )} 97 |
98 | ); 99 | }; 100 | 101 | export default EditableContent; 102 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ErrorInfo, ReactNode } from "react"; 2 | 3 | type ErrorBoundaryProps = { 4 | logTips?: string; 5 | placeholder: ReactNode; 6 | children: ReactNode; 7 | }; 8 | 9 | type ErrorBoundaryState = { 10 | hasError: boolean; 11 | logTips?: string; 12 | }; 13 | 14 | class ErrorBoundary extends Component { 15 | constructor(props: ErrorBoundaryProps) { 16 | super(props); 17 | this.state = { hasError: false, logTips: props.logTips }; 18 | } 19 | 20 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 21 | console.error(this.state.logTips ?? "ErrorBoundary"); 22 | console.error(errorInfo); 23 | console.error(error); 24 | 25 | this.setState({ hasError: true }); 26 | } 27 | 28 | render() { 29 | if (this.state.hasError) { 30 | return this.props.placeholder; 31 | } 32 | 33 | return this.props.children; 34 | } 35 | } 36 | 37 | export default ErrorBoundary; 38 | -------------------------------------------------------------------------------- /src/components/FlexibleSider/flexible-sider.module.scss: -------------------------------------------------------------------------------- 1 | .sider_resize { 2 | width: 300px; 3 | min-width: 240px; 4 | max-width: 45vw; 5 | resize: horizontal; 6 | overflow: scroll; 7 | height: 100%; 8 | opacity: 0; 9 | 10 | &::-webkit-scrollbar { 11 | height: calc(100vh - 40px); 12 | } 13 | } 14 | 15 | .sider_bar { 16 | position: absolute; 17 | right: 0; 18 | top: 0; 19 | bottom: 0; 20 | border-left: 1px solid #e8eaef; 21 | pointer-events: none; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/FlexibleSider/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import * as React from "react"; 3 | 4 | import styles from "./flexible-sider.module.scss"; 5 | 6 | const FlexibleSider = ({ 7 | needHidden, 8 | children, 9 | wrapClassName, 10 | }: { 11 | needHidden: boolean; 12 | wrapClassName?: string; 13 | children: React.ReactNode; 14 | }) => ( 15 | 32 | ); 33 | 34 | export default FlexibleSider; 35 | -------------------------------------------------------------------------------- /src/components/OIMAvatar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar as AntdAvatar, AvatarProps } from "antd"; 2 | import clsx from "clsx"; 3 | import * as React from "react"; 4 | import { useMemo } from "react"; 5 | 6 | import default_group from "@/assets/images/contact/group.png"; 7 | import { avatarList, getDefaultAvatar } from "@/utils/avatar"; 8 | 9 | const default_avatars = avatarList.map((item) => item.name); 10 | 11 | interface IOIMAvatarProps extends AvatarProps { 12 | text?: string; 13 | color?: string; 14 | bgColor?: string; 15 | isgroup?: boolean; 16 | isnotification?: boolean; 17 | size?: number; 18 | } 19 | 20 | const OIMAvatar: React.FC = (props) => { 21 | const { 22 | src, 23 | text, 24 | size = 42, 25 | color = "#fff", 26 | bgColor = "#0289FA", 27 | isgroup = false, 28 | isnotification, 29 | } = props; 30 | const [errorHolder, setErrorHolder] = React.useState(); 31 | 32 | const getAvatarUrl = useMemo(() => { 33 | if (src) { 34 | if (default_avatars.includes(src as string)) 35 | return getDefaultAvatar(src as string); 36 | 37 | return src; 38 | } 39 | return isgroup ? default_group : undefined; 40 | }, [src, isgroup, isnotification]); 41 | 42 | const avatarProps = { ...props, isgroup: undefined, isnotification: undefined }; 43 | 44 | React.useEffect(() => { 45 | if (!isgroup) { 46 | setErrorHolder(undefined); 47 | } 48 | }, [isgroup]); 49 | 50 | const errorHandler = () => { 51 | if (isgroup) { 52 | setErrorHolder(default_group); 53 | } 54 | }; 55 | 56 | return ( 57 | 76 | {text} 77 | 78 | ); 79 | }; 80 | 81 | export default OIMAvatar; 82 | -------------------------------------------------------------------------------- /src/components/SettingRow/index.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from "antd"; 2 | import clsx from "clsx"; 3 | import { FC, ReactNode, useState } from "react"; 4 | 5 | interface ISettingRowProps { 6 | title: string; 7 | value?: boolean; 8 | hidden?: boolean; 9 | className?: string; 10 | children?: ReactNode; 11 | tryChange?: (checked: boolean) => Promise; 12 | rowClick?: () => void; 13 | } 14 | 15 | const SettingRow: FC = ({ 16 | title, 17 | value, 18 | hidden, 19 | className, 20 | children, 21 | tryChange, 22 | rowClick, 23 | }) => { 24 | const [loading, setLoading] = useState(false); 25 | const onClick = async (checked: boolean) => { 26 | setLoading(true); 27 | await tryChange?.(checked); 28 | setLoading(false); 29 | }; 30 | 31 | if (hidden) { 32 | return null; 33 | } 34 | 35 | return ( 36 |
40 |
{title}
41 | {children ?? ( 42 | 48 | )} 49 |
50 | ); 51 | }; 52 | 53 | export default SettingRow; 54 | -------------------------------------------------------------------------------- /src/components/WindowControlBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Platform } from "@openim/wasm-client-sdk"; 2 | import { useKeyPress } from "ahooks"; 3 | 4 | import win_close from "@/assets/images/topSearchBar/win_close.png"; 5 | import win_max from "@/assets/images/topSearchBar/win_max.png"; 6 | import win_min from "@/assets/images/topSearchBar/win_min.png"; 7 | 8 | const WindowControlBar = () => { 9 | useKeyPress("esc", () => { 10 | window.electronAPI?.ipcInvoke("minimizeWindow"); 11 | }); 12 | 13 | if (!window.electronAPI || window.electronAPI?.getPlatform() === Platform.MacOSX) { 14 | return null; 15 | } 16 | return ( 17 |
18 |
window.electronAPI?.ipcInvoke("minimizeWindow")} 21 | > 22 | win_min 28 |
29 | win_max window.electronAPI?.ipcInvoke("maxmizeWindow")} 35 | /> 36 | win_close window.electronAPI?.ipcInvoke("closeWindow")} 42 | /> 43 |
44 | ); 45 | }; 46 | 47 | export default WindowControlBar; 48 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export const APP_NAME = "OpenCorp-Base"; 2 | export const APP_VERSION = "v3.8.3"; 3 | export const SDK_VERSION = "SDK(ffi) v3.8.3"; 4 | export const isSaveLog = process.env.NODE_ENV !== "development"; 5 | -------------------------------------------------------------------------------- /src/constants/errcode.ts: -------------------------------------------------------------------------------- 1 | import { t } from "i18next"; 2 | 3 | export const ErrCodeMap: Record = { 4 | 20001: t("errCode.passwordError"), 5 | 20002: t("errCode.accountNotExist"), 6 | 20003: t("errCode.phoneNumberRegistered"), 7 | 20004: t("errCode.accountRegistered"), 8 | 20005: t("errCode.operationTooFrequent"), 9 | 20012: t("errCode.operationRestriction"), 10 | 20014: t("errCode.accountRegistered"), 11 | }; 12 | 13 | export enum SendFailedErrCode { 14 | Blacked = 1302, 15 | } 16 | -------------------------------------------------------------------------------- /src/constants/im.ts: -------------------------------------------------------------------------------- 1 | import { MessageType, SessionType } from "@openim/wasm-client-sdk"; 2 | 3 | export const GroupSessionTypes = [SessionType.Group, SessionType.WorkingGroup]; 4 | 5 | export const GroupSystemMessageTypes = [ 6 | MessageType.GroupCreated, 7 | MessageType.GroupInfoUpdated, 8 | MessageType.MemberQuit, 9 | MessageType.GroupOwnerTransferred, 10 | MessageType.MemberKicked, 11 | MessageType.MemberInvited, 12 | MessageType.MemberEnter, 13 | MessageType.GroupDismissed, 14 | MessageType.GroupMemberMuted, 15 | MessageType.GroupMuted, 16 | MessageType.GroupCancelMuted, 17 | MessageType.GroupMemberCancelMuted, 18 | MessageType.GroupNameUpdated, 19 | ]; 20 | 21 | export const SystemMessageTypes = [ 22 | MessageType.RevokeMessage, 23 | MessageType.FriendAdded, 24 | MessageType.BurnMessageChange, 25 | ...GroupSystemMessageTypes, 26 | ]; 27 | 28 | export enum CustomType { 29 | CallingInvite = 200, 30 | CallingAccept = 201, 31 | CallingReject = 202, 32 | CallingCancel = 203, 33 | CallingHungup = 204, 34 | } 35 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./errcode"; 2 | export * from "./im"; 3 | -------------------------------------------------------------------------------- /src/hooks/useConversationToggle.ts: -------------------------------------------------------------------------------- 1 | import type { SessionType } from "@openim/wasm-client-sdk"; 2 | import { ConversationItem } from "@openim/wasm-client-sdk/lib/types/entity"; 3 | import { useCallback } from "react"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | import { IMSDK } from "@/layout/MainContentWrap"; 7 | import { useConversationStore } from "@/store"; 8 | import { feedbackToast } from "@/utils/common"; 9 | 10 | export type ToSpecifiedConversationParams = { 11 | sourceID: string; 12 | sessionType: SessionType; 13 | isJump?: boolean; 14 | isChildWindow?: boolean; 15 | }; 16 | 17 | export function useConversationToggle() { 18 | const navigate = useNavigate(); 19 | const updateCurrentConversation = useConversationStore( 20 | (state) => state.updateCurrentConversation, 21 | ); 22 | 23 | const getConversation = async ({ 24 | sourceID, 25 | sessionType, 26 | }: { 27 | sourceID: string; 28 | sessionType: SessionType; 29 | }): Promise => { 30 | let conversation = useConversationStore 31 | .getState() 32 | .conversationList.find( 33 | (item) => item.userID === sourceID || item.groupID === sourceID, 34 | ); 35 | if (!conversation) { 36 | try { 37 | conversation = ( 38 | await IMSDK.getOneConversation({ 39 | sourceID, 40 | sessionType, 41 | }) 42 | ).data; 43 | } catch (error) { 44 | feedbackToast({ error }); 45 | } 46 | } 47 | return conversation; 48 | }; 49 | 50 | const toSpecifiedConversation = useCallback( 51 | async (params: ToSpecifiedConversationParams) => { 52 | const { sourceID, sessionType, isJump } = params; 53 | const conversation = await getConversation({ sourceID, sessionType }); 54 | if ( 55 | !conversation || 56 | useConversationStore.getState().currentConversation?.conversationID === 57 | conversation.conversationID 58 | ) 59 | return; 60 | await updateCurrentConversation({ ...conversation }, isJump); 61 | navigate(`/chat/${conversation.conversationID}`); 62 | }, 63 | [], 64 | ); 65 | 66 | return { 67 | toSpecifiedConversation, 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/hooks/useCurrentMemberRole.ts: -------------------------------------------------------------------------------- 1 | import { GroupMemberRole } from "@openim/wasm-client-sdk"; 2 | 3 | import { useConversationStore } from "@/store"; 4 | 5 | export function useCurrentMemberRole() { 6 | const currentMemberInGroup = useConversationStore( 7 | (state) => state.currentMemberInGroup, 8 | ); 9 | 10 | const isOwner = currentMemberInGroup?.roleLevel === GroupMemberRole.Owner; 11 | const isAdmin = currentMemberInGroup?.roleLevel === GroupMemberRole.Admin; 12 | const isNomal = currentMemberInGroup?.roleLevel === GroupMemberRole.Normal; 13 | const currentRolevel = currentMemberInGroup?.roleLevel ?? 0; 14 | const currentIsMuted = (currentMemberInGroup?.muteEndTime ?? 0) > Date.now(); 15 | 16 | return { 17 | isOwner, 18 | isAdmin, 19 | isNomal, 20 | isJoinGroup: Boolean(currentMemberInGroup?.userID), 21 | currentRolevel, 22 | currentIsMuted, 23 | currentMemberInGroup, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useGroupMembers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GroupMemberItem, 3 | } from "@openim/wasm-client-sdk/lib/types/entity"; 4 | import { useLatest } from "ahooks"; 5 | import { useCallback, useState } from "react"; 6 | 7 | import { IMSDK } from "@/layout/MainContentWrap"; 8 | import { useConversationStore } from "@/store"; 9 | import { feedbackToast } from "@/utils/common"; 10 | export interface FetchStateType { 11 | offset: number; 12 | count: number; 13 | loading: boolean; 14 | hasMore: boolean; 15 | groupMemberList: GroupMemberItem[]; 16 | } 17 | 18 | interface UseGroupMembersProps { 19 | groupID?: string; 20 | } 21 | 22 | export default function useGroupMembers(props?: UseGroupMembersProps) { 23 | const { groupID } = props ?? {}; 24 | const [fetchState, setFetchState] = useState({ 25 | offset: 0, 26 | count: 20, 27 | loading: false, 28 | hasMore: true, 29 | groupMemberList: [], 30 | }); 31 | const latestFetchState = useLatest(fetchState); 32 | 33 | const getMemberData = useCallback( 34 | async (refresh = false) => { 35 | const sourceID = 36 | groupID ?? useConversationStore.getState().currentConversation?.groupID ?? ""; 37 | if (!sourceID) return; 38 | 39 | if ( 40 | (latestFetchState.current.loading || !latestFetchState.current.hasMore) && 41 | !refresh 42 | ) 43 | return; 44 | 45 | setFetchState((state) => ({ 46 | ...state, 47 | loading: true, 48 | })); 49 | try { 50 | const { data } = await IMSDK.getGroupMemberList({ 51 | groupID: sourceID, 52 | offset: refresh ? 0 : latestFetchState.current.offset, 53 | count: refresh ? 500 : 100, 54 | filter: 0, 55 | }); 56 | setFetchState((state) => ({ 57 | ...state, 58 | groupMemberList: [...(refresh ? [] : state.groupMemberList), ...data], 59 | hasMore: data.length === (refresh ? 500 : 100), 60 | offset: state.offset + (refresh ? 500 : 100), 61 | loading: false, 62 | })); 63 | } catch (error) { 64 | feedbackToast({ 65 | msg: "getMemberFailed", 66 | error, 67 | }); 68 | setFetchState((state) => ({ 69 | ...state, 70 | loading: false, 71 | })); 72 | } 73 | }, 74 | [groupID], 75 | ); 76 | 77 | const resetState = () => { 78 | setFetchState({ 79 | offset: 0, 80 | count: 20, 81 | loading: false, 82 | hasMore: true, 83 | groupMemberList: [], 84 | }); 85 | }; 86 | 87 | return { 88 | fetchState, 89 | getMemberData, 90 | resetState, 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /src/hooks/useOverlayVisible.ts: -------------------------------------------------------------------------------- 1 | import { ForwardedRef, useCallback, useImperativeHandle, useState } from "react"; 2 | 3 | export interface OverlayVisibleHandle { 4 | isOverlayOpen: boolean; 5 | openOverlay: () => void; 6 | closeOverlay: () => void; 7 | } 8 | 9 | export function useOverlayVisible(ref: ForwardedRef) { 10 | const [isOverlayOpen, setIsOverlayOpen] = useState(false); 11 | 12 | const openOverlay = useCallback(() => { 13 | setIsOverlayOpen(true); 14 | }, []); 15 | const closeOverlay = useCallback(() => { 16 | setIsOverlayOpen(false); 17 | }, []); 18 | 19 | useImperativeHandle(ref, () => ({ 20 | isOverlayOpen, 21 | openOverlay, 22 | closeOverlay, 23 | })); 24 | 25 | return { 26 | isOverlayOpen, 27 | openOverlay, 28 | closeOverlay, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import "dayjs/locale/zh-cn"; 2 | 3 | import dayjs from "dayjs"; 4 | import i18n from "i18next"; 5 | // import LanguageDetector from "i18next-browser-languagedetector"; 6 | import { initReactI18next } from "react-i18next"; 7 | 8 | import { getLocale } from "@/utils/storage"; 9 | 10 | import translation_en from "./resources/en.json"; 11 | import translation_zh from "./resources/zh.json"; 12 | 13 | const resources = { 14 | "en-US": { 15 | translation: translation_en, 16 | }, 17 | "zh-CN": { 18 | translation: translation_zh, 19 | }, 20 | zh: { 21 | translation: translation_zh, 22 | }, 23 | }; 24 | 25 | i18n 26 | .use(initReactI18next) 27 | // .use(LanguageDetector) 28 | .init({ 29 | resources, 30 | lng: getLocale(), 31 | fallbackLng: "en-US", 32 | interpolation: { 33 | escapeValue: false, 34 | }, 35 | }) 36 | .then(() => dayjs.locale(i18n.language)) 37 | .catch(() => console.error("i18n init error")); 38 | 39 | i18n.on("languageChanged", () => dayjs.locale(i18n.language)); 40 | 41 | export default i18n; 42 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @use "./styles/global.scss"; 2 | @use "./styles/antd.scss"; 3 | @use "./styles/svg.scss"; 4 | 5 | @tailwind base; 6 | @tailwind components; 7 | @tailwind utilities; 8 | -------------------------------------------------------------------------------- /src/layout/LeftNavBar/BlackList.tsx: -------------------------------------------------------------------------------- 1 | import { CloseOutlined } from "@ant-design/icons"; 2 | import { BlackUserItem } from "@openim/wasm-client-sdk/lib/types/entity"; 3 | import { Button, Empty, Modal } from "antd"; 4 | import { t } from "i18next"; 5 | import { forwardRef, ForwardRefRenderFunction, memo, useState } from "react"; 6 | 7 | import OIMAvatar from "@/components/OIMAvatar"; 8 | import { useContactStore } from "@/store/contact"; 9 | import { feedbackToast } from "@/utils/common"; 10 | 11 | import { OverlayVisibleHandle, useOverlayVisible } from "../../hooks/useOverlayVisible"; 12 | import { IMSDK } from "../MainContentWrap"; 13 | 14 | const BlackList: ForwardRefRenderFunction = (_, ref) => { 15 | const { isOverlayOpen, closeOverlay } = useOverlayVisible(ref); 16 | 17 | return ( 18 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default memo(forwardRef(BlackList)); 42 | 43 | const BlackItem = ({ 44 | black, 45 | removeBlack, 46 | }: { 47 | black: BlackUserItem; 48 | removeBlack: (userID: string) => Promise; 49 | }) => { 50 | const [loading, setLoading] = useState(false); 51 | 52 | const tryRemove = async () => { 53 | setLoading(true); 54 | await removeBlack(black.userID); 55 | setLoading(false); 56 | }; 57 | 58 | return ( 59 |
60 |
61 | 62 |
{black.nickname}
63 |
64 | 67 |
68 | ); 69 | }; 70 | 71 | export const BlackListContent = ({ closeOverlay }: { closeOverlay?: () => void }) => { 72 | const blackList = useContactStore((state) => state.blackList); 73 | 74 | const removeBlack = async (userID: string) => { 75 | try { 76 | await IMSDK.removeBlack(userID); 77 | } catch (error) { 78 | feedbackToast({ error }); 79 | } 80 | }; 81 | 82 | return ( 83 |
84 |
85 | {t("placeholder.blackList")} 86 | 91 |
92 |
93 | {blackList.length > 0 ? ( 94 | blackList.map((black) => ( 95 | 96 | )) 97 | ) : ( 98 | 99 | )} 100 |
101 |
102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /src/layout/LeftNavBar/left-nav-bar.module.scss: -------------------------------------------------------------------------------- 1 | .avatar-wrapper { 2 | @apply mr-4 relative; 3 | 4 | &:hover { 5 | .mask { 6 | @apply opacity-100 cursor-pointer; 7 | } 8 | } 9 | 10 | .mask { 11 | @apply flex justify-center items-center bg-[rgba(0,0,0,.6)] absolute z-10 top-0 left-0 w-full h-full rounded-md opacity-0 transition-opacity; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/layout/MainContentLayout.tsx: -------------------------------------------------------------------------------- 1 | import { useMount } from "ahooks"; 2 | import { Layout, Spin } from "antd"; 3 | import { t } from "i18next"; 4 | import { Outlet, useMatches, useNavigate } from "react-router-dom"; 5 | 6 | import { useUserStore } from "@/store"; 7 | 8 | import LeftNavBar from "./LeftNavBar"; 9 | import TopSearchBar from "./TopSearchBar"; 10 | import { useGlobalEvent } from "./useGlobalEvents"; 11 | 12 | export const MainContentLayout = () => { 13 | useGlobalEvent(); 14 | const matches = useMatches(); 15 | const navigate = useNavigate(); 16 | 17 | const progress = useUserStore((state) => state.progress); 18 | const syncState = useUserStore((state) => state.syncState); 19 | const reinstall = useUserStore((state) => state.reinstall); 20 | const isLogining = useUserStore((state) => state.isLogining); 21 | 22 | useMount(() => { 23 | const isRoot = !matches.find((item) => item.pathname !== "/"); 24 | const inConversation = matches.some((item) => item.params.conversationID); 25 | if (isRoot || inConversation) { 26 | navigate("chat", { 27 | replace: true, 28 | }); 29 | } 30 | }); 31 | 32 | const loadingTip = isLogining ? t("toast.loading") : `${progress}%`; 33 | const showLockLoading = isLogining || (reinstall && syncState === "loading"); 34 | 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/layout/MainContentWrap.tsx: -------------------------------------------------------------------------------- 1 | import { getWithRenderProcess } from "@openim/electron-client-sdk/lib/render"; 2 | import { AllowType } from "@openim/wasm-client-sdk"; 3 | import { useEffect } from "react"; 4 | import { Outlet, useLocation, useNavigate } from "react-router-dom"; 5 | 6 | import { useConversationStore, useUserStore } from "@/store"; 7 | import { emit } from "@/utils/events"; 8 | import { getIMToken, getIMUserID } from "@/utils/storage"; 9 | 10 | // const isElectronProd = import.meta.env.MODE !== "development" && window.electronAPI; 11 | 12 | const { instance } = getWithRenderProcess({ 13 | wasmConfig: { 14 | coreWasmPath: "./openIM.wasm", 15 | sqlWasmPath: `/sql-wasm.wasm`, 16 | }, 17 | }); 18 | const openIMSDK = instance; 19 | 20 | export const IMSDK = openIMSDK; 21 | 22 | export const MainContentWrap = () => { 23 | const updateAppSettings = useUserStore((state) => state.updateAppSettings); 24 | 25 | const navigate = useNavigate(); 26 | const location = useLocation(); 27 | 28 | useEffect(() => { 29 | const loginCheck = async () => { 30 | const IMToken = await getIMToken(); 31 | const IMUserID = await getIMUserID(); 32 | if (!IMToken || !IMUserID) { 33 | navigate("/login"); 34 | return; 35 | } 36 | }; 37 | 38 | loginCheck(); 39 | }, [location.pathname]); 40 | 41 | useEffect(() => { 42 | window.userClick = (userID?: string, groupID?: string) => { 43 | if (!userID || userID === "AtAllTag") return; 44 | 45 | const currentGroupInfo = useConversationStore.getState().currentGroupInfo; 46 | 47 | if (groupID && currentGroupInfo?.lookMemberInfo === AllowType.NotAllowed) { 48 | return; 49 | } 50 | 51 | emit("OPEN_USER_CARD", { 52 | userID, 53 | groupID, 54 | isSelf: userID === useUserStore.getState().selfInfo.userID, 55 | notAdd: 56 | Boolean(groupID) && 57 | currentGroupInfo?.applyMemberFriend === AllowType.NotAllowed, 58 | }); 59 | }; 60 | }, []); 61 | 62 | useEffect(() => { 63 | const initSettingStore = async () => { 64 | if (!window.electronAPI) return; 65 | updateAppSettings({ 66 | closeAction: 67 | (await window.electronAPI?.ipcInvoke("getKeyStore", { 68 | key: "closeAction", 69 | })) || "miniSize", 70 | }); 71 | window.electronAPI?.ipcInvoke("main-win-ready"); 72 | }; 73 | 74 | initSettingStore(); 75 | }, []); 76 | 77 | return ; 78 | }; 79 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import "./index.scss"; 2 | import "./i18n/index"; 3 | 4 | import log from "electron-log/renderer"; 5 | import ReactDOM from "react-dom/client"; 6 | 7 | import App from "./App"; 8 | import { isSaveLog } from "./config"; 9 | 10 | if (window.electronAPI && isSaveLog) { 11 | const sdkLogger = log.scope("openim-sdk-core"); 12 | console.debug = sdkLogger.debug.bind(sdkLogger); 13 | const rendererLogger = log.scope("renderer"); 14 | console.info = rendererLogger.info.bind(rendererLogger); 15 | console.error = rendererLogger.error.bind(rendererLogger); 16 | } 17 | 18 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(); 19 | 20 | postMessage({ payload: "removeLoading" }, "*"); 21 | -------------------------------------------------------------------------------- /src/pages/chat/ConversationSider/ConversationItem.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ConversationItem, 3 | ConversationItem as ConversationItemType, 4 | MessageItem, 5 | } from "@openim/wasm-client-sdk/lib/types/entity"; 6 | import { Badge } from "antd"; 7 | import clsx from "clsx"; 8 | import { t } from "i18next"; 9 | import { memo, useMemo } from "react"; 10 | import { useNavigate } from "react-router-dom"; 11 | import OIMAvatar from "@/components/OIMAvatar"; 12 | import { useConversationStore, useUserStore } from "@/store"; 13 | import { formatConversionTime, getConversationContent } from "@/utils/imCommon"; 14 | 15 | import styles from "./conversation-item.module.scss"; 16 | 17 | interface IConversationProps { 18 | isActive: boolean; 19 | conversation: ConversationItemType; 20 | } 21 | 22 | const ConversationItem = ({ isActive, conversation }: IConversationProps) => { 23 | const navigate = useNavigate(); 24 | const updateCurrentConversation = useConversationStore( 25 | (state) => state.updateCurrentConversation, 26 | ); 27 | const currentUser = useUserStore((state) => state.selfInfo.userID); 28 | 29 | const toSpecifiedConversation = async () => { 30 | if (isActive) { 31 | return; 32 | } 33 | await updateCurrentConversation({ ...conversation }); 34 | navigate(`/chat/${conversation.conversationID}`); 35 | }; 36 | 37 | const latestMessageContent = useMemo(() => { 38 | let content = ""; 39 | if (!conversation.latestMsg) { 40 | return ""; 41 | } 42 | try { 43 | content = getConversationContent( 44 | JSON.parse(conversation.latestMsg) as MessageItem, 45 | ); 46 | } catch (error) { 47 | content = t("messageDescription.catchMessage"); 48 | } 49 | return content; 50 | }, [conversation.draftText, conversation.latestMsg, isActive, currentUser]); 51 | 52 | const latestMessageTime = formatConversionTime(conversation.latestMsgSendTime); 53 | 54 | return ( 55 |
63 | 64 | 69 | 70 | 71 |
72 |
73 |
{conversation.showName}
74 |
{latestMessageTime}
75 |
76 | 77 |
78 |
79 |
85 |
86 |
87 |
88 |
89 | ); 90 | }; 91 | 92 | export default memo(ConversationItem); 93 | -------------------------------------------------------------------------------- /src/pages/chat/ConversationSider/conversation-item.module.scss: -------------------------------------------------------------------------------- 1 | .conversation-item { 2 | @apply relative my-1 flex items-center rounded-md p-2 hover:bg-[var(--primary-active)]; 3 | 4 | &-pined { 5 | &::after { 6 | content: ""; 7 | position: absolute; 8 | right: 0; 9 | top: 0; 10 | border: 4px solid; 11 | border-color: var(--primary) var(--primary) transparent transparent; 12 | } 13 | } 14 | 15 | :global(.emojione) { 16 | width: 16px; 17 | height: 16px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/chat/ConversationSider/index.module.scss: -------------------------------------------------------------------------------- 1 | @keyframes loading { 2 | 0% { 3 | transform: rotate(0deg); 4 | } 5 | 6 | 100% { 7 | transform: rotate(360deg); 8 | } 9 | } 10 | 11 | .loading { 12 | animation: loading 1.5s infinite; 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/chat/ConversationSider/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { t } from "i18next"; 3 | import { useRef } from "react"; 4 | import { useParams } from "react-router-dom"; 5 | import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; 6 | 7 | import sync from "@/assets/images/common/sync.png"; 8 | import sync_error from "@/assets/images/common/sync_error.png"; 9 | import FlexibleSider from "@/components/FlexibleSider"; 10 | import { useConversationStore, useUserStore } from "@/store"; 11 | 12 | import ConversationItemComp from "./ConversationItem"; 13 | import styles from "./index.module.scss"; 14 | 15 | const ConnectBar = () => { 16 | const userStore = useUserStore(); 17 | const showLoading = 18 | userStore.syncState === "loading" || userStore.connectState === "loading"; 19 | const showFailed = 20 | userStore.syncState === "failed" || userStore.connectState === "failed"; 21 | 22 | const loadingTip = 23 | userStore.syncState === "loading" ? t("connect.syncing") : t("connect.connecting"); 24 | 25 | const errorTip = 26 | userStore.syncState === "failed" 27 | ? t("connect.syncFailed") 28 | : t("connect.connectFailed"); 29 | 30 | if (userStore.reinstall) { 31 | return null; 32 | } 33 | 34 | return ( 35 | <> 36 | {showLoading && ( 37 |
38 | sync 43 | {loadingTip} 44 |
45 | )} 46 | {showFailed && ( 47 |
48 | sync 49 | {errorTip} 50 |
51 | )} 52 | 53 | ); 54 | }; 55 | 56 | const ConversationSider = () => { 57 | const { conversationID } = useParams(); 58 | const conversationList = useConversationStore((state) => state.conversationList); 59 | const getConversationListByReq = useConversationStore( 60 | (state) => state.getConversationListByReq, 61 | ); 62 | const virtuoso = useRef(null); 63 | const hasmore = useRef(true); 64 | const loading = useRef(false); 65 | 66 | const endReached = async () => { 67 | if (!hasmore.current || loading.current) return; 68 | loading.current = true; 69 | hasmore.current = await getConversationListByReq(true); 70 | loading.current = false; 71 | }; 72 | 73 | return ( 74 |
75 | 76 | 80 | item.conversationID} 86 | itemContent={(_, conversation) => ( 87 | 91 | )} 92 | /> 93 | 94 |
95 | ); 96 | }; 97 | 98 | export default ConversationSider; 99 | -------------------------------------------------------------------------------- /src/pages/chat/EmptyChat.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Layout } from "antd"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import empty_chat_bg from "@/assets/images/empty_chat_bg.png"; 5 | import { emit } from "@/utils/events"; 6 | 7 | export const EmptyChat = () => { 8 | const { t } = useTranslation(); 9 | const createNow = () => { 10 | emit("OPEN_CHOOSE_MODAL", { 11 | type: "CRATE_GROUP", 12 | }); 13 | }; 14 | 15 | return ( 16 | 17 |
18 |
19 |
{t("placeholder.createGroup")}
20 |
21 | {t("placeholder.createGroupToast")} 22 |
23 |
24 | 25 | 26 |
27 | 30 |
31 |
32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/pages/chat/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from "antd"; 2 | import { Outlet } from "react-router-dom"; 3 | 4 | import ConversationSider from "./ConversationSider"; 5 | 6 | export const Chat = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/ChatContent.tsx: -------------------------------------------------------------------------------- 1 | import { SessionType } from "@openim/wasm-client-sdk"; 2 | import { Layout, Spin } from "antd"; 3 | import clsx from "clsx"; 4 | import { memo, useEffect, useRef } from "react"; 5 | import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; 6 | 7 | import { SystemMessageTypes } from "@/constants/im"; 8 | import { useUserStore } from "@/store"; 9 | import emitter from "@/utils/events"; 10 | 11 | import MessageItem from "./MessageItem"; 12 | import NotificationMessage from "./NotificationMessage"; 13 | import { useHistoryMessageList } from "./useHistoryMessageList"; 14 | 15 | const ChatContent = () => { 16 | const virtuoso = useRef(null); 17 | const selfUserID = useUserStore((state) => state.selfInfo.userID); 18 | 19 | const scrollToBottom = () => { 20 | setTimeout(() => { 21 | virtuoso.current?.scrollToIndex({ 22 | index: 9999, 23 | align: "end", 24 | behavior: "auto", 25 | }); 26 | }); 27 | }; 28 | 29 | const { SPLIT_COUNT, conversationID, loadState, moreOldLoading, getMoreOldMessages } = 30 | useHistoryMessageList(); 31 | 32 | useEffect(() => { 33 | emitter.on("CHAT_LIST_SCROLL_TO_BOTTOM", scrollToBottom); 34 | return () => { 35 | emitter.off("CHAT_LIST_SCROLL_TO_BOTTOM", scrollToBottom); 36 | }; 37 | }, []); 38 | 39 | const loadMoreMessage = () => { 40 | if (!loadState.hasMoreOld || moreOldLoading) return; 41 | 42 | getMoreOldMessages(); 43 | }; 44 | 45 | return ( 46 | 50 | {loadState.initLoading ? ( 51 |
52 | 53 |
54 | ) : ( 55 | 66 | loadState.hasMoreOld ? ( 67 |
73 | 74 |
75 | ) : null, 76 | }} 77 | computeItemKey={(_, item) => item.clientMsgID} 78 | itemContent={(_, message) => { 79 | if (SystemMessageTypes.includes(message.contentType)) { 80 | return ( 81 | 82 | ); 83 | } 84 | const isSender = selfUserID === message.sendID; 85 | return ( 86 | 93 | ); 94 | }} 95 | /> 96 | )} 97 |
98 | ); 99 | }; 100 | 101 | export default memo(ChatContent); 102 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/ChatFooter/SendActionBar/CallPopContent.tsx: -------------------------------------------------------------------------------- 1 | import { SessionType } from "@openim/wasm-client-sdk"; 2 | import { PublicUserItem } from "@openim/wasm-client-sdk/lib/types/entity"; 3 | import { t } from "i18next"; 4 | import { memo } from "react"; 5 | import { v4 as uuidV4 } from "uuid"; 6 | 7 | import call_audio from "@/assets/images/chatFooter/call_audio.png"; 8 | import call_video from "@/assets/images/chatFooter/call_video.png"; 9 | import { useConversationStore, useUserStore } from "@/store"; 10 | import emitter from "@/utils/events"; 11 | 12 | const callList = [ 13 | { 14 | idx: 0, 15 | title: t("placeholder.videoCall"), 16 | icon: call_video, 17 | }, 18 | { 19 | idx: 1, 20 | title: t("placeholder.voiceCall"), 21 | icon: call_audio, 22 | }, 23 | ]; 24 | 25 | const CallPopContent = ({ closeAllPop }: { closeAllPop?: () => void }) => { 26 | const prepareCall = (idx: number) => { 27 | const conversation = useConversationStore.getState().currentConversation!; 28 | const mediaType = idx ? "audio" : "video"; 29 | emitter.emit("OPEN_RTC_MODAL", { 30 | invitation: { 31 | inviterUserID: useUserStore.getState().selfInfo.userID, 32 | inviteeUserIDList: [conversation.userID], 33 | groupID: "", 34 | roomID: uuidV4(), 35 | timeout: 60, 36 | mediaType, 37 | sessionType: SessionType.Single, 38 | platformID: window.electronAPI?.getPlatform() ?? 5, 39 | }, 40 | participant: { 41 | userInfo: { 42 | nickname: conversation.showName, 43 | userID: conversation.userID, 44 | faceURL: conversation.faceURL, 45 | ex: "", 46 | }, 47 | }, 48 | }); 49 | closeAllPop?.(); 50 | }; 51 | return ( 52 |
53 | {callList.map((item) => ( 54 |
prepareCall(item.idx)} 58 | > 59 | call_video 60 |
{item.title}
61 |
62 | ))} 63 |
64 | ); 65 | }; 66 | export default memo(CallPopContent); 67 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/ChatFooter/SendActionBar/useFileMessage.ts: -------------------------------------------------------------------------------- 1 | import { MessageItem } from "@openim/wasm-client-sdk"; 2 | import { v4 as uuidV4 } from "uuid"; 3 | 4 | import { IMSDK } from "@/layout/MainContentWrap"; 5 | import { base64toFile, canSendImageTypeList } from "@/utils/common"; 6 | 7 | export interface FileWithPath extends File { 8 | path?: string; 9 | } 10 | 11 | export function useFileMessage() { 12 | const getImageMessage = async (file: FileWithPath) => { 13 | const { width, height } = await getPicInfo(file); 14 | const baseInfo = { 15 | uuid: uuidV4(), 16 | type: file.type, 17 | size: file.size, 18 | width, 19 | height, 20 | url: URL.createObjectURL(file), 21 | }; 22 | 23 | if (window.electronAPI) { 24 | const imageMessage = (await IMSDK.createImageMessageFromFullPath(file.path!)) 25 | .data; 26 | imageMessage.pictureElem!.sourcePicture.url = baseInfo.url; 27 | return imageMessage; 28 | } 29 | const options = { 30 | sourcePicture: baseInfo, 31 | bigPicture: baseInfo, 32 | snapshotPicture: baseInfo, 33 | sourcePath: "", 34 | file, 35 | }; 36 | 37 | return (await IMSDK.createImageMessageByFile(options)).data; 38 | }; 39 | 40 | const getPicInfo = (file: File): Promise => 41 | new Promise((resolve, reject) => { 42 | const _URL = window.URL || window.webkitURL; 43 | const img = new Image(); 44 | img.onload = function () { 45 | resolve(img); 46 | }; 47 | img.src = _URL.createObjectURL(file); 48 | }); 49 | 50 | 51 | return { 52 | getImageMessage 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/ChatFooter/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLatest } from "ahooks"; 2 | import { Button } from "antd"; 3 | import { t } from "i18next"; 4 | import { forwardRef, ForwardRefRenderFunction, memo, useState } from "react"; 5 | 6 | import CKEditor from "@/components/CKEditor"; 7 | import { getCleanText } from "@/components/CKEditor/utils"; 8 | import i18n from "@/i18n"; 9 | import { IMSDK } from "@/layout/MainContentWrap"; 10 | 11 | import SendActionBar from "./SendActionBar"; 12 | import { useFileMessage } from "./SendActionBar/useFileMessage"; 13 | import { useSendMessage } from "./useSendMessage"; 14 | 15 | const sendActions = [ 16 | { label: t("placeholder.sendWithEnter"), key: "enter" }, 17 | { label: t("placeholder.sendWithShiftEnter"), key: "enterwithshift" }, 18 | ]; 19 | 20 | i18n.on("languageChanged", () => { 21 | sendActions[0].label = t("placeholder.sendWithEnter"); 22 | sendActions[1].label = t("placeholder.sendWithShiftEnter"); 23 | }); 24 | 25 | const ChatFooter: ForwardRefRenderFunction = (_, ref) => { 26 | const [html, setHtml] = useState(""); 27 | const latestHtml = useLatest(html); 28 | 29 | const { getImageMessage } = useFileMessage(); 30 | const { sendMessage } = useSendMessage(); 31 | 32 | const onChange = (value: string) => { 33 | setHtml(value); 34 | }; 35 | 36 | const enterToSend = async () => { 37 | const cleanText = getCleanText(latestHtml.current); 38 | const message = (await IMSDK.createTextMessage(cleanText)).data; 39 | setHtml(""); 40 | if (!cleanText) return; 41 | 42 | sendMessage({ message }); 43 | }; 44 | 45 | return ( 46 |
47 |
48 | 49 |
50 | 51 |
52 | 55 |
56 |
57 |
58 |
59 | ); 60 | }; 61 | 62 | export default memo(forwardRef(ChatFooter)); 63 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/ChatFooter/useSendMessage.ts: -------------------------------------------------------------------------------- 1 | import { MessageStatus } from "@openim/wasm-client-sdk"; 2 | import { MessageItem, WsResponse } from "@openim/wasm-client-sdk/lib/types/entity"; 3 | import { SendMsgParams } from "@openim/wasm-client-sdk/lib/types/params"; 4 | import { useCallback } from "react"; 5 | 6 | import { IMSDK } from "@/layout/MainContentWrap"; 7 | import { useConversationStore } from "@/store"; 8 | import { emit } from "@/utils/events"; 9 | 10 | import { pushNewMessage, updateOneMessage } from "../useHistoryMessageList"; 11 | 12 | export type SendMessageParams = Partial> & { 13 | message: MessageItem; 14 | needPush?: boolean; 15 | }; 16 | 17 | export function useSendMessage() { 18 | const sendMessage = useCallback( 19 | async ({ recvID, groupID, message, needPush }: SendMessageParams) => { 20 | const currentConversation = useConversationStore.getState().currentConversation; 21 | const sourceID = recvID || groupID; 22 | const inCurrentConversation = 23 | currentConversation?.userID === sourceID || 24 | currentConversation?.groupID === sourceID || 25 | !sourceID; 26 | needPush = needPush ?? inCurrentConversation; 27 | 28 | if (needPush) { 29 | pushNewMessage(message); 30 | emit("CHAT_LIST_SCROLL_TO_BOTTOM"); 31 | } 32 | 33 | const options = { 34 | recvID: recvID ?? currentConversation?.userID ?? "", 35 | groupID: groupID ?? currentConversation?.groupID ?? "", 36 | message, 37 | }; 38 | 39 | try { 40 | const { data: successMessage } = await IMSDK.sendMessage(options); 41 | updateOneMessage(successMessage); 42 | } catch (error) { 43 | updateOneMessage({ 44 | ...message, 45 | status: MessageStatus.Failed, 46 | }); 47 | } 48 | }, 49 | [], 50 | ); 51 | 52 | return { 53 | sendMessage, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/GroupSetting/GroupMemberList.tsx: -------------------------------------------------------------------------------- 1 | import { GroupMemberItem } from "@openim/wasm-client-sdk/lib/types/entity"; 2 | import { Empty, Spin } from "antd"; 3 | import { t } from "i18next"; 4 | import { FC, memo, useEffect } from "react"; 5 | import { Virtuoso } from "react-virtuoso"; 6 | 7 | import OIMAvatar from "@/components/OIMAvatar"; 8 | import { useCurrentMemberRole } from "@/hooks/useCurrentMemberRole"; 9 | import useGroupMembers from "@/hooks/useGroupMembers"; 10 | import { useUserStore } from "@/store"; 11 | 12 | import styles from "./group-setting.module.scss"; 13 | import { GroupMemberRole } from "@openim/wasm-client-sdk"; 14 | 15 | const GroupMemberList: FC = () => { 16 | const selfUserID = useUserStore((state) => state.selfInfo.userID); 17 | const { currentMemberInGroup } = useCurrentMemberRole(); 18 | const { fetchState, getMemberData, resetState } = useGroupMembers(); 19 | 20 | useEffect(() => { 21 | if (currentMemberInGroup?.groupID) { 22 | getMemberData(true); 23 | } 24 | return () => { 25 | resetState(); 26 | }; 27 | }, [currentMemberInGroup?.groupID]); 28 | 29 | const endReached = () => { 30 | getMemberData(); 31 | }; 32 | 33 | return ( 34 |
35 | {fetchState.groupMemberList.length === 0 ? ( 36 | 40 | ) : ( 41 | (fetchState.loading ? : null), 47 | }} 48 | itemContent={(_, member) => ( 49 | 50 | )} 51 | /> 52 | )} 53 |
54 | ); 55 | }; 56 | 57 | export default GroupMemberList; 58 | 59 | interface IMemberItemProps { 60 | member: GroupMemberItem; 61 | selfUserID: string; 62 | } 63 | 64 | const MemberItem = memo(({ member }: IMemberItemProps) => { 65 | const isOwner = member.roleLevel === GroupMemberRole.Owner; 66 | return ( 67 |
68 |
window.userClick(member.userID, member.groupID)} 71 | > 72 | 73 |
74 |
{member.nickname}
75 | {isOwner && ( 76 | 77 | {t("placeholder.groupOwner")} 78 | 79 | )} 80 |
81 |
82 |
83 | ); 84 | }); 85 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/GroupSetting/GroupMemberListHeader.tsx: -------------------------------------------------------------------------------- 1 | import { LeftOutlined } from "@ant-design/icons"; 2 | import { t } from "i18next"; 3 | import { memo } from "react"; 4 | 5 | import invite_header from "@/assets/images/chatSetting/invite_header.png"; 6 | import { useConversationStore } from "@/store"; 7 | import { emit } from "@/utils/events"; 8 | 9 | const GroupMemberListHeader = ({ back2Settings }: { back2Settings: () => void }) => { 10 | return ( 11 |
12 |
13 | 18 |
{t("placeholder.memberList")}
19 |
20 |
21 | 27 | emit("OPEN_CHOOSE_MODAL", { 28 | type: "INVITE_TO_GROUP", 29 | extraData: useConversationStore.getState().currentConversation?.groupID, 30 | }) 31 | } 32 | /> 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default memo(GroupMemberListHeader); 39 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/GroupSetting/GroupMemberRow.tsx: -------------------------------------------------------------------------------- 1 | import { GroupItem } from "@openim/wasm-client-sdk/lib/types/entity"; 2 | import clsx from "clsx"; 3 | import { t } from "i18next"; 4 | import { memo, useEffect } from "react"; 5 | 6 | import invite from "@/assets/images/chatSetting/invite.png"; 7 | import kick from "@/assets/images/chatSetting/kick.png"; 8 | import OIMAvatar from "@/components/OIMAvatar"; 9 | import useGroupMembers from "@/hooks/useGroupMembers"; 10 | import { emit } from "@/utils/events"; 11 | 12 | import styles from "./group-setting.module.scss"; 13 | 14 | const GroupMemberRow = ({ 15 | currentGroupInfo, 16 | isNomal, 17 | updateTravel, 18 | }: { 19 | currentGroupInfo: GroupItem; 20 | isNomal: boolean; 21 | updateTravel: () => void; 22 | }) => { 23 | const { fetchState, getMemberData, resetState } = useGroupMembers(); 24 | 25 | useEffect(() => { 26 | if (currentGroupInfo?.groupID) { 27 | getMemberData(true); 28 | } 29 | return () => { 30 | resetState(); 31 | }; 32 | }, [currentGroupInfo?.groupID]); 33 | 34 | const sliceCount = isNomal ? 17 : 16; 35 | 36 | const inviteMember = (e: React.MouseEvent) => { 37 | e.stopPropagation(); 38 | emit("OPEN_CHOOSE_MODAL", { 39 | type: "INVITE_TO_GROUP", 40 | extraData: currentGroupInfo.groupID, 41 | }); 42 | }; 43 | 44 | const kickMember = (e: React.MouseEvent) => { 45 | e.stopPropagation(); 46 | emit("OPEN_CHOOSE_MODAL", { 47 | type: "KICK_FORM_GROUP", 48 | extraData: currentGroupInfo.groupID, 49 | }); 50 | }; 51 | 52 | return ( 53 |
54 |
55 | {t("placeholder.groupMember")} 56 | {currentGroupInfo?.memberCount} 57 |
58 |
59 | {fetchState.groupMemberList.slice(0, sliceCount).map((member) => ( 60 |
window.userClick(member.userID, member.groupID)} 65 | > 66 | 67 |
68 | {member.nickname} 69 |
70 |
71 | ))} 72 |
76 | invite 77 |
78 | {t("placeholder.add")} 79 |
80 |
81 | {!isNomal && ( 82 |
86 | kick 87 |
88 | {t("placeholder.remove")} 89 |
90 |
91 | )} 92 |
93 |
97 | {t("placeholder.viewMore")} 98 |
99 |
100 | ); 101 | }; 102 | 103 | export default memo(GroupMemberRow); 104 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/GroupSetting/group-setting.module.scss: -------------------------------------------------------------------------------- 1 | .member-item { 2 | @apply flex flex-col items-center w-9 mr-3 mb-3; 3 | 4 | &:nth-child(9n) { 5 | margin-right: 0; 6 | } 7 | } 8 | 9 | .list-member-item { 10 | @apply flex items-center justify-between px-3.5 py-1 rounded-md; 11 | 12 | .tools-row { 13 | @apply flex items-center invisible; 14 | } 15 | 16 | &:hover { 17 | @apply bg-[var(--primary-active)]; 18 | 19 | .tools-row { 20 | @apply visible; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/GroupSetting/index.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer } from "antd"; 2 | import { t } from "i18next"; 3 | import { forwardRef, ForwardRefRenderFunction, memo, useRef, useState } from "react"; 4 | 5 | import { OverlayVisibleHandle, useOverlayVisible } from "@/hooks/useOverlayVisible"; 6 | 7 | import GroupMemberList from "./GroupMemberList"; 8 | import GroupMemberListHeader from "./GroupMemberListHeader"; 9 | import GroupSettings from "./GroupSettings"; 10 | 11 | const GroupSetting: ForwardRefRenderFunction = ( 12 | _, 13 | ref, 14 | ) => { 15 | const [isPreviewMembers, setIsPreviewMembers] = useState(false); 16 | 17 | const { isOverlayOpen, closeOverlay } = useOverlayVisible(ref); 18 | 19 | const closePreviewMembers = () => { 20 | setIsPreviewMembers(false); 21 | }; 22 | 23 | return ( 24 | 30 | ) 31 | } 32 | destroyOnClose 33 | placement="right" 34 | rootClassName="chat-drawer" 35 | onClose={closeOverlay} 36 | afterOpenChange={(visible) => { 37 | if (!visible) { 38 | closePreviewMembers(); 39 | } 40 | }} 41 | open={isOverlayOpen} 42 | maskClassName="opacity-0" 43 | maskMotion={{ 44 | visible: false, 45 | }} 46 | width={460} 47 | getContainer={"#chat-container"} 48 | > 49 | {!isPreviewMembers ? ( 50 | setIsPreviewMembers(true)} 53 | /> 54 | ) : ( 55 | 56 | )} 57 | 58 | ); 59 | }; 60 | 61 | export default memo(forwardRef(GroupSetting)); 62 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/GroupSetting/useGroupSettings.tsx: -------------------------------------------------------------------------------- 1 | import { GroupItem } from "@openim/wasm-client-sdk/lib/types/entity"; 2 | import { t } from "i18next"; 3 | import { useCallback, useRef } from "react"; 4 | 5 | import { modal } from "@/AntdGlobalComp"; 6 | import { IMSDK } from "@/layout/MainContentWrap"; 7 | import { useConversationStore } from "@/store"; 8 | import { feedbackToast } from "@/utils/common"; 9 | 10 | export type PermissionField = "applyMemberFriend" | "lookMemberInfo"; 11 | 12 | export function useGroupSettings({ closeOverlay }: { closeOverlay: () => void }) { 13 | const currentGroupInfo = useConversationStore((state) => state.currentGroupInfo); 14 | 15 | const modalRef = useRef<{ 16 | destroy: () => void; 17 | } | null>(null); 18 | 19 | const updateGroupInfo = useCallback( 20 | async (value: Partial) => { 21 | if (!currentGroupInfo) return; 22 | try { 23 | await IMSDK.setGroupInfo({ 24 | ...value, 25 | groupID: currentGroupInfo.groupID, 26 | }); 27 | } catch (error) { 28 | feedbackToast({ error, msg: t("toast.updateGroupInfoFailed") }); 29 | } 30 | }, 31 | [currentGroupInfo?.groupID], 32 | ); 33 | 34 | const tryDismissGroup = () => { 35 | if (!currentGroupInfo || modalRef.current) return; 36 | 37 | modalRef.current = modal.confirm({ 38 | title: t("placeholder.disbandGroup"), 39 | content: ( 40 |
41 |
{t("toast.confirmDisbandGroup")}
42 | 43 | {t("placeholder.disbandGroupToast")} 44 | 45 |
46 | ), 47 | onOk: async () => { 48 | try { 49 | await IMSDK.dismissGroup(currentGroupInfo.groupID); 50 | closeOverlay(); 51 | } catch (error) { 52 | feedbackToast({ error }); 53 | } 54 | modalRef.current = null; 55 | }, 56 | onCancel: () => { 57 | modalRef.current = null; 58 | }, 59 | }); 60 | }; 61 | 62 | const tryQuitGroup = () => { 63 | if (!currentGroupInfo || modalRef.current) return; 64 | 65 | modalRef.current = modal.confirm({ 66 | title: t("placeholder.exitGroup"), 67 | content: ( 68 |
69 |
{t("toast.confirmExitGroup")}
70 | 71 | {t("placeholder.exitGroupToast")} 72 | 73 |
74 | ), 75 | onOk: async () => { 76 | try { 77 | await IMSDK.quitGroup(currentGroupInfo.groupID); 78 | closeOverlay(); 79 | } catch (error) { 80 | feedbackToast({ error }); 81 | } 82 | modalRef.current = null; 83 | }, 84 | onCancel: () => { 85 | modalRef.current = null; 86 | }, 87 | }); 88 | }; 89 | 90 | return { 91 | currentGroupInfo, 92 | updateGroupInfo, 93 | tryQuitGroup, 94 | tryDismissGroup, 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/MessageItem/CatchMsgRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import styles from "./message-item.module.scss"; 5 | 6 | const CatchMessageRender: FC = () => { 7 | const { t } = useTranslation(); 8 | 9 | return
{t("messageDescription.catchMessage")}
; 10 | }; 11 | 12 | export default CatchMessageRender; 13 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/MessageItem/MediaMessageRender.tsx: -------------------------------------------------------------------------------- 1 | import { MessageStatus } from "@openim/wasm-client-sdk"; 2 | import { Image, Spin } from "antd"; 3 | import { FC } from "react"; 4 | 5 | import { IMessageItemProps } from "."; 6 | 7 | const min = (a: number, b: number) => (a > b ? b : a); 8 | 9 | const MediaMessageRender: FC = ({ message }) => { 10 | const imageHeight = message.pictureElem!.sourcePicture.height; 11 | const imageWidth = message.pictureElem!.sourcePicture.width; 12 | const snapshotMaxHeight = message.pictureElem!.snapshotPicture?.height ?? imageHeight; 13 | const minHeight = min(200, imageWidth) * (imageHeight / imageWidth) + 2; 14 | const adaptedHight = min(minHeight, snapshotMaxHeight) + 10; 15 | const adaptedWidth = min(imageWidth, 200) + 10; 16 | 17 | const sourceUrl = 18 | message.pictureElem!.snapshotPicture?.url || message.pictureElem!.sourcePicture.url; 19 | const isSending = message.status === MessageStatus.Sending; 20 | const minStyle = { minHeight: `${adaptedHight}px`, minWidth: `${adaptedWidth}px` }; 21 | 22 | return ( 23 | 24 |
25 | 32 | 33 |
34 | } 35 | /> 36 | 37 |
38 | ); 39 | }; 40 | 41 | export default MediaMessageRender; 42 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/MessageItem/MessageItemErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { MessageItem } from "@openim/wasm-client-sdk/lib/types/entity"; 2 | import { Component, ErrorInfo, ReactNode } from "react"; 3 | 4 | import CatchMessageRender from "./CatchMsgRenderer"; 5 | 6 | type MessageItemErrorBoundaryProps = { 7 | children: ReactNode; 8 | message: MessageItem; 9 | }; 10 | 11 | type MessageItemErrorBoundaryState = { 12 | hasError: boolean; 13 | message: MessageItem; 14 | }; 15 | 16 | class MessageItemErrorBoundary extends Component< 17 | MessageItemErrorBoundaryProps, 18 | MessageItemErrorBoundaryState 19 | > { 20 | constructor(props: MessageItemErrorBoundaryProps) { 21 | super(props); 22 | this.state = { hasError: false, message: props.message }; 23 | } 24 | 25 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 26 | console.error("MessageItemErrorBoundary:::"); 27 | console.error(this.state.message); 28 | console.error(error); 29 | 30 | this.setState({ hasError: true }); 31 | } 32 | 33 | render() { 34 | if (this.state.hasError) { 35 | return ; 36 | } 37 | 38 | return this.props.children; 39 | } 40 | } 41 | 42 | export default MessageItemErrorBoundary; 43 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/MessageItem/MessageSuffix.tsx: -------------------------------------------------------------------------------- 1 | import { ExclamationCircleFilled, LoadingOutlined } from "@ant-design/icons"; 2 | import { MessageStatus, MessageType } from "@openim/wasm-client-sdk"; 3 | import { Spin } from "antd"; 4 | import { FC, useEffect, useState } from "react"; 5 | 6 | import { IMessageItemProps } from "."; 7 | import styles from "./message-item.module.scss"; 8 | 9 | const MessageSuffix: FC = ({ message }) => { 10 | const [showSending, setShowSending] = useState(false); 11 | 12 | useEffect(() => { 13 | if (message.status !== MessageStatus.Sending) return; 14 | const timer = setTimeout(() => { 15 | if (message.status === MessageStatus.Sending) { 16 | setShowSending(true); 17 | } 18 | }, 1000); 19 | return () => { 20 | clearTimeout(timer); 21 | }; 22 | }, [message.status]); 23 | 24 | return ( 25 |
26 | {showSending && message.status === MessageStatus.Sending && ( 27 | } 30 | /> 31 | )} 32 | {message.status === MessageStatus.Failed && ( 33 | 37 | )} 38 |
39 | ); 40 | }; 41 | 42 | export default MessageSuffix; 43 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/MessageItem/TextMessageRender.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { formatBr } from "@/utils/common"; 4 | 5 | import { IMessageItemProps } from "."; 6 | import styles from "./message-item.module.scss"; 7 | 8 | const TextMessageRender: FC = ({ message }) => { 9 | let content = message.textElem?.content; 10 | 11 | content = formatBr(content!); 12 | 13 | return ( 14 |
15 | ); 16 | }; 17 | 18 | export default TextMessageRender; 19 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/MessageItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { MessageItem as MessageItemType, MessageType } from "@openim/wasm-client-sdk"; 2 | import clsx from "clsx"; 3 | import { FC, memo, useCallback, useRef, useState } from "react"; 4 | 5 | import OIMAvatar from "@/components/OIMAvatar"; 6 | import { formatMessageTime } from "@/utils/imCommon"; 7 | 8 | import CatchMessageRender from "./CatchMsgRenderer"; 9 | import MediaMessageRender from "./MediaMessageRender"; 10 | import styles from "./message-item.module.scss"; 11 | import MessageItemErrorBoundary from "./MessageItemErrorBoundary"; 12 | import MessageSuffix from "./MessageSuffix"; 13 | import TextMessageRender from "./TextMessageRender"; 14 | 15 | export interface IMessageItemProps { 16 | message: MessageItemType; 17 | isSender: boolean; 18 | disabled?: boolean; 19 | conversationID?: string; 20 | messageUpdateFlag?: string; 21 | } 22 | 23 | const components: Record> = { 24 | [MessageType.TextMessage]: TextMessageRender, 25 | [MessageType.PictureMessage]: MediaMessageRender, 26 | }; 27 | 28 | const MessageItem: FC = ({ 29 | message, 30 | disabled, 31 | isSender, 32 | conversationID, 33 | }) => { 34 | const messageWrapRef = useRef(null); 35 | const [showMessageMenu, setShowMessageMenu] = useState(false); 36 | const MessageRenderComponent = components[message.contentType] || CatchMessageRender; 37 | 38 | const closeMessageMenu = useCallback(() => { 39 | setShowMessageMenu(false); 40 | }, []); 41 | 42 | const canShowMessageMenu = !disabled; 43 | 44 | return ( 45 | <> 46 |
50 |
56 | 61 | 62 |
63 |
64 |
71 | {message.senderNickname} 72 |
73 |
74 | {formatMessageTime(message.sendTime)} 75 |
76 |
77 | 78 |
79 | 80 | 85 | 86 | 87 | 93 |
94 |
95 |
96 |
97 | 98 | ); 99 | }; 100 | 101 | export default memo(MessageItem); 102 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/MessageItem/message-item.module.scss: -------------------------------------------------------------------------------- 1 | .message-container { 2 | @apply flex flex-1 overflow-hidden; 3 | 4 | .message-wrap { 5 | @apply ml-3 flex flex-1 flex-col overflow-hidden; 6 | 7 | .message-profile { 8 | @apply mb-1 flex w-full text-xs; 9 | } 10 | 11 | .bubble { 12 | @apply w-fit rounded-md p-2.5; 13 | word-break: break-all; 14 | background-color: var(--chat-bubble); 15 | } 16 | 17 | .suffix { 18 | @apply ml-3 flex items-center; 19 | } 20 | } 21 | 22 | .menu-wrap { 23 | @apply flex w-fit; 24 | } 25 | 26 | &-sender { 27 | @apply flex-row-reverse; 28 | 29 | .message-wrap { 30 | @apply mr-3 items-end; 31 | 32 | .message-profile { 33 | @apply flex-row-reverse; 34 | } 35 | 36 | .bubble { 37 | background-color: var(--chat-bubble-sender); 38 | } 39 | 40 | .suffix { 41 | @apply ml-0 mr-3; 42 | } 43 | } 44 | 45 | .menu-wrap { 46 | @apply flex-row-reverse; 47 | } 48 | } 49 | } 50 | 51 | .animate-container { 52 | // background-color: var(--primary-active); 53 | animation: animate 2s ease-in-out; 54 | } 55 | @keyframes animate { 56 | from { 57 | background-color: var(--primary-active); 58 | } 59 | to { 60 | background-color: transparent; 61 | } 62 | } 63 | 64 | .card-shadow { 65 | @apply w-60 cursor-pointer overflow-hidden rounded-md shadow-md; 66 | box-shadow: 3px 3px 8px 1px rgba(81, 94, 112, 0.1); 67 | } 68 | 69 | .bubble { 70 | word-wrap: break-word; 71 | word-break: break-word; 72 | white-space: pre-wrap; 73 | } 74 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/NotificationMessage.tsx: -------------------------------------------------------------------------------- 1 | import { MessageItem } from "@openim/wasm-client-sdk"; 2 | import clsx from "clsx"; 3 | import { FC, memo, useRef } from "react"; 4 | 5 | import { notificationMessageFormat } from "@/utils/imCommon"; 6 | 7 | const NotificationMessage: FC<{ 8 | message: MessageItem; 9 | }> = ({ message }) => { 10 | const messageWrapRef = useRef(null); 11 | 12 | return ( 13 |
14 |
21 |
22 | ); 23 | }; 24 | 25 | export default memo(NotificationMessage); 26 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/index.tsx: -------------------------------------------------------------------------------- 1 | import { InfoCircleOutlined } from "@ant-design/icons"; 2 | import { SessionType } from "@openim/wasm-client-sdk"; 3 | import { useUnmount } from "ahooks"; 4 | import { Layout } from "antd"; 5 | import { t } from "i18next"; 6 | import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; 7 | 8 | import { useConversationStore } from "@/store"; 9 | 10 | import ChatContent from "./ChatContent"; 11 | import ChatFooter from "./ChatFooter"; 12 | import ChatHeader from "./ChatHeader"; 13 | import useConversationState from "./useConversationState"; 14 | 15 | export const QueryChat = () => { 16 | const updateCurrentConversation = useConversationStore( 17 | (state) => state.updateCurrentConversation, 18 | ); 19 | 20 | useConversationState(); 21 | 22 | useUnmount(() => { 23 | updateCurrentConversation(); 24 | }); 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/pages/chat/queryChat/useConversationState.ts: -------------------------------------------------------------------------------- 1 | import { useLatest, useThrottleFn, useUpdateEffect } from "ahooks"; 2 | import { useEffect } from "react"; 3 | 4 | import { IMSDK } from "@/layout/MainContentWrap"; 5 | import { useConversationStore, useUserStore } from "@/store"; 6 | 7 | export default function useConversationState() { 8 | const syncState = useUserStore((state) => state.syncState); 9 | const latestSyncState = useLatest(syncState); 10 | const currentConversation = useConversationStore( 11 | (state) => state.currentConversation, 12 | ); 13 | const latestCurrentConversation = useLatest(currentConversation); 14 | 15 | useUpdateEffect(() => { 16 | if (syncState !== "loading") { 17 | checkConversationState(); 18 | } 19 | }, [syncState]); 20 | 21 | useUpdateEffect(() => { 22 | throttleCheckConversationState(); 23 | }, [currentConversation?.unreadCount]); 24 | 25 | useEffect(() => { 26 | checkConversationState(); 27 | }, [currentConversation?.conversationID]); 28 | 29 | const checkConversationState = () => { 30 | if ( 31 | !latestCurrentConversation.current || 32 | latestSyncState.current === "loading" 33 | ) 34 | return; 35 | 36 | if (latestCurrentConversation.current.unreadCount > 0) { 37 | IMSDK.markConversationMessageAsRead( 38 | latestCurrentConversation.current.conversationID, 39 | ); 40 | } 41 | }; 42 | 43 | const { run: throttleCheckConversationState } = useThrottleFn( 44 | checkConversationState, 45 | { wait: 2000, leading: false }, 46 | ); 47 | 48 | return { 49 | currentConversation, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/common/ChooseModal/ChooseBox/CheckItem.tsx: -------------------------------------------------------------------------------- 1 | import { CloseOutlined, RightOutlined } from "@ant-design/icons"; 2 | import { SessionType } from "@openim/wasm-client-sdk"; 3 | import { 4 | ConversationItem, 5 | FriendUserItem, 6 | GroupItem, 7 | } from "@openim/wasm-client-sdk/lib/types/entity"; 8 | import { Checkbox } from "antd"; 9 | import clsx from "clsx"; 10 | import { FC, memo } from "react"; 11 | 12 | import OIMAvatar from "@/components/OIMAvatar"; 13 | 14 | interface ICheckItemProps { 15 | data: CheckListItem; 16 | isChecked?: boolean; 17 | showCheck?: boolean; 18 | disabled?: boolean; 19 | itemClick?: (data: CheckListItem) => void; 20 | cancelClick?: (data: CheckListItem) => void; 21 | } 22 | 23 | export type CheckListItem = Partial< 24 | FriendUserItem & ConversationItem & GroupItem & { disabled?: boolean } 25 | >; 26 | 27 | const CheckItem: FC = (props) => { 28 | const { data, isChecked, showCheck, disabled, itemClick, cancelClick } = props; 29 | const showName = data.remark || data.nickname || data.groupName || data.showName; 30 | const isDisabled = disabled ?? data.disabled; 31 | return ( 32 |
!isDisabled && itemClick?.(data)} 38 | > 39 |
40 | {showCheck && ( 41 | 42 | )} 43 | 51 |
{showName}
52 |
53 | {showCheck ? ( 54 | 58 | ) : ( 59 | { 63 | e.stopPropagation(); 64 | cancelClick?.(data); 65 | }} 66 | /> 67 | )} 68 |
69 | ); 70 | }; 71 | 72 | export default memo(CheckItem); 73 | -------------------------------------------------------------------------------- /src/pages/common/ChooseModal/ChooseBox/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | import { RightOutlined } from "@ant-design/icons"; 2 | 3 | import { ChooseMenuItem } from "."; 4 | 5 | const MenuItem = ({ 6 | menu, 7 | menuClick, 8 | }: { 9 | menu: ChooseMenuItem; 10 | menuClick: (idx: number) => void; 11 | }) => ( 12 |
menuClick(menu.idx)} 16 | > 17 |
18 | 19 |
{menu.title}
20 |
21 | 22 |
23 | ); 24 | 25 | export default MenuItem; 26 | -------------------------------------------------------------------------------- /src/pages/common/RtcCallModal/Counter.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | forwardRef, 3 | ForwardRefRenderFunction, 4 | memo, 5 | useEffect, 6 | useImperativeHandle, 7 | useRef, 8 | useState, 9 | } from "react"; 10 | 11 | import { secondsToMS } from "@/utils/common"; 12 | 13 | export type CounterHandle = { 14 | getTimeStr: () => string; 15 | }; 16 | const Counter: ForwardRefRenderFunction< 17 | CounterHandle, 18 | { 19 | isConnected: boolean; 20 | className?: string; 21 | } 22 | > = ({ isConnected, className }, ref) => { 23 | const [count, setCount] = useState(0); 24 | const timer = useRef(null); 25 | 26 | useEffect(() => { 27 | if (isConnected) { 28 | countStart(); 29 | } 30 | return () => { 31 | if (timer.current) { 32 | clearInterval(timer.current); 33 | } 34 | }; 35 | }, [isConnected]); 36 | 37 | const countStart = () => { 38 | timer.current = setInterval(() => { 39 | setCount((prev) => prev + 1); 40 | }, 1000); 41 | }; 42 | 43 | useImperativeHandle(ref, () => ({ 44 | getTimeStr: () => secondsToMS(count), 45 | })); 46 | 47 | return ( 48 |
49 |
{secondsToMS(count)}
50 |
51 | ); 52 | }; 53 | 54 | export const ForwardCounter = memo(forwardRef(Counter)); 55 | -------------------------------------------------------------------------------- /src/pages/common/RtcCallModal/data.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GroupItem, 3 | GroupMemberItem, 4 | PublicUserItem, 5 | RtcInvite, 6 | } from "@openim/wasm-client-sdk/lib/types/entity"; 7 | 8 | export interface InviteData { 9 | invitation?: RtcInvite; 10 | participant?: ParticipantInfo; 11 | isJoin?: boolean; 12 | } 13 | 14 | export interface ParticipantInfo { 15 | userInfo: PublicUserItem; 16 | groupMemberInfo?: GroupMemberItem; 17 | groupInfo?: GroupItem; 18 | } 19 | 20 | export interface RtcInviteResults { 21 | liveURL: string; 22 | roomID: string; 23 | token: string; 24 | busyLineUserIDList?: string[]; 25 | } 26 | 27 | export interface AuthData { 28 | serverUrl: string; 29 | token: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/common/UserCardModal/SendRequest.tsx: -------------------------------------------------------------------------------- 1 | import { LeftOutlined } from "@ant-design/icons"; 2 | import { useRequest } from "ahooks"; 3 | import { Button, Input } from "antd"; 4 | import { t } from "i18next"; 5 | import { useState } from "react"; 6 | 7 | import OIMAvatar from "@/components/OIMAvatar"; 8 | import { IMSDK } from "@/layout/MainContentWrap"; 9 | import { feedbackToast } from "@/utils/common"; 10 | 11 | import { CardInfo } from "."; 12 | 13 | const SendRequest = ({ 14 | cardInfo, 15 | backToCard, 16 | }: { 17 | cardInfo: CardInfo; 18 | backToCard: () => void; 19 | }) => { 20 | const [reqMsg, setReqMsg] = useState(""); 21 | const { runAsync, loading } = useRequest(IMSDK.addFriend, { 22 | manual: true, 23 | }); 24 | 25 | const sendApplication = async () => { 26 | try { 27 | await runAsync({ 28 | toUserID: cardInfo.userID!, 29 | reqMsg, 30 | }); 31 | feedbackToast({ msg: t("toast.sendFreiendRequestSuccess") }); 32 | } catch (error) { 33 | feedbackToast({ error, msg: t("toast.sendApplicationFailed") }); 34 | } 35 | backToCard(); 36 | }; 37 | 38 | return ( 39 |
40 |
41 |
42 | 47 |
{t("placeholder.friendVerification")}
48 |
49 |
50 |
51 |
52 | 53 |
54 |
58 | {cardInfo?.nickname} 59 |
60 |
61 | {cardInfo?.userID} 62 |
63 |
64 |
65 |
66 |
67 | {t("application.information")} 68 |
69 |
70 | setReqMsg(e.target.value)} 80 | className="bg-[var(--chat-bubble)] hover:bg-[var(--chat-bubble)]" 81 | /> 82 |
83 |
84 |
85 | 93 |
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default SendRequest; 100 | -------------------------------------------------------------------------------- /src/pages/contact/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from "antd"; 2 | import { Outlet } from "react-router-dom"; 3 | 4 | import ContactSider from "@/pages/contact/ContactSider"; 5 | 6 | export const Contact = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/pages/contact/myFriends/AlphabetIndex.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { 3 | forwardRef, 4 | ForwardRefRenderFunction, 5 | memo, 6 | useImperativeHandle, 7 | useState, 8 | } from "react"; 9 | 10 | type IAlphabetIndexProps = { 11 | indexList: string[]; 12 | scrollToLetter: (idx: number) => void; 13 | }; 14 | 15 | const AlphabetIndex: ForwardRefRenderFunction< 16 | { updateCurrentLetter: (letter: string) => void }, 17 | IAlphabetIndexProps 18 | > = ({ indexList, scrollToLetter }, ref) => { 19 | const [currentAlphabet, setCurrentAlphabet] = useState(""); 20 | 21 | const jumpToLetter = (idx: number, letter: string) => { 22 | scrollToLetter(idx); 23 | setCurrentAlphabet(letter); 24 | }; 25 | 26 | useImperativeHandle( 27 | ref, 28 | () => ({ 29 | updateCurrentLetter: (letter: string) => setCurrentAlphabet(letter), 30 | }), 31 | [], 32 | ); 33 | 34 | return ( 35 |
36 | {indexList.map((letter, idx) => ( 37 | jumpToLetter(idx, letter)} 43 | > 44 | {letter} 45 | 46 | ))} 47 |
48 | ); 49 | }; 50 | 51 | export default memo(forwardRef(AlphabetIndex)); 52 | -------------------------------------------------------------------------------- /src/pages/contact/myFriends/FriendListItem.tsx: -------------------------------------------------------------------------------- 1 | import { FriendUserItem } from "@openim/wasm-client-sdk/lib/types/entity"; 2 | 3 | import OIMAvatar from "@/components/OIMAvatar"; 4 | 5 | const FriendListItem = ({ 6 | friend, 7 | showUserCard, 8 | }: { 9 | friend: FriendUserItem; 10 | showUserCard: (userID: string) => void; 11 | }) => { 12 | return ( 13 |
showUserCard(friend.userID)} 16 | > 17 | 18 |
{friend.remark || friend.nickname}
19 |
20 | ); 21 | }; 22 | 23 | export default FriendListItem; 24 | -------------------------------------------------------------------------------- /src/pages/contact/myGroups/GroupListItem.tsx: -------------------------------------------------------------------------------- 1 | import { GroupItem } from "@openim/wasm-client-sdk/lib/types/entity"; 2 | 3 | import OIMAvatar from "@/components/OIMAvatar"; 4 | 5 | const GroupListItem = ({ 6 | source, 7 | showGroupCard, 8 | }: { 9 | source: GroupItem; 10 | showGroupCard: (group: GroupItem) => void; 11 | }) => { 12 | return ( 13 |
showGroupCard(source)} 16 | > 17 | 18 |
19 |

{source.groupName}

20 |

{source.memberCount}

21 |
22 |
23 | ); 24 | }; 25 | 26 | export default GroupListItem; 27 | -------------------------------------------------------------------------------- /src/pages/contact/myGroups/index.tsx: -------------------------------------------------------------------------------- 1 | import { GroupItem } from "@openim/wasm-client-sdk/lib/types/entity"; 2 | import { Select } from "antd"; 3 | import { useCallback, useState } from "react"; 4 | import { useTranslation } from "react-i18next"; 5 | import { Virtuoso } from "react-virtuoso"; 6 | 7 | import { useContactStore, useUserStore } from "@/store"; 8 | import { emit } from "@/utils/events"; 9 | 10 | import GroupListItem from "./GroupListItem"; 11 | 12 | export enum GroupTypeEnum { 13 | JoinedGroup, 14 | CreatedGroup, 15 | } 16 | 17 | export const MyGroups = () => { 18 | const { t } = useTranslation(); 19 | const [selectGroup, setSelectGroup] = useState(GroupTypeEnum.CreatedGroup); 20 | 21 | const joinedGroupList = useContactStore((state) => state.groupList); 22 | const { userID } = useUserStore((state) => state.selfInfo); 23 | 24 | const handleChange = (value: string) => { 25 | setSelectGroup(Number(value)); 26 | }; 27 | 28 | const filterGroup = joinedGroupList.filter((group) => { 29 | if (selectGroup === GroupTypeEnum.JoinedGroup) { 30 | return group.creatorUserID !== userID; 31 | } else if (selectGroup === GroupTypeEnum.CreatedGroup) { 32 | return group.creatorUserID === userID; 33 | } 34 | return false; 35 | }); 36 | 37 | const showGroupCard = useCallback((group: GroupItem) => { 38 | emit("OPEN_GROUP_CARD", group); 39 | }, []); 40 | 41 | return ( 42 |
43 |
44 |

{t("placeholder.myGroup")}

45 |