diff --git a/client/web/tailchat.d.ts b/client/web/tailchat.d.ts index 21a171d2..e60fe980 100644 --- a/client/web/tailchat.d.ts +++ b/client/web/tailchat.d.ts @@ -244,7 +244,38 @@ declare module '@capital/common' { export const pluginPanelActions: any; - export const regPluginPanelAction: any; + interface BasePluginPanelActionProps { + /** + * 唯一标识 + */ + name: string; + /** + * 显示名 + */ + label: string; + /** + * 来自iconify的图标标识 + */ + icon: string; + } + + export const regPluginPanelAction: ( + action: + | { + name: string; + label: string; + icon: string; + position: 'group'; + onClick: (ctx: { groupId: string; panelId: string }) => void; + } + | { + name: string; + label: string; + icon: string; + position: 'dm'; + onClick: (ctx: { converseId: string }) => void; + } + ) => void; export const pluginPermission: any; @@ -367,6 +398,7 @@ declare module '@capital/component' { size?: 'small' | 'middle' | 'large'; shape?: 'circle' | 'square'; title?: string; + danger?: boolean; onClick?: React.MouseEventHandler; }>; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e22dbd7..689b3adb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -935,6 +935,30 @@ importers: devDependencies: typescript: 4.7.4 + server/plugins/com.msgbyte.agora: + specifiers: + '@types/react': 18.0.20 + mini-star: '*' + tailchat-server-sdk: '*' + dependencies: + tailchat-server-sdk: link:../../packages/sdk + devDependencies: + '@types/react': 18.0.20 + mini-star: 2.0.5 + + server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora: + specifiers: + '@types/styled-components': ^5.1.26 + agora-rtc-react: ^1.1.3 + react: 18.2.0 + styled-components: ^5.3.6 + dependencies: + agora-rtc-react: 1.1.3_react@18.2.0 + devDependencies: + '@types/styled-components': 5.1.26 + react: 18.2.0 + styled-components: 5.3.6_react@18.2.0 + server/plugins/com.msgbyte.github: specifiers: '@octokit/webhooks-types': ^5.4.0 @@ -3900,6 +3924,7 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color + dev: false /@babel/traverse/7.20.5: resolution: {integrity: sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==} @@ -3918,6 +3943,23 @@ packages: transitivePeerDependencies: - supports-color + /@babel/traverse/7.20.5_supports-color@5.5.0: + resolution: {integrity: sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.20.5 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/parser': 7.20.5 + '@babel/types': 7.20.5 + debug: 4.3.4_supports-color@5.5.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + /@babel/types/7.18.13: resolution: {integrity: sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==} engines: {node: '>=6.9.0'} @@ -8231,7 +8273,6 @@ packages: magic-string: 0.25.9 resolve: 1.22.1 rollup: 2.79.1 - dev: false /@rollup/plugin-commonjs/21.1.0_rollup@2.78.1: resolution: {integrity: sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA==} @@ -8275,7 +8316,6 @@ packages: dependencies: '@rollup/pluginutils': 3.1.0_rollup@2.79.1 rollup: 2.79.1 - dev: false /@rollup/plugin-node-resolve/11.2.1_rollup@2.79.1: resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} @@ -8320,7 +8360,6 @@ packages: is-module: 1.0.0 resolve: 1.22.1 rollup: 2.79.1 - dev: false /@rollup/plugin-node-resolve/9.0.0_rollup@2.78.1: resolution: {integrity: sha512-gPz+utFHLRrd41WMP13Jq5mqqzHL3OXrfj3/MkSyB6UBIcuNt9j60GCbarzMzdf1VHFpOxfQh/ez7wyadLMqkg==} @@ -8379,7 +8418,6 @@ packages: make-dir: 3.1.0 mime: 2.6.0 rollup: 2.79.1 - dev: false /@rollup/pluginutils/3.1.0_rollup@2.78.1: resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} @@ -11465,7 +11503,7 @@ packages: /@types/resolve/1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 15.14.9 + '@types/node': 18.11.16 /@types/responselike/1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} @@ -12384,6 +12422,26 @@ packages: clean-stack: 2.2.0 indent-string: 4.0.0 + /agora-rtc-react/1.1.3_react@18.2.0: + resolution: {integrity: sha512-AgmDoeLoL3xC7u60lTehHi1GvHsP8l09CX3bn+NyHx6E0Dmbbxdtqkn+YrlakWa4IYOAn0d/9oKvuGrNtUiOpA==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + agora-rtc-sdk-ng: 4.15.1 + react: 18.2.0 + dev: false + + /agora-rtc-sdk-ng/4.15.1: + resolution: {integrity: sha512-U1I1jOjs9NJSDPAsFuRUgiFgKRx8/J5rH12zNaAGyyXt3A+MPfYAccgfvlJIEhWn02ey97xXDklvufGercJcvw==} + dependencies: + agora-rte-extension: 1.2.3 + dev: false + + /agora-rte-extension/1.2.3: + resolution: {integrity: sha512-k3yNrYVyzJRoQJjaJUktKUI1XRtf8J1XsW8OzYKFqGlS8WQRMsES1+Phj2rfuEriiLObfuyuCimG6KHQCt5tiw==} + dev: false + /ahooks-v3-count/1.0.0: resolution: {integrity: sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ==} dev: false @@ -20794,7 +20852,6 @@ packages: postcss: ^8.1.0 dependencies: postcss: 8.4.20 - dev: true /idb-keyval/5.1.5: resolution: {integrity: sha512-J1utxYWQokYjy01LvDQ7WmiAtZCGUSkVi9EIBfUSyLOr/BesnMIxNGASTh9A1LzeISSjSqEPsfFdTss7EE7ofQ==} @@ -21123,12 +21180,11 @@ packages: mute-stream: 0.0.8 ora: 5.4.1 run-async: 2.4.1 - rxjs: 7.5.6 + rxjs: 7.8.0 string-width: 4.2.3 strip-ansi: 6.0.1 through: 2.3.8 wrap-ansi: 7.0.0 - dev: false /inquirer/9.1.4: resolution: {integrity: sha512-9hiJxE5gkK/cM2d1mTEnuurGTAoHebbkX0BYl3h7iEg7FYfuNIom+nDfBCSWtvSnoSrWCeBxqqBZu26xdlJlXA==} @@ -22817,7 +22873,6 @@ packages: /jsonc-parser/3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} - dev: false /jsonfile/2.4.0: resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} @@ -24149,7 +24204,6 @@ packages: yargs: 16.2.0 transitivePeerDependencies: - supports-color - dev: false /minimalistic-assert/1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -26935,7 +26989,6 @@ packages: postcss: ^8.1.0 dependencies: postcss: 8.4.20 - dev: true /postcss-modules-local-by-default/3.0.3: resolution: {integrity: sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==} @@ -26968,7 +27021,6 @@ packages: postcss: 8.4.20 postcss-selector-parser: 6.0.11 postcss-value-parser: 4.2.0 - dev: true /postcss-modules-scope/2.2.0: resolution: {integrity: sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==} @@ -26995,7 +27047,6 @@ packages: dependencies: postcss: 8.4.20 postcss-selector-parser: 6.0.11 - dev: true /postcss-modules-values/3.0.0: resolution: {integrity: sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==} @@ -27021,7 +27072,6 @@ packages: dependencies: icss-utils: 5.1.0_postcss@8.4.20 postcss: 8.4.20 - dev: true /postcss-nested/5.0.6_postcss@8.4.17: resolution: {integrity: sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==} @@ -30912,7 +30962,6 @@ packages: rollup: 2.79.1 transitivePeerDependencies: - supports-color - dev: false /rollup-plugin-inject-process-env/1.3.1: resolution: {integrity: sha512-kKDoL30IZr0wxbNVJjq+OS92RJSKRbKV6B5eNW4q3mZTFqoWDh6lHy+mPDYuuGuERFNKXkG+AKxvYqC9+DRpKQ==} @@ -30991,24 +31040,23 @@ packages: dependencies: '@rollup/pluginutils': 4.2.1 '@types/cssnano': 4.0.1 - cosmiconfig: 7.0.1 + cosmiconfig: 7.1.0 cssnano: 4.1.11 fs-extra: 9.1.0 - icss-utils: 5.1.0_postcss@8.4.17 + icss-utils: 5.1.0_postcss@8.4.20 mime-types: 2.1.35 p-queue: 6.6.2 - postcss: 8.4.17 - postcss-modules-extract-imports: 3.0.0_postcss@8.4.17 - postcss-modules-local-by-default: 4.0.0_postcss@8.4.17 - postcss-modules-scope: 3.0.0_postcss@8.4.17 - postcss-modules-values: 4.0.0_postcss@8.4.17 + postcss: 8.4.20 + postcss-modules-extract-imports: 3.0.0_postcss@8.4.20 + postcss-modules-local-by-default: 4.0.0_postcss@8.4.20 + postcss-modules-scope: 3.0.0_postcss@8.4.20 + postcss-modules-values: 4.0.0_postcss@8.4.20 postcss-value-parser: 4.2.0 query-string: 6.14.1 resolve: 1.22.1 rollup: 2.79.1 source-map: 0.7.4 - tslib: 2.4.0 - dev: false + tslib: 2.4.1 /rollup-plugin-terser/6.1.0_rollup@2.78.1: resolution: {integrity: sha512-4fB3M9nuoWxrwm39habpd4hvrbrde2W2GG4zEGPQg1YITNkM3Tqur5jSuXlWNzbv/2aMLJ+dZJaySc3GCD8oDw==} @@ -31151,7 +31199,6 @@ packages: resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} dependencies: tslib: 2.4.1 - dev: true /safari-14-idb-fix/1.0.6: resolution: {integrity: sha512-oTEQOdMwRX+uCtWCKT1nx2gAeSdpr8elg/2gcaKUH00SJU2xWESfkx11nmXwTRHy7xfQoj1o4TTQvdmuBosTnA==} @@ -32583,7 +32630,7 @@ packages: react-is: '>= 16.8.0' dependencies: '@babel/helper-module-imports': 7.18.6 - '@babel/traverse': 7.18.13_supports-color@5.5.0 + '@babel/traverse': 7.20.5_supports-color@5.5.0 '@emotion/is-prop-valid': 1.2.0 '@emotion/stylis': 0.8.5 '@emotion/unitless': 0.7.5 diff --git a/server/plugins/com.msgbyte.agora/.ministarrc.js b/server/plugins/com.msgbyte.agora/.ministarrc.js new file mode 100644 index 00000000..3c4db179 --- /dev/null +++ b/server/plugins/com.msgbyte.agora/.ministarrc.js @@ -0,0 +1,14 @@ +const path = require('path'); + +module.exports = { + externalDeps: [ + 'react', + 'react-router', + 'axios', + 'styled-components', + 'zustand', + 'zustand/middleware/immer', + ], + pluginRoot: path.resolve(__dirname, './web'), + outDir: path.resolve(__dirname, '../../public'), +}; diff --git a/server/plugins/com.msgbyte.agora/models/agora.ts b/server/plugins/com.msgbyte.agora/models/agora.ts new file mode 100644 index 00000000..eca792ca --- /dev/null +++ b/server/plugins/com.msgbyte.agora/models/agora.ts @@ -0,0 +1,20 @@ +import { db } from 'tailchat-server-sdk'; +const { getModelForClass, prop, modelOptions, TimeStamps } = db; + +@modelOptions({ + options: { + customName: 'p_agora', + }, +}) +export class Agora extends TimeStamps implements db.Base { + _id: db.Types.ObjectId; + id: string; +} + +export type AgoraDocument = db.DocumentType; + +const model = getModelForClass(Agora); + +export type AgoraModel = typeof model; + +export default model; diff --git a/server/plugins/com.msgbyte.agora/package.json b/server/plugins/com.msgbyte.agora/package.json new file mode 100644 index 00000000..758175e5 --- /dev/null +++ b/server/plugins/com.msgbyte.agora/package.json @@ -0,0 +1,20 @@ +{ + "name": "tailchat-plugin-agora", + "version": "1.0.0", + "main": "index.js", + "author": "moonrailgun", + "description": "为Tailchat增加声网音视频通信功能", + "license": "MIT", + "private": true, + "scripts": { + "build:web": "ministar buildPlugin all", + "build:web:watch": "ministar watchPlugin all" + }, + "devDependencies": { + "@types/react": "18.0.20", + "mini-star": "*" + }, + "dependencies": { + "tailchat-server-sdk": "*" + } +} diff --git a/server/plugins/com.msgbyte.agora/services/agora.service.dev.ts b/server/plugins/com.msgbyte.agora/services/agora.service.dev.ts new file mode 100644 index 00000000..06b3edd9 --- /dev/null +++ b/server/plugins/com.msgbyte.agora/services/agora.service.dev.ts @@ -0,0 +1,36 @@ +import { TcService, TcDbService } from 'tailchat-server-sdk'; +import type { AgoraDocument, AgoraModel } from '../models/agora'; + +/** + * 声网音视频 + * + * 为Tailchat增加声网音视频通信功能 + */ +interface AgoraService + extends TcService, + TcDbService {} +class AgoraService extends TcService { + get serviceName() { + return 'plugin:com.msgbyte.agora'; + } + + /** + * 声网服务端的appid + */ + get serverAppId() { + return process.env.AGORA_APP_ID; + } + + /** + * 声网服务端的app证书 + */ + get serverAppCertificate() { + return process.env.AGORA_APP_CERT; + } + + onInit() { + // this.registerLocalDb(require('../models/agora').default); + } +} + +export default AgoraService; diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/manifest.json b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/manifest.json new file mode 100644 index 00000000..7086c99e --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/manifest.json @@ -0,0 +1,9 @@ +{ + "label": "声网音视频", + "name": "com.msgbyte.agora", + "url": "{BACKEND}/plugins/com.msgbyte.agora/index.js", + "version": "0.0.0", + "author": "moonrailgun", + "description": "为Tailchat增加声网音视频通信功能", + "requireRestart": true +} diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/package.json b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/package.json new file mode 100644 index 00000000..d5cb1b9c --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/package.json @@ -0,0 +1,18 @@ +{ + "name": "@plugins/com.msgbyte.agora", + "main": "src/index.tsx", + "version": "0.0.0", + "description": "为Tailchat增加声网音视频通信功能", + "private": true, + "scripts": { + "sync:declaration": "tailchat declaration github" + }, + "dependencies": { + "agora-rtc-react": "^1.1.3" + }, + "devDependencies": { + "@types/styled-components": "^5.1.26", + "react": "18.2.0", + "styled-components": "^5.3.6" + } +} diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Controls.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Controls.tsx new file mode 100644 index 00000000..0b5f3271 --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Controls.tsx @@ -0,0 +1,65 @@ +import { IconBtn } from '@capital/component'; +import type { ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-react'; +import React, { useState } from 'react'; +import { useClient } from './client'; + +export const Controls: React.FC<{ + tracks: [IMicrophoneAudioTrack, ICameraVideoTrack]; + setStart: React.Dispatch>; + onClose: () => void; +}> = React.memo((props) => { + const client = useClient(); + const { tracks, setStart } = props; + const [trackState, setTrackState] = useState({ video: true, audio: true }); + + const mute = async (type: 'audio' | 'video') => { + if (type === 'audio') { + await tracks[0].setEnabled(!trackState.audio); + setTrackState((ps) => { + return { ...ps, audio: !ps.audio }; + }); + } else if (type === 'video') { + await tracks[1].setEnabled(!trackState.video); + setTrackState((ps) => { + return { ...ps, video: !ps.video }; + }); + } + }; + + const leaveChannel = async () => { + await client.leave(); + client.removeAllListeners(); + // we close the tracks to perform cleanup + tracks[0].close(); + tracks[1].close(); + setStart(false); + props.onClose(); + }; + + return ( +
+ mute('video')} + /> + + mute('audio')} + /> + + +
+ ); +}); +Controls.displayName = 'Controls'; diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Videos.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Videos.tsx new file mode 100644 index 00000000..527f5170 --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Videos.tsx @@ -0,0 +1,40 @@ +import { + AgoraVideoPlayer, + IAgoraRTCRemoteUser, + ICameraVideoTrack, + IMicrophoneAudioTrack, +} from 'agora-rtc-react'; +import React from 'react'; + +export const Videos: React.FC<{ + users: IAgoraRTCRemoteUser[]; + tracks: [IMicrophoneAudioTrack, ICameraVideoTrack]; +}> = React.memo((props) => { + const { users, tracks } = props; + + return ( +
+
+ {/* AgoraVideoPlayer component takes in the video track to render the stream, + you can pass in other props that get passed to the rendered div */} + + + {users.length > 0 && + users.map((user) => { + if (user.videoTrack) { + return ( + + ); + } else { + return null; + } + })} +
+
+ ); +}); +Videos.displayName = 'Videos'; diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/client.ts b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/client.ts new file mode 100644 index 00000000..9848a126 --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/client.ts @@ -0,0 +1,17 @@ +import { + ClientConfig, + createClient, + createMicrophoneAndCameraTracks, +} from 'agora-rtc-react'; + +const config: ClientConfig = { + mode: 'rtc', + codec: 'vp8', +}; + +// TODO 应该从本地设置或者远程中获取 +export const appId = ''; //ENTER APP ID HERE +export const token = ''; + +export const useClient = createClient(config); +export const useMicrophoneAndCameraTracks = createMicrophoneAndCameraTracks(); diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/index.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/index.tsx new file mode 100644 index 00000000..30850def --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/index.tsx @@ -0,0 +1,34 @@ +import { showToasts } from '@capital/common'; +import { PortalAdd, PortalRemove, ErrorBoundary } from '@capital/component'; +import React from 'react'; +import { FloatMeetingWindow } from './window'; + +let currentMeeting: string | null = null; + +/** + * TODO + * + * 启动快速会议 + * + * 表现形式是在浏览器内有个小的浮动窗口 + */ +export function startFastMeeting(meetingId: string) { + if (currentMeeting) { + showToasts('当前已有正在进行中的通话, 请先结束上一场通话'); + return; + } + + currentMeeting = meetingId; + + const key = PortalAdd( + + { + PortalRemove(key); + currentMeeting = null; + }} + /> + + ); +} diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/window.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/window.tsx new file mode 100644 index 00000000..546a4bdf --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/window.tsx @@ -0,0 +1,164 @@ +import React, { useEffect, useState } from 'react'; +import { getJWTUserInfo } from '@capital/common'; +import type { IAgoraRTCRemoteUser } from 'agora-rtc-react'; +import styled from 'styled-components'; +import { + appId, + token, + useClient, + useMicrophoneAndCameraTracks, +} from './client'; +import { Videos } from './Videos'; +import { Controls } from './Controls'; + +const FloatWindow = styled.div` + z-index: 100; + position: fixed; + background-color: var(--tc-content-background-color); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + left: 0; + right: 0; + top: 0; + min-height: 240px; + transition: all 0.2s ease-in-out; + display: flex; + flex-direction: column; + + .body { + flex: 1; + + .videos { + height: 70vh; + align-self: flex-start; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(440px, 1fr)); + justify-items: center; + align-items: center; + + .vid { + height: 95%; + width: 95%; + position: relative; + background-color: black; + border-width: 1px; + border-color: #38373a; + border-style: solid; + } + } + } + + .controller { + text-align: center; + padding: 10px 0; + + * + * { + margin-left: 10px; + } + } + + .folder-btn { + background-color: var(--tc-content-background-color); + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); + position: absolute; + bottom: -30px; + height: 30px; + line-height: 30px; + left: 50%; + width: 60px; + margin-left: -30px; + text-align: center; + cursor: pointer; + border-radius: 0 0 3px 3px; + } +`; + +/** + * 音视频会议弹窗 + */ +export const FloatMeetingWindow: React.FC<{ + meetingId: string; + onClose: () => void; +}> = React.memo((props) => { + const [folder, setFolder] = useState(false); + const client = useClient(); + const { ready, tracks } = useMicrophoneAndCameraTracks(); + const channelName = props.meetingId; + const [users, setUsers] = useState([]); + const [start, setStart] = useState(false); + + useEffect(() => { + // function to initialise the SDK + const init = async (channel: string) => { + client.on('user-published', async (user, mediaType) => { + await client.subscribe(user, mediaType); + console.log('subscribe success'); + if (mediaType === 'video') { + setUsers((prevUsers) => { + return [...prevUsers, user]; + }); + } + if (mediaType === 'audio') { + user.audioTrack?.play(); + } + }); + + client.on('user-unpublished', (user, type) => { + console.log('unpublished', user, type); + if (type === 'audio') { + user.audioTrack?.stop(); + } + if (type === 'video') { + setUsers((prevUsers) => { + return prevUsers.filter((User) => User.uid !== user.uid); + }); + } + }); + + client.on('user-left', (user) => { + console.log('leaving', user); + setUsers((prevUsers) => { + return prevUsers.filter((User) => User.uid !== user.uid); + }); + }); + + const { _id } = await getJWTUserInfo(); + await client.join(appId, channel, token, _id); + if (tracks) { + await client.publish([tracks[0], tracks[1]]); + } + setStart(true); + }; + + if (ready && tracks) { + console.log('init ready'); + init(channelName); + } + }, [channelName, client, ready, tracks]); + + return ( + +
+ {start && tracks && } +
+ +
+ {ready && tracks && ( + + )} +
+ +
setFolder(!folder)}> + {folder ? '展开' : '收起'} +
+
+ ); +}); +FloatMeetingWindow.displayName = 'FloatMeetingWindow'; diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/index.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/index.tsx new file mode 100644 index 00000000..de280f5b --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/index.tsx @@ -0,0 +1,27 @@ +import { regPluginPanelAction } from '@capital/common'; +import { openConfirmModal } from '@capital/component'; + +console.log('Plugin 声网音视频 is loaded'); + +async function startFastMeeting(meetingId: string) { + const module = await import('./FloatWindow'); + module.startFastMeeting(meetingId); // 仅用于测试 +} + +// 发起群组会议 +regPluginPanelAction({ + name: 'plugin:com.msgbyte.meeting/groupAction', + label: '发起通话', + position: 'group', + icon: 'mdi:video-box', + onClick: ({ groupId, panelId }) => { + openConfirmModal({ + title: '发起通话', + content: '是否通过声网插件在当前会话开启音视频通讯?', + onConfirm: async () => { + // startFastMeeting(`${groupId}|${panelId}`); + startFastMeeting('123456'); // for test + }, + }); + }, +}); diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/tsconfig.json b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/tsconfig.json new file mode 100644 index 00000000..79e593c1 --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "jsx": "react", + "importsNotUsedAsValues": "error" + } +} diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/types/tailchat.d.ts b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/types/tailchat.d.ts new file mode 100644 index 00000000..e60fe980 --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/types/tailchat.d.ts @@ -0,0 +1,485 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/// + +/** + * 该文件由 Tailchat 自动生成 + * 用于插件的类型声明 + * 生成命令: pnpm run plugins:declaration:generate + */ + +/** + * Tailchat 通用 + */ +declare module '@capital/common' { + export const useGroupPanelParams: any; + + /** + * 打开模态框 + * @deprecated 请从 @capital/component 引入 + */ + export const openModal: ( + content: React.ReactNode, + + props?: { + /** + * 是否显示右上角的关闭按钮 + * @default false + */ + closable?: boolean; + + /** + * 遮罩层是否可关闭 + */ + maskClosable?: boolean; + + /** + * 关闭modal的回调 + */ + onCloseModal?: () => void; + } + ) => number; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const closeModal: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const ModalWrapper: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const useModalContext: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const openConfirmModal: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const openReconfirmModal: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const Loadable: any; + + export const getGlobalState: any; + + export const useGlobalSocketEvent: ( + eventName: string, + callback: (data: T) => void + ) => void; + + export const getJWTUserInfo: () => Promise<{ + _id?: string; + nickname?: string; + email?: string; + avatar?: string; + }>; + + export const dataUrlToFile: any; + + export const urlSearchStringify: any; + + export const urlSearchParse: any; + + export const appendUrlSearch: any; + + export const getServiceWorkerRegistration: any; + + export const getServiceUrl: () => string; + + export const getCachedUserInfo: ( + userId: string, + refetch?: boolean + ) => Promise<{ + _id: string; + email: string; + nickname: string; + discriminator: string; + avatar: string | null; + temporary: boolean; + }>; + + export const getCachedConverseInfo: any; + + /** + * 本地翻译 + * @example + * localTrans({'zh-CN': '你好', 'en-US': 'Hello'}); + * + * @param trans 翻译对象 + */ + export const localTrans: (trans: Record<'zh-CN' | 'en-US', string>) => string; + + export const getLanguage: any; + + export const sharedEvent: any; + + export const useAsync: Promise>( + fn: T, + deps?: React.DependencyList + ) => { loading: boolean; value?: any; error?: Error }; + + export const useAsyncFn: Promise>( + fn: T, + deps?: React.DependencyList + ) => [{ loading: boolean; value?: any; error?: Error }, T]; + + export const useAsyncRefresh: Promise>( + fn: T, + deps?: React.DependencyList + ) => { loading: boolean; value?: any; error?: Error; refresh: () => void }; + + export const useAsyncRequest: Promise>( + fn: T, + deps?: React.DependencyList + ) => [{ loading: boolean; value?: any }, T]; + + export const uploadFile: any; + + export const showToasts: ( + message: string, + type?: 'info' | 'success' | 'error' | 'warning' + ) => void; + + export const showSuccessToasts: any; + + export const showErrorToasts: (error: any) => void; + + export const fetchAvailableServices: any; + + export const isValidStr: (str: any) => str is string; + + export const useGroupPanelInfo: any; + + export const sendMessage: any; + + export const showMessageTime: any; + + export const useLocation: any; + + export const useNavigate: any; + + /** + * @deprecated please use createMetaFormSchema from @capital/component + */ + export const createFastFormSchema: any; + + /** + * @deprecated please use metaFormFieldSchema from @capital/component + */ + export const fieldSchema: any; + + export const useCurrentUserInfo: any; + + export const createPluginRequest: (pluginName: string) => { + get: (actionName: string, config?: any) => Promise; + post: (actionName: string, data?: any, config?: any) => Promise; + }; + + export const postRequest: any; + + export const pluginCustomPanel: any; + + export const regCustomPanel: any; + + export const pluginGroupPanel: any; + + export const regGroupPanel: any; + + export const messageInterpreter: { + name?: string; + explainMessage: (message: string) => React.ReactNode; + }[]; + + export const regMessageInterpreter: (interpreter: { + name?: string; + explainMessage: (message: string) => React.ReactNode; + }) => void; + + export const getMessageRender: (message: string) => React.ReactNode; + + export const regMessageRender: ( + render: (message: string) => React.ReactNode + ) => void; + + export const getMessageTextDecorators: any; + + export const regMessageTextDecorators: any; + + export const ChatInputActionContextProps: any; + + export const pluginChatInputActions: any; + + export const regChatInputAction: any; + + export const regSocketEventListener: (item: { + eventName: string; + eventFn: (...args: any[]) => void; + }) => void; + + export const pluginColorScheme: any; + + export const regPluginColorScheme: any; + + export const pluginInspectServices: any; + + export const regInspectService: any; + + export const pluginMessageExtraParsers: any; + + export const regMessageExtraParser: any; + + export const pluginRootRoute: any; + + export const regPluginRootRoute: any; + + export const pluginPanelActions: any; + + interface BasePluginPanelActionProps { + /** + * 唯一标识 + */ + name: string; + /** + * 显示名 + */ + label: string; + /** + * 来自iconify的图标标识 + */ + icon: string; + } + + export const regPluginPanelAction: ( + action: + | { + name: string; + label: string; + icon: string; + position: 'group'; + onClick: (ctx: { groupId: string; panelId: string }) => void; + } + | { + name: string; + label: string; + icon: string; + position: 'dm'; + onClick: (ctx: { converseId: string }) => void; + } + ) => void; + + export const pluginPermission: any; + + export const regPluginPermission: (permission: { + /** + * 权限唯一key, 用于写入数据库 + * 如果为插件则权限点应当符合命名规范, 如: plugin.com.msgbyte.github.manage + */ + key: string; + /** + * 权限点显示名称 + */ + title: string; + /** + * 权限描述 + */ + desc: string; + /** + * 是否默认开启 + */ + default: boolean; + /** + * 是否依赖其他权限点 + */ + required?: string[]; + }) => void; + + export const pluginGroupPanelBadges: any; + + export const regGroupPanelBadge: any; + + export const pluginGroupTextPanelExtraMenus: any; + + export const regPluginGroupTextPanelExtraMenu: any; + + export const useGroupIdContext: () => string; + + export const useGroupPanelContext: () => { + groupId: string; + panelId: string; + } | null; + + export const useSocketContext: any; +} + +/** + * Tailchat 组件 + */ +declare module '@capital/component' { + export const Button: any; + + export const Checkbox: any; + + export const Input: any; + + export const Divider: any; + + export const Space: any; + + export const Menu: any; + + export const Table: any; + + export const Switch: any; + + export const Tooltip: any; + + /** + * @link https://ant.design/components/notification-cn/ + */ + export const notification: any; + + export const Empty: React.FC< + React.PropsWithChildren<{ + prefixCls?: string; + className?: string; + style?: React.CSSProperties; + imageStyle?: React.CSSProperties; + image?: React.ReactNode; + description?: React.ReactNode; + }> + >; + + export const TextArea: any; + + export const Avatar: any; + + export const SensitiveText: React.FC<{ className?: string; text: string }>; + + export const Icon: React.FC<{ icon: string } & React.SVGProps>; + + export const CopyableText: React.FC<{ + className?: string; + style?: React.CSSProperties; + config?: + | boolean + | { + text?: string; + onCopy?: (event?: React.MouseEvent) => void; + icon?: React.ReactNode; + tooltips?: boolean | React.ReactNode; + format?: 'text/plain' | 'text/html'; + }; + }>; + + export const WebFastForm: any; + + export const WebMetaForm: any; + + export const createMetaFormSchema: any; + + export const metaFormFieldSchema: any; + + export const Image: any; + + export const IconBtn: React.FC<{ + icon: string; + className?: string; + iconClassName?: string; + size?: 'small' | 'middle' | 'large'; + shape?: 'circle' | 'square'; + title?: string; + danger?: boolean; + onClick?: React.MouseEventHandler; + }>; + + export const PillTabs: any; + + export const PillTabPane: any; + + export const LoadingSpinner: React.FC<{ tip?: string }>; + + export const FullModalField: any; + + export const DefaultFullModalInputEditorRender: any; + + export const DefaultFullModalTextAreaEditorRender: any; + + export const openModal: ( + content: React.ReactNode, + + props?: { + /** + * 是否显示右上角的关闭按钮 + * @default false + */ + closable?: boolean; + + /** + * 遮罩层是否可关闭 + */ + maskClosable?: boolean; + + /** + * 关闭modal的回调 + */ + onCloseModal?: () => void; + } + ) => number; + + export const closeModal: any; + + export const ModalWrapper: any; + + export const useModalContext: any; + + export const openConfirmModal: any; + + export const openReconfirmModal: any; + + export const Loadable: any; + + export const Loading: React.FC<{ + spinning: boolean; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; + }>; + + export const LoadingOnFirst: React.FC<{ + spinning: boolean; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; + }>; + + export const SidebarView: any; + + export const GroupPanelSelector: any; + + export const Emoji: any; + + export const PortalAdd: any; + + export const PortalRemove: any; + + export const ErrorBoundary: any; + + export const UserAvatar: any; + + export const UserName: React.FC<{ + userId: string; + className?: string; + }>; + + export const Markdown: any; +} diff --git a/server/plugins/com.msgbyte.meeting/web/plugins/com.msgbyte.meeting/src/helper.ts b/server/plugins/com.msgbyte.meeting/web/plugins/com.msgbyte.meeting/src/helper.ts index ff8b1811..fa2a710a 100644 --- a/server/plugins/com.msgbyte.meeting/web/plugins/com.msgbyte.meeting/src/helper.ts +++ b/server/plugins/com.msgbyte.meeting/web/plugins/com.msgbyte.meeting/src/helper.ts @@ -16,6 +16,6 @@ export async function createMeetingAndShare(groupId: string, panelId: string) { sendMessage({ groupId, converseId: panelId, - content: `${userInfo.nickname} 发起了会话,点击链接快速加入会议: ${fullUrl}`, + content: `${userInfo.nickname} 发起了通话,点击链接快速加入会议: ${fullUrl}`, }); } diff --git a/server/services/core/chat/ack.service.ts b/server/services/core/chat/ack.service.ts index 9add3e40..fd6cd331 100644 --- a/server/services/core/chat/ack.service.ts +++ b/server/services/core/chat/ack.service.ts @@ -51,7 +51,7 @@ class AckService extends TcService { } ); - // TODO: 如果要实现可以在此处基于会话id进行通知 + // TODO: 如果要实现消息已读可以在此处基于会话id进行通知 } /**