diff --git a/client/web/plugins/com.msgbyte.mdpanel/src/index.tsx b/client/web/plugins/com.msgbyte.mdpanel/src/index.tsx index 583e3aa6..e663a84c 100644 --- a/client/web/plugins/com.msgbyte.mdpanel/src/index.tsx +++ b/client/web/plugins/com.msgbyte.mdpanel/src/index.tsx @@ -7,6 +7,9 @@ const PLUGIN_NAME = 'Markdown Panel'; console.log(`Plugin ${PLUGIN_NAME}(${PLUGIN_ID}) is loaded`); +/** + * @note 应该用 PLUGIN_ID 而不是 PLUGIN_NAME, 后续需要逐步迁移 + */ regGroupPanel({ name: `${PLUGIN_NAME}/customwebpanel`, label: Translate.name, diff --git a/client/web/src/plugin/component/index.tsx b/client/web/src/plugin/component/index.tsx index 1ef1e156..1d3a88b8 100644 --- a/client/web/src/plugin/component/index.tsx +++ b/client/web/src/plugin/component/index.tsx @@ -35,6 +35,7 @@ export { Link } from 'react-router-dom'; export { MessageAckContainer } from '@/components/ChatBox/ChatMessageList/MessageAckContainer'; export { BaseChatInputButton } from '@/components/ChatBox/ChatInputBox/BaseChatInputButton'; export { useChatInputActionContext } from '@/components/ChatBox/ChatInputBox/context'; +export { GroupPanelContainer } from '@/components/Panel/group/shared/GroupPanelContainer'; export { GroupExtraDataPanel } from '@/components/Panel/group/GroupExtraDataPanel'; export { Image } from '@/components/Image'; export { IconBtn } from '@/components/IconBtn'; diff --git a/client/web/tailchat.d.ts b/client/web/tailchat.d.ts index 16bca834..c290d79a 100644 --- a/client/web/tailchat.d.ts +++ b/client/web/tailchat.d.ts @@ -153,7 +153,9 @@ declare module '@capital/common' { deps?: React.DependencyList ) => [{ loading: boolean; value?: any }, T]; - export const useEvent: any; + export const useEvent: any>( + fn: T + ) => T; export const uploadFile: any; @@ -204,7 +206,12 @@ declare module '@capital/common' { export const getTextColorHex: any; - export const useCurrentUserInfo: any; + export const useCurrentUserInfo: () => { + email?: string; + nickname?: string; + discriminator: string; + avatar?: string; + }; export const createPluginRequest: (pluginName: string) => { get: (actionName: string, config?: any) => Promise; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e4c8686..5449c54b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1939,6 +1939,44 @@ importers: specifier: ^5.0.0 version: 5.0.0 + server/plugins/com.msgbyte.livekit: + dependencies: + tailchat-server-sdk: + specifier: '*' + version: link:../../packages/sdk + devDependencies: + '@types/react': + specifier: 18.0.20 + version: 18.0.20 + mini-star: + specifier: '*' + version: 1.3.1 + + server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit: + dependencies: + '@livekit/components-core': + specifier: ^0.6.11 + version: 0.6.11(livekit-client@1.12.1) + '@livekit/components-react': + specifier: ^1.0.8 + version: 1.0.8(livekit-client@1.12.1)(react-dom@18.2.0)(react@18.2.0) + '@livekit/components-styles': + specifier: ^1.0.4 + version: 1.0.4 + livekit-client: + specifier: ^1.12.1 + version: 1.12.1 + devDependencies: + '@types/styled-components': + specifier: ^5.1.26 + version: 5.1.26 + react: + specifier: 18.2.0 + version: 18.2.0 + styled-components: + specifier: ^5.3.6 + version: 5.3.10(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + server/plugins/com.msgbyte.meeting: devDependencies: less: @@ -6711,6 +6749,16 @@ packages: text-decoding: 1.0.0 dev: false + /@floating-ui/core@1.3.1: + resolution: {integrity: sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==} + dev: false + + /@floating-ui/dom@1.4.5: + resolution: {integrity: sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw==} + dependencies: + '@floating-ui/core': 1.3.1 + dev: false + /@gar/promisify@1.1.3: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} dev: true @@ -7126,6 +7174,41 @@ packages: engines: {node: '>=10.0.0'} dev: false + /@livekit/components-core@0.6.11(livekit-client@1.12.1): + resolution: {integrity: sha512-FufCqJ/G0rznKR4hCY1EM+9GtwGK+easNgOPw4WPvhFEOO4L00KuEYWpzbWBzVFBHYqBuypkZS4enOsSHwPAJQ==} + engines: {node: '>=14'} + peerDependencies: + livekit-client: ^1.12.0 + dependencies: + '@floating-ui/dom': 1.4.5 + email-regex: 5.0.0 + global-tld-list: 0.0.1139 + livekit-client: 1.12.1 + loglevel: 1.8.1 + rxjs: 7.8.0 + dev: false + + /@livekit/components-react@1.0.8(livekit-client@1.12.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-JOGcoLwOXI2vQW0U7Zwh+DXIoC/Nm9/yOg7U6tAv6+Sps/BlAjyY09XVnI6GBLeJpiaqzlY2ualmfMKiWDgDhw==} + engines: {node: '>=14'} + peerDependencies: + livekit-client: ^1.12.0 + react: '>=18' + react-dom: '>=18' + dependencies: + '@livekit/components-core': 0.6.11(livekit-client@1.12.1) + '@react-hook/latest': 1.0.3(react@18.2.0) + clsx: 1.2.1 + livekit-client: 1.12.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@livekit/components-styles@1.0.4: + resolution: {integrity: sha512-bLCj+asyZLKtviKK+wU2WJ/pe0K/QSkzolXAOKkVedojs2Yc4e6i1tjOr0rTRSJxl4opYrZK9Ye89HLq6J04fA==} + engines: {node: '>=14'} + dev: false + /@loadable/component@5.15.3(react@18.2.0): resolution: {integrity: sha512-VOgYgCABn6+/7aGIpg7m0Ruj34tGetaJzt4bQ345FwEovDQZ+dua+NWLmuJKv8rWZyxOUSfoJkmGnzyDXH2BAQ==} engines: {node: '>=8'} @@ -7961,6 +8044,49 @@ packages: - supports-color dev: false + /@protobufjs/aspromise@1.1.2: + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + dev: false + + /@protobufjs/base64@1.1.2: + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + dev: false + + /@protobufjs/codegen@2.0.4: + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + dev: false + + /@protobufjs/eventemitter@1.1.0: + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + dev: false + + /@protobufjs/fetch@1.1.0: + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + dev: false + + /@protobufjs/float@1.0.2: + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + dev: false + + /@protobufjs/inquire@1.1.0: + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + dev: false + + /@protobufjs/path@1.1.2: + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + dev: false + + /@protobufjs/pool@1.1.0: + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + dev: false + + /@protobufjs/utf8@1.1.0: + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + dev: false + /@rc-component/portal@1.1.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-tbXM9SB1r5FOuZjRCljERFByFiEUcMmCWMXLog/NmgCzlAzreXyf23Vei3ZpSMxSMavzPnhCovfZjZdmxS3d1w==} engines: {node: '>=8.x'} @@ -8011,6 +8137,14 @@ packages: resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==} dev: false + /@react-hook/latest@1.0.3(react@18.2.0): + resolution: {integrity: sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==} + peerDependencies: + react: '>=16.8' + dependencies: + react: 18.2.0 + dev: false + /@reduxjs/toolkit@1.8.5(react-redux@8.0.2)(react@18.2.0): resolution: {integrity: sha512-f4D5EXO7A7Xq35T0zRbWq5kJQyXzzscnHKmjnu2+37B3rwHU6mX9PYlbfXdnxcY6P/7zfmjhgan0Z+yuOfeBmA==} peerDependencies: @@ -17369,6 +17503,23 @@ packages: /electron-to-chromium@1.4.310: resolution: {integrity: sha512-/xlATgfwkm5uDDwLw5nt/MNEf7c1oazLURMZLy39vOioGYyYzLWIDT8fZMJak6qTiAJ7udFTy7JG7ziyjNutiA==} + /elliptic@6.5.4: + resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: false + + /email-regex@5.0.0: + resolution: {integrity: sha512-he76Cm8JFxb6OGQHabLBPdsiStgPmJeAEhctmw0uhonUh1pCBsHpI6/rB62s2GNzjBb0YlhIcF/1l9Lp5AfH0Q==} + engines: {node: '>=12'} + dev: false + /emittery@0.8.1: resolution: {integrity: sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==} engines: {node: '>=10'} @@ -18269,6 +18420,10 @@ packages: /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + /eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + dev: false + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -19591,6 +19746,10 @@ packages: which: 1.3.1 dev: false + /global-tld-list@0.0.1139: + resolution: {integrity: sha512-TCWjAwHPzFV6zbQ5jnJvJTctesHGJr9BppxivRuIxTiIFUzaxy1F0674cxjoJecW5s8V32Q5i35dBFqvAy7eGQ==} + dev: false + /global@4.4.0: resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} dependencies: @@ -22758,6 +22917,17 @@ packages: wrap-ansi: 7.0.0 dev: true + /livekit-client@1.12.1: + resolution: {integrity: sha512-/mob04a/Mb0D+4sIzB7/pqakpJMCORSK+Qu5oTIcuSpgL+eBYGzHPE2sutGCGoe3Ns9sITAqUTyiui5+GN3i2w==} + dependencies: + eventemitter3: 5.0.1 + loglevel: 1.8.0 + protobufjs: 7.2.4 + sdp-transform: 2.14.1 + ts-debounce: 4.0.0 + webrtc-adapter: 8.2.3 + dev: false + /load-json-file@1.1.0: resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==} engines: {node: '>=0.10.0'} @@ -23009,6 +23179,15 @@ packages: engines: {node: '>= 0.6.0'} dev: false + /loglevel@1.8.1: + resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==} + engines: {node: '>= 0.6.0'} + dev: false + + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + /longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} dev: false @@ -27094,6 +27273,25 @@ packages: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} dev: true + /protobufjs@7.2.4: + resolution: {integrity: sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 18.11.9 + long: 5.2.3 + dev: false + /protocols@2.0.1: resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} dev: true @@ -29820,6 +30018,10 @@ packages: hasBin: true dev: false + /sdp@3.2.0: + resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==} + dev: false + /section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -31943,6 +32145,10 @@ packages: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} dev: false + /ts-debounce@4.0.0: + resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==} + dev: false + /ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -33636,6 +33842,13 @@ packages: std-env: 3.3.2 webpack: 5.75.0(esbuild@0.12.29)(webpack-cli@4.10.0) + /webrtc-adapter@8.2.3: + resolution: {integrity: sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==} + engines: {node: '>=6.0.0', npm: '>=3.10.0'} + dependencies: + sdp: 3.2.0 + dev: false + /websocket-driver@0.7.4: resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} engines: {node: '>=0.8.0'} diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/package.json b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/package.json index 6a997c5a..902a5cd1 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/package.json +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/package.json @@ -7,7 +7,12 @@ "scripts": { "sync:declaration": "tailchat declaration github" }, - "dependencies": {}, + "dependencies": { + "@livekit/components-core": "^0.6.11", + "@livekit/components-react": "^1.0.8", + "@livekit/components-styles": "^1.0.4", + "livekit-client": "^1.12.1" + }, "devDependencies": { "@types/styled-components": "^5.1.26", "react": "18.2.0", diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/LivekitContainer.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/LivekitContainer.tsx new file mode 100644 index 00000000..9b2948d8 --- /dev/null +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/LivekitContainer.tsx @@ -0,0 +1,9 @@ +import styled from 'styled-components'; +import '@livekit/components-styles'; + +export const LivekitContainer = styled.div.attrs({ + 'data-lk-theme': 'default', +})` + height: 100%; + background-color: var(--lk-bg); +`; diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/PreJoinView.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/PreJoinView.tsx new file mode 100644 index 00000000..559b4a49 --- /dev/null +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/PreJoinView.tsx @@ -0,0 +1,316 @@ +import { + useEvent, + getJWTUserInfo, + useAsync, + useCurrentUserInfo, +} from '@capital/common'; +import { Avatar, Button } from '@capital/component'; +import { MediaDeviceMenu, TrackToggle } from '@livekit/components-react'; +import type { + CreateLocalTracksOptions, + LocalAudioTrack, + LocalTrack, + LocalVideoTrack, +} from 'livekit-client'; +import { + createLocalTracks, + facingModeFromLocalTrack, + Track, +} from 'livekit-client'; +import * as React from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { log } from '@livekit/components-core'; +import { Translate } from '../translate'; + +/** + * Fork from "@livekit/components-react" + */ + +/** @public */ +export type LocalUserChoices = { + username: string; + videoEnabled: boolean; + audioEnabled: boolean; + videoDeviceId: string; + audioDeviceId: string; +}; + +const DEFAULT_USER_CHOICES = { + username: '', + videoEnabled: true, + audioEnabled: true, + videoDeviceId: 'default', + audioDeviceId: 'default', +}; + +/** @public */ +export type PreJoinProps = Omit< + React.HTMLAttributes, + 'onSubmit' +> & { + /** This function is called with the `LocalUserChoices` if validation is passed. */ + onSubmit?: (values: LocalUserChoices) => void; + /** + * Provide your custom validation function. Only if validation is successful the user choices are past to the onSubmit callback. + */ + onValidate?: (values: LocalUserChoices) => boolean; + onError?: (error: Error) => void; + /** Prefill the input form with initial values. */ + defaults?: Partial; + /** Display a debug window for your convenience. */ + debug?: boolean; + joinLabel?: string; + micLabel?: string; + camLabel?: string; + userLabel?: string; +}; + +/** + * The PreJoin prefab component is normally presented to the user before he enters a room. + * This component allows the user to check and select the preferred media device (camera und microphone). + * On submit the user decisions are returned, which can then be passed on to the LiveKitRoom so that the user enters the room with the correct media devices. + * + * @remarks + * This component is independent from the LiveKitRoom component and don't has to be nested inside it. + * Because it only access the local media tracks this component is self contained and works without connection to the LiveKit server. + * + * @example + * ```tsx + * + * ``` + * @public + */ +export const PreJoinView: React.FC = React.memo( + ({ + defaults = {}, + onValidate, + onSubmit, + onError, + debug, + joinLabel = Translate.joinLabel, + micLabel = Translate.micLabel, + camLabel = Translate.camLabel, + ...htmlProps + }) => { + const { nickname, avatar } = useCurrentUserInfo(); + const [userChoices, setUserChoices] = useState(DEFAULT_USER_CHOICES); + const [videoEnabled, setVideoEnabled] = useState( + defaults.videoEnabled ?? DEFAULT_USER_CHOICES.videoEnabled + ); + const initialVideoDeviceId = + defaults.videoDeviceId ?? DEFAULT_USER_CHOICES.videoDeviceId; + const [videoDeviceId, setVideoDeviceId] = + useState(initialVideoDeviceId); + const initialAudioDeviceId = + defaults.audioDeviceId ?? DEFAULT_USER_CHOICES.audioDeviceId; + const [audioEnabled, setAudioEnabled] = useState( + defaults.audioEnabled ?? DEFAULT_USER_CHOICES.audioEnabled + ); + const [audioDeviceId, setAudioDeviceId] = + useState(initialAudioDeviceId); + + const tracks = usePreviewTracks( + { + audio: audioEnabled ? { deviceId: initialAudioDeviceId } : false, + video: videoEnabled ? { deviceId: initialVideoDeviceId } : false, + }, + onError + ); + + const videoEl = useRef(null); + + const videoTrack = useMemo( + () => + tracks?.filter( + (track) => track.kind === Track.Kind.Video + )[0] as LocalVideoTrack, + [tracks] + ); + + const facingMode = useMemo(() => { + if (videoTrack) { + const { facingMode } = facingModeFromLocalTrack(videoTrack); + return facingMode; + } else { + return 'undefined'; + } + }, [videoTrack]); + + const audioTrack = useMemo( + () => + tracks?.filter( + (track) => track.kind === Track.Kind.Audio + )[0] as LocalAudioTrack, + [tracks] + ); + + useEffect(() => { + if (videoEl.current && videoTrack) { + videoTrack.unmute(); + videoTrack.attach(videoEl.current); + } + + return () => { + videoTrack?.detach(); + }; + }, [videoTrack]); + + const [isValid, setIsValid] = useState(); + + const handleValidation = useEvent((values: LocalUserChoices) => { + if (typeof onValidate === 'function') { + return onValidate(values); + } else { + return values.username !== ''; + } + }); + + useEffect(() => { + const newUserChoices = { + username: nickname, + videoEnabled: videoEnabled, + videoDeviceId: videoDeviceId, + audioEnabled: audioEnabled, + audioDeviceId: audioDeviceId, + }; + setUserChoices(newUserChoices); + setIsValid(handleValidation(newUserChoices)); + }, [ + videoEnabled, + handleValidation, + audioEnabled, + audioDeviceId, + videoDeviceId, + ]); + + function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + if (handleValidation(userChoices)) { + if (typeof onSubmit === 'function') { + onSubmit(userChoices); + } + } else { + log.warn('Validation failed with: ', userChoices); + } + } + + return ( +
+
+ {videoTrack && ( +
+
+
+ setAudioEnabled(enabled)} + > + {micLabel} + +
+ setAudioDeviceId(id)} + /> +
+
+
+ setVideoEnabled(enabled)} + > + {camLabel} + +
+ setVideoDeviceId(id)} + /> +
+
+
+ + + + {debug && ( + <> + User Choices: +
    +
  • Video Enabled: {`${userChoices.videoEnabled}`}
  • +
  • Audio Enabled: {`${userChoices.audioEnabled}`}
  • +
  • Video Device: {`${userChoices.videoDeviceId}`}
  • +
  • Audio Device: {`${userChoices.audioDeviceId}`}
  • +
+ + )} +
+ ); + } +); +PreJoinView.displayName = 'PreJoinView'; + +/** @alpha */ +function usePreviewTracks( + options: CreateLocalTracksOptions, + onError?: (err: Error) => void +) { + const [tracks, setTracks] = useState(); + + useEffect(() => { + let trackPromise: Promise | undefined = undefined; + let needsCleanup = false; + if (options.audio || options.video) { + trackPromise = createLocalTracks(options); + trackPromise + .then((tracks) => { + if (needsCleanup) { + tracks.forEach((tr) => tr.stop()); + } else { + setTracks(tracks); + } + }) + .catch(onError); + } + + return () => { + needsCleanup = true; + trackPromise?.then((tracks) => + tracks.forEach((track) => { + track.stop(); + }) + ); + }; + }, [JSON.stringify(options)]); + + return tracks; +} diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/group/LivekitPanel.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/group/LivekitPanel.tsx new file mode 100644 index 00000000..fcd26b30 --- /dev/null +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/group/LivekitPanel.tsx @@ -0,0 +1,52 @@ +import { + showErrorToasts, + useEvent, + useGroupPanelContext, +} from '@capital/common'; +import { GroupPanelContainer } from '@capital/component'; +import React, { useState } from 'react'; +import { + LiveKitRoom, + LocalUserChoices, + useToken, + VideoConference, + formatChatMessageLinks, +} from '@livekit/components-react'; +import { LogLevel, RoomOptions, VideoPresets } from 'livekit-client'; +import { PreJoinView } from '../components/PreJoinView'; +import { LivekitContainer } from '../components/LivekitContainer'; + +export const LivekitPanel: React.FC = React.memo(() => { + const { groupId, panelId } = useGroupPanelContext(); + const [preJoinChoices, setPreJoinChoices] = useState< + LocalUserChoices | undefined + >(undefined); + + const handleError = useEvent((err: Error) => { + showErrorToasts('error while setting up prejoin'); + console.log('error while setting up prejoin', err); + }); + + return ( + + +
+ { + console.log('Joining with: ', values); + setPreJoinChoices(values); + }} + /> +
+
+
+ ); +}); +LivekitPanel.displayName = 'LivekitPanel'; + +export default LivekitPanel; diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/index.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/index.tsx index a9bffb81..0f627313 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/index.tsx +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/index.tsx @@ -1 +1,16 @@ +import { regGroupPanel } from '@capital/common'; +import { Loadable } from '@capital/component'; +import { Translate } from './translate'; + +const PLUGIN_ID = 'com.msgbyte.livekit'; + console.log('Plugin livekit is loaded'); + +regGroupPanel({ + name: `${PLUGIN_ID}/livekitPanel`, + label: Translate.voiceChannel, + provider: PLUGIN_ID, + render: Loadable(() => import('./group/LivekitPanel'), { + componentName: `${PLUGIN_ID}:LivekitPanel`, + }), +}); diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/translate.ts b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/translate.ts new file mode 100644 index 00000000..2d906da4 --- /dev/null +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/translate.ts @@ -0,0 +1,20 @@ +import { localTrans } from '@capital/common'; + +export const Translate = { + voiceChannel: localTrans({ + 'zh-CN': '语音频道', + 'en-US': 'Voice Channel', + }), + joinLabel: localTrans({ + 'zh-CN': '加入房间', + 'en-US': 'Join Room', + }), + micLabel: localTrans({ + 'zh-CN': '麦克风', + 'en-US': 'Microphone', + }), + camLabel: localTrans({ + 'zh-CN': '摄像头', + 'en-US': 'Camera', + }), +}; diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/types/tailchat.d.ts b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/types/tailchat.d.ts index 88c97661..13d316c3 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/types/tailchat.d.ts +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/types/tailchat.d.ts @@ -573,4 +573,6 @@ declare module '@capital/component' { export const JumpToConverseButton: any; export const NoData: any; + + export const GroupPanelContainer: any; }