feat: 增加声网插件基本功能集成

pull/64/head
moonrailgun 2 years ago
parent 2a0695014a
commit 63ee943eec

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

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

@ -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'),
};

@ -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<Agora>;
const model = getModelForClass(Agora);
export type AgoraModel = typeof model;
export default model;

@ -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": "*"
}
}

@ -0,0 +1,36 @@
import { TcService, TcDbService } from 'tailchat-server-sdk';
import type { AgoraDocument, AgoraModel } from '../models/agora';
/**
*
*
* Tailchat
*/
interface AgoraService
extends TcService,
TcDbService<AgoraDocument, AgoraModel> {}
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;

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

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

@ -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<React.SetStateAction<boolean>>;
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 (
<div className="controller">
<IconBtn
icon={trackState.audio ? 'mdi:video' : 'mdi:video-off'}
title={trackState.audio ? '关闭摄像头' : '开启摄像头'}
size="large"
onClick={() => mute('video')}
/>
<IconBtn
icon={trackState.video ? 'mdi:microphone' : 'mdi:microphone-off'}
title={trackState.video ? '关闭麦克风' : '开启麦克风'}
size="large"
onClick={() => mute('audio')}
/>
<IconBtn
icon="mdi:phone-remove-outline"
title="挂断"
danger={true}
size="large"
onClick={leaveChannel}
/>
</div>
);
});
Controls.displayName = 'Controls';

@ -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 (
<div>
<div className="videos">
{/* AgoraVideoPlayer component takes in the video track to render the stream,
you can pass in other props that get passed to the rendered div */}
<AgoraVideoPlayer className="vid" videoTrack={tracks[1]} />
{users.length > 0 &&
users.map((user) => {
if (user.videoTrack) {
return (
<AgoraVideoPlayer
className="vid"
videoTrack={user.videoTrack}
key={user.uid}
/>
);
} else {
return null;
}
})}
</div>
</div>
);
});
Videos.displayName = 'Videos';

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

@ -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(
<ErrorBoundary>
<FloatMeetingWindow
meetingId={meetingId}
onClose={() => {
PortalRemove(key);
currentMeeting = null;
}}
/>
</ErrorBoundary>
);
}

@ -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<IAgoraRTCRemoteUser[]>([]);
const [start, setStart] = useState<boolean>(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 (
<FloatWindow
style={{
transform: folder ? 'translateY(-100%)' : 'none',
}}
>
<div className="body">
{start && tracks && <Videos users={users} tracks={tracks} />}
</div>
<div className="controller">
{ready && tracks && (
<Controls
tracks={tracks}
setStart={setStart}
onClose={props.onClose}
/>
)}
</div>
<div className="folder-btn" onClick={() => setFolder(!folder)}>
{folder ? '展开' : '收起'}
</div>
</FloatWindow>
);
});
FloatMeetingWindow.displayName = 'FloatMeetingWindow';

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

@ -0,0 +1,7 @@
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"importsNotUsedAsValues": "error"
}
}

@ -0,0 +1,485 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/// <reference types="react" />
/**
* 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: <T>(
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: <T extends (...args: any[]) => Promise<any>>(
fn: T,
deps?: React.DependencyList
) => { loading: boolean; value?: any; error?: Error };
export const useAsyncFn: <T extends (...args: any[]) => Promise<any>>(
fn: T,
deps?: React.DependencyList
) => [{ loading: boolean; value?: any; error?: Error }, T];
export const useAsyncRefresh: <T extends (...args: any[]) => Promise<any>>(
fn: T,
deps?: React.DependencyList
) => { loading: boolean; value?: any; error?: Error; refresh: () => void };
export const useAsyncRequest: <T extends (...args: any[]) => Promise<any>>(
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<any>;
post: (actionName: string, data?: any, config?: any) => Promise<any>;
};
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<SVGSVGElement>>;
export const CopyableText: React.FC<{
className?: string;
style?: React.CSSProperties;
config?:
| boolean
| {
text?: string;
onCopy?: (event?: React.MouseEvent<HTMLDivElement>) => 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<HTMLElement>;
}>;
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;
}

@ -16,6 +16,6 @@ export async function createMeetingAndShare(groupId: string, panelId: string) {
sendMessage({
groupId,
converseId: panelId,
content: `${userInfo.nickname} 发起了话,点击链接快速加入会议: ${fullUrl}`,
content: `${userInfo.nickname} 发起了话,点击链接快速加入会议: ${fullUrl}`,
});
}

@ -51,7 +51,7 @@ class AckService extends TcService {
}
);
// TODO: 如果要实现可以在此处基于会话id进行通知
// TODO: 如果要实现消息已读可以在此处基于会话id进行通知
}
/**

Loading…
Cancel
Save