From 7a9e004cbc8290fd3ac16abe67474e79c5d840d8 Mon Sep 17 00:00:00 2001 From: Binbim_ProMax Date: Sat, 4 Jul 2026 07:15:55 +0800 Subject: [PATCH] feat: migrate to LiveKit, remove Agora/payment, add domain certs for Caddy --- config/webpack.config.js | 3 + package.json | 2 +- pnpm-lock.yaml | 226 ++++------ public/locales/de/chat.json | 10 +- public/locales/de/common.json | 16 +- public/locales/de/member.json | 2 - public/locales/de/setting.json | 48 +- public/locales/de/welcome.json | 6 +- public/locales/en/chat.json | 11 +- public/locales/en/common.json | 16 +- public/locales/en/member.json | 2 - public/locales/en/setting.json | 49 +- public/locales/en/welcome.json | 6 +- public/locales/es/auth.json | 6 +- public/locales/es/chat.json | 39 +- public/locales/es/common.json | 12 - public/locales/es/member.json | 2 - public/locales/es/setting.json | 42 +- public/locales/es/welcome.json | 6 +- public/locales/fr/auth.json | 148 +++---- public/locales/fr/chat.json | 127 +++--- public/locales/fr/common.json | 12 - public/locales/fr/fav.json | 12 +- public/locales/fr/file.json | 12 +- public/locales/fr/member.json | 76 ++-- public/locales/fr/setting.json | 42 +- public/locales/fr/welcome.json | 6 +- public/locales/fr/widget.json | 10 +- public/locales/jp/chat.json | 7 +- public/locales/jp/common.json | 17 +- public/locales/jp/setting.json | 34 +- public/locales/jp/welcome.json | 6 +- public/locales/pt/chat.json | 9 +- public/locales/pt/common.json | 14 +- public/locales/pt/member.json | 2 - public/locales/pt/setting.json | 42 +- public/locales/pt/welcome.json | 6 +- public/locales/ru/chat.json | 9 +- public/locales/ru/common.json | 14 - public/locales/ru/member.json | 2 - public/locales/ru/setting.json | 46 +- public/locales/ru/welcome.json | 6 +- public/locales/tr/chat.json | 9 +- public/locales/tr/common.json | 12 - public/locales/tr/member.json | 1 - public/locales/tr/setting.json | 38 +- public/locales/tr/welcome.json | 6 +- public/locales/zh/chat.json | 10 +- public/locales/zh/common.json | 15 - public/locales/zh/member.json | 1 - public/locales/zh/setting.json | 48 +- public/locales/zh/welcome.json | 6 +- src/app/config.ts | 64 --- src/app/services/base.query.ts | 3 - src/app/services/server.ts | 130 +----- src/app/slices/server.ts | 8 +- src/app/slices/voice.ts | 17 +- src/components/BlankPlaceholder.tsx | 29 +- src/components/Profile/index.tsx | 8 +- src/components/Voice/DMCalling.tsx | 24 +- src/components/Voice/Operations.tsx | 49 +- src/components/Voice/index.tsx | 348 +++++++-------- src/components/Voice/useVoice.ts | 417 +++++++++--------- src/hooks/useConfig.ts | 54 +-- src/hooks/useLicense.ts | 6 +- src/hooks/useStreaming/index.ts | 50 ++- src/hooks/useUserOperation.ts | 24 +- src/routes/callback/PaymentSuccess.tsx | 53 --- src/routes/callback/index.tsx | 15 +- src/routes/chat/Layout/DMVoicing.tsx | 26 +- src/routes/chat/Layout/LicenseOutdatedTip.tsx | 17 +- src/routes/chat/Layout/index.tsx | 4 +- src/routes/chat/RTCWidget.tsx | 4 +- src/routes/chat/VoiceChat/VoiceManagement.tsx | 6 +- src/routes/chat/VoiceChat/index.tsx | 115 +---- src/routes/chat/VoiceFullscreen.tsx | 4 +- src/routes/chat/index.tsx | 3 + .../setting/License/LicensePriceListModal.tsx | 200 --------- .../setting/License/UpdateLicenseModal.tsx | 4 +- src/routes/setting/License/index.tsx | 31 +- src/routes/setting/config/Agora.tsx | 174 -------- src/routes/setting/config/Livekit.tsx | 88 ++++ src/routes/setting/config/Vocespace.tsx | 396 ----------------- src/routes/setting/navs.tsx | 12 +- src/types/common.ts | 11 - src/types/global.d.ts | 23 +- src/types/server.ts | 70 +-- src/types/sse.ts | 8 + src/utils.tsx | 25 +- 89 files changed, 1067 insertions(+), 2762 deletions(-) delete mode 100644 src/routes/callback/PaymentSuccess.tsx delete mode 100644 src/routes/setting/License/LicensePriceListModal.tsx delete mode 100644 src/routes/setting/config/Agora.tsx create mode 100644 src/routes/setting/config/Livekit.tsx delete mode 100644 src/routes/setting/config/Vocespace.tsx diff --git a/config/webpack.config.js b/config/webpack.config.js index 8fca5dd8..8dd60dea 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -21,6 +21,8 @@ const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin" const createEnvironmentHash = require("./webpack/persistentCache/createEnvironmentHash"); // Source maps are resource heavy and can cause out of memory issue for large source files. const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false"; +const shouldAnalyzeBundle = + process.env.ANALYZE === "true" || process.env.REACT_APP_ANALYZE_BUNDLE === "true"; const reactRefreshRuntimeEntry = require.resolve("react-refresh/runtime"); const reactRefreshWebpackPluginRuntimeEntry = require.resolve( @@ -404,6 +406,7 @@ module.exports = function (webpackEnv) { isEnvProduction && !process.env.REACT_APP_RELEASE && !process.env.REACT_APP_OFFICIAL_DEMO && + shouldAnalyzeBundle && new BundleAnalyzerPlugin({ openAnalyzer: false }), diff --git a/package.json b/package.json index c478e5f5..8c179971 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@udecode/plate-paragraph": "^33.0.0", "@udecode/plate-trailing-block": "^33.0.0", "@uiball/loaders": "^1.3.1", - "agora-rtc-sdk-ng": "^4.24.0", "broadcast-channel": "^7.1.0", "browserslist": "^4.25.1", "clsx": "^2.1.1", @@ -45,6 +44,7 @@ "linkify-react": "^4.3.2", "linkify-string": "^4.3.2", "linkifyjs": "^4.3.2", + "livekit-client": "^2.20.0", "localforage": "^1.10.0", "localforage-setitems": "^1.4.0", "lodash": "^4.17.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ce8a5f2..a831d7af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,9 +79,6 @@ importers: '@uiball/loaders': specifier: ^1.3.1 version: 1.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - agora-rtc-sdk-ng: - specifier: ^4.24.0 - version: 4.24.0 broadcast-channel: specifier: ^7.1.0 version: 7.1.0 @@ -142,6 +139,9 @@ importers: linkifyjs: specifier: ^4.3.2 version: 4.3.2 + livekit-client: + specifier: ^2.20.0 + version: 2.20.0(@types/dom-mediacapture-record@1.0.22) localforage: specifier: ^1.10.0 version: 1.10.0 @@ -353,15 +353,6 @@ importers: packages: - '@agora-js/media@4.24.0': - resolution: {integrity: sha512-foii2klr5+qonLznxN0ZZFejoxLt/W8do79wmIsADPZLw2uZjRP35m0lqUGiLXBKeQ8u3i4UygPzEdFaY26hrw==} - - '@agora-js/report@4.24.0': - resolution: {integrity: sha512-MYbtkdY1Ls0KW0iagUzrPzyvqMWlyCWSC5odEb1SQaraAl7DJeDUkf91a3wxKzrjVah+LCxFxsS4lCFDxvKgNA==} - - '@agora-js/shared@4.24.0': - resolution: {integrity: sha512-Vj67ZcTHZI+1ctWusrEPSSGLM3l6CFiAze/Bi8r7YHRMLivzhZR79nV6GiKvHS3muLAON2YAExznvjPIly6lcg==} - '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1053,6 +1044,9 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} + '@bufbuild/protobuf@1.10.1': + resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==} + '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -1409,6 +1403,12 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + '@livekit/mutex@1.1.1': + resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==} + + '@livekit/protocol@1.46.6': + resolution: {integrity: sha512-upzlHP1vi/kZ/QqALZTFskQ0ifqc2f15RKucHYOsIHJsaXvEYanG75mAb7o+Yomfs4XhQ4BaRsdY+TFHXpaqrg==} + '@metamask/onboarding@1.0.1': resolution: {integrity: sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ==} @@ -1918,6 +1918,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/dom-mediacapture-record@1.0.22': + resolution: {integrity: sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -2370,12 +2373,6 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} - agora-rtc-sdk-ng@4.24.0: - resolution: {integrity: sha512-2apG/07EtsuX21ncSF77q+dr6/kDgu9B/RpKtstCtaq46l4/Eraoecewi4zXRUCY3Im+8dzTIXx6jUwyPdxdHQ==} - - agora-rte-extension@1.2.4: - resolution: {integrity: sha512-0ovZz1lbe30QraG1cU+ji7EnQ8aUu+Hf3F+a8xPml3wPOyUQEK6CTdxV9kMecr9t+fIDrGeW7wgJTsM1DQE7Nw==} - ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -2463,9 +2460,6 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} @@ -2481,9 +2475,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.9.0: - resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} - babel-loader@9.2.1: resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} engines: {node: '>= 14.15.0'} @@ -2667,10 +2658,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - comma-separated-tokens@1.0.8: resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} @@ -2950,10 +2937,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -3257,10 +3240,6 @@ packages: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} engines: {node: '>=0.8.0'} - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -3356,18 +3335,10 @@ packages: vue-template-compiler: optional: true - form-data@4.0.1: - resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} - engines: {node: '>= 6'} - format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3938,6 +3909,9 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + jotai-optics@0.3.2: resolution: {integrity: sha512-RH6SvqU5hmkVqnHmaqf9zBXvIAs4jLxkDHS4fr5ljuBKHs8+HQ02v+9hX7ahTppxx6dUb0GGUE80jQKJ0kFTLw==} peerDependencies: @@ -4070,6 +4044,11 @@ packages: linkifyjs@4.3.2: resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + livekit-client@2.20.0: + resolution: {integrity: sha512-RIJcpvBmOmwz3jTj3rmdY6Dzr55HrhcaJjMgY+HSmoEM+yIRyA40m7r8UKv0hnZWM3z/AYhP1q8C8ciz5UWFKQ==} + peerDependencies: + '@types/dom-mediacapture-record': ^1 + loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} @@ -4124,6 +4103,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loglevel@1.9.2: + resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} + engines: {node: '>= 0.6.0'} + long@5.2.5: resolution: {integrity: sha512-e0r9YBBgNCq1D1o5Dp8FMH0N5hsFtXDBiVa0qoJPHpakvZkmDKPRoGffZJII/XsHvj9An9blm+cRJ01yQqU+Dw==} @@ -4373,11 +4356,6 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -4527,9 +4505,6 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - pako@2.1.0: - resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} @@ -4940,9 +4915,6 @@ packages: proxy-compare@2.6.0: resolution: {integrity: sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -5289,6 +5261,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -5328,6 +5303,10 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + sdp-transform@2.15.0: + resolution: {integrity: sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==} + hasBin: true + sdp@3.2.0: resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==} @@ -5491,6 +5470,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -5750,6 +5730,9 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typed-emitter@2.1.0: + resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==} + typescript-eslint@8.39.0: resolution: {integrity: sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5762,10 +5745,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ua-parser-js@0.7.40: - resolution: {integrity: sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==} - hasBin: true - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -5975,10 +5954,6 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} @@ -6039,8 +6014,8 @@ packages: webpack-cli: optional: true - webrtc-adapter@8.2.0: - resolution: {integrity: sha512-umxCMgedPAVq4Pe/jl3xmelLXLn4XZWFEMR5Iipb5wJ+k1xMX0yC4ZY9CueZUU1MjapFxai1tFGE7R/kotH6Ww==} + webrtc-adapter@9.0.5: + resolution: {integrity: sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==} engines: {node: '>=6.0.0', npm: '>=3.10.0'} websocket-driver@0.7.4: @@ -6251,30 +6226,6 @@ packages: snapshots: - '@agora-js/media@4.24.0': - dependencies: - '@agora-js/report': 4.24.0 - '@agora-js/shared': 4.24.0 - agora-rte-extension: 1.2.4 - axios: 1.9.0 - webrtc-adapter: 8.2.0 - transitivePeerDependencies: - - debug - - '@agora-js/report@4.24.0': - dependencies: - '@agora-js/shared': 4.24.0 - axios: 1.9.0 - transitivePeerDependencies: - - debug - - '@agora-js/shared@4.24.0': - dependencies: - axios: 1.9.0 - ua-parser-js: 0.7.40 - transitivePeerDependencies: - - debug - '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -7162,6 +7113,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@bufbuild/protobuf@1.10.1': {} + '@discoveryjs/json-ext@0.5.7': {} '@emoji-mart/data@1.2.1': {} @@ -7642,6 +7595,12 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} + '@livekit/mutex@1.1.1': {} + + '@livekit/protocol@1.46.6': + dependencies: + '@bufbuild/protobuf': 1.10.1 + '@metamask/onboarding@1.0.1': dependencies: bowser: 2.11.0 @@ -8122,6 +8081,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/dom-mediacapture-record@1.0.22': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -8691,22 +8652,6 @@ snapshots: address@1.2.2: {} - agora-rtc-sdk-ng@4.24.0: - dependencies: - '@agora-js/media': 4.24.0 - '@agora-js/report': 4.24.0 - '@agora-js/shared': 4.24.0 - agora-rte-extension: 1.2.4 - axios: 1.9.0 - formdata-polyfill: 4.0.10 - pako: 2.1.0 - ua-parser-js: 0.7.40 - webrtc-adapter: 8.2.0 - transitivePeerDependencies: - - debug - - agora-rte-extension@1.2.4: {} - ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -8786,8 +8731,6 @@ snapshots: async@3.2.6: {} - asynckit@0.4.0: {} - at-least-node@1.0.0: {} autoprefixer@10.4.21(postcss@8.5.6): @@ -8804,14 +8747,6 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axios@1.9.0: - dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.1 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - babel-loader@9.2.1(@babel/core@7.28.0)(webpack@5.101.0): dependencies: '@babel/core': 7.28.0 @@ -9040,10 +8975,6 @@ snapshots: colorette@2.0.20: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - comma-separated-tokens@1.0.8: {} comma-separated-tokens@2.0.3: {} @@ -9323,8 +9254,6 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - delayed-stream@1.0.0: {} - depd@1.1.2: {} depd@2.0.0: {} @@ -9724,11 +9653,6 @@ snapshots: dependencies: websocket-driver: 0.7.4 - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -9868,18 +9792,8 @@ snapshots: optionalDependencies: eslint: 9.32.0(jiti@1.21.7) - form-data@4.0.1: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - format@0.2.2: {} - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - forwarded@0.2.0: {} fraction.js@4.3.7: {} @@ -10488,6 +10402,8 @@ snapshots: jiti@1.21.7: {} + jose@6.2.3: {} + jotai-optics@0.3.2(jotai@2.11.3(@types/react@19.1.9)(react@19.1.1))(optics-ts@2.4.1): dependencies: jotai: 2.11.3(@types/react@19.1.9)(react@19.1.1) @@ -10584,6 +10500,19 @@ snapshots: linkifyjs@4.3.2: {} + livekit-client@2.20.0(@types/dom-mediacapture-record@1.0.22): + dependencies: + '@livekit/mutex': 1.1.1 + '@livekit/protocol': 1.46.6 + '@types/dom-mediacapture-record': 1.0.22 + events: 3.3.0 + jose: 6.2.3 + loglevel: 1.9.2 + sdp-transform: 2.15.0 + tslib: 2.8.1 + typed-emitter: 2.1.0 + webrtc-adapter: 9.0.5 + loader-runner@4.3.0: {} loader-utils@3.3.1: {} @@ -10629,6 +10558,8 @@ snapshots: lodash@4.17.21: {} + loglevel@1.9.2: {} + long@5.2.5: {} longest-streak@3.1.0: {} @@ -10959,8 +10890,6 @@ snapshots: lower-case: 2.0.2 tslib: 2.8.1 - node-domexception@1.0.0: {} - node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -11097,8 +11026,6 @@ snapshots: package-json-from-dist@1.0.1: {} - pako@2.1.0: {} - param-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -11502,8 +11429,6 @@ snapshots: proxy-compare@2.6.0: {} - proxy-from-env@1.1.0: {} - punycode@2.3.1: {} qrcode.react@4.2.0(react@19.1.1): @@ -11880,6 +11805,11 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + optional: true + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -11931,6 +11861,8 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 + sdp-transform@2.15.0: {} + sdp@3.2.0: {} select-hose@2.0.0: {} @@ -12452,6 +12384,10 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typed-emitter@2.1.0: + optionalDependencies: + rxjs: 7.8.2 + typescript-eslint@8.39.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.2): dependencies: '@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.2))(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.2) @@ -12465,8 +12401,6 @@ snapshots: typescript@5.9.2: {} - ua-parser-js@0.7.40: {} - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.3 @@ -12645,8 +12579,6 @@ snapshots: web-namespaces@2.0.1: {} - web-streams-polyfill@3.3.3: {} - web-vitals@4.2.4: {} webidl-conversions@3.0.1: {} @@ -12770,7 +12702,7 @@ snapshots: - esbuild - uglify-js - webrtc-adapter@8.2.0: + webrtc-adapter@9.0.5: dependencies: sdp: 3.2.0 diff --git a/public/locales/de/chat.json b/public/locales/de/chat.json index a8e6634b..0718980c 100644 --- a/public/locales/de/chat.json +++ b/public/locales/de/chat.json @@ -11,7 +11,6 @@ "welcome_channel": "Willkommen in {{name}}", "welcome_desc": "Dies ist der Beginn des Kanals #{{name}}.", "edit_channel": "Kanal bearbeiten", - "channel_name": "Kanalname", "private_channel": "Privater Kanal", "create_channel": "Neuen Kanal erstellen", @@ -19,7 +18,6 @@ "create_private_channel_desc": "Dies ist ein privater Kanal, nur ausgewählte Mitglieder können beitreten.", "search_user_placeholder": "Benutzernamen zum Suchen eingeben", "welcome_msg": "Willkommen im Kanal {{name}}", - "invite_title": "Freunde zu {{name}} einladen", "invite_by_email": "Per E-Mail einladen", "enable_smtp": "Zuerst SMTP aktivieren", @@ -29,23 +27,18 @@ "invite_link_edit": "Einladungslink bearbeiten", "invite_link_setting_tip": "Einladungslink läuft ab in {{expire}}, maximale Nutzungen: {{times}}", "generate_new_link": "Neuen Link generieren", - "send_to": "Senden an", "edited": "bearbeitet", - "license_tip": "Deine Lizenz hat das Limit erreicht, aktualisiere die Lizenz oder kontaktiere den Admin!", - + "license_tip": "Das aktuelle Lizenzlimit für Benutzer wurde erreicht. Bitte kontaktiere den Admin.", "delete_msg_title": "Nachricht löschen", "delete_msg_desc": "Bist du sicher, dass du {{msg}} löschen möchtest?", "delete_msg_this": "diese Nachricht", "delete_msg_these": "diese Nachrichten", - "new_msg": "{{num}} neue Nachrichten", "mark_read": "Als gelesen markieren", "pin_chat": "Chat anheften", "unpin_chat": "Lösen", - "reply_msg_del": "Diese Nachricht wurde gelöscht.", - "file": "Datei", "image": "Bild", "forward": "Weiterleiten", @@ -65,7 +58,6 @@ "contact_block_tip": "Dieser Benutzer wurde von dir blockiert", "file_expired": "Datei abgelaufen", "only_owner_can_send": "Nur der Kanalbesitzer kann Nachrichten senden!", - "remark": "Spitzname hinzufügen", "remark_clear": "Spitzname zurücksetzen", "remark_intro": "Finde Freunde schneller mit einem persönlichen Spitznamen. Dieser ist nur für dich in deinen Direktnachrichten sichtbar.", diff --git a/public/locales/de/common.json b/public/locales/de/common.json index d8fcf8c0..8ed10f2c 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -51,7 +51,6 @@ "install": "Installieren", "download_origin": "Original herunterladen", "close": "Schließen", - "upgrade": "Upgrade", "view_pwd": "Passwort anzeigen", "change_pwd": "Passwort ändern" }, @@ -59,7 +58,6 @@ "uploading": "Wird hochgeladen" }, "tip": { - "email_msg_tip": "E-Mail-Benachrichtigungen sind in deiner Region blockiert. Upgrade auf Pro für diese Funktion.", "update": "Erfolgreich aktualisiert!", "delete": "Erfolgreich gelöscht!", "reg": "Erfolgreich registriert!", @@ -71,22 +69,10 @@ }, "new_version": "<0>Neue Version verfügbar", "mobile_app": "Entdecke unsere <0>Mobile App", - "price": { - "pro": { - "title": "VoceChat Pro", - "desc": "", - "price": "$49" - }, - "supreme": { - "title": "VoceSpace Private Deployment Video [499$]", - "desc": "", - "price": "" - } - }, "server_update": { "version_needed": "Diese Funktion benötigt Server-Version: <0>{{version}} 🚨", "current_version": "Aktuelle Version: <0>{{version}}", - "update_tip": "Bitte Server upgraden!", + "update_tip": "Bitte Server aktualisieren!", "howto": "Anleitung zum Update" }, "inactive": { diff --git a/public/locales/de/member.json b/public/locales/de/member.json index 8140919d..55fc7683 100644 --- a/public/locales/de/member.json +++ b/public/locales/de/member.json @@ -12,7 +12,6 @@ "current_pwd": "Aktuelles Passwort", "new_pwd": "Neues Passwort", "confirm_new_pwd": "Neues Passwort bestätigen", - "manage_members": "Kontakte verwalten", "manage_tip": "Deaktivieren ermöglicht späteres Wiederherstellen des Kontos.", "admin": "Admin", @@ -28,7 +27,6 @@ "roles": "Rollen", "set_admin": "Admin", "set_normal": "Benutzer", - "search_not_found": "Nicht gefunden oder Suchberechtigung fehlt.", "search_by_id": "Nach ID suchen", "search_by_id_ph": "Benutzer-ID eingeben", diff --git a/public/locales/de/setting.json b/public/locales/de/setting.json index aad62e8d..f356beed 100644 --- a/public/locales/de/setting.json +++ b/public/locales/de/setting.json @@ -15,7 +15,6 @@ "bot": "Bot & Webhook", "notification_channels": "Benachrichtigungskanäle", "firebase": "Firebase", - "agora": "Agora", "smtp": "SMTP", "login_method": "Anmeldemethoden", "third_app": "Anmeldung über Drittanbieter", @@ -27,26 +26,6 @@ "video": "Videoanrufe", "notification": "Benachrichtigungen" }, - "vocespace": { - "add": "Hinzufügen", - "check": "Überprüfen", - "checked": "Bestanden", - "failed": "Fehlgeschlagen", - "title": "VoceSpace", - "desc": "Selbstgehosteter VoceSpace (Automatisierte Bereitstellung)", - "prerequisite": [ - "Voraussetzungen", - "Docker-Umgebung", - "Die Ports 80, 443, 7880, 7881, 3008 und 3000 sind geöffnet", - "Die Domain ist registriert", - "Für Unternehmensanpassungen oder bei Fragen wenden Sie sich bitte an han@privoce.com oder fügen Sie uns auf WeChat hinzu: Privoce" - ], - "sub_desc": "Jedes VoceSpace-Konto bietet unbegrenzte Gesprächsminuten, 5 gleichzeitige Teilnehmer und eine kostenlose 4K-Auflösung. Es ist keine Kontoerstellung erforderlich. Befolgen Sie die unten stehenden Anweisungen, um Ihren VoceSpace korrekt zu konfigurieren.", - "domain_desc": "Diese Domain benötigt einen DNS-A-Eintrag, der auf die IP-Adresse des aktuellen Servers verweist, auf dem Sie VoceChat hosten.", - "enable": "Aktivieren", - "how_to": "Nach der Aktivierung legen Sie bitte eine benutzerdefinierte Domain fest. Klicken Sie auf „Speichern“, und das System aktiviert VoceSpace automatisch.", - "old": "Ihre aktuelle Serverversion ist veraltet. Wir haben den offiziellen VoceSpace (vocespace.com) als Standard-Audio-/Videodienst aktiviert. Um Ihre eigene private Installation zu nutzen, aktualisieren Sie bitte vocechat-server auf Version 0.5.8 oder höher." - }, "channel": { "leave": "Kanal verlassen", "leave_desc": "Bist du sicher, dass du diesen Kanal verlassen möchtest?", @@ -197,23 +176,8 @@ "expire": "Läuft ab am", "create": "Erstellt am", "value": "Lizenzwert", - "renew": "Lizenz verlängern", "update": "Manuell aktualisieren", - "update_placeholder": "Neuen Lizenzwert eingeben", - "renew_select": "Preis auswählen", - "tip": { - "title": "Chance auf kostenloses Lizenz-Upgrade!", - "user_test": "Kostenloses Upgrade durch 25-minütigen Benutzertest", - "contact": "Termin buchen: " - }, - "payment_success": "Zahlung erfolgreich!", - "back_home": "Zurück zur Startseite", - "tip_renewing": "Lizenz wird verlängert – Fenster nicht schließen!", - "tip_renewed": "Lizenz erfolgreich verlängert!", - "tip_renew_error": "Ungültige Stripe-Sitzungs-ID", - "tip_domain": "Die Lizenz ist an eine Domain (ohne Port) gebunden. Bitte bestätigen oder aktualisieren:", - "tip_port": "Domain ohne Port eingeben.", - "tip_confirm": "Zahlung starten" + "update_placeholder": "Neuen Lizenzwert eingeben" }, "bot": { "add_api_key": "API-Schlüssel hinzufügen", @@ -273,7 +237,7 @@ "client_email": "Client-E-Mail" }, "smtp": { - "desc": "Für E-Mail-Benachrichtigungen (Pro-Funktion) und Anmeldungsbestätigungen wird SMTP benötigt.", + "desc": "Für E-Mail-Benachrichtigungen und Anmeldungsbestätigungen wird SMTP benötigt.", "sub_desc": "SMTP-Einstellungen konfigurieren:", "enable": "Aktivieren", "host": "Host", @@ -284,12 +248,6 @@ "how_to": "SMTP einrichten?", "send_test_email": "Test-E-Mail senden" }, - "agora": { - "desc": "Für Video-/Sprachanrufe nutzen wir Agora.", - "sub_desc": "Jedes Agora-Projekt bietet 10.000 kostenlose Minuten/Monat. Erstelle ein Projekt und folge der Anleitung.", - "enable": "Aktivieren", - "how_to": "Agora einrichten?" - }, "third_app": { "key": "API-Sicherheitsschlüssel", "update": "Geheimnis aktualisieren", @@ -352,7 +310,7 @@ "step_1": "Schritt 1: Eigene Server-URL eintragen", "step_2": "Schritt 2: Login-Token eingeben", "step_2_desc": "Token per X-API-Key-Header abrufen oder unten kopieren:", - "last": "Jede API hat einen „Testen"-Button zur Debugging." + "last": "Jede API hat einen „Testen“-Button zur Debugging." }, "notification": { "desc": "Benachrichtigungseinstellungen für verschiedene Kanäle konfigurieren", diff --git a/public/locales/de/welcome.json b/public/locales/de/welcome.json index 1438ad85..f6ca6214 100644 --- a/public/locales/de/welcome.json +++ b/public/locales/de/welcome.json @@ -7,8 +7,7 @@ "download": "Mobile Apps herunterladen", "help": "Fragen? Besuche unser Hilfe-Center", "sign_in_tip": "Zum Senden anmelden", - "vocespace": " VoceSpace Videoanruf", - "license": "VoceChat Upgrade-Lizenz", + "license": "VoceChat Lizenz", "page_editor": { "title": "Page Management", "status_select": "Select State", @@ -66,5 +65,6 @@ "tunnel_progress": "Starting tunnel...", "tunnel_error": "Tunnel error", "tunnel_get_domain": "Get Public Domain" - } + }, + "video": "Video" } diff --git a/public/locales/en/chat.json b/public/locales/en/chat.json index 130f91f0..4b1023e6 100644 --- a/public/locales/en/chat.json +++ b/public/locales/en/chat.json @@ -18,7 +18,6 @@ "created_at": "Created", "updated_at": "Last updated", "dismiss_announcement": "Dismiss announcement", - "channel_name": "Channel Name", "private_channel": "Private Channel", "create_channel": "Create New Channel", @@ -26,7 +25,6 @@ "create_private_channel_desc": "This is a private channel, only select members will be able to join.", "search_user_placeholder": "Type Username to search", "welcome_msg": "Welcome to channel {{name}}", - "invite_title": "Add friends to {{name}}", "invite_by_email": "Invite by Email", "enable_smtp": "Enable SMTP First", @@ -36,23 +34,18 @@ "invite_link_edit": "Edit invite link", "invite_link_setting_tip": "Invite link expires in {{expire}}, max use times: {{times}}", "generate_new_link": "Generate New Link", - "send_to": "Send To", "edited": "edited", - "license_tip": "Your license has reached the limit, upgrade the License or contact the Admin!", - + "license_tip": "The current license user limit has been reached. Contact the admin.", "delete_msg_title": "Delete Message", "delete_msg_desc": "Are you sure want to delete {{msg}}?", "delete_msg_this": "this message", "delete_msg_these": "these messages", - "new_msg": "{{num}} new messages", "mark_read": "Mark as Read", "pin_chat": "Pin Chat", "unpin_chat": "Unpin", - "reply_msg_del": "This message has been deleted.", - "file": "file", "image": "image", "forward": "forward", @@ -72,12 +65,10 @@ "contact_block_tip": "This user has been blocked by you", "file_expired": "File Expired", "only_owner_can_send": "only Channel owner can send message!", - "remark": "Add friend nickname", "remark_clear": "Reset friend nickname", "remark_intro": "Find a friend faster with a personal nickname. It will only be visible to you in your direct messages.", "remark_placeholder": "Set Nickname", - "guest_mode_warning": { "title": "This Channel Will Be Visible to Guests", "desc": "If you continue, guest users may be able to see messages in this public channel.", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index e9a9a3c7..e3c3d078 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -51,7 +51,6 @@ "install": "Install", "download_origin": "Download original", "close": "Close", - "upgrade": "Upgrade", "view_pwd": "View Password", "change_pwd": "Update Password" }, @@ -59,7 +58,6 @@ "uploading": "Uploading" }, "tip": { - "email_msg_tip": "Your region has blocked notifications, upgrade to Pro to use email notification.", "update": "Update Successfully!", "delete": "Delete Successfully!", "reg": "Register Successfully!", @@ -71,22 +69,10 @@ }, "new_version": "<0>New Version Available", "mobile_app": "Check out our <0>Mobile APP", - "price": { - "pro": { - "title": "VoceChat Pro", - "desc": "", - "price": "$49" - }, - "supreme": { - "title": "VoceSpace Private Deployment Video [499$]", - "desc": "", - "price": "" - } - }, "server_update": { "version_needed": "This function needs server version:<0>{{version}} at least 🚨", "current_version": "Your current version:<0>{{version}}", - "update_tip": "Please upgrade the Server!", + "update_tip": "Please update the Server!", "howto": "How to Update VoceChat Server" }, "inactive": { diff --git a/public/locales/en/member.json b/public/locales/en/member.json index 178671e4..f434420b 100644 --- a/public/locales/en/member.json +++ b/public/locales/en/member.json @@ -12,7 +12,6 @@ "current_pwd": "Current Password", "new_pwd": "New Password", "confirm_new_pwd": "Confirm New Password", - "manage_members": "Manage Contacts", "manage_tip": "Disabling your account means you can recover it at any time after taking this action.", "admin": "Admin", @@ -28,7 +27,6 @@ "roles": "Roles", "set_admin": "Admin", "set_normal": "User", - "search_not_found": "Not found, or this user is not allowed to be searched.", "search_disabled": "User search has been disabled by the administrator.", "add_to_contact": "Add to contact", diff --git a/public/locales/en/setting.json b/public/locales/en/setting.json index 9dfb5f7b..9014f9c2 100644 --- a/public/locales/en/setting.json +++ b/public/locales/en/setting.json @@ -16,8 +16,6 @@ "bot": "Bot & Webhook", "notification_channels": "Notification Channels", "firebase": "Firebase", - "agora": "Agora", - "vocespace": "Video", "smtp": "SMTP", "login_method": "Login Methods", "third_app": "Third-party Login", @@ -201,23 +199,8 @@ "expire": "Expired At", "create": "Created At", "value": "License Value", - "renew": "Renew License", "update": "Update Manually", - "update_placeholder": "Please input the new license value", - "renew_select": "Please select the price", - "tip": { - "title": "A chance to get a free license upgrade!", - "user_test": "Get a free license upgrade through a 25 min user testing", - "contact": "Book a time here: " - }, - "payment_success": "Payment Success!", - "back_home": "Back Home", - "tip_renewing": "Renewing the License, do not close the window!", - "tip_renewed": "Renew the License Successfully!", - "tip_renew_error": "Invalided Stripe Session ID", - "tip_domain": "The license is bound to domain (without port), please confirm or update your setting:", - "tip_port": "Fill out your domain name without port.", - "tip_confirm": "Start Payment" + "update_placeholder": "Please input the new license value" }, "bot": { "add_api_key": "Add API Key", @@ -277,7 +260,7 @@ "client_email": "Client Email" }, "smtp": { - "desc": "For new message Email notification (advanced feature, upgrade to <0>VoceChat Pro) and new sign-up email verification, we use SMTP. ", + "desc": "For new message email notifications and new sign-up email verification, we use SMTP.", "sub_desc": "Please set up your SMTP below.", "enable": "Enable", "host": "Host", @@ -288,32 +271,6 @@ "how_to": "How to setup SMTP?", "send_test_email": "Send Test Email" }, - "agora": { - "desc": "For video and audio calls, we use Agora API. ", - "sub_desc": "Each Agora project will enjoy 10,000 mins free API usages per month. Please create a project by yourself. Follow the instruction below to configure your Agora correctly.", - "enable": "Enable", - "how_to": "How to setup Agora?" - }, - "vocespace": { - "add": "Add", - "check": "Check", - "checked": "Pass", - "failed": "Failed", - "title": "VoceSpace", - "desc": "Use self-hosted VoceSpace (Automated Deployment)", - "prerequisite": [ - "Prerequisites", - "Docker environment", - "Ports 80, 443, 7880, 7881, 3008, 3000 are open", - "The domain has been registered", - "For enterprises customization or need help, please contact han@privoce.com or add WeChat: Privoce" - ], - "sub_desc": "Each VoceSpace account will enjoy no limit minutes, 5 person concurrent and 4k resolution for free. No account creation required. Follow the instruction below to configure your VoceSpace correctly.", - "domain_desc": "This domain needs to have a DNS A record pointing to the current server IP you are hosting VoceChat.", - "enable": "Enable", - "how_to": "After enabling, please set a custom domain then click save, and the system will automatically enable VoceSpace.", - "old": "Your current server version is outdated. We have enabled the official VoceSpace (vocespace.com) as your default audio/video service. To use your own private deployment, please update vocechat-server to v0.5.8 or higher." - }, "third_app": { "key": "API Secure Key", "update": "Update Secret", @@ -480,4 +437,4 @@ "error": "Error" } } -} \ No newline at end of file +} diff --git a/public/locales/en/welcome.json b/public/locales/en/welcome.json index 427593b4..c33e49d3 100644 --- a/public/locales/en/welcome.json +++ b/public/locales/en/welcome.json @@ -7,8 +7,7 @@ "download": "Download Mobile apps", "help": "Got questions? Visit our help center", "sign_in_tip": "Please sign in to send message", - "vocespace": "VoceSpace Video Call", - "license": "VoceChat Upgrade License", + "license": "VoceChat License", "page_editor": { "title": "Page Management", "status_select": "Select State", @@ -66,5 +65,6 @@ "tunnel_progress": "Starting tunnel...", "tunnel_error": "Tunnel error", "tunnel_get_domain": "Get Public Domain" - } + }, + "video": "Video" } diff --git a/public/locales/es/auth.json b/public/locales/es/auth.json index 8b32fc6e..06749315 100644 --- a/public/locales/es/auth.json +++ b/public/locales/es/auth.json @@ -43,9 +43,9 @@ "github_logging_in": "Iniciando sesión con GitHub...", "github_cb_tip": "Por favor, cierra esta ventana y vuelve a la ventana del widget", "magic_link_expire": { - "title": "Enlace mágico inválido o caducado", - "desc": "Por favor, solicita un nuevo enlace mágico.", - "desc_close": "Puedes cerrar esta ventana ahora." + "title": "Enlace mágico inválido o caducado", + "desc": "Por favor, solicita un nuevo enlace mágico.", + "desc_close": "Puedes cerrar esta ventana ahora." }, "invite_mobile": { "join": "Únete a nuestro servidor", diff --git a/public/locales/es/chat.json b/public/locales/es/chat.json index c2a2cc26..496b997b 100644 --- a/public/locales/es/chat.json +++ b/public/locales/es/chat.json @@ -1,26 +1,23 @@ { -"pin": "Fijar", -"pin_desc": "¿Quieres fijar este mensaje en", -"pinned_msg": "Mensaje Fijado", -"pin_empty_tip": "Este canal aún no tiene ningún mensaje fijado.", -"fav": "Favorito", -"fav_msg": "Mensaje Guardado", -"fav_empty_tip": "Este canal aún no tiene ningún mensaje guardado.", -"channel_members": "Miembros del Canal", -"add_channel_members": "Agregar miembros", -"welcome_channel": "Bienvenido a {{name}}", -"welcome_desc": "Este es el inicio del canal #{{name}}.", -"edit_channel": "Editar Canal" - + "pin": "Fijar", + "pin_desc": "¿Quieres fijar este mensaje en", + "pinned_msg": "Mensaje Fijado", + "pin_empty_tip": "Este canal aún no tiene ningún mensaje fijado.", + "fav": "Favorito", + "fav_msg": "Mensaje Guardado", + "fav_empty_tip": "Este canal aún no tiene ningún mensaje guardado.", + "channel_members": "Miembros del Canal", + "add_channel_members": "Agregar miembros", + "welcome_channel": "Bienvenido a {{name}}", + "welcome_desc": "Este es el inicio del canal #{{name}}.", + "edit_channel": "Editar Canal", "channel_name": "Nombre del Canal", "private_channel": "Canal Privado", "create_channel": "Crear Nuevo Canal", "create_channel_desc": "Este es un canal público, todos en este equipo pueden verlo.", "create_private_channel_desc": "Este es un canal privado, solo miembros seleccionados podrán unirse.", "search_user_placeholder": "Escribe el nombre de usuario para buscar", - "welcome_msg": "Bienvenido al canal {{name}}" - - + "welcome_msg": "Bienvenido al canal {{name}}", "invite_title": "Agregar amigos a {{name}}", "invite_by_email": "Invitar por Correo Electrónico", "enable_smtp": "Habilitar SMTP Primero", @@ -29,24 +26,19 @@ "invite_link_faq": "¿Enlace de invitación incorrecto?", "invite_link_edit": "Editar enlace de invitación", "invite_link_setting_tip": "El enlace de invitación expira en {{expire}}, máximo de usos: {{times}}", - "generate_new_link": "Generar Nuevo Enlace" - + "generate_new_link": "Generar Nuevo Enlace", "send_to": "Enviar a", "edited": "editado", - "license_tip": "¡Tu licencia ha alcanzado el límite! ¡Actualiza la licencia o contacta al administrador!", - + "license_tip": "Se alcanzó el límite de usuarios de la licencia actual. Contacta al administrador.", "delete_msg_title": "Eliminar Mensaje", "delete_msg_desc": "¿Estás seguro de que quieres eliminar {{msg}}?", "delete_msg_this": "este mensaje", "delete_msg_these": "estos mensajes", - "new_msg": "{{num}} nuevos mensajes", "mark_read": "Marcar como Leído", "pin_chat": "Fijar Chat", "unpin_chat": "Desfijar", - "reply_msg_del": "Este mensaje ha sido eliminado.", - "file": "archivo", "image": "imagen", "forward": "reenviar", @@ -65,5 +57,4 @@ "contact_tip": "Este usuario no está en tus contactos", "contact_block_tip": "Este usuario ha sido bloqueado por ti", "file_expired": "Archivo Expirado" - } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index af8cddf0..0dba7804 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -67,18 +67,6 @@ }, "new_version": "<0>Nueva Versión Disponible", "mobile_app": "Echa un vistazo a nuestra <0>Aplicación Móvil", - "price": { - "pro": { - "title": "VoceChat Pro", - "desc": "", - "price": "$49" - }, - "supreme": { - "title": "Vídeo de implementación privada de VoceSpace [499$]", - "desc": "", - "price": "" - } - }, "server_update": { "version_needed": "Esta función requiere la versión del servidor:<0>{{version}} como mínimo 🚨", "current_version": "Tu versión actual:<0>{{version}}", diff --git a/public/locales/es/member.json b/public/locales/es/member.json index 5cb692f0..c458a1eb 100644 --- a/public/locales/es/member.json +++ b/public/locales/es/member.json @@ -12,7 +12,6 @@ "current_pwd": "Contraseña actual", "new_pwd": "Nueva contraseña", "confirm_new_pwd": "Confirmar nueva contraseña", - "manage_members": "Gestionar Contactos", "manage_tip": "Desactivar tu cuenta significa que puedes recuperarla en cualquier momento después de tomar esta acción.", "admin": "Admin", @@ -28,7 +27,6 @@ "roles": "Roles", "set_admin": "Admin", "set_normal": "Usuario", - "search_not_found": "No encontrado, o este usuario no está permitido para ser buscado.", "search_by_id": "Buscar por ID", "search_by_id_ph": "Ingrese el ID de usuario", diff --git a/public/locales/es/setting.json b/public/locales/es/setting.json index 3c053f60..67fc6bb3 100644 --- a/public/locales/es/setting.json +++ b/public/locales/es/setting.json @@ -15,7 +15,6 @@ "bot": "Bot & Webhook", "notification_channels": "Canales de Notificación", "firebase": "Firebase", - "agora": "Agora", "smtp": "SMTP", "login_method": "Métodos de Inicio de Sesión", "third_app": "Inicio de Sesión de Terceros", @@ -27,26 +26,6 @@ "video": "Video", "notification": "Notificaciones" }, - "vocespace": { - "add": "Agregar", - "check": "Comprobar", - "checked": "Aprobado", - "failed": "Fallido", - "title": "VoceSpace", - "desc": "VoceSpace implementado de forma privada (implementación automática)", - "sub_desc": "Cada cuenta de VoceSpace disfrutará de llamadas ilimitadas gratuitas, 5 usuarios simultáneos y resolución 4K. No es necesario crear una cuenta. Siga las instrucciones a continuación para configurar correctamente su VoceSpace.", - "prerequisite": [ - "Prerrequisitos", - "Entorno Docker", - "Puertos 80, 443, 7880, 7881, 3008 y 3000 abiertos", - "Nombre de dominio que resuelve a la dirección IP local (para usuarios en China continental, como Alibaba Cloud y Tencent Cloud, se requiere el registro ICP)", - "Para personalización empresarial o asistencia, contacte con han@privoce.com o agregue WeChat: Privoce" - ], - "domain_desc": "Los nombres de dominio personalizados deben resolver a la dirección IP local (para usuarios en China continental, como Alibaba Cloud y Tencent Cloud, se requiere el registro ICP)", - "enable": "Habilitar", - "how_to": "Después de habilitar, configure el nombre de dominio y guarde. Los pasos restantes se completarán automáticamente.", - "old": "Su versión actual del servidor está desactualizada. Hemos habilitado el VoceSpace oficial (vocespace.com) como su servicio de audio/video predeterminado. Para usar su propia implementación privada, actualice vocechat-server a la versión 0.5.8 o superior." - }, "channel": { "leave": "Abandonar Canal", "leave_desc": "¿Estás seguro de que deseas abandonar este canal?", @@ -146,23 +125,8 @@ "expire": "Caduca en", "create": "Creado en", "value": "Valor de la Licencia", - "renew": "Renovar Licencia", "update": "Actualizar Manualmente", - "update_placeholder": "Por favor, introduce el nuevo valor de la licencia", - "renew_select": "Por favor, selecciona el precio", - "tip": { - "title": "¡Oportunidad de obtener una actualización gratuita de la licencia!", - "user_test": "Obtén una actualización gratuita de la licencia a través de una prueba de usuario de 25 minutos", - "contact": "Reserva un horario aquí: " - }, - "payment_success": "¡Pago Exitoso!", - "back_home": "Volver a Inicio", - "tip_renewing": "¡Renovando la Licencia, no cierres la ventana!", - "tip_renewed": "¡Licencia Renovada Exitosamente!", - "tip_renew_error": "ID de Sesión de Stripe Inválido", - "tip_domain": "La licencia está vinculada al dominio (sin puerto), por favor confirma o actualiza tu configuración:", - "tip_port": "Ingresa tu nombre de dominio sin puerto.", - "tip_confirm": "Iniciar Pago" + "update_placeholder": "Por favor, introduce el nuevo valor de la licencia" }, "bot": { "add_api_key": "Agregar Clave API", @@ -229,10 +193,6 @@ "how_to": "¿Cómo configurar SMTP?", "send_test_email": "Enviar Correo Electrónico de Prueba" }, - "agora": { - "enable": "Habilitar", - "how_to": "¿Cómo configurar Agora?" - }, "third_app": { "key": "Clave de Seguridad API", "update": "Actualizar Secreto", diff --git a/public/locales/es/welcome.json b/public/locales/es/welcome.json index 86b8212c..2adb6694 100644 --- a/public/locales/es/welcome.json +++ b/public/locales/es/welcome.json @@ -7,8 +7,7 @@ "download": "Descarga las aplicaciones móviles", "help": "¿Tienes preguntas? Visita nuestro centro de ayuda", "sign_in_tip": "Por favor, inicia sesión para enviar mensajes", - "vocespace": "Llamada de video VoceSpace", - "license": "VoceChat Licencia de Actualización", + "license": "Licencia de VoceChat", "page_editor": { "title": "Page Management", "status_select": "Select State", @@ -66,5 +65,6 @@ "tunnel_progress": "Starting tunnel...", "tunnel_error": "Tunnel error", "tunnel_get_domain": "Get Public Domain" - } + }, + "video": "Video" } diff --git a/public/locales/fr/auth.json b/public/locales/fr/auth.json index e5146eee..3940257c 100644 --- a/public/locales/fr/auth.json +++ b/public/locales/fr/auth.json @@ -1,74 +1,74 @@ -{ - "login": { - "title": "Connexion à {{name}}", - "google": "Se connecter avec Google", - "github": "Se connecter avec GitHub", - "metamask": "Se connecter avec MetaMask", - "password": "Se connecter avec Mot de Passe", - "oidc": "Se connecter avec OIDC", - "no_account": "Vous n'avez pas de compte ?" - }, - "reg": { - "title": "Inscription à {{name}}", - "google": "S'inscrire avec Google", - "github": "S'inscrire avec GitHub", - "metamask": "S'inscrire avec MetaMask", - "oidc": "S'inscrire avec OIDC", - "have_account": "Vous avez un compte ?", - "input_name": "Quel est votre nom", - "input_name_tip": "Entrez un nom ou un pseudo pour que les gens sachent comment vous souhaitez être appelé. Votre nom ne sera visible que dans les espaces que vous avez rejoints." - }, - "logout": { - "title": "Déconnexion", - "desc": "Êtes-vous sûr de vouloir vous déconnecter de ce compte ?", - "clear_local": "Effacer les données locales", - "role_changed": "Votre rôle a changé, voulez-vous vous reconnecter pour que cela prenne effet ?", - "later": "Peut-être plus tard" - }, - "continue": "Continuer avec Email", - "placeholder_name": "Entrez votre Nom", - "placeholder_email": "Entrez votre Email", - "placeholder_pwd": "Entrez le Mot de Passe", - "placeholder_confirm_pwd": "Confirmez le Mot de Passe", - "check_email": "Vérifiez votre boîte de réception", - "check_email_desc": "Nous venons d'envoyer un lien magique à <0>{{email}}. Une fois qu'il arrive, il sera valide pendant 15 minutes.", - "back_sign_in": "Retour à la connexion", - "welcome": "Bienvenue sur le serveur {{name}}", - "guest_login_tip": "Veuillez scanner le code QR ou vous connecter pour envoyer un message", - "enter": "Se connecter à ", - "sign_in": "Se Connecter", - "sign_up": "S'inscrire", - "signing_up": "Inscription en cours", - "github_login_success": "Connexion GitHub réussie !", - "github_logging_in": "Connexion GitHub en cours...", - "github_cb_tip": "Veuillez fermer cette fenêtre et revenir à la fenêtre du widget", - "magic_link_expire": { - "title": "Lien magique invalide ou expiré", - "desc": "Veuillez demander un nouveau lien magique.", - "desc_close": "Vous pouvez fermer cette fenêtre maintenant." - }, - "invite_mobile": { - "join": "Rejoignez notre Serveur", - "start_download": "Commencez par télécharger l'application mobile VoceChat", - "open": "Ouvrir VoceChat", - "have_already": "Vous avez déjà l'application ?" - }, - "invite_expire": { - "min30": "30 minutes", - "h1": "1 heure", - "h6": "6 heures", - "h12": "12 heures", - "d1": "1 jour", - "d7": "7 jours", - "d30": "30 jours" - }, - "invite_times": { - "no_limit": "sans limite", - "time1": "1 utilisation", - "times5": "5 utilisations", - "times10": "10 utilisations", - "times25": "25 utilisations", - "times50": "50 utilisations", - "times100": "100 utilisations" - } -} \ No newline at end of file +{ + "login": { + "title": "Connexion à {{name}}", + "google": "Se connecter avec Google", + "github": "Se connecter avec GitHub", + "metamask": "Se connecter avec MetaMask", + "password": "Se connecter avec Mot de Passe", + "oidc": "Se connecter avec OIDC", + "no_account": "Vous n'avez pas de compte ?" + }, + "reg": { + "title": "Inscription à {{name}}", + "google": "S'inscrire avec Google", + "github": "S'inscrire avec GitHub", + "metamask": "S'inscrire avec MetaMask", + "oidc": "S'inscrire avec OIDC", + "have_account": "Vous avez un compte ?", + "input_name": "Quel est votre nom", + "input_name_tip": "Entrez un nom ou un pseudo pour que les gens sachent comment vous souhaitez être appelé. Votre nom ne sera visible que dans les espaces que vous avez rejoints." + }, + "logout": { + "title": "Déconnexion", + "desc": "Êtes-vous sûr de vouloir vous déconnecter de ce compte ?", + "clear_local": "Effacer les données locales", + "role_changed": "Votre rôle a changé, voulez-vous vous reconnecter pour que cela prenne effet ?", + "later": "Peut-être plus tard" + }, + "continue": "Continuer avec Email", + "placeholder_name": "Entrez votre Nom", + "placeholder_email": "Entrez votre Email", + "placeholder_pwd": "Entrez le Mot de Passe", + "placeholder_confirm_pwd": "Confirmez le Mot de Passe", + "check_email": "Vérifiez votre boîte de réception", + "check_email_desc": "Nous venons d'envoyer un lien magique à <0>{{email}}. Une fois qu'il arrive, il sera valide pendant 15 minutes.", + "back_sign_in": "Retour à la connexion", + "welcome": "Bienvenue sur le serveur {{name}}", + "guest_login_tip": "Veuillez scanner le code QR ou vous connecter pour envoyer un message", + "enter": "Se connecter à ", + "sign_in": "Se Connecter", + "sign_up": "S'inscrire", + "signing_up": "Inscription en cours", + "github_login_success": "Connexion GitHub réussie !", + "github_logging_in": "Connexion GitHub en cours...", + "github_cb_tip": "Veuillez fermer cette fenêtre et revenir à la fenêtre du widget", + "magic_link_expire": { + "title": "Lien magique invalide ou expiré", + "desc": "Veuillez demander un nouveau lien magique.", + "desc_close": "Vous pouvez fermer cette fenêtre maintenant." + }, + "invite_mobile": { + "join": "Rejoignez notre Serveur", + "start_download": "Commencez par télécharger l'application mobile VoceChat", + "open": "Ouvrir VoceChat", + "have_already": "Vous avez déjà l'application ?" + }, + "invite_expire": { + "min30": "30 minutes", + "h1": "1 heure", + "h6": "6 heures", + "h12": "12 heures", + "d1": "1 jour", + "d7": "7 jours", + "d30": "30 jours" + }, + "invite_times": { + "no_limit": "sans limite", + "time1": "1 utilisation", + "times5": "5 utilisations", + "times10": "10 utilisations", + "times25": "25 utilisations", + "times50": "50 utilisations", + "times100": "100 utilisations" + } +} diff --git a/public/locales/fr/chat.json b/public/locales/fr/chat.json index 62cdf7bc..e7329f96 100644 --- a/public/locales/fr/chat.json +++ b/public/locales/fr/chat.json @@ -1,67 +1,60 @@ -{ - "pin": "Épingler", - "pin_desc": "Voulez-vous épingler ce message à", - "pinned_msg": "Message Épinglé", - "pin_empty_tip": "Ce canal n'a pas encore de message épinglé.", - "fav": "Favori", - "fav_msg": "Message Enregistré", - "fav_empty_tip": "Ce canal n'a pas encore de message enregistré.", - "channel_members": "Membres du Canal", - "add_channel_members": "Ajouter des membres", - "welcome_channel": "Bienvenue dans {{name}}", - "welcome_desc": "Ceci est le début du canal #{{name}}.", - "edit_channel": "Modifier le Canal", - - "channel_name": "Nom du Canal", - "private_channel": "Canal Privé", - "create_channel": "Créer un Nouveau Canal", - "create_channel_desc": "Ceci est un canal public, tout le monde dans cette équipe peut le voir.", - "create_private_channel_desc": "Ceci est un canal privé, seuls certains membres pourront y accéder.", - "search_user_placeholder": "Tapez un Nom d'utilisateur pour chercher", - "welcome_msg": "Bienvenue dans le canal {{name}}", - - "invite_title": "Ajouter des amis à {{name}}", - "invite_by_email": "Inviter par Email", - "enable_smtp": "Activez SMTP d'abord", - "send_invite_link": "Ou envoyez le lien d'invitation à vos amis", - "share_invite_link": "Partagez ce lien pour inviter des personnes à ce serveur.", - "invite_link_faq": "Lien d'invitation incorrect ?", - "invite_link_edit": "Modifier le lien d'invitation", - "invite_link_setting_tip": "Le lien d'invitation expire dans {{expire}}, max de fois d'utilisation : {{times}}", - "generate_new_link": "Générer un Nouveau Lien", - - "send_to": "Envoyer À", - "edited": "modifié", - "license_tip": "Votre licence a atteint sa limite, veuillez mettre à niveau la Licence ou contacter l'Administrateur !", - - "delete_msg_title": "Supprimer le Message", - "delete_msg_desc": "Êtes-vous sûr de vouloir supprimer {{msg}} ?", - "delete_msg_this": "ce message", - "delete_msg_these": "ces messages", - - "new_msg": "{{num}} nouveaux messages", - "mark_read": "Marquer comme Lu", - "pin_chat": "Épingler la Conversation", - "unpin_chat": "Désépingler", - - "reply_msg_del": "Ce message a été supprimé.", - - "file": "fichier", - "image": "image", - "forward": "transférer", - "voice_message": "message vocal", - "voice": "Appel Vocal", - "deafen": "Se rendre sourd", - "undeafen": "Rendre à nouveau l'ouïe", - "mute": "Couper le son", - "unmute": "Réactiver le son", - "camera_on": "Activer la caméra", - "camera_off": "Désactiver la caméra", - "leave_voice": "Quitter l'Appel Vocal", - "add_contact": "Ajouter aux contacts", - "block": "Bloquer", - "unblock": "Débloquer", - "contact_tip": "Cet utilisateur n'est pas dans vos contacts", - "contact_block_tip": "Cet utilisateur a été bloqué par vous", - "file_expired": "Fichier Expiré" -} \ No newline at end of file +{ + "pin": "Épingler", + "pin_desc": "Voulez-vous épingler ce message à", + "pinned_msg": "Message Épinglé", + "pin_empty_tip": "Ce canal n'a pas encore de message épinglé.", + "fav": "Favori", + "fav_msg": "Message Enregistré", + "fav_empty_tip": "Ce canal n'a pas encore de message enregistré.", + "channel_members": "Membres du Canal", + "add_channel_members": "Ajouter des membres", + "welcome_channel": "Bienvenue dans {{name}}", + "welcome_desc": "Ceci est le début du canal #{{name}}.", + "edit_channel": "Modifier le Canal", + "channel_name": "Nom du Canal", + "private_channel": "Canal Privé", + "create_channel": "Créer un Nouveau Canal", + "create_channel_desc": "Ceci est un canal public, tout le monde dans cette équipe peut le voir.", + "create_private_channel_desc": "Ceci est un canal privé, seuls certains membres pourront y accéder.", + "search_user_placeholder": "Tapez un Nom d'utilisateur pour chercher", + "welcome_msg": "Bienvenue dans le canal {{name}}", + "invite_title": "Ajouter des amis à {{name}}", + "invite_by_email": "Inviter par Email", + "enable_smtp": "Activez SMTP d'abord", + "send_invite_link": "Ou envoyez le lien d'invitation à vos amis", + "share_invite_link": "Partagez ce lien pour inviter des personnes à ce serveur.", + "invite_link_faq": "Lien d'invitation incorrect ?", + "invite_link_edit": "Modifier le lien d'invitation", + "invite_link_setting_tip": "Le lien d'invitation expire dans {{expire}}, max de fois d'utilisation : {{times}}", + "generate_new_link": "Générer un Nouveau Lien", + "send_to": "Envoyer À", + "edited": "modifié", + "license_tip": "La limite d'utilisateurs de la licence actuelle est atteinte. Contactez l'administrateur.", + "delete_msg_title": "Supprimer le Message", + "delete_msg_desc": "Êtes-vous sûr de vouloir supprimer {{msg}} ?", + "delete_msg_this": "ce message", + "delete_msg_these": "ces messages", + "new_msg": "{{num}} nouveaux messages", + "mark_read": "Marquer comme Lu", + "pin_chat": "Épingler la Conversation", + "unpin_chat": "Désépingler", + "reply_msg_del": "Ce message a été supprimé.", + "file": "fichier", + "image": "image", + "forward": "transférer", + "voice_message": "message vocal", + "voice": "Appel Vocal", + "deafen": "Se rendre sourd", + "undeafen": "Rendre à nouveau l'ouïe", + "mute": "Couper le son", + "unmute": "Réactiver le son", + "camera_on": "Activer la caméra", + "camera_off": "Désactiver la caméra", + "leave_voice": "Quitter l'Appel Vocal", + "add_contact": "Ajouter aux contacts", + "block": "Bloquer", + "unblock": "Débloquer", + "contact_tip": "Cet utilisateur n'est pas dans vos contacts", + "contact_block_tip": "Cet utilisateur a été bloqué par vous", + "file_expired": "Fichier Expiré" +} diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index 3c00685c..58a21c4c 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -67,18 +67,6 @@ }, "new_version": "<0>Nouvelle Version Disponible", "mobile_app": "Découvrez notre <0>Application Mobile", - "price": { - "pro": { - "title": "VoceChat Pro", - "desc": "", - "price": "$49" - }, - "supreme": { - "title": "Vidéo de déploiement privé VoceSpace [499$]", - "desc": "", - "price": "" - } - }, "server_update": { "version_needed": "Cette fonction nécessite la version serveur :<0>{{version}} au minimum 🚨", "current_version": "Votre version actuelle :<0>{{version}}", diff --git a/public/locales/fr/fav.json b/public/locales/fr/fav.json index d5982b3a..f994bc0d 100644 --- a/public/locales/fr/fav.json +++ b/public/locales/fr/fav.json @@ -1,6 +1,6 @@ -{ - "all_items": "Tous les Éléments", - "image": "Images", - "video": "Vidéos", - "audio": "Audio" -} \ No newline at end of file +{ + "all_items": "Tous les Éléments", + "image": "Images", + "video": "Vidéos", + "audio": "Audio" +} diff --git a/public/locales/fr/file.json b/public/locales/fr/file.json index b2c1f643..f0ca25bc 100644 --- a/public/locales/fr/file.json +++ b/public/locales/fr/file.json @@ -1,6 +1,6 @@ -{ - "from": "De", - "channel": "Canal", - "type": "Type", - "date": "Date" -} \ No newline at end of file +{ + "from": "De", + "channel": "Canal", + "type": "Type", + "date": "Date" +} diff --git a/public/locales/fr/member.json b/public/locales/fr/member.json index 18758623..73d23690 100644 --- a/public/locales/fr/member.json +++ b/public/locales/fr/member.json @@ -1,39 +1,37 @@ -{ - "username": "Nom d'utilisateur", - "email": "Email", - "password": "Mot de Passe", - "delete_account": "Supprimer le Compte", - "change_name": "Changer votre nom d'utilisateur", - "change_name_desc": "Entrez un nouveau nom d'utilisateur", - "change_email": "Changer votre email", - "change_email_desc": "Entrez un nouvel email.", - "change_pwd": "Changer votre mot de passe", - "change_pwd_desc": "Entrez le mot de passe actuel et le nouveau mot de passe", - "current_pwd": "Mot de Passe Actuel", - "new_pwd": "Nouveau Mot de Passe", - "confirm_new_pwd": "Confirmer le Nouveau Mot de Passe", - - "manage_members": "Gérer les Contacts", - "manage_tip": "Désactiver votre compte signifie que vous pouvez le récupérer à tout moment après avoir pris cette action.", - "admin": "Administrateur", - "user": "Utilisateur", - "copy_email": "Copier l'email", - "send_msg": "Envoyer un message", - "call": "Appeler", - "remove": "Retirer du serveur", - "remove_from_contact": "Retirer des contacts", - "remove_account": "Retirer le compte", - "remove_account_desc": "Êtes-vous sûr de vouloir retirer ce compte ?", - "remove_from_channel": "Retirer du canal", - "roles": "Rôles", - "set_admin": "Administrateur", - "set_normal": "Utilisateur", - - "search_not_found": "Non trouvé, ou cet utilisateur n'est pas autorisé à être recherché.", - "search_by_id": "Rechercher par ID", - "search_by_id_ph": "Saisir l'ID de l'utilisateur", - "search_by_email": "Rechercher par email", - "search_by_email_ph": "Saisir l'email de l'utilisateur", - "search_by_name": "Rechercher par nom", - "search_by_name_ph": "Saisir le nom de l'utilisateur" -} \ No newline at end of file +{ + "username": "Nom d'utilisateur", + "email": "Email", + "password": "Mot de Passe", + "delete_account": "Supprimer le Compte", + "change_name": "Changer votre nom d'utilisateur", + "change_name_desc": "Entrez un nouveau nom d'utilisateur", + "change_email": "Changer votre email", + "change_email_desc": "Entrez un nouvel email.", + "change_pwd": "Changer votre mot de passe", + "change_pwd_desc": "Entrez le mot de passe actuel et le nouveau mot de passe", + "current_pwd": "Mot de Passe Actuel", + "new_pwd": "Nouveau Mot de Passe", + "confirm_new_pwd": "Confirmer le Nouveau Mot de Passe", + "manage_members": "Gérer les Contacts", + "manage_tip": "Désactiver votre compte signifie que vous pouvez le récupérer à tout moment après avoir pris cette action.", + "admin": "Administrateur", + "user": "Utilisateur", + "copy_email": "Copier l'email", + "send_msg": "Envoyer un message", + "call": "Appeler", + "remove": "Retirer du serveur", + "remove_from_contact": "Retirer des contacts", + "remove_account": "Retirer le compte", + "remove_account_desc": "Êtes-vous sûr de vouloir retirer ce compte ?", + "remove_from_channel": "Retirer du canal", + "roles": "Rôles", + "set_admin": "Administrateur", + "set_normal": "Utilisateur", + "search_not_found": "Non trouvé, ou cet utilisateur n'est pas autorisé à être recherché.", + "search_by_id": "Rechercher par ID", + "search_by_id_ph": "Saisir l'ID de l'utilisateur", + "search_by_email": "Rechercher par email", + "search_by_email_ph": "Saisir l'email de l'utilisateur", + "search_by_name": "Rechercher par nom", + "search_by_name_ph": "Saisir le nom de l'utilisateur" +} diff --git a/public/locales/fr/setting.json b/public/locales/fr/setting.json index ece86c7d..70dcf4f0 100644 --- a/public/locales/fr/setting.json +++ b/public/locales/fr/setting.json @@ -15,7 +15,6 @@ "bot": "Bot & Webhook", "notification_channels": "Canaux de Notification", "firebase": "Firebase", - "agora": "Agora", "smtp": "SMTP", "login_method": "Méthodes de Connexion", "third_app": "Connexion Tiers", @@ -27,26 +26,6 @@ "video": "Vidéo", "notification": "Notifications" }, - "vocespace": { - "add": "Ajouter", - "check": "Vérifier", - "checked": "Réussi", - "failed": "Échoué", - "title": "VoceSpace", - "desc": "VoceSpace déployé en mode privé (déploiement automatique)", - "sub_desc": "Chaque compte VoceSpace bénéficie d'appels illimités gratuits, de 5 utilisateurs simultanés et d'une résolution 4K. Aucune création de compte n'est requise. Suivez les instructions ci-dessous pour configurer correctement votre VoceSpace.", - "prerequisite": [ - "Prérequis", - "Environnement Docker", - "Les ports 80, 443, 7880, 7881, 3008 et 3000 sont ouverts", - "Nom de domaine pointant vers l'adresse IP locale (pour les utilisateurs de Chine continentale, tels qu'Alibaba Cloud et Tencent Cloud, un enregistrement ICP est requis)", - "Pour toute personnalisation ou assistance en entreprise, veuillez contacter han@privoce.com ou ajouter Privoce sur WeChat" - ], - "domain_desc": "Les noms de domaine personnalisés doivent pointer vers l'adresse IP locale (pour les utilisateurs de Chine continentale, tels qu'Alibaba Cloud et Tencent Cloud, un enregistrement ICP est requis)", - "enable": "Activer", - "how_to": "Après l'activation, veuillez définir le nom de domaine et enregistrer. Les étapes suivantes seront effectuées automatiquement.", - "old": "Votre version actuelle du serveur est obsolète. Nous avons activé le VoceSpace officiel (vocespace.com) comme votre service audio/vidéo par défaut. Pour utiliser votre propre déploiement privé, veuillez mettre à jour vocechat-server vers la version 0.5.8 ou supérieure." - }, "channel": { "leave": "Quitter le Canal", "leave_desc": "Êtes-vous sûr de vouloir quitter ce canal ?", @@ -152,23 +131,8 @@ "expire": "Expiré À", "create": "Créé À", "value": "Valeur de la Licence", - "renew": "Renouveler la Licence", "update": "Mettre à Jour Manuellement", - "update_placeholder": "Veuillez saisir la nouvelle valeur de la licence", - "renew_select": "Veuillez sélectionner le prix", - "tip": { - "title": "Une chance d'obtenir une mise à niveau de licence gratuite !", - "user_test": "Obtenez une mise à niveau de licence gratuite grâce à un test utilisateur de 25 minutes", - "contact": "Réservez un créneau ici : " - }, - "payment_success": "Paiement Réussi !", - "back_home": "Retour à l'Accueil", - "tip_renewing": "Renouvellement de la Licence, ne fermez pas la fenêtre !", - "tip_renewed": "Licence Renouvelée avec Succès !", - "tip_renew_error": "ID de Session Stripe Invalidé", - "tip_domain": "La licence est liée au domaine (sans port), veuillez confirmer ou mettre à jour vos paramètres :", - "tip_port": "Remplissez votre nom de domaine sans port.", - "tip_confirm": "Commencer le Paiement" + "update_placeholder": "Veuillez saisir la nouvelle valeur de la licence" }, "bot": { "add_api_key": "Ajouter une Clé API", @@ -235,10 +199,6 @@ "how_to": "Comment configurer SMTP ?", "send_test_email": "Envoyer un Email de Test" }, - "agora": { - "enable": "Activer", - "how_to": "Comment configurer Agora ?" - }, "third_app": { "key": "Clé Sécurisée API", "update": "Mettre à Jour le Secret", diff --git a/public/locales/fr/welcome.json b/public/locales/fr/welcome.json index 083b9c37..69cedb72 100644 --- a/public/locales/fr/welcome.json +++ b/public/locales/fr/welcome.json @@ -7,8 +7,7 @@ "download": "Téléchargez les applications mobiles", "help": "Vous avez des questions ? Visitez notre centre d'aide", "sign_in_tip": "Veuillez vous connecter pour envoyer un message", - "vocespace": "Appel Vidéo VoceSpace", - "license": "VoceChat Licence de Mise à Niveau", + "license": "Licence VoceChat", "page_editor": { "title": "Page Management", "status_select": "Select State", @@ -66,5 +65,6 @@ "tunnel_progress": "Starting tunnel...", "tunnel_error": "Tunnel error", "tunnel_get_domain": "Get Public Domain" - } + }, + "video": "Vidéo" } diff --git a/public/locales/fr/widget.json b/public/locales/fr/widget.json index 2fc91924..0226689a 100644 --- a/public/locales/fr/widget.json +++ b/public/locales/fr/widget.json @@ -1,5 +1,5 @@ -{ - "welcome": "👋 Bonjour, Ravi de vous rencontrer !", - "placeholder_send": "Tapez et appuyez sur entrer", - "start_chat": "Commencer le Chat" -} \ No newline at end of file +{ + "welcome": "👋 Bonjour, Ravi de vous rencontrer !", + "placeholder_send": "Tapez et appuyez sur entrer", + "start_chat": "Commencer le Chat" +} diff --git a/public/locales/jp/chat.json b/public/locales/jp/chat.json index 4152c4e3..2f10cc2e 100644 --- a/public/locales/jp/chat.json +++ b/public/locales/jp/chat.json @@ -11,22 +11,19 @@ "welcome_channel": " {{name}} チャンネルへようこそ", "welcome_desc": " #{{name}} の最初のところ", "edit_channel": "チャネルを編集する", - "channel_name": "チャネル名", "private_channel": "プライベートチャネル", "create_channel": "チャネルを作る", "create_channel_desc": "これはオープンチャンネルで、誰もが見ることができます", "create_private_channel_desc": "これはプライベートチャンネルで、指定された人だけが見ることができます", "search_user_placeholder": "ユーザー名を入力してください", - "invite_title": "友達を{{name}}に招待する", "invite_by_email": "招待メールを送信する", "enable_smtp": "始めにSMTPを有効にする", "send_invite_link": "もしくは、招待用URLをコピーする", "share_invite_link": "このリンクを使って、友達を招待する", - "invite_link_setting_tip": "招待用URLの有効期限は48時間です", "generate_new_link": "新しいリンクを生成する", - - "send_to": "送信先" + "send_to": "送信先", + "license_tip": "現在のライセンスのユーザー上限に達しました。管理者に連絡してください。" } diff --git a/public/locales/jp/common.json b/public/locales/jp/common.json index d938e4ae..89f24cdf 100644 --- a/public/locales/jp/common.json +++ b/public/locales/jp/common.json @@ -8,7 +8,6 @@ "more": "もっと", "action": { "login": "Sign In", - "logout": "ログアウト", "change_avatar": "アバターを変更", "cancel": "キャンセル", @@ -36,7 +35,6 @@ "reply": "返信", "pin": "ピン留め", "unpin": "ピン留め解除", - "send": "送信", "send_msg": "メッセージを送る", "copy": "コピー", @@ -50,7 +48,6 @@ "uploading": "アップロード中" }, "tip": { - "email_msg_tip": "お住まいの地域では通知がブロックされています。メール通知をご利用いただくには、Pro にアップグレードしてください。", "update": "更新に成功しました!", "delete": "削除に成功しました!", "reg": "登録に成功しました!", @@ -62,22 +59,10 @@ }, "new_version": "<0>新バージョンをご利用いただけます", "mobile_app": "<0>モバイルアプリをご覧ください", - "price": { - "pro": { - "title": "VoceChat Pro", - "desc": "", - "price": "$49" - }, - "supreme": { - "title": "VoceSpace プライベート導入ビデオ [$499]", - "desc": "", - "price": "" - } - }, "server_update": { "version_needed": "この機能には、サーバーバージョン<0>{{version}} 🚨以上が必要です", "current_version": "現在のバージョン: <0>{{version}} 0>", - "update_tip": "サーバーをアップグレードしてください!", + "update_tip": "サーバーを更新してください!", "howto": "VoceChatサーバーのアップデート方法" }, "inactive": { diff --git a/public/locales/jp/setting.json b/public/locales/jp/setting.json index 3de41271..6e9694f5 100644 --- a/public/locales/jp/setting.json +++ b/public/locales/jp/setting.json @@ -12,7 +12,6 @@ "bot": "ボット&Webhook", "notification_channels": "通知チャンネル", "firebase": "Firebase", - "agora": "Agora", "smtp": "SMTP", "login_method": "ログイン方法", "third_app": "サードパーティー", @@ -24,34 +23,12 @@ "video": "ビデオ", "notification": "通知設定" }, - "vocespace": { - "add": "追加", - "check": "確認", - "checked": "合格", - "failed": "失敗", - "title": "VoceSpace", - "desc": "VoceSpace をプライベートに導入(自動導入)", - "sub_desc": "VoceSpace アカウントごとに、通話時間無制限、同時接続ユーザー5名、4K解像度のサービスを無料でご利用いただけます。アカウント作成は不要です。VoceSpace を適切に設定するには、以下の手順に従ってください。", - "prerequisite": [ - "前提条件", - "Docker 環境", - "ポート 80、443、7880、7881、3008、3000 が開いている", - "ローカル IP アドレスに解決されるドメイン名(Alibaba Cloud や Tencent Cloud など、中国本土のユーザーの場合は ICP 申請が必要です)", - "エンタープライズカスタマイズやサポートについては、han@privoce.com までお問い合わせいただくか、WeChat: Privoce を追加してください" - ], - - "domain_desc": "カスタムドメイン名はローカル IP アドレスに解決される必要があります(Alibaba Cloud や Tencent Cloud など、中国本土のユーザーの場合は ICP 申請が必要です)", - "enable": "有効にする", - "how_to": "有効にした後、ドメイン名を設定して保存してください。残りの手順は自動的に完了します。", - "old": "現在のサーバーバージョンは古くなっています。公式の VoceSpace (vocespace.com) をデフォルトのオーディオ/ビデオサービスとして有効にしました。独自のプライベート導入を使用するには、vocechat-server を v0.5.8 以上に更新してください。" - }, "channel": { "leave": "チャンネルを離れる", "leave_desc": "このチャンネルを離れますか?", "transfer_desc": "離れる前にこのチャンネルのコントロール権を他のユーザーに譲渡しなければならない", "delete": "チャンネルを削除する", "delete_desc": "このチャンネルを削除しますか?", - "name": "チャンネル名", "topic": "トピック", "topic_placeholder": "チャンネルのトピックを入力してください", @@ -63,7 +40,6 @@ "name": "サーバー名", "desc": "サーバーの説明", "upload_desc": "画像サイズとして、128x128 は必須、512x512 はおすすめ。大きさは 5M を超えないでください。", - "sign_up": { "title": "登録方法", "desc": "登録方法を設定してください", @@ -88,16 +64,8 @@ "expire": "有効期限", "create": "作成日時", "value": "内容", - "renew": "ライセンスキをリニューアル", "update": "ライセンスキーをアップロード", - "update_placeholder": "ライセンスキーをアップロードしてください", - - "renew_select": "アップグするライセンスを選択してください", - "tip": { - "title": "無料アップグレードを取得するチャンス!", - "user_test": "テストに参加することで、無料アップグレードを取得する機会を得る", - "contact": "ここで予約する:" - } + "update_placeholder": "ライセンスキーをアップロードしてください" }, "firebase": { "enable": "有効にする", diff --git a/public/locales/jp/welcome.json b/public/locales/jp/welcome.json index 6f6517c1..23faa79a 100644 --- a/public/locales/jp/welcome.json +++ b/public/locales/jp/welcome.json @@ -7,8 +7,7 @@ "download": "アプリをダウンロードしましょう", "help": "まだ聞きたいことがある? ヘルプにアクセスしましょう", "sign_in_tip": "Please sign in to send message", - "vocespace": " VoceSpace ビデオ通話", - "license": "VoceChat アップグレードライセンス", + "license": "VoceChat ライセンス", "page_editor": { "title": "Page Management", "status_select": "Select State", @@ -55,5 +54,6 @@ "admin_account": "管理者アカウント", "who_sign_up": "登録に関する権限設定", "invites": "招待" - } + }, + "video": "ビデオ" } diff --git a/public/locales/pt/chat.json b/public/locales/pt/chat.json index 8ef6d5d0..888823cc 100644 --- a/public/locales/pt/chat.json +++ b/public/locales/pt/chat.json @@ -11,7 +11,6 @@ "welcome_channel": "Welcome to {{name}}", "welcome_desc": "This is the start of the #{{name}} channel.", "edit_channel": "Edit Channel", - "channel_name": "Channel Name", "private_channel": "Private Channel", "create_channel": "Create New Channel", @@ -19,7 +18,6 @@ "create_private_channel_desc": "This is a private channel, only select members will be able to join.", "search_user_placeholder": "Type Username to search", "welcome_msg": "Welcome to channel {{name}}", - "invite_title": "Add friends to {{name}}", "invite_by_email": "Invite by Email", "enable_smtp": "Enable SMTP First", @@ -29,23 +27,18 @@ "invite_link_edit": "Edit invite link", "invite_link_setting_tip": "Invite link expires in {{expire}}, max use times: {{times}}", "generate_new_link": "Generate New Link", - "send_to": "Send To", "edited": "edited", - "license_tip": "Your license has reached the limit, upgrade the License or contact the Admin!", - + "license_tip": "O limite de usuários da licença atual foi atingido. Entre em contato com o administrador.", "delete_msg_title": "Delete Message", "delete_msg_desc": "Are you sure want to delete {{msg}}?", "delete_msg_this": "this message", "delete_msg_these": "these messages", - "new_msg": "{{num}} new messages", "mark_read": "Mark as Read", "pin_chat": "Pin Chat", "unpin_chat": "Unpin", - "reply_msg_del": "This message has been deleted.", - "file": "file", "image": "image", "forward": "forward", diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json index 177b3b79..39740b74 100644 --- a/public/locales/pt/common.json +++ b/public/locales/pt/common.json @@ -67,22 +67,10 @@ }, "new_version": "<0>New Version Available", "mobile_app": "Check out our <0>Mobile APP", - "price": { - "pro": { - "title": "VoceChat Pro", - "desc": "", - "price": "$49" - }, - "supreme": { - "title": "VoceSpace Private Deployment Video [499$]", - "desc": "", - "price": "" - } - }, "server_update": { "version_needed": "This function needs server version:<0>{{version}} at least 🚨", "current_version": "Your current version:<0>{{version}}", - "update_tip": "Please upgrade the Server!", + "update_tip": "Please update the Server!", "howto": "How to Update VoceChat Server" }, "inactive": { diff --git a/public/locales/pt/member.json b/public/locales/pt/member.json index c3cebb6c..02804d6a 100644 --- a/public/locales/pt/member.json +++ b/public/locales/pt/member.json @@ -12,7 +12,6 @@ "current_pwd": "Senha Atual", "new_pwd": "Nova Senha", "confirm_new_pwd": "Confirmar Nova Senha", - "manage_members": "Gerenciar Contatos", "manage_tip": "Disabling your account means you can recover it at any time after taking this action.", "admin": "Admin", @@ -28,7 +27,6 @@ "roles": "Roles", "set_admin": "Admin", "set_normal": "User", - "search_not_found": "Not found, or this user is not allowed to be searched.", "search_by_id": "Search by ID", "search_by_id_ph": "Input user ID", diff --git a/public/locales/pt/setting.json b/public/locales/pt/setting.json index c57495dd..240ad488 100644 --- a/public/locales/pt/setting.json +++ b/public/locales/pt/setting.json @@ -15,7 +15,6 @@ "bot": "Bot & Webhook", "notification_channels": "Canais de Notificação", "firebase": "Firebase", - "agora": "Agora", "smtp": "SMTP", "login_method": "Login Methods", "third_app": "Third-party Login", @@ -27,26 +26,6 @@ "video": "Video", "notification": "Notificações" }, - "vocespace": { - "add": "Adicionar", - "check": "Verificar", - "checked": "Aprovado", - "failed": "Falhou", - "title": "VoceSpace", - "desc": "VoceSpace déployé en mode privé (déploiement automatique)", - "sub_desc": "Chaque compte VoceSpace bénéficie d'appels illimités gratuits, de 5 utilisateurs simultanés et d'une résolution 4K. Aucune création de compte n'est requise. Suivez les instructions ci-dessous pour configurer correctement votre VoceSpace.", - "prerequisite": [ - "Prérequis", - "Environnement Docker", - "Les ports 80, 443, 7880, 7881, 3008 et 3000 sont ouverts", - "Nom de domaine pointant vers l'adresse IP locale (pour les utilisateurs de Chine continentale, tels qu'Alibaba Cloud et Tencent Cloud, un enregistrement ICP est requis)", - "Pour toute personnalisation ou assistance en entreprise, veuillez contacter han@privoce.com ou ajouter Privoce sur WeChat" - ], - "domain_desc": "Les noms de domaine personnalisés doivent pointer vers l'adresse IP locale (pour les utilisateurs de Chine continentale, tels qu'Alibaba Cloud et Tencent Cloud, un enregistrement ICP est requis)", - "enable": "Activer", - "how_to": "Après l'activation, veuillez définir le nom de domaine et enregistrer. Les étapes suivantes seront effectuées automatiquement.", - "old": "Votre version actuelle du serveur est obsolète. Nous avons activé le VoceSpace officiel (vocespace.com) comme votre service audio/vidéo par défaut. Pour utiliser votre propre déploiement privé, veuillez mettre à jour vocechat-server vers la version 0.5.8 ou supérieure." - }, "channel": { "leave": "Leave Channel", "leave_desc": "Are you sure want to leave this channel?", @@ -145,23 +124,8 @@ "expire": "Expired At", "create": "Created At", "value": "License Value", - "renew": "Renew License", "update": "Update Manually", - "update_placeholder": "Please input the new license value", - "renew_select": "Please select the price", - "tip": { - "title": "A chance to get a free license upgrade!", - "user_test": "Get a free license upgrade through a 25 min user testing", - "contact": "Book a time here: " - }, - "payment_success": "Payment Success!", - "back_home": "Back Home", - "tip_renewing": "Renewing the License, do not close the window!", - "tip_renewed": "Renew the License Successfully!", - "tip_renew_error": "Invalided Stripe Session ID", - "tip_domain": "The license is bound to domain (without port), please confirm or update your setting:", - "tip_port": "Fill out your domain name without port.", - "tip_confirm": "Start Payment" + "update_placeholder": "Please input the new license value" }, "bot": { "add_api_key": "Add API Key", @@ -228,10 +192,6 @@ "how_to": "How to setup SMTP?", "send_test_email": "Send Test Email" }, - "agora": { - "enable": "Enable", - "how_to": "How to setup Agora?" - }, "third_app": { "key": "API Secure Key", "update": "Update Secret", diff --git a/public/locales/pt/welcome.json b/public/locales/pt/welcome.json index 048fdee8..26d7d7b0 100644 --- a/public/locales/pt/welcome.json +++ b/public/locales/pt/welcome.json @@ -7,8 +7,7 @@ "download": "Download Mobile apps", "help": "Tem perguntas? Visite nossa central de ajuda", "sign_in_tip": "Faça login para enviar mensagem", - "vocespace": "Chamada de vídeo VoceSpace", - "license": "VoceChat Licença de Atualização", + "license": "Licença VoceChat", "page_editor": { "title": "Page Management", "status_select": "Select State", @@ -66,5 +65,6 @@ "tunnel_progress": "Starting tunnel...", "tunnel_error": "Tunnel error", "tunnel_get_domain": "Get Public Domain" - } + }, + "video": "Vídeo" } diff --git a/public/locales/ru/chat.json b/public/locales/ru/chat.json index c153c689..e2fc9559 100644 --- a/public/locales/ru/chat.json +++ b/public/locales/ru/chat.json @@ -11,7 +11,6 @@ "welcome_channel": "Добро пожаловать в {{name}}", "welcome_desc": "Это начало #{{name}} канала.", "edit_channel": "Редактировать канал", - "channel_name": "Имя канала", "private_channel": "Частный канал", "create_channel": "Создать новый канал", @@ -19,7 +18,6 @@ "create_private_channel_desc": "Это закрытый канал, присоединиться к которому смогут только избранные участники.", "search_user_placeholder": "Введите имя пользователя для поиска", "welcome_msg": "Добро пожаловать на канал {{name}}", - "invite_title": "Добавить друзей в {{name}}", "invite_by_email": "Пригласить по электронной почте", "enable_smtp": "Сначала включите SMTP", @@ -29,23 +27,18 @@ "invite_link_edit": "Изменить ссылку приглашения", "invite_link_setting_tip": "Срок действия ссылки-приглашения истекает через {{expire}}, максимальное количество использований: {{times}}", "generate_new_link": "Создать новую ссылку", - "send_to": "Отправить", "edited": "отредактировано", - "license_tip": "Ваша лицензия достигла лимита, обновите лицензию или обратитесь к администратору!", - + "license_tip": "Достигнут лимит пользователей текущей лицензии. Обратитесь к администратору.", "delete_msg_title": "Удалить сообщение", "delete_msg_desc": "Вы уверены, что хотите удалить {{msg}}?", "delete_msg_this": "это сообщение", "delete_msg_these": "эти сообщения", - "new_msg": "{{num}} новое сообщение", "mark_read": "Отметить как прочитанное", "pin_chat": "Закрепить чат", "unpin_chat": "Открепить", - "reply_msg_del": "Это сообщение было удалено.", - "file": "Файл", "image": "Изображение", "forward": "Вперед", diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index acd0474d..9269f17a 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -51,7 +51,6 @@ "install": "Установка", "download_origin": "Скачать оригинал", "close": "Закрыть", - "upgrade": "Обновление", "view_pwd": "Посмотреть пароль", "change_pwd": "Обновить пароль" }, @@ -59,7 +58,6 @@ "uploading": "Загрузка" }, "tip": { - "email_msg_tip": "В вашем регионе заблокированы уведомления. Перейдите на версию Pro, чтобы использовать уведомления по электронной почте.", "update": "Обновление успешно выполнено!", "delete": "Удаление успешно выполнено!", "reg": "Регистрация прошла успешно!", @@ -71,18 +69,6 @@ }, "new_version": "<0>New Version Available", "mobile_app": "Check out our <0>Mobile APP", - "price": { - "pro": { - "title": "VoceChat Pro", - "desc": "", - "price": "$49" - }, - "supreme": { - "title": "Видеоинструкция по развертыванию VoceSpace Private [499$]", - "desc": "", - "price": "" - } - }, "server_update": { "version_needed": "Для этой функции требуется серверная версия:<0>{{version}} по крайней мере 🚨 ", "current_version": "Ваша текущая версия:<0>{{version}}", diff --git a/public/locales/ru/member.json b/public/locales/ru/member.json index 02f76b72..3453932b 100644 --- a/public/locales/ru/member.json +++ b/public/locales/ru/member.json @@ -12,7 +12,6 @@ "current_pwd": "Текущий пароль", "new_pwd": "Новый пароль", "confirm_new_pwd": "Подтвердите новый пароль", - "manage_members": "Управление контактами", "manage_tip": "Отключение вашей учетной записи означает, что вы сможете восстановить ее в любое время после выполнения этого действия.", "admin": "Администратор", @@ -28,7 +27,6 @@ "roles": "Роли", "set_admin": "Администратор", "set_normal": "Пользователь", - "search_not_found": "Не найдено, или поиск этого пользователя запрещен.", "search_by_id": "Поиск по идентификатору", "search_by_id_ph": "Введите идентификатор пользователя", diff --git a/public/locales/ru/setting.json b/public/locales/ru/setting.json index c86e0c9f..a60f720d 100644 --- a/public/locales/ru/setting.json +++ b/public/locales/ru/setting.json @@ -15,7 +15,6 @@ "bot": "Bot & Webhook", "notification_channels": "Каналы уведомлений", "firebase": "Firebase", - "agora": "Agora", "smtp": "SMTP", "login_method": "Методы входа", "third_app": "Вход третьей стороны", @@ -27,26 +26,6 @@ "video": "Видео", "notification": "Уведомления" }, - "vocespace": { - "add": "Добавить", - "check": "Проверить", - "checked": "Пройдено", - "failed": "Не пройдено", - "title": "VoceSpace", - "desc": "VoceSpace развернут в частном порядке (автоматическое развертывание)", - "sub_desc": "Каждая учетная запись VoceSpace будет пользоваться бесплатным неограниченным временем звонков, 5 одновременными пользователями и разрешением 4K. Создание учетной записи не требуется. Следуйте приведенным ниже инструкциям для правильной настройки VoceSpace.", - "prerequisite": [ - "Предварительные условия", - "Среда Docker", - "Открыты порты 80, 443, 7880, 7881, 3008, 3000", - "Доменное имя, разрешающееся в локальный IP-адрес (для пользователей в материковом Китае, таких как Alibaba Cloud и Tencent Cloud, требуется подача заявки на ICP)", - "Для корпоративной настройки или получения помощи, пожалуйста, свяжитесь с han@privoce.com или добавьте нас в WeChat: Privoce" - ], - "domain_desc": "Пользовательские доменные имена должны разрешаться в локальный IP-адрес (для пользователей в материковом Китае, таких как Alibaba Cloud и Tencent Cloud, требуется подача заявки на ICP)", - "enable": "Включить", - "how_to": "После включения, пожалуйста, укажите доменное имя и сохраните. Остальные шаги будут выполнены автоматически.", - "old": "Ваша текущая версия сервера устарела. Мы включили официальный VoceSpace (vocespace.com) в качестве вашей службы аудио/видео по умолчанию. Чтобы использовать собственное частное развертывание, обновите vocechat-server до версии 0.5.8 или выше." - }, "channel": { "leave": "Покинуть канал", "leave_desc": "Вы уверены, что хотите покинуть этот канал?", @@ -191,23 +170,8 @@ "expire": "Истек срок действия", "create": "Создано в", "value": "Стоимость лицензии", - "renew": "Продлить лицензию", "update": "Обновить вручную", - "update_placeholder": "Введите новое значение лицензии.", - "renew_select": "Пожалуйста, выберите цену", - "tip": { - "title": "Шанс получить бесплатное обновление лицензии!", - "user_test": "Получите бесплатное обновление лицензии, пройдя 25-минутное пользовательское тестирование", - "contact": "Забронируйте время здесь: " - }, - "payment_success": "Платеж прошел успешно!", - "back_home": "Назад домой", - "tip_renewing": "Продлевая лицензию, не закрывайте окно!", - "tip_renewed": "Продление лицензии прошло успешно!", - "tip_renew_error": "Неверный идентификатор сеанса Stripe", - "tip_domain": "Лицензия привязана к домену (без порта), подтвердите или обновите настройки:", - "tip_port": "Укажите доменное имя без порта.", - "tip_confirm": "Начать оплату" + "update_placeholder": "Введите новое значение лицензии." }, "bot": { "add_api_key": "Добавить API-ключ", @@ -267,7 +231,7 @@ "client_email": "Электронная почта клиента" }, "smtp": { - "desc": "Для нового сообщения Уведомление по электронной почте (расширенная функция, обновите до <0>VoceChat Pro) и для подтверждения адреса электронной почты при новой регистрации мы используем SMTP. ", + "desc": "Для уведомлений о новых сообщениях по электронной почте и подтверждения электронной почты при регистрации мы используем SMTP.", "sub_desc": "Пожалуйста, настройте ваш SMTP ниже.", "enable": "Давать возможность", "host": "Хост", @@ -278,12 +242,6 @@ "how_to": "Как настроить SMTP?", "send_test_email": "Отправить тестовое письмо" }, - "agora": { - "desc": "Для видео- и аудиозвонков мы используем Agora API. ", - "sub_desc": "Каждый проект Agora получит 10 000 минут бесплатного использования API в месяц. Создайте проект самостоятельно. Следуйте инструкциям ниже, чтобы правильно настроить Agora.", - "enable": "Давать возможность", - "how_to": "Как настроить Agora?" - }, "third_app": { "key": "Безопасный ключ API", "update": "Обновление секрета", diff --git a/public/locales/ru/welcome.json b/public/locales/ru/welcome.json index ef82c3d7..8aefdc45 100644 --- a/public/locales/ru/welcome.json +++ b/public/locales/ru/welcome.json @@ -7,8 +7,7 @@ "download": "Загрузить мобильные приложения", "help": "Есть вопросы? Посетите наш справочный центр", "sign_in_tip": "Пожалуйста, войдите, чтобы отправить сообщение", - "vocespace": "Видеозвонок VoceSpace", - "license": "VoceChat Лицензия на обновление", + "license": "Лицензия VoceChat", "page_editor": { "title": "Page Management", "status_select": "Select State", @@ -66,5 +65,6 @@ "tunnel_progress": "Starting tunnel...", "tunnel_error": "Tunnel error", "tunnel_get_domain": "Get Public Domain" - } + }, + "video": "Видео" } diff --git a/public/locales/tr/chat.json b/public/locales/tr/chat.json index 1868865b..e0eaf8d6 100644 --- a/public/locales/tr/chat.json +++ b/public/locales/tr/chat.json @@ -11,7 +11,6 @@ "welcome_channel": "{{name}} kanalına hoşgeldiniz!", "welcome_desc": "Burası #{{name}} kanal'ının başlangıcıdır.", "edit_channel": "Kanal'ı düzenle", - "channel_name": "Kanal Adı", "private_channel": "Özel Kanal", "create_channel": "Yeni Kanal Yarat", @@ -19,7 +18,6 @@ "create_private_channel_desc": "Bu özel bir kanaldır, sadece seçilen üyeler görebilir.", "search_user_placeholder": "Kullanıcı araması için birşeyler yazın", "welcome_msg": "{{name}} kanalına hoşgeldiniz", - "invite_title": "{{name}} kullanıcısını arkadaş olarak ekle", "invite_by_email": "E-posta ile davet et", "enable_smtp": "Öncelikle SMTP'ye izin verin ", @@ -28,21 +26,16 @@ "invite_link_faq": "Hatalı davet bağlantısı mı?", "invite_link_setting_tip": "Davet bağlantısı 48 saat geçerli olacaktır.", "generate_new_link": "Yeni bağlantı yarat", - "send_to": "Şuna gönder", "edited": "düzenlenmiş", - "license_tip": "Lisansınız sınırına ulaşmıştır, Lisansınızı yükseltin veya sistem yöneticinize başvurun!", - + "license_tip": "Geçerli lisansın kullanıcı sınırına ulaşıldı. Sistem yöneticisine başvurun.", "delete_msg_title": "İletiyi Sil", "delete_msg_desc": "{{msg}} iletisini silmek istediğinize emin misiniz?", "delete_msg_this": "bu ileti", "delete_msg_these": "bu iletiler", - "new_msg": "{{num}} yeni ileti", "mark_read": "Okundu olarak işaretle", - "reply_msg_del": "Bu ileti silinmiş.", - "file": "dosya", "image": "resim", "forward": "ilet" diff --git a/public/locales/tr/common.json b/public/locales/tr/common.json index a6616d62..3306db6b 100644 --- a/public/locales/tr/common.json +++ b/public/locales/tr/common.json @@ -65,18 +65,6 @@ }, "new_version": "<0>Yeni Sürüm Mevcut", "mobile_app": "Yeni <0>Mobil Uygulama'yı deneyin", - "price": { - "pro": { - "title": "VoceChat Pro", - "desc": "", - "price": "$49" - }, - "supreme": { - "title": "VoceSpace Özel Dağıtım Videosu [499$]", - "desc": "", - "price": "" - } - }, "server_update": { "version_needed": "Bu yöntem için sunucu sürümü en az <0>{{version}} olmalı 🚨", "current_version": "Sizin mevcut sürümünüz:<0>{{version}}", diff --git a/public/locales/tr/member.json b/public/locales/tr/member.json index 38f1da53..9099db83 100644 --- a/public/locales/tr/member.json +++ b/public/locales/tr/member.json @@ -12,7 +12,6 @@ "current_pwd": "Mevcut Parola", "new_pwd": "Yeni Parola", "confirm_new_pwd": "Yeni Parola Onayı", - "manage_members": "Üyeleri Yönet", "manage_tip": "Hesabınızı etkisizleştirmeniz durumunda istediğiniz zaman kaldığınız yerden devam edebilirsiniz.", "admin": "Sistem Yöneticisi", diff --git a/public/locales/tr/setting.json b/public/locales/tr/setting.json index e3acfaa3..1a6ee8bb 100644 --- a/public/locales/tr/setting.json +++ b/public/locales/tr/setting.json @@ -14,7 +14,6 @@ "bot": "Bot & Webhook", "notification_channels": "Bildirim Kanalları", "firebase": "Firebase", - "agora": "Agora", "smtp": "SMTP", "login_method": "Giriş Yöntemleri", "third_app": "Third-party Girişler", @@ -28,26 +27,6 @@ "video": "Video", "notification": "Bildirimler" }, - "vocespace": { - "add": "Ekle", - "check": "Kontrol Et", - "checked": "Geçti", - "failed": "Başarısız", - "title": "VoceSpace", - "desc": "VoceSpace özel olarak dağıtıldı (otomatik dağıtım)", - "sub_desc": "Her VoceSpace hesabı ücretsiz sınırsız arama süresi, 5 eş zamanlı kullanıcı ve 4K çözünürlük hizmetinden yararlanacaktır. Hesap oluşturma gerekmez. VoceSpace'inizi doğru şekilde yapılandırmak için aşağıdaki talimatları izleyin.", - "prerequisite": [ - "Önkoşullar", - "Docker ortamı", - "80, 443, 7880, 7881, 3008, 3000 portları açık olmalı", - "Yerel IP adresine çözümlenen alan adı (Alibaba Cloud ve Tencent Cloud gibi Çin anakarasındaki kullanıcılar için ICP dosyalama gereklidir)", - "Kurumsal özelleştirme veya yardım için lütfen han@privoce.com ile iletişime geçin veya WeChat: Privoce ekleyin" - ], - "domain_desc": "Özel alan adları yerel IP adresine çözümlenmelidir (Alibaba Cloud ve Tencent Cloud gibi Çin anakarasındaki kullanıcılar için ICP dosyalama gereklidir)", - "enable": "Etkinleştir", - "how_to": "Etkinleştirdikten sonra lütfen alan adını ayarlayın ve kaydedin. Kalan adımlar otomatik olarak tamamlanacaktır.", - "old": "Mevcut sunucu sürümünüz güncel değil. Resmi VoceSpace (vocespace.com) varsayılan ses/video hizmetiniz olarak etkinleştirildi. Kendi özel dağıtımınızı kullanmak için lütfen vocechat-server'ı v0.5.8 veya daha yüksek bir sürüme güncelleyin." - }, "channel": { "leave": "Kanalı Terket", "leave_desc": "Bu kanalı terketmek istediğinize emin misiniz?", @@ -104,23 +83,8 @@ "expire": "Süre Dolum Tarihi", "create": "Yaratılma Tarihi", "value": "Lisans değeri", - "renew": "Lisansı Yenile", "update": "El ile Yenile", - "update_placeholder": "Lütfen yeni lisans değerini girin", - "renew_select": "Lütfen ücret seçin", - "tip": { - "title": "Ücretsiz Lisans yükseltme Fırsatı!", - "user_test": "25 Dakika Kullanıcı Testi ile ücretsiz lisans yükseltin.", - "contact": "Şuradan bir zaman ayarlayın: " - }, - "payment_success": "Ödeme Başarılı!", - "back_home": "Ana Sayfaya Dön", - "tip_renewing": "Lisans yenileniyor lütfen bu sayfayı kapatmayın!", - "tip_renewed": "Lisans başarı ile yenilendi!", - "tip_renew_error": "Invalided Stripe Session ID", - "tip_domain": "Lisansınız Alan adı ve Porta bağlıdır, lütfen onaylayın veya ayarlarınız güncelleyin:", - "tip_port": "Fill out your domain name without port.", - "tip_confirm": "Ödemeye başlayın" + "update_placeholder": "Lütfen yeni lisans değerini girin" }, "bot": { "add_api_key": "API Anahtarı Ekleyin", diff --git a/public/locales/tr/welcome.json b/public/locales/tr/welcome.json index 11e0b6ce..e5163fa3 100644 --- a/public/locales/tr/welcome.json +++ b/public/locales/tr/welcome.json @@ -7,8 +7,7 @@ "download": "Mobil uygulamaları indirin", "help": "Sorunuz var mı? Yardım merkezimizi ziyaret edin", "sign_in_tip": "İleti göndermek için lütfen oturum açın", - "vocespace": " VoceSpace Video Görüşmesi", - "license": "VoceChat Yükseltme Lisansı", + "license": "VoceChat Lisansı", "page_editor": { "title": "Page Management", "status_select": "Select State", @@ -66,5 +65,6 @@ "tunnel_progress": "Starting tunnel...", "tunnel_error": "Tunnel error", "tunnel_get_domain": "Get Public Domain" - } + }, + "video": "Video" } diff --git a/public/locales/zh/chat.json b/public/locales/zh/chat.json index 429737c3..2a4dfe58 100644 --- a/public/locales/zh/chat.json +++ b/public/locales/zh/chat.json @@ -18,7 +18,6 @@ "created_at": "创建时间", "updated_at": "最后更新", "dismiss_announcement": "关闭公告", - "channel_name": "频道名称", "private_channel": "私密频道", "create_channel": "创建新频道", @@ -26,7 +25,6 @@ "create_private_channel_desc": "这是一个私密频道,只有被选择的人才能看到", "search_user_placeholder": "请输入用户名", "welcome_msg": "欢迎来到 {{name}} 频道", - "invite_title": "邀请朋友加入{{name}}", "invite_by_email": "邮件邀请", "enable_smtp": "首先开启 SMTP", @@ -36,22 +34,18 @@ "invite_link_edit": "编辑邀请链接", "invite_link_setting_tip": "邀请链接有效期:{{expire}},最多使用次数:{{times}}", "generate_new_link": "生成新邀请链接", - "send_to": "发送至", "edited": "已编辑", - "license_tip": "您的使用人数已达证书限制上限,请升级证书或者联系管理员!", - + "license_tip": "您的使用人数已达当前证书限制上限,请联系管理员处理。", "delete_msg_title": "删除消息", "delete_msg_desc": "确定删除{{msg}}?", "delete_msg_this": "此消息", "delete_msg_these": "这些消息", - "new_msg": "{{num}} 条新消息", "mark_read": "设为已读", "pin_chat": "置顶", "unpin_chat": "取消置顶", "reply_msg_del": "被回复的消息已删除!", - "file": "文件", "image": "图片", "forward": "转发", @@ -71,12 +65,10 @@ "contact_block_tip": "该用户已被你屏蔽", "file_expired": "文件已过期", "only_owner_can_send": "只有频道创建者才能发消息!", - "remark": "备注", "remark_clear": "重置备注", "remark_intro": "给联系人备注,方便日后查找,备注仅您可见", "remark_placeholder": "备注该用户", - "guest_mode_warning": { "title": "该频道将对访客可见", "desc": "继续创建后,访客用户可能会看到该公共频道中的消息。", diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 6435ff5a..754ec552 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -38,7 +38,6 @@ "reply": "回复", "pin": "置顶", "unpin": "取消置顶", - "send": "发送", "send_msg": "发消息", "copy": "复制", @@ -51,7 +50,6 @@ "install": "安装", "download_origin": "下载原图", "close": "关闭", - "upgrade": "升级", "view_pwd": "查看密码", "change_pwd": "修改密码" }, @@ -59,7 +57,6 @@ "uploading": "上传中" }, "tip": { - "email_msg_tip": "您的地区收不到消息提醒,升级到 Pro 版本可以使用邮件提醒功能", "update": "更新成功!", "delete": "删除成功!", "reg": "注册成功!", @@ -71,18 +68,6 @@ }, "new_version": "有<0>新版本", "mobile_app": "欢迎下载使用<0>手机 APP", - "price": { - "pro": { - "title": "VoceChat 专业版", - "desc": "", - "price": "$49" - }, - "supreme": { - "title": "VoceSpace 私有部署音视频[499$]", - "desc": "", - "price": "" - } - }, "server_update": { "version_needed": "支持该特性的最低 Server 版本:<0>{{version}}", "current_version": "当前的 Server 版本:<0>{{version}}", diff --git a/public/locales/zh/member.json b/public/locales/zh/member.json index 53c76d8a..e6045981 100644 --- a/public/locales/zh/member.json +++ b/public/locales/zh/member.json @@ -27,7 +27,6 @@ "roles": "设置角色", "set_admin": "管理员", "set_normal": "普通用户", - "search_not_found": "用户不存在,或者不允许搜索", "search_disabled": "管理员已关闭用户搜索功能", "add_to_contact": "加为联系人", diff --git a/public/locales/zh/setting.json b/public/locales/zh/setting.json index f369621d..37173397 100644 --- a/public/locales/zh/setting.json +++ b/public/locales/zh/setting.json @@ -15,7 +15,6 @@ "bot": "机器人&Webhook", "notification_channels": "通知渠道", "firebase": "Firebase", - "agora": "Agora", "smtp": "SMTP", "login_method": "登录设置", "third_app": "第三方认证", @@ -198,23 +197,8 @@ "expire": "过期时间", "create": "创建时间", "value": "证书内容", - "renew": "升级证书", "update": "手动更新", - "update_placeholder": "请输入新的证书内容", - "renew_select": "请选择价格套餐", - "tip": { - "title": "一个获取免费升级的机会!", - "user_test": "通过 blog 文章或社交媒体分享使用体验,获取免费升级的机会", - "contact": "加微信获得:" - }, - "payment_success": "付款成功", - "back_home": "回到首页", - "tip_renewing": "正在更新证书,请勿关闭窗口!", - "tip_renewed": "证书更新成功!", - "tip_renew_error": "无效的 Stripe ID", - "tip_domain": "证书与域名 (不带端口) 相绑定,请确认或修改:", - "tip_port": "端口号无需填写", - "tip_confirm": "开始支付" + "update_placeholder": "请输入新的证书内容" }, "bot": { "add_api_key": "新增 API Key", @@ -274,7 +258,7 @@ "client_email": "客户端邮箱" }, "smtp": { - "desc": "对于新消息电子邮件通知(高级功能,升级到 <0>VoceChat Pro)和新注册电子邮件验证,我们使用 SMTP。", + "desc": "对于新消息电子邮件通知和新注册电子邮件验证,我们使用 SMTP。", "sub_desc": "请在下方设置您的 SMTP。", "enable": "开启", "host": "主机", @@ -285,32 +269,6 @@ "how_to": "如何设置 SMTP?", "send_test_email": "发送测试邮件" }, - "agora": { - "desc": "对于视频和音频通话,我们使用 Agora API。", - "sub_desc": "每个 Agora 项目每月可享受 10,000 分钟的免费 API 使用时间。请自行创建一个项目。按照以下说明正确配置您的 Agora。", - "enable": "开启", - "how_to": "如何配置 Agora?" - }, - "vocespace": { - "add": "增加", - "check": "检测", - "checked": "通过", - "failed": "检测失败", - "title": "VoceSpace", - "desc": "使用私有部署的VoceSpace(自动部署)", - "sub_desc": "每个VoceSpace账户将享受免费无限制通话时长、5人并发及4K分辨率服务。无需创建账户。按照以下说明正确配置您的VoceSpace。", - "prerequisite": [ - "预备条件", - "Docker环境", - "端口 80, 443, 7880, 7881, 3008, 3000 已开放", - "解析到本机IP的域名(中国大陆用户,如阿里云和腾讯云,需备案)", - "企业定制或需要帮助,请联系 han@privoce.com 或添加微信:Privoce" - ], - "domain_desc": "自定义域名需要是解析到本机IP的域名 (中国大陆用户,如阿里云和腾讯云,需备案)", - "enable": "启用", - "how_to": "启用后,请设置域名并保存,其余步骤会自动化完成", - "old": "您当前的服务器版本过旧,已开启官方部署的VoceSpace地址(vocespace.com)作为默认音视频服务,如果依然需要私有部署,请更新vocechat-server到0.5.8以上版本再尝试。" - }, "third_app": { "key": "API 安全密钥", "update": "更新安全密钥", @@ -477,4 +435,4 @@ "error": "错误" } } -} \ No newline at end of file +} diff --git a/public/locales/zh/welcome.json b/public/locales/zh/welcome.json index 1c01e588..8f977952 100644 --- a/public/locales/zh/welcome.json +++ b/public/locales/zh/welcome.json @@ -7,8 +7,7 @@ "download": "下载移动端APP", "help": "还有问题?访问我们的帮助中心", "sign_in_tip": "请登录后发消息", - "vocespace": " VoceSpace 视频通话", - "license": "VoceChat 升级证书", + "license": "VoceChat 证书", "page_editor": { "title": "页面管理", "status_select": "状态选择", @@ -66,5 +65,6 @@ "tunnel_progress": "正在启动 Tunnel...", "tunnel_error": "Tunnel 出错", "tunnel_get_domain": "获取公网域名" - } + }, + "video": "视频通话" } diff --git a/src/app/config.ts b/src/app/config.ts index ce5a14b6..832396ae 100644 --- a/src/app/config.ts +++ b/src/app/config.ts @@ -1,16 +1,5 @@ import i18n from "../i18n"; -import { Price } from "../types/common"; -const prices: Price[] = [ - { - type: "payment", - limit: 999999, - pid: "price_1MbF30GGoUDRyc3jwOg30dVQ", - }, - { - type: "booking", - }, -]; const official_dev = `https://dev.voce.chat`; const local_dev = `http://localhost:3000`; // const local_dev = `https://dev.voce.chat`; @@ -37,59 +26,6 @@ export const BASE_ORIGIN = getBaseOrigin(); export const IS_OFFICIAL_DEMO = BASE_ORIGIN === official_dev; const BASE_URL = `${BASE_ORIGIN}/api`; -export const getLicensePriceList = () => { - const ps = prices.map((p, idx) => { - switch (idx) { - // pro - case 0: - { - p.title = i18n.t("price.pro.title"); - p.desc = i18n.t("price.pro.desc"); - p.price = i18n.t("price.pro.price"); - } - break; - // supreme - case 1: - { - p.title = i18n.t("price.supreme.title"); - p.desc = i18n.t("price.supreme.desc"); - p.price = i18n.t("price.supreme.price"); - } - break; - - default: - break; - } - return p; - }); - return process.env.NODE_ENV === "development" - ? // 开发环境加入两个测试价格 - [ - ...ps, - { - type: "payment", - price: "1", - title: "Test VoceChat Enterprise", - limit: 99999, - pid: "price_1LkQGpGGoUDRyc3jGTh3GYHw", - desc: "test price", - }, - { - title: "VoceChat Pro", - price: "1", - limit: 100, - pid: "price_1MMNNCGGoUDRyc3jSIGIsb3C", - desc: "test subscription price", - type: "subscription", - sub_dur: "year", //day month year - }, - ] - : ps; -}; -export const PAYMENT_URL_PREFIX = - process.env.NODE_ENV === "production" - ? `https://vera.nicegoodthings.com` - : `http://localhost:4000`; export const CACHE_VERSION = `0.4.3`; export const WIDGET_USER_PWD = `123123`; export const GuestRoutes = ["/", "/chat", "/chat/channel/:channel_id"]; diff --git a/src/app/services/base.query.ts b/src/app/services/base.query.ts index 6665c796..8dedfab0 100644 --- a/src/app/services/base.query.ts +++ b/src/app/services/base.query.ts @@ -27,15 +27,12 @@ const whiteList = [ "createAdmin", "getBotRelatedChannels", "sendMessageByBot", - "getAgoraVoicingList", "preCheckFileFromUrl", "passkeyLoginStart", "passkeyLoginFinish", "getAutoTunnelInfo", ]; const whiteList401 = [ - "getAgoraVoicingList", - "getAgoraChannels", "getGoogleAuthConfig", "getGithubAuthConfig", "getAutoTunnelInfo", diff --git a/src/app/services/server.ts b/src/app/services/server.ts index 604c88dd..e0220deb 100644 --- a/src/app/services/server.ts +++ b/src/app/services/server.ts @@ -3,10 +3,6 @@ import { createApi } from "@reduxjs/toolkit/query/react"; import { Channel } from "@/types/channel"; import { ContentTypeKey } from "@/types/message"; import { - AgoraChannelUsersResponse, - AgoraConfig, - AgoraTokenResponse, - AgoraVoicingListResponse, AutoTunnelInfo, CloudflaredStatus, CreateAdminDTO, @@ -14,14 +10,14 @@ import { GithubAuthConfig, GoogleAuthConfig, LicenseResponse, + LivekitCallRequest, + LivekitConfig, + LivekitTokenResponse, LoginConfig, - RenewLicense, - RenewLicenseResponse, Server, SMTPConfig, SystemCommon, TestEmailDTO, - VocespaceConfig, } from "@/types/server"; import { User } from "@/types/user"; import { compareVersion, encodeBase64 } from "@/utils"; @@ -29,11 +25,8 @@ import BASE_URL, { ContentTypes, IS_OFFICIAL_DEMO, KEY_SERVER_VERSION, - PAYMENT_URL_PREFIX, } from "../config"; import { updateInfo } from "../slices/server"; -import { updateCallInfo, upsertVoiceList } from "../slices/voice"; -import { RootState } from "../store"; import baseQuery from "./base.query"; import { GetFilesDTO, VoceChatFile } from "@/types/resource"; import { GroupAnnouncement } from "@/types/sse"; @@ -130,89 +123,29 @@ export const serverApi = createApi({ body: data, }), }), - getVocespaceConfig: builder.query({ - query: () => ({ url: `/admin/vocespace/config` }), + getLivekitConfig: builder.query({ + query: () => ({ url: `/admin/livekit/config` }), }), - getAgoraConfig: builder.query({ - query: () => ({ url: `/admin/agora/config` }), - }), - getAgoraChannels: builder.query< - AgoraVoicingListResponse, - { page_no: number; page_size: number } - >({ - query: (param = { page_no: 0, page_size: 100 }) => ({ - url: `/admin/agora/channel/${param.page_no}/${param.page_size}`, - }), - async onQueryStarted(data, { dispatch, queryFulfilled, getState }) { - try { - const { - voice: { callingFrom }, - authData, - } = getState() as RootState; - const { data: resp } = await queryFulfilled; - const { success } = resp; - if (success) { - const arr = resp.data.channels.map((data) => { - const [type, id] = data.channel_name.split(":").slice(-2); - const count = data.user_count; - const context = type === "group" ? ("channel" as const) : ("dm" as const); - return { - id: +id, - context, - memberCount: count, - channelName: data.channel_name, - }; - }); - dispatch(upsertVoiceList(arr)); - const hasMyself = arr.some( - (data) => data.context === "dm" && data.id == authData?.user?.uid - ); - const sendByMe = callingFrom && callingFrom === authData?.user?.uid; - // reset dm call setting - if (callingFrom && !sendByMe && !hasMyself) { - dispatch(updateCallInfo({ from: 0, to: 0, calling: false })); - } - } - } catch { - console.error("get voice list error"); - } - }, - }), - getAgoraUsersByChannel: builder.query({ - query: (channel_name) => ({ url: `/admin/agora/channel/user/${channel_name}/false` }), - transformResponse: (resp: AgoraChannelUsersResponse) => { - if (resp.success && resp.data.channel_exist) { - return resp.data.users ?? []; - } - return []; - }, - }), - updateAgoraConfig: builder.mutation({ + updateLivekitConfig: builder.mutation({ query: (data) => ({ - url: `/admin/agora/config`, + url: `/admin/livekit/config`, method: "POST", body: data, }), }), - updateVocespaceConfig: builder.mutation({ + getLivekitStatus: builder.query({ + query: () => ({ url: `/admin/livekit/enabled` }), + }), + generateLivekitToken: builder.mutation({ query: (data) => ({ - url: `/admin/vocespace/config`, + url: `/admin/livekit/token`, method: "POST", body: data, }), }), - healthCheckVocespace: builder.mutation({ + signalLivekitCall: builder.mutation({ query: (data) => ({ - url: `/admin/vocespace/health/${encodeURIComponent(data.url)}`, - method: "GET", - }), - }), - getAgoraStatus: builder.query({ - query: () => ({ url: `/admin/agora/enabled` }), - }), - generateAgoraToken: builder.mutation({ - query: (data) => ({ - url: `/admin/agora/token`, + url: `/admin/livekit/call`, method: "POST", body: data, }), @@ -364,14 +297,14 @@ export const serverApi = createApi({ // vocechat 官方 demo 则忽略 if (IS_OFFICIAL_DEMO) return; const rootStore = getState() as RootState; - const { upgraded: prevValue } = rootStore.server; + const { licensed: prevValue } = rootStore.server; try { const { data: { user_limit }, } = await queryFulfilled; const currValue = user_limit > 20; if (prevValue !== currValue) { - dispatch(updateInfo({ upgraded: currValue })); + dispatch(updateInfo({ licensed: currValue })); } } catch { console.error("get license failed "); @@ -379,18 +312,6 @@ export const serverApi = createApi({ }, }), - getLicensePaymentUrl: builder.mutation({ - query: (data) => ({ - url: `${PAYMENT_URL_PREFIX}/vocechat/payment/create`, - method: "POST", - body: data, - }), - }), - getGeneratedLicense: builder.query<{ license: string }, string>({ - query: (session_id) => ({ - url: `${PAYMENT_URL_PREFIX}/vocechat/licenses/${session_id}`, - }), - }), checkLicense: builder.mutation({ query: (license) => ({ url: "/license/check", @@ -543,14 +464,14 @@ export const { useUpdateFirebaseConfigMutation, useGetFirebaseConfigQuery, useLazyGetFirebaseConfigQuery, - useLazyGetAgoraConfigQuery, + useLazyGetLivekitConfigQuery, useLazyGetSMTPConfigQuery, useLazyGetLoginConfigQuery, useGetLoginConfigQuery, useUpdateLoginConfigMutation, useGetSMTPConfigQuery, useUpdateSMTPConfigMutation, - useUpdateAgoraConfigMutation, + useUpdateLivekitConfigMutation, useGetServerQuery, useLazyGetServerQuery, useUpdateServerMutation, @@ -561,27 +482,20 @@ export const { useUpsertLicenseMutation, useCheckLicenseMutation, useGetLicenseQuery, - useGetLicensePaymentUrlMutation, - useLazyGetGeneratedLicenseQuery, useLazyGetBotRelatedChannelsQuery, useSendMessageByBotMutation, useUpdateFrontendUrlMutation, useGetFrontendUrlQuery, - useGetAgoraConfigQuery, - useGetAgoraStatusQuery, - useGetAgoraChannelsQuery, + useGetLivekitConfigQuery, + useGetLivekitStatusQuery, useUpdateSystemCommonMutation, useLazyGetSystemCommonQuery, useGetSystemCommonQuery, - useGenerateAgoraTokenMutation, - useLazyGetAgoraUsersByChannelQuery, + useGenerateLivekitTokenMutation, + useSignalLivekitCallMutation, useLazyClearAllFilesQuery, useLazyClearAllMessagesQuery, useLazyGetFilesQuery, - useGetVocespaceConfigQuery, - useLazyGetVocespaceConfigQuery, - useUpdateVocespaceConfigMutation, - useHealthCheckVocespaceMutation, useGetGroupAnnouncementQuery, useCreateOrUpdateGroupAnnouncementMutation, useDeleteGroupAnnouncementMutation, diff --git a/src/app/slices/server.ts b/src/app/slices/server.ts index 7eff833f..a8c601b4 100644 --- a/src/app/slices/server.ts +++ b/src/app/slices/server.ts @@ -3,7 +3,7 @@ import { LoginConfig, Server } from "@/types/server"; export interface StoredServer extends Server { version: string; - upgraded: boolean; + licensed: boolean; logo: string; inviteLink?: { link: string; @@ -13,7 +13,7 @@ export interface StoredServer extends Server { } const initialState: StoredServer = { version: "", - upgraded: false, + licensed: false, name: "", description: "", logo: "", @@ -45,7 +45,7 @@ const serverSlice = createSlice({ fillServer(state, action: PayloadAction) { const { version, - upgraded, + licensed, inviteLink = { link: "", expire: 0 @@ -67,7 +67,7 @@ const serverSlice = createSlice({ } = action.payload || {}; return { version: state.version || version, - upgraded: state.upgraded || upgraded, + licensed: Boolean(state.licensed || licensed), name: state.name || name, logo: state.logo || logo, description: state.description || description, diff --git a/src/app/slices/voice.ts b/src/app/slices/voice.ts index a0b4bdab..2f317187 100644 --- a/src/app/slices/voice.ts +++ b/src/app/slices/voice.ts @@ -1,5 +1,4 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { ConnectionState } from "agora-rtc-sdk-ng"; import { ChatContext } from "@/types/common"; import { KEY_UID } from "../config"; import { resetAuthData } from "./auth.data"; @@ -13,7 +12,7 @@ export type VoiceBasicInfo = { export type VoicingInfo = { downlinkNetworkQuality?: number; joining?: boolean; - connectionState?: ConnectionState; + connectionState?: string; } & VoiceBasicInfo & VoicingMemberInfo; @@ -168,7 +167,7 @@ const voiceSlice = createSlice({ }; } }, - updateConnectionState(state, { payload }: PayloadAction) { + updateConnectionState(state, { payload }: PayloadAction) { if (state.voicing) { state.voicing.connectionState = payload; } @@ -230,18 +229,18 @@ const voiceSlice = createSlice({ builder.addCase(resetAuthData, (state) => { // reset voicing info if (window.VOICE_CLIENT) { - window.VOICE_CLIENT.leave(); + window.VOICE_CLIENT.disconnect(); Object.entries(window.VOICE_TRACK_MAP).forEach(([uid, track]) => { - if (track && "close" in track) { - track.close(); + if (track && "detach" in track) { + track.detach().forEach((el) => el.remove()); } delete window.VOICE_TRACK_MAP[+uid]; }); Object.entries(window.VIDEO_TRACK_MAP).forEach(([uid, track]) => { - if (track && "close" in track) { - track?.close(); + if (track && "detach" in track) { + track.detach().forEach((el) => el.remove()); } - delete window.VOICE_TRACK_MAP[+uid]; + delete window.VIDEO_TRACK_MAP[+uid]; }); } state.voicing = null; diff --git a/src/components/BlankPlaceholder.tsx b/src/components/BlankPlaceholder.tsx index 48fc74fc..09d734dc 100644 --- a/src/components/BlankPlaceholder.tsx +++ b/src/components/BlankPlaceholder.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useMemo, useRef, useState } from "react"; +import { FC, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { NavLink, useNavigate } from "react-router-dom"; import Linkify from "linkify-react"; @@ -10,7 +10,6 @@ import { BASE_ORIGIN } from "../app/config"; import { compareVersion } from "../utils"; import { useLazyGetPageHtmlQuery, useUploadPageHtmlMutation, useResetPageHtmlMutation, useGeneratePageApiKeyMutation } from "@/app/services/server"; import useServerExtSetting from "../hooks/useServerExtSetting"; -import useLicense from "@/hooks/useLicense"; import ChannelModal from "./ChannelModal"; import InviteModal from "./InviteModal"; import UsersModal from "./UsersModal"; @@ -49,8 +48,7 @@ const BlankPlaceholder: FC = ({ type = "chat" }) => { const navigate = useNavigate(); const useIframe = compareVersion(server.version, IFRAME_MIN_VERSION) >= 0; - const { license: licenseInfo } = useLicense(true); - const showVoceSpace = useMemo(() => compareVersion(server.version, "0.5.6") >= 0, [server.version]); + const showVideoSettings = Boolean(isAdmin); const [inviteModalVisible, setInviteModalVisible] = useState(false); const [createChannelVisible, setCreateChannelVisible] = useState(false); @@ -302,7 +300,6 @@ Inform me of the preview URL, then ask your clarifying questions and wait for my if (!useIframe) { const chatTip = type === "chat" ? t("start_by_channel") : t("start_by_dm"); const chatHandler = type === "chat" ? () => setCreateChannelVisible(true) : () => setUserListVisible(true); - const toLicensePage = () => navigate("/setting/license"); return ( <>
@@ -338,21 +335,11 @@ Inform me of the preview URL, then ask your clarifying questions and wait for my
{t("invite")}
)} - {licenseInfo?.user_limit == 20 && showVoceSpace ? ( - - ) : ( - - )} - {showVoceSpace ? ( + + {showVideoSettings ? ( @@ -363,7 +350,7 @@ Inform me of the preview URL, then ask your clarifying questions and wait for my -
{t("vocespace")}
+
{t("video")}
) : ( diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index d98615c5..a69517a6 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -4,7 +4,7 @@ import { NavLink } from "react-router-dom"; import Tippy from "@tippyjs/react"; import clsx from "clsx"; -import { useGetAgoraStatusQuery } from "@/app/services/server"; +import { useGetLivekitStatusQuery } from "@/app/services/server"; import { useAppSelector } from "@/app/store"; import useUserOperation from "@/hooks/useUserOperation"; import IconCall from "@/assets/icons/call.svg"; @@ -24,7 +24,7 @@ interface Props { const Profile: FC = ({ uid, type = "embed", cid }) => { const [remarkVisible, setRemarkVisible] = useState(false); - const { data: agoraEnabled } = useGetAgoraStatusQuery(); + const { data: livekitEnabled } = useGetLivekitStatusQuery(); const { t } = useTranslation("member"); const { t: chatTrans } = useTranslation("chat"); const { t: ct } = useTranslation(); @@ -91,7 +91,7 @@ const Profile: FC = ({ uid, type = "embed", cid }) => { {t("send_msg")} - {agoraEnabled && type == "embed" && ( + {livekitEnabled && type == "embed" && (
  • {t("call")} @@ -112,7 +112,7 @@ const Profile: FC = ({ uid, type = "embed", cid }) => { title: chatTrans("remark"), handler: setRemarkVisible.bind(null, true), }, - agoraEnabled && + livekitEnabled && type == "card" && { title: t("call"), handler: startCall, diff --git a/src/components/Voice/DMCalling.tsx b/src/components/Voice/DMCalling.tsx index 07a2f740..ee1ff8e2 100644 --- a/src/components/Voice/DMCalling.tsx +++ b/src/components/Voice/DMCalling.tsx @@ -7,11 +7,12 @@ import { Waveform } from "@uiball/loaders"; import clsx from "clsx"; import { motion } from "framer-motion"; +import { useSignalLivekitCallMutation } from "../../app/services/server"; import IconCallOff from "@/assets/icons/call.off.svg"; import IconCallAnswer from "@/assets/icons/call.svg"; import { updateCalling } from "../../app/slices/voice"; import { useAppSelector } from "../../app/store"; -import { playAgoraVideo } from "../../utils"; +import { playLivekitVideo } from "../../utils"; import Avatar from "../Avatar"; import Tooltip from "../Tooltip"; import useVoice from "./useVoice"; @@ -25,38 +26,41 @@ const DMCalling = ({ from, to = 0 }: Props) => { const navigate = useNavigate(); const { pathname } = useLocation(); const dispatch = useDispatch(); - const { leave, joinVoice, joining } = useVoice({ id: to, context: "dm" }); const containerRef = useRef(null); const loginUid = useAppSelector((store) => store.authData.user?.uid, shallowEqual); + const peerUid = loginUid === to ? from : to; + const { leave, joinVoice, joining } = useVoice({ id: peerUid, context: "dm" }); + const [signalLivekitCall] = useSignalLivekitCallMutation(); const calling = useAppSelector((store) => store.voice.calling, shallowEqual); const voicingMembers = useAppSelector((store) => store.voice.voicingMembers, shallowEqual); const fromUser = useAppSelector((store) => store.users.byId[from], shallowEqual); const toUser = useAppSelector((store) => store.users.byId[to], shallowEqual); - const sendByMe = loginUid !== toUser.uid; useEffect(() => { const ids = voicingMembers.ids; ids.forEach((id) => { - playAgoraVideo(id); + playLivekitVideo(id); }); }, [voicingMembers.ids]); const handleCancel = () => { - console.log("cancel"); + signalLivekitCall({ uid: peerUid, action: "cancel" }); if (sendByMe || voicingMembers.ids.length == 2) { leave(); } // 拒绝 dispatch(updateCalling(false)); }; - const handleAnswer = () => { - joinVoice(); - navigate(`/chat/dm/${from}`); + const handleAnswer = async () => { + const joinedLivekit = await joinVoice(); + if (!joinedLivekit) return; + navigate(`/chat/dm/${peerUid}`); }; + if (!fromUser || !toUser) return null; + const sendByMe = loginUid !== toUser.uid; const { name, avatar } = sendByMe ? toUser : fromUser; const connected = voicingMembers.ids.length == 2; - console.log("dm calling", !fromUser, !toUser, connected); const atChatPath = sendByMe ? pathname == `/chat/dm/${to}` : pathname == `/chat/dm/${from}`; - if (!fromUser || !toUser || connected || atChatPath || !calling) return null; + if (connected || atChatPath || !calling) return null; return (
    { - const loginUid = useAppSelector((store) => store.authData.user?.uid ?? 0, shallowEqual); const dispatch = useDispatch(); // const { t } = useTranslation("chat"); const toggleVisible = (evt: MouseEvent) => { @@ -55,46 +53,10 @@ const DeviceList = ({ // evt.stopPropagation(); const { deviceId = "", kind, selected = "" } = evt.currentTarget.dataset; if (selected == deviceId || !kind) return; - switch (kind as MediaDeviceKind) { - case "audiooutput": - case "audioinput": - { - const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; - dispatch(updateSelectDeviceId({ kind: kind as MediaDeviceKind, value: deviceId })); - if (localAudioTrack) { - localAudioTrack - .setDevice(deviceId) - .then(() => { - console.log("audioinput setDevice", deviceId); - }) - .catch((err) => { - console.log("audioinput setDevice error", err); - }); - } - } - - break; - case "videoinput": - { - const localCameraTrack = window.VIDEO_TRACK_MAP[loginUid] as ICameraVideoTrack; - if (localCameraTrack) { - localCameraTrack - .setDevice(deviceId) - .then(() => { - console.log("videoinput setDevice", deviceId); - dispatch(updateSelectDeviceId({ kind: kind as MediaDeviceKind, value: deviceId })); - }) - .catch((err) => { - console.log("videoinput setDevice error", err); - }); - } - } - - break; - - default: - break; - } + window.VOICE_CLIENT?.switchActiveDevice(kind as MediaDeviceKind, deviceId).catch((err) => { + console.log("switch active device error", err); + }); + dispatch(updateSelectDeviceId({ kind: kind as MediaDeviceKind, value: deviceId })); }; const handleBlockClick = (evt: MouseEvent) => { evt.stopPropagation(); @@ -189,7 +151,8 @@ const Operations = ({ id, context, mode = "channel" }: Props) => { } = useVoice({ id, context }); const { t } = useTranslation("chat"); useEffect(() => { - AgoraRTC.getDevices() + navigator.mediaDevices + .enumerateDevices() .then((devices) => { console.log("devices", devices); dispatch( diff --git a/src/components/Voice/index.tsx b/src/components/Voice/index.tsx index bd58ea17..714f88e1 100644 --- a/src/components/Voice/index.tsx +++ b/src/components/Voice/index.tsx @@ -1,208 +1,212 @@ import { memo, useEffect } from "react"; import { shallowEqual, useDispatch } from "react-redux"; -import AgoraRTC from "agora-rtc-sdk-ng"; - import { - useGetAgoraChannelsQuery, - useGetAgoraStatusQuery, - useLazyGetAgoraUsersByChannelQuery -} from "../../app/services/server"; + Participant, + RemoteAudioTrack, + RemoteTrack, + RemoteTrackPublication, + Room, + RoomEvent, + Track, +} from "livekit-client"; + import { addVoiceMember, removeVoiceMember, - updateCallInfo, updateConnectionState, updateVoicingMember, - updateVoicingNetworkQuality + upsertVoiceList, } from "../../app/slices/voice"; import { useAppSelector } from "../../app/store"; -import { isInIframe, playAgoraVideo } from "../../utils"; -import DMCalling from "./DMCalling"; -import useVoice from "./useVoice"; +import { isInIframe, playLivekitVideo } from "../../utils"; -AgoraRTC.setLogLevel(process.env.NODE_ENV === "development" ? 0 : 4); window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {}; window.VIDEO_TRACK_MAP = window.VIDEO_TRACK_MAP ?? {}; + const inIframe = isInIframe(); -// let tmpUids: number[] = []; + +const uidFromParticipant = (participant: Participant) => Number(participant.identity); + +const ensureAudioContainer = () => { + let container = document.getElementById("LIVEKIT_AUDIO_OUTPUTS"); + if (!container) { + container = document.createElement("div"); + container.id = "LIVEKIT_AUDIO_OUTPUTS"; + container.className = "hidden"; + document.body.appendChild(container); + } + return container; +}; + +const detachTrack = (track?: RemoteTrack | null) => { + track?.detach().forEach((el) => el.remove()); +}; + const Voice = () => { const loginUid = useAppSelector((store) => store.authData.user?.uid, shallowEqual); - const from = useAppSelector((store) => store.voice.callingFrom, shallowEqual); - const to = useAppSelector((store) => store.voice.callingTo, shallowEqual); - const voiceList = useAppSelector((store) => store.voice.list, shallowEqual); const voicingInfo = useAppSelector((store) => store.voice.voicing, shallowEqual); - const { data: enabled } = useGetAgoraStatusQuery(); - const [getUsersByChannel] = useLazyGetAgoraUsersByChannelQuery(); - useGetAgoraChannelsQuery( - { page_no: 0, page_size: 100 }, - { - skip: !enabled || !navigator.onLine, - pollingInterval: 10000 - } - ); const dispatch = useDispatch(); + useEffect(() => { - const initializeAgoraClient = async () => { - // 创建agora客户端实例 - const agoraEngine = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" }); - // 无论频道内是否有人说话,都会每两秒返回提示音量 - agoraEngine.enableAudioVolumeIndicator(); - // Listen for the "user-published" event to retrieve an AgoraRTCRemoteUser object. - agoraEngine.on("user-published", async (user, mediaType) => { - // Subscribe to the remote user when the SDK triggers the "user-published" event. - await agoraEngine.subscribe(user, mediaType); - console.log(user, " has published at the channel"); - if (mediaType == "audio" && user.hasAudio) { - // 播放远端音频 - user.audioTrack?.play(); - window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack; - } - if (mediaType == "video" && user.hasVideo) { - // const label = user.videoTrack?.getMediaStreamTrack()?.label; - // console.log("labell", label); + if (inIframe || window.VOICE_CLIENT) return; + window.VOICE_CLIENT = new Room({ + adaptiveStream: true, + dynacast: true, + }); + }, []); - // if (label?.includes("screen")) { - // // 远端用户共享屏幕 - // // const screenTrack = user.videoTrack as ShareScreenTrack; - // // screenTrack.on("track-ended", () => { - // // // 远端用户停止共享屏幕 - // dispatch(updateVoicingMember({ uid: +user.uid, info: { shareScreen: true } })); - // } - dispatch(updateVoicingMember({ uid: +user.uid, info: { video: true } })); - window.VIDEO_TRACK_MAP[+user.uid] = user.videoTrack; - playAgoraVideo(+user.uid); - } - agoraEngine.on("user-unpublished", (user) => { - if (!user.hasAudio) { - // 远端用户取消了音频(muted) - dispatch(updateVoicingMember({ uid: +user.uid, info: { muted: true } })); - } - if (!user.hasVideo) { - // 远端用户取消了视频 - dispatch( - updateVoicingMember({ uid: +user.uid, info: { video: false, shareScreen: false } }) - ); - // 注销本地视频变量 - window.VIDEO_TRACK_MAP[+user.uid] = null; - // 关闭视频后,需要把视频的高度设置回去 - const playerEle = document.querySelector(`#CAMERA_${user.uid}`) as HTMLElement; - playerEle.classList.remove("h-[120px]"); - } - }); - //remote user leave - agoraEngine.on("user-left", (user, reason) => { - console.log(user, "has left the channel", reason); - switch (reason) { - case "Quit": - case "ServerTimeOut": - { - dispatch(removeVoiceMember(+user.uid as number)); - // clear tracks - window.VOICE_TRACK_MAP[+user.uid] = null; - window.VIDEO_TRACK_MAP[+user.uid] = null; - // if (voicingInfo && voicingInfo.context == "dm") { - // // 有人离开,就断开 - // dispatch(updateCallInfo({ from: 0 })); - // } - } - break; - default: - break; - } - }); - // 报告频道内正在说话的远端用户及其音量的回调。 - agoraEngine.on("volume-indicator", (vols) => { - vols.forEach((vol, index) => { - console.log(`${index} UID ${vol.uid} Level ${vol.level}`); - const { uid, level } = vol; - dispatch(updateVoicingMember({ uid: +uid, info: { speakingVolume: level } })); - }); - }); - // 信号强度 - agoraEngine.on("network-quality", (qlt) => { - const { downlinkNetworkQuality } = qlt; - dispatch(updateVoicingNetworkQuality(downlinkNetworkQuality)); - }); - // 连接状态有变化 - agoraEngine.on("connection-state-change", (state, prevState, reason) => { - console.log("connection-state-change", state, prevState, reason); - dispatch(updateConnectionState(state)); - }); - // 用户状态变化 - agoraEngine.on("user-info-updated", (uid, msg) => { - console.log("user-info-updated", uid, msg); - switch (msg) { - case "mute-audio": - // todo - dispatch(updateVoicingMember({ uid: +uid, info: { muted: true } })); - break; - case "unmute-audio": - // todo - dispatch(updateVoicingMember({ uid: +uid, info: { muted: false } })); - break; - case "mute-video": - // todo - dispatch(updateVoicingMember({ uid: +uid, info: { video: false } })); - break; - case "unmute-video": - // todo - dispatch(updateVoicingMember({ uid: +uid, info: { video: true } })); - break; - default: - break; - } - }); - }); - // 有新用户加入 - agoraEngine.on("user-joined", async (user) => { - console.log(user.uid, " has joined the channel", user); - dispatch(addVoiceMember(+user.uid)); - }); - AgoraRTC.onMicrophoneChanged = (info) => { - console.log("onMicrophoneChanged", info); - }; + useEffect(() => { + const room = window.VOICE_CLIENT; + if (!room || inIframe) return; - window.VOICE_CLIENT = agoraEngine; + const updateCurrentVoiceList = () => { + if (!voicingInfo) return; + dispatch( + upsertVoiceList({ + id: voicingInfo.id, + context: voicingInfo.context, + memberCount: room.remoteParticipants.size + 1, + channelName: room.name, + }) + ); }; + + const handleParticipantConnected = (participant: Participant) => { + const uid = uidFromParticipant(participant); + if (Number.isFinite(uid)) { + dispatch(addVoiceMember(uid)); + updateCurrentVoiceList(); + } + }; + + const handleParticipantDisconnected = (participant: Participant) => { + const uid = uidFromParticipant(participant); + if (Number.isFinite(uid)) { + dispatch(removeVoiceMember(uid)); + window.VOICE_TRACK_MAP[uid]?.detach().forEach((el) => el.remove()); + window.VIDEO_TRACK_MAP[uid]?.detach().forEach((el) => el.remove()); + window.VOICE_TRACK_MAP[uid] = null; + window.VIDEO_TRACK_MAP[uid] = null; + updateCurrentVoiceList(); + } + }; + + const handleTrackSubscribed = ( + track: RemoteTrack, + publication: RemoteTrackPublication, + participant: Participant + ) => { + const uid = uidFromParticipant(participant); + if (!Number.isFinite(uid)) return; + dispatch(addVoiceMember(uid)); + + if (track.kind === Track.Kind.Audio) { + const audioTrack = track as RemoteAudioTrack; + window.VOICE_TRACK_MAP[uid] = audioTrack; + audioTrack.detach().forEach((el) => el.remove()); + const audioElement = audioTrack.attach(); + audioElement.autoplay = true; + audioElement.id = `LIVEKIT_AUDIO_${uid}_${publication.trackSid}`; + ensureAudioContainer().appendChild(audioElement); + } + + if (track.kind === Track.Kind.Video) { + window.VIDEO_TRACK_MAP[uid] = track; + const shareScreen = publication.source === Track.Source.ScreenShare; + dispatch( + updateVoicingMember({ + uid, + info: { video: !shareScreen, shareScreen }, + }) + ); + playLivekitVideo(uid, track); + } + }; + + const handleTrackUnsubscribed = ( + track: RemoteTrack, + publication: RemoteTrackPublication, + participant: Participant + ) => { + const uid = uidFromParticipant(participant); + detachTrack(track); + if (!Number.isFinite(uid)) return; + + if (track.kind === Track.Kind.Audio) { + window.VOICE_TRACK_MAP[uid] = null; + } + if (track.kind === Track.Kind.Video) { + window.VIDEO_TRACK_MAP[uid] = null; + dispatch( + updateVoicingMember({ + uid, + info: { + video: false, + shareScreen: publication.source === Track.Source.ScreenShare ? false : undefined, + }, + }) + ); + const playerEle = document.querySelector(`#CAMERA_${uid}`) as HTMLElement | null; + playerEle?.classList.remove("h-[120px]"); + playerEle?.replaceChildren(); + } + }; + + const handleActiveSpeakersChanged = (speakers: Participant[]) => { + const active = new Set(speakers.map((participant) => uidFromParticipant(participant))); + room.remoteParticipants.forEach((participant) => { + const uid = uidFromParticipant(participant); + dispatch( + updateVoicingMember({ + uid, + info: { speakingVolume: active.has(uid) ? 100 : 0 }, + }) + ); + }); + if (loginUid) { + dispatch( + updateVoicingMember({ + uid: loginUid, + info: { speakingVolume: active.has(loginUid) ? 100 : 0 }, + }) + ); + } + }; + + const handleConnectionStateChanged = (state: string) => { + dispatch(updateConnectionState(state)); + }; + + room.on(RoomEvent.ParticipantConnected, handleParticipantConnected); + room.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected); + room.on(RoomEvent.TrackSubscribed, handleTrackSubscribed); + room.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed); + room.on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakersChanged); + room.on(RoomEvent.ConnectionStateChanged, handleConnectionStateChanged); + + room.remoteParticipants.forEach(handleParticipantConnected); + const handlePageUnload = (evt: BeforeUnloadEvent) => { - console.log("unload"); - if (window.VOICE_CLIENT?.connectionState === "CONNECTED") { + if (window.VOICE_CLIENT?.state === "connected") { evt.preventDefault(); return (evt.returnValue = ""); } }; - if (!window.VOICE_CLIENT && !inIframe) { - initializeAgoraClient(); - window.addEventListener("beforeunload", handlePageUnload, { capture: true }); - } + window.addEventListener("beforeunload", handlePageUnload, { capture: true }); return () => { + room.off(RoomEvent.ParticipantConnected, handleParticipantConnected); + room.off(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected); + room.off(RoomEvent.TrackSubscribed, handleTrackSubscribed); + room.off(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed); + room.off(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakersChanged); + room.off(RoomEvent.ConnectionStateChanged, handleConnectionStateChanged); window.removeEventListener("beforeunload", handlePageUnload, { capture: true }); }; - }, [voicingInfo]); + }, [dispatch, loginUid, voicingInfo]); - useEffect(() => { - if (inIframe) return; - // 有人呼叫我 - const callMeList = voiceList.filter((item) => item.context == "dm" && item.id == loginUid); - if (callMeList.length) { - const [firstCall] = callMeList; - const { memberCount, channelName, id } = firstCall; - if (memberCount == 1) { - // 呼叫的人在频道里 - getUsersByChannel(channelName).then((resp) => { - const [uid] = resp.data ?? []; - if (uid && uid != loginUid) { - dispatch(updateCallInfo({ from: uid, to: id })); - } - }); - } - } - }, [voiceList, loginUid, inIframe]); - // return ; - if (from !== 0) return ; return null; }; -export { useVoice }; +export { default as useVoice } from "./useVoice"; export default memo(Voice); diff --git a/src/components/Voice/useVoice.ts b/src/components/Voice/useVoice.ts index ddd56b4e..09c32c24 100644 --- a/src/components/Voice/useVoice.ts +++ b/src/components/Voice/useVoice.ts @@ -1,25 +1,73 @@ +import toast from "react-hot-toast"; import { shallowEqual, useDispatch } from "react-redux"; -import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from "agora-rtc-sdk-ng"; +import type { LocalAudioTrack, LocalVideoTrack, RemoteAudioTrack } from "livekit-client"; +import { Track } from "livekit-client"; import AudioJoin from "@/assets/join.wav"; -import { useGenerateAgoraTokenMutation } from "../../app/services/server"; -import { updateChannelVisibleAside, updateDMVisibleAside } from "../../app/slices/footprint"; import { - addVoiceMember, - updatePin, - updateVoicingInfo, - upsertVoiceList -} from "../../app/slices/voice"; + useGenerateLivekitTokenMutation, + useSignalLivekitCallMutation, +} from "../../app/services/server"; +import { updateChannelVisibleAside, updateDMVisibleAside } from "../../app/slices/footprint"; +import { addVoiceMember, updatePin, updateVoicingInfo, upsertVoiceList } from "../../app/slices/voice"; import { useAppSelector } from "../../app/store"; import { ChatContext } from "../../types/common"; -import { ShareScreenTrack } from "../../types/global"; -import { playAgoraVideo } from "../../utils"; +import { playLivekitVideo } from "../../utils"; type VoiceProps = { id: number; context?: ChatContext; }; + const audioJoin = new Audio(AudioJoin); + +const removeVideoElement = (uid: number) => { + const track = window.VIDEO_TRACK_MAP[uid]; + if (track) { + track.detach().forEach((el) => el.remove()); + } + window.VIDEO_TRACK_MAP[uid] = null; + const playerEle = document.querySelector(`#CAMERA_${uid}`) as HTMLElement | null; + playerEle?.classList.remove("h-[120px]"); + playerEle?.replaceChildren(); +}; + +const setActiveDevice = async (kind: MediaDeviceKind, deviceId: string) => { + if (!deviceId || !window.VOICE_CLIENT) return; + const room = window.VOICE_CLIENT as any; + if (typeof room.switchActiveDevice === "function") { + await room.switchActiveDevice(kind, deviceId); + } +}; + +const getLocalTrackBySource = (source: string): T | undefined => { + const localParticipant = window.VOICE_CLIENT?.localParticipant as any; + if (!localParticipant) return undefined; + const maps = [ + localParticipant.videoTrackPublications, + localParticipant.audioTrackPublications, + localParticipant.trackPublications, + ].filter(Boolean); + + for (const map of maps) { + const publication = Array.from(map.values()).find((pub: any) => pub.source === source) as + | { track?: T } + | undefined; + if (publication?.track) return publication.track; + } + return undefined; +}; + +const updateLocalVoiceList = (id: number, context: ChatContext, roomName: string) => { + const memberCount = (window.VOICE_CLIENT?.remoteParticipants?.size ?? 0) + 1; + return upsertVoiceList({ + id, + context, + memberCount, + channelName: roomName, + }); +}; + const useVoice = ({ id, context = "channel" }: VoiceProps) => { const dispatch = useDispatch(); const loginUid = useAppSelector((store) => store.authData.user?.uid ?? 0, shallowEqual); @@ -55,241 +103,190 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { (store) => store.voice.videoInputDeviceId, shallowEqual ); - const [generateToken] = useGenerateAgoraTokenMutation(); - // const [joining, setJoining] = useState(false); + const [generateToken] = useGenerateLivekitTokenMutation(); + const [signalLivekitCall] = useSignalLivekitCallMutation(); + const joinVoice = async () => { - // setJoining(true); + if (!window.VOICE_CLIENT) { + toast.error("LiveKit client is not ready."); + return false; + } + dispatch( updateVoicingInfo({ id, context, - joining: true + joining: true, }) ); + const resp = await generateToken(context == "channel" ? { gid: id } : { uid: id }); if ("error" in resp) { - console.error("generate agora token error"); - dispatch( - updateVoicingInfo({ - joining: false, - id, - context - }) - ); - } else { - console.table(resp.data); - const { channel_name, app_id, agora_token, uid } = resp.data; - if (window.VOICE_CLIENT) { - await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid); - // Create a local audio track from the microphone audio. - const localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack({ - microphoneId: audioInputDeviceId - }); - // Publish the local audio track in the channel. - await window.VOICE_CLIENT.publish(localAudioTrack); - // play the join audio - try { - await audioJoin.play(); - } catch (error) { - console.warn("play join sound failed!", error); - } - console.log("Publish success!,joined the channel"); - dispatch( - updateVoicingInfo({ - deafen: false, - muted: false, - joining: false, - id, - context - }) - ); - // 把自己加进去 - dispatch(addVoiceMember(uid)); - // 放到全局变量里 + console.error("generate livekit token error", resp.error); + toast.error("LiveKit is not configured or unavailable."); + dispatch(updateVoicingInfo({ joining: false, id, context })); + return false; + } + + try { + const { url, token, room } = resp.data; + if (window.VOICE_CLIENT.state !== "disconnected") { + await window.VOICE_CLIENT.disconnect(); + } + await window.VOICE_CLIENT.connect(url, token, { autoSubscribe: true }); + await setActiveDevice("audioinput", audioInputDeviceId); + await window.VOICE_CLIENT.localParticipant.setMicrophoneEnabled(true); + + const localAudioTrack = getLocalTrackBySource(Track.Source.Microphone); + if (localAudioTrack) { window.VOICE_TRACK_MAP[loginUid] = localAudioTrack; } - } - // setJoining(false); - }; - const openCamera = async () => { - const localVideoTrack = await AgoraRTC.createCameraVideoTrack({ cameraId: videoInputDeviceId }); - // 取消正在进行的桌面共享 - await stopShareScreen(); - await window.VOICE_CLIENT?.publish(localVideoTrack); - dispatch( - updateVoicingInfo({ - video: true, - shareScreen: false, - id, - context - }) - ); - // 放到全局变量里 - window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack; - playAgoraVideo(loginUid); - }; - const closeCamera = async () => { - const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ICameraVideoTrack; - if (localVideoTrack) { - await window.VOICE_CLIENT?.unpublish(localVideoTrack); - localVideoTrack.close(); - window.VIDEO_TRACK_MAP[loginUid] = null; - // 关闭视频后,需要把视频的高度设置回去 - const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement; - playerEle.classList.remove("h-[120px]"); - dispatch( - updateVoicingInfo({ - video: false, - shareScreen: false, - id, - context - }) - ); - } - }; - const stopShareScreen = async () => { - let localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ShareScreenTrack | null; - if (!localVideoTrack) return; - await window.VOICE_CLIENT?.unpublish(localVideoTrack); - if ("close" in localVideoTrack) { - localVideoTrack.close(); - } else { - localVideoTrack[0].close(); - // localVideoTrack[0]=null - } - // localVideoTrack.close(); - window.VIDEO_TRACK_MAP[loginUid] = null; - // 关闭视频后,需要把视频的高度设置回去 - const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement; - playerEle.classList.remove("h-[120px]"); - dispatch( - updateVoicingInfo({ - video: false, - shareScreen: false, - id, - context - }) - ); - }; - const startShareScreen = async () => { - try { - const localVideoTrack = await AgoraRTC.createScreenVideoTrack({ - // electronScreenSourceId: "share_screen", - selfBrowserSurface: "exclude", - // 配置屏幕共享编码参数,详情请查看 API 文档。 - encoderConfig: "1080p_1", - // 设置视频传输优化策略为清晰优先或流畅优先。 - optimizationMode: "detail" - }); - // 取消正在进行的视频 - await closeCamera(); - await window.VOICE_CLIENT?.publish(localVideoTrack); + try { + await audioJoin.play(); + } catch (error) { + console.warn("play join sound failed!", error); + } + dispatch( updateVoicingInfo({ - video: false, - shareScreen: true, + deafen: false, + muted: false, + joining: false, + connectionState: window.VOICE_CLIENT.state, id, - context + context, }) ); - // 放到全局变量里 - window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack; - playAgoraVideo(loginUid); - // 进入全屏并Pin自己 + dispatch(addVoiceMember(loginUid)); + dispatch(updateLocalVoiceList(id, context, room)); + return true; + } catch (error) { + console.error("join livekit room failed", error); + toast.error("Failed to join LiveKit room."); + dispatch(updateVoicingInfo({ joining: false, id, context })); + return false; + } + }; + + const openCamera = async () => { + const room = window.VOICE_CLIENT; + if (!room) return; + try { + await stopShareScreen(); + await setActiveDevice("videoinput", videoInputDeviceId); + await room.localParticipant.setCameraEnabled(true); + const localVideoTrack = getLocalTrackBySource(Track.Source.Camera); + if (localVideoTrack) { + window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack; + playLivekitVideo(loginUid, localVideoTrack); + } + dispatch(updateVoicingInfo({ video: true, shareScreen: false, id, context })); + } catch (error) { + console.error("open camera failed", error); + toast.error("Failed to open camera."); + } + }; + + const closeCamera = async () => { + const room = window.VOICE_CLIENT; + if (!room) return; + const hadCamera = Boolean(getLocalTrackBySource(Track.Source.Camera)); + if (!hadCamera) return; + await room.localParticipant.setCameraEnabled(false); + removeVideoElement(loginUid); + dispatch(updateVoicingInfo({ video: false, shareScreen: false, id, context })); + }; + + const stopShareScreen = async () => { + const room = window.VOICE_CLIENT; + if (!room) return; + const hadScreen = Boolean(getLocalTrackBySource(Track.Source.ScreenShare)); + if (!hadScreen) return; + await room.localParticipant.setScreenShareEnabled(false); + removeVideoElement(loginUid); + dispatch(updateVoicingInfo({ video: false, shareScreen: false, id, context })); + }; + + const startShareScreen = async () => { + const room = window.VOICE_CLIENT; + if (!room) return; + try { + await closeCamera(); + await room.localParticipant.setScreenShareEnabled(true); + const localVideoTrack = getLocalTrackBySource(Track.Source.ScreenShare); + if (localVideoTrack) { + window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack; + playLivekitVideo(loginUid, localVideoTrack); + } + dispatch(updateVoicingInfo({ video: false, shareScreen: true, id, context })); if (context == "channel") { dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" })); } else { dispatch(updateDMVisibleAside({ id, aside: "voice_fullscreen" })); } dispatch(updatePin({ uid: loginUid, action: "pin" })); - // 监听屏幕共享结束事件 - if ("close" in localVideoTrack) { - localVideoTrack.getMediaStreamTrack().onended = () => { - stopShareScreen(); - }; - } } catch (error) { - console.log("start share screen error", error); + console.error("start share screen error", error); + toast.error("Failed to share screen."); } }; const leave = async () => { - const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; - const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ICameraVideoTrack; - if (window.VOICE_CLIENT) { - await window.VOICE_CLIENT.leave(); - - if (localAudioTrack) { - localAudioTrack.close(); - window.VOICE_TRACK_MAP[loginUid] = null; - } - if (localVideoTrack) { - localVideoTrack.close(); - window.VIDEO_TRACK_MAP[loginUid] = null; - } - // reset tracks - window.VOICE_TRACK_MAP = {}; - window.VIDEO_TRACK_MAP = {}; - // 重置voicingInfo - dispatch(updateVoicingInfo(null)); - const updateAside = context == "channel" ? updateChannelVisibleAside : updateDMVisibleAside; - dispatch(updateAside({ id, aside: context == "dm" ? "voice" : null })); - // 即时更新对应的活跃列表信息 - dispatch( - upsertVoiceList({ - id, - context, - memberCount: context == "dm" ? 1 : 0, - // will fix it - channelName: `vocechat:${context}:${id}` - }) - ); - } - }; - const setMute = (mute: boolean) => { - const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; - if (!localAudioTrack) return; - localAudioTrack.setMuted(mute); - if (mute == false && voicingInfo?.deafen) { - // 远端音频,恢复原音 - Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => { - audioTrack?.setVolume(100); - }); - dispatch( - updateVoicingInfo({ - muted: false, - id, - context - }) - ); + const room = window.VOICE_CLIENT; + if (!room) return; + if (context == "dm") { + signalLivekitCall({ uid: id, action: "cancel" }); } + await room.disconnect(); + Object.keys(window.VOICE_TRACK_MAP).forEach((uid) => { + const track = window.VOICE_TRACK_MAP[+uid]; + track?.detach().forEach((el) => el.remove()); + delete window.VOICE_TRACK_MAP[+uid]; + }); + Object.keys(window.VIDEO_TRACK_MAP).forEach((uid) => { + removeVideoElement(+uid); + delete window.VIDEO_TRACK_MAP[+uid]; + }); + dispatch(updateVoicingInfo(null)); + const updateAside = context == "channel" ? updateChannelVisibleAside : updateDMVisibleAside; + dispatch(updateAside({ id, aside: context == "dm" ? "voice" : null })); dispatch( - updateVoicingInfo({ - muted: mute, + upsertVoiceList({ id, - context + context, + memberCount: context == "dm" ? 1 : 0, + channelName: `coldbreeze:${context}:${id}`, }) ); }; - const setDeafen = (deafen: boolean) => { - const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; - if (!localAudioTrack) return; - if (deafen) { - localAudioTrack.setMuted(true); - // 远端音频,全部静音 - Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => { - audioTrack?.setVolume(0); - }); - } else { - localAudioTrack.setMuted(false); - // 远端音频,恢复原音 - Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => { - audioTrack?.setVolume(100); + + const setMute = async (mute: boolean) => { + const room = window.VOICE_CLIENT; + if (!room) return; + await room.localParticipant.setMicrophoneEnabled(!mute); + const localAudioTrack = getLocalTrackBySource(Track.Source.Microphone); + window.VOICE_TRACK_MAP[loginUid] = localAudioTrack ?? null; + if (!mute && voicingInfo?.deafen) { + Object.values(window.VOICE_TRACK_MAP).forEach((audioTrack) => { + (audioTrack as RemoteAudioTrack | undefined)?.setVolume?.(100); }); + dispatch(updateVoicingInfo({ muted: false, deafen: false, id, context })); + return; } + dispatch(updateVoicingInfo({ muted: mute, id, context })); + }; + + const setDeafen = async (deafen: boolean) => { + const room = window.VOICE_CLIENT; + if (!room) return; + await room.localParticipant.setMicrophoneEnabled(!deafen); + Object.values(window.VOICE_TRACK_MAP).forEach((audioTrack) => { + (audioTrack as RemoteAudioTrack | undefined)?.setVolume?.(deafen ? 0 : 100); + }); dispatch(updateVoicingInfo({ deafen, id, context })); }; + const enterFullscreen = (uid?: number) => { if (context == "channel") { dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" })); @@ -300,6 +297,7 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { dispatch(updatePin({ uid, action: "pin" })); } }; + const exitFullscreen = () => { if (context == "channel") { dispatch(updateChannelVisibleAside({ id, aside: "voice" })); @@ -307,14 +305,15 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { dispatch(updateDMVisibleAside({ id, aside: null })); } }; + const joinedAtThisContext = voicingInfo ? voicingInfo.id == id && voicingInfo.context == context : false; + return { setMute, setDeafen, leave, - // canVoice, voicingInfo, joining: voicingInfo ? voicingInfo.joining : undefined, joinedAtThisContext, @@ -332,7 +331,7 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { videoInputDevices, audioInputDeviceId, audioOutputDeviceId, - videoInputDeviceId + videoInputDeviceId, }; }; diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index b18ad222..fd7bb834 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -4,30 +4,22 @@ import { useTranslation } from "react-i18next"; import { isEqual } from "lodash"; import { - useGetAgoraConfigQuery, useGetFirebaseConfigQuery, + useGetLivekitConfigQuery, useGetLoginConfigQuery, useGetSMTPConfigQuery, - useGetVocespaceConfigQuery, - useUpdateAgoraConfigMutation, useUpdateFirebaseConfigMutation, + useUpdateLivekitConfigMutation, useUpdateLoginConfigMutation, useUpdateSMTPConfigMutation, - useUpdateVocespaceConfigMutation, } from "@/app/services/server"; -import { - AgoraConfig, - FirebaseConfig, - LoginConfig, - SMTPConfig, - VocespaceConfig, -} from "@/types/server"; +import { FirebaseConfig, LivekitConfig, LoginConfig, SMTPConfig } from "@/types/server"; -// config: smtp agora login firebase -type ConfigType = "smtp" | "agora" | "login" | "firebase" | "vocespace"; +// config: smtp livekit login firebase +type ConfigType = "smtp" | "livekit" | "login" | "firebase"; type ConfigMap = Record< ConfigType, - AgoraConfig | FirebaseConfig | LoginConfig | SMTPConfig | VocespaceConfig + LivekitConfig | FirebaseConfig | LoginConfig | SMTPConfig >; type valuesOf = T[keyof T]; let originalValue: valuesOf | undefined = undefined; @@ -40,21 +32,13 @@ export default function useConfig(config: keyof ConfigMap = "smtp") { useUpdateLoginConfigMutation(); const [updateSMTPConfig, { isSuccess: SMTPUpdated, isLoading: SMTPUpdating }] = useUpdateSMTPConfigMutation(); - const [updateAgoraConfig, { isSuccess: AgoraUpdated, isLoading: AgoraUpdating }] = - useUpdateAgoraConfigMutation(); - const [updateVocespaceConfig, { isSuccess: VocespaceUpdated, isLoading: VocespaceUpdating }] = - useUpdateVocespaceConfigMutation(); + const [updateLivekitConfig, { isSuccess: LivekitUpdated, isLoading: LivekitUpdating }] = + useUpdateLivekitConfigMutation(); const [updateFirebaseConfig, { isSuccess: FirebaseUpdated, isLoading: FirebaseUpdating }] = useUpdateFirebaseConfigMutation(); - const { refetch: getAgoraConfig, data: agoraConfig } = useGetAgoraConfigQuery(undefined, { - skip: config !== "agora", + const { refetch: getLivekitConfig, data: livekitConfig } = useGetLivekitConfigQuery(undefined, { + skip: config !== "livekit", }); - const { refetch: getVocespaceConfig, data: vocespaceConfig } = useGetVocespaceConfigQuery( - undefined, - { - skip: config !== "vocespace", - } - ); const { refetch: getLoginConfig, data: loginConfig } = useGetLoginConfigQuery(undefined, { skip: config !== "login", }); @@ -71,30 +55,26 @@ export default function useConfig(config: keyof ConfigMap = "smtp") { const updateFns = { login: updateLoginConfig, smtp: updateSMTPConfig, - agora: updateAgoraConfig, + livekit: updateLivekitConfig, firebase: updateFirebaseConfig, - vocespace: updateVocespaceConfig, }; const requests = { smtp: getSMTPConfig, - agora: getAgoraConfig, + livekit: getLivekitConfig, firebase: getFirebaseConfig, login: getLoginConfig, - vocespace: getVocespaceConfig, }; const updates = { login: LoginUpdated, smtp: SMTPUpdated, - agora: AgoraUpdated, + livekit: LivekitUpdated, firebase: FirebaseUpdated, - vocespace: VocespaceUpdated, }; const updatings = { login: LoginUpdating, smtp: SMTPUpdating, - agora: AgoraUpdating, + livekit: LivekitUpdating, firebase: FirebaseUpdating, - vocespace: VocespaceUpdating, }; const updateConfig = updateFns[config]; const refetch = requests[config]; @@ -121,12 +101,12 @@ export default function useConfig(config: keyof ConfigMap = "smtp") { } }, [updated]); useEffect(() => { - const _config = smtpConfig || firebaseConfig || loginConfig || agoraConfig || vocespaceConfig; + const _config = smtpConfig || firebaseConfig || loginConfig || livekitConfig; if (_config) { originalValue = _config; setValues(_config); } - }, [smtpConfig, firebaseConfig, loginConfig, agoraConfig, vocespaceConfig]); + }, [smtpConfig, firebaseConfig, loginConfig, livekitConfig]); useEffect(() => { // 空对象 if (!values || Object.keys(values).length == 0) return; @@ -144,7 +124,7 @@ export default function useConfig(config: keyof ConfigMap = "smtp") { reset, changed, updateConfig, - agoraConfig, + livekitConfig, values, setValues, toggleEnable, diff --git a/src/hooks/useLicense.ts b/src/hooks/useLicense.ts index e2da9d59..2e9d6637 100644 --- a/src/hooks/useLicense.ts +++ b/src/hooks/useLicense.ts @@ -15,7 +15,7 @@ import { updateInfo } from "@/app/slices/server"; const useLicense = (refetchOnMountOrArgChange = false) => { const dispatch = useDispatch(); const userCount = useAppSelector((store) => store.users.ids.length, shallowEqual); - const upgraded = useAppSelector((store) => store.server.upgraded, shallowEqual); + const licensed = useAppSelector((store) => store.server.licensed, shallowEqual); const isGuest = useAppSelector((store) => store.authData.guest, shallowEqual); const { data: license, @@ -49,12 +49,12 @@ const useLicense = (refetchOnMountOrArgChange = false) => { const lUserLimit = license?.user_limit ?? Number.MAX_SAFE_INTEGER; useEffect(() => { if (lUserLimit > 20) { - dispatch(updateInfo({ upgraded: true })); + dispatch(updateInfo({ licensed: true })); } }, [lUserLimit]); return { - upgraded, + licensed, reachLimit: userCount >= lUserLimit, license, checked, diff --git a/src/hooks/useStreaming/index.ts b/src/hooks/useStreaming/index.ts index a104007b..de84b103 100644 --- a/src/hooks/useStreaming/index.ts +++ b/src/hooks/useStreaming/index.ts @@ -18,6 +18,7 @@ import { removePinChats, resetFootprint, updateAutoDeleteSetting, + updateDMVisibleAside, updateMute, updateReadChannels, updateReadUsers, @@ -37,7 +38,7 @@ import { removeUserSession, resetUserMsg } from "@/app/slices/message.user"; import { updateInfo } from "@/app/slices/server"; import { setReady, updateSSEStatus } from "@/app/slices/ui"; import { updateContactStatus, updateUsersByLogs, updateUsersStatus } from "@/app/slices/users"; -import { updateCallInfo } from "@/app/slices/voice"; +import { updateCallInfo, updateVoicingInfo } from "@/app/slices/voice"; import { useAppDispatch, useAppSelector } from "@/app/store"; import { ServerEvent, @@ -69,6 +70,9 @@ export default function useStreaming() { const guest = useAppSelector((store) => store.authData.guest, shallowEqual); const readUsers = useAppSelector((store) => store.footprint.readUsers, shallowEqual); const readChannels = useAppSelector((store) => store.footprint.readChannels, shallowEqual); + const voicing = useAppSelector((store) => store.voice.voicing, shallowEqual); + const callingFrom = useAppSelector((store) => store.voice.callingFrom, shallowEqual); + const callingTo = useAppSelector((store) => store.voice.callingTo, shallowEqual); const dispatch = useAppDispatch(); const loginUid = user?.uid || 0; @@ -78,6 +82,9 @@ export default function useStreaming() { const readChannelsRef = useRef(readChannels); const userRef = useRef(user); const guestRef = useRef(guest); + const voicingRef = useRef(voicing); + const callingFromRef = useRef(callingFrom); + const callingToRef = useRef(callingTo); // Update refs whenever values change useEffect(() => { @@ -100,6 +107,18 @@ export default function useStreaming() { guestRef.current = guest; }, [guest]); + useEffect(() => { + voicingRef.current = voicing; + }, [voicing]); + + useEffect(() => { + callingFromRef.current = callingFrom; + }, [callingFrom]); + + useEffect(() => { + callingToRef.current = callingTo; + }, [callingTo]); + const keepAlive = (timeout?: number) => { console.info("debug SSE: start new keepalive"); clearTimeout(aliveInter); @@ -243,6 +262,35 @@ export default function useStreaming() { dispatch(updateCallInfo({ from: uid, to: target, calling: true })); break; } + case "rtc_call": { + const { from_uid, to_uid, action } = data; + const peerUid = from_uid === loginUidRef.current ? to_uid : from_uid; + if (action === "invite") { + if (from_uid !== loginUidRef.current) { + dispatch(updateCallInfo({ from: from_uid, to: to_uid, calling: true })); + } + break; + } + + const isCurrentCall = + [callingFromRef.current, callingToRef.current].includes(from_uid) && + [callingFromRef.current, callingToRef.current].includes(to_uid); + if (isCurrentCall) { + dispatch(updateCallInfo({ from: 0, to: 0, calling: false })); + } + if (voicingRef.current?.context === "dm" && voicingRef.current.id === peerUid) { + window.VOICE_CLIENT?.disconnect(); + Object.values(window.VOICE_TRACK_MAP ?? {}).forEach((track) => { + track?.detach().forEach((el) => el.remove()); + }); + Object.values(window.VIDEO_TRACK_MAP ?? {}).forEach((track) => { + track?.detach().forEach((el) => el.remove()); + }); + dispatch(updateVoicingInfo(null)); + dispatch(updateDMVisibleAside({ id: peerUid, aside: null })); + } + break; + } case "users_snapshot": { const { version } = data; diff --git a/src/hooks/useUserOperation.ts b/src/hooks/useUserOperation.ts index 921fe44b..94d82314 100644 --- a/src/hooks/useUserOperation.ts +++ b/src/hooks/useUserOperation.ts @@ -6,6 +6,10 @@ import { shallowEqual, useDispatch } from "react-redux"; import { useMatch, useNavigate } from "react-router-dom"; import { hideAll } from "tippy.js"; +import { + useGetLivekitStatusQuery, + useSignalLivekitCallMutation, +} from "@/app/services/server"; import { useRemoveMembersMutation } from "@/app/services/channel"; import { useLazyDeleteUserQuery, @@ -34,6 +38,8 @@ const useUserOperation = ({ uid, cid }: IProps) => { const [passedUid, setPassedUid] = useState(undefined); const isUserDetailPath = useMatch(`/users/${uid}`); const [updateContactStatus] = useUpdateContactStatusMutation(); + const { data: livekitEnabled } = useGetLivekitStatusQuery(); + const [signalLivekitCall] = useSignalLivekitCallMutation(); const [updateUser, { isSuccess: updateUserSuccess }] = useUpdateUserMutation(); const [removeUser, { isSuccess: removeUserSuccess }] = useLazyDeleteUserQuery(); const [removeInChannel, { isSuccess: removeSuccess }] = useRemoveMembersMutation(); @@ -48,7 +54,7 @@ const useUserOperation = ({ uid, cid }: IProps) => { shallowEqual ); const loginUser = useAppSelector((store) => store.authData.user, shallowEqual); - const isPro = useAppSelector((store) => store.server.upgraded, shallowEqual); + const isLicensed = useAppSelector((store) => store.server.licensed, shallowEqual); const { show_email = true, dm_to_member = true } = channel ?? {}; useEffect(() => { setPassedUid(uid ?? loginUser?.uid); @@ -93,12 +99,21 @@ const useUserOperation = ({ uid, cid }: IProps) => { copy(finalEmail || ""); hideAll(); }; - const startCall = () => { + const startCall = async () => { + if (!livekitEnabled) { + if (loginUser?.is_admin) { + navigateTo("/setting/video"); + } else { + toast.error("LiveKit is not configured. Contact the admin."); + } + return; + } if (joining || joined) { alert("You have joined another channel, please leave first!"); return; } - joinVoice(); + const joinedLivekit = await joinVoice(); + if (!joinedLivekit || !uid) return; const data = { id: uid ?? 0, aside: "voice" as const @@ -107,6 +122,7 @@ const useUserOperation = ({ uid, cid }: IProps) => { // 实时显示 calling box if (!joinedAtThisContext) { dispatch(updateCallInfo({ from: loginUser?.uid ?? 0, to: uid, calling: false })); + signalLivekitCall({ uid, action: "invite" }); } navigateTo(`/chat/dm/${uid}`); }; @@ -149,7 +165,7 @@ const useUserOperation = ({ uid, cid }: IProps) => { const canBlock: boolean = loginUid != uid; const canRemoveFromContact: boolean = loginUid != uid; const canInviteChannel = !!cid && (loginUser?.is_admin || channel?.owner == loginUser?.uid); - const canViewPassword: boolean = !!loginUser?.is_admin && isPro; + const canViewPassword: boolean = !!loginUser?.is_admin && isLicensed; const canUpdatePassword: boolean = !!loginUser?.is_admin && loginUid != uid && uid !== 1; return { showEmailInChannel: show_email, diff --git a/src/routes/callback/PaymentSuccess.tsx b/src/routes/callback/PaymentSuccess.tsx deleted file mode 100644 index 945f1973..00000000 --- a/src/routes/callback/PaymentSuccess.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; - -import { useLazyGetGeneratedLicenseQuery } from "@/app/services/server"; -import Button from "@/components/styled/Button"; -import useLicense from "@/hooks/useLicense"; - -type Props = { - sid: string; -}; - -const PaymentSuccess = ({ sid }: Props) => { - const { t } = useTranslation("setting", { keyPrefix: "license" }); - const navigateTo = useNavigate(); - const { upsertLicense, upserting, upserted } = useLicense(); - const [getGeneratedLicense, { data, isError, isLoading, isSuccess }] = - useLazyGetGeneratedLicenseQuery(); - useEffect(() => { - if (sid) { - getGeneratedLicense(sid); - } - }, [sid]); - useEffect(() => { - if (isSuccess && data) { - const l = data.license; - upsertLicense(l); - } - }, [data, isSuccess]); - const handleBack = () => { - navigateTo("/"); - }; - return ( -
    - check icon -

    {t("payment_success")}

    -

    - {upserting ? t("tip_renewing") : ""} - {upserted ? t("tip_renewed") : ""} - {isError ? t("tip_renew_error") : ""} -

    - -
    - ); -}; - -export default PaymentSuccess; diff --git a/src/routes/callback/index.tsx b/src/routes/callback/index.tsx index f2119b3d..bc332d69 100644 --- a/src/routes/callback/index.tsx +++ b/src/routes/callback/index.tsx @@ -2,7 +2,6 @@ import { DOMAttributes, ReactNode } from "react"; import { useParams } from "react-router-dom"; import GithubCallback, { GithubLoginSource } from "./GithubCallback"; -import PaymentSuccess from "./PaymentSuccess"; const StyledWrapper = ({ children }: DOMAttributes & { children?: ReactNode }) => { return ( @@ -11,21 +10,9 @@ const StyledWrapper = ({ children }: DOMAttributes & { children?
    ); }; -// type Props = { -// type: "payment_success"; -// }; -// 该页面服务于一些第三方服务的回调,比如 stripe 付款成功的回调,GitHub 登录成功的回调 +// This page handles third-party callbacks such as GitHub login. export default function CallbackPage() { const { type = "", payload = "" } = useParams(); - // stripe 付款成功 - if (type == "payment_success") { - return ( - - - - ); - } - // github 授权成功 if (type == "github") { const query = new URLSearchParams(location.search); const code = query.get("code") ?? ""; diff --git a/src/routes/chat/Layout/DMVoicing.tsx b/src/routes/chat/Layout/DMVoicing.tsx index adc3587f..d345cf30 100644 --- a/src/routes/chat/Layout/DMVoicing.tsx +++ b/src/routes/chat/Layout/DMVoicing.tsx @@ -2,13 +2,14 @@ import { useEffect } from "react"; import { shallowEqual, useDispatch } from "react-redux"; import clsx from "clsx"; +import { useSignalLivekitCallMutation } from "@/app/services/server"; import { updateCallInfo } from "@/app/slices/voice"; import { useAppSelector } from "@/app/store"; import Avatar from "@/components/Avatar"; import Tooltip from "@/components/Tooltip"; import { useVoice } from "@/components/Voice"; import Operations from "@/components/Voice/Operations"; -import { playAgoraVideo } from "@/utils"; +import { playLivekitVideo } from "@/utils"; import IconCallOff from "@/assets/icons/call.off.svg"; import IconCallAnswer from "@/assets/icons/call.svg"; import IconMicOff from "@/assets/icons/mic.off.svg"; @@ -120,21 +121,28 @@ const DMVoice = ({ uid }: Props) => { (store) => store.voice, shallowEqual ); - const { leave, joinVoice, joining } = useVoice({ id: callingTo, context: "dm" }); + const peerUid = loginUid === callingTo ? callingFrom : callingTo; + const { leave, joinVoice, joining } = useVoice({ id: peerUid, context: "dm" }); + const [signalLivekitCall] = useSignalLivekitCallMutation(); useEffect(() => { const ids = voicingMembers.ids; ids.forEach((id) => { - playAgoraVideo(id); + playLivekitVideo(id); }); }, [voicingMembers.ids]); if (![callingFrom, callingTo].includes(uid)) return null; - const { name: fromUsername, avatar: fromAvatar } = userData[callingFrom]; - const { name: toUsername, avatar: toAvatar, uid: toUid } = userData[callingTo]; + const fromUser = userData[callingFrom]; + const toUser = userData[callingTo]; + if (!fromUser || !toUser) return null; + + const { name: fromUsername, avatar: fromAvatar } = fromUser; + const { name: toUsername, avatar: toAvatar, uid: toUid } = toUser; const sendByMe = loginUid !== toUid; const onlyToSelf = voicingMembers.ids.length == 1 && voicingMembers.ids[0] == callingTo; const handleCancel = () => { - console.log("cancel"); + const peerUid = sendByMe ? callingTo : callingFrom; + signalLivekitCall({ uid: peerUid, action: "cancel" }); if (sendByMe || voicingMembers.ids.length == 2 || onlyToSelf) { leave(); } @@ -143,8 +151,8 @@ const DMVoice = ({ uid }: Props) => { dispatch(updateCallInfo({ from: 0, to: 0 })); } }; - const handleAnswer = () => { - joinVoice(); + const handleAnswer = async () => { + await joinVoice(); }; const connected = voicingMembers.ids.length == 2; // const { muted, shareScreen, video } = voicingInfo ?? {} as VoicingInfo; @@ -211,7 +219,7 @@ const DMVoice = ({ uid }: Props) => { )} {connected && ( <> - + )}
  • diff --git a/src/routes/chat/Layout/LicenseOutdatedTip.tsx b/src/routes/chat/Layout/LicenseOutdatedTip.tsx index 3e00fc61..ac43c61d 100644 --- a/src/routes/chat/Layout/LicenseOutdatedTip.tsx +++ b/src/routes/chat/Layout/LicenseOutdatedTip.tsx @@ -1,27 +1,16 @@ -// import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import Button from "@/components/styled/Button"; - -// type Props = {}; - -const LicenseUpgradeTip = () => { +const LicenseOutdatedTip = () => { const { t } = useTranslation("chat"); - const navigateTo = useNavigate(); - const handleRedirect = () => { - navigateTo("/setting/license"); - }; return ( -
    +
    🚨 {t("license_tip")} -
    ); }; -export default LicenseUpgradeTip; +export default LicenseOutdatedTip; diff --git a/src/routes/chat/Layout/index.tsx b/src/routes/chat/Layout/index.tsx index ce21d250..1074c7fa 100644 --- a/src/routes/chat/Layout/index.tsx +++ b/src/routes/chat/Layout/index.tsx @@ -14,7 +14,7 @@ import IconWarning from "@/assets/icons/warning.svg"; import AddContactTip from "./AddContactTip"; import DMVoice from "./DMVoicing"; import DnDTip from "./DnDTip"; -import LicenseUpgradeTip from "./LicenseOutdatedTip"; +import LicenseOutdatedTip from "./LicenseOutdatedTip"; import LoginTip from "./LoginTip"; import Operations from "./Operations"; import VirtualMessageFeed, { VirtualMessageFeedHandle } from "./VirtualMessageFeed"; @@ -109,7 +109,7 @@ const Layout: FC = ({ {readonly ? ( ) : reachLimit ? ( - + ) : (
    diff --git a/src/routes/chat/RTCWidget.tsx b/src/routes/chat/RTCWidget.tsx index 8b350264..d1bfaba0 100644 --- a/src/routes/chat/RTCWidget.tsx +++ b/src/routes/chat/RTCWidget.tsx @@ -39,11 +39,11 @@ const RTCWidget = ({ id, context = "channel" }: Props) => { const callFrom = useAppSelector((store) => store.voice.callingFrom, shallowEqual); const callTo = useAppSelector((store) => store.voice.callingTo, shallowEqual); const channelData = useAppSelector((store) => store.channels.byId, shallowEqual); - const userData = useAppSelector((store) => store.channels.byId, shallowEqual); + const userData = useAppSelector((store) => store.users.byId, shallowEqual); if (!loginUser || !voicingInfo || joining) return null; // const name = voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name; // if (!name) return null; - const isReConnecting = voicingInfo.connectionState == "RECONNECTING"; + const isReConnecting = voicingInfo.connectionState == "reconnecting"; const targetDMUsername = callFrom == loginUser.uid ? userData[callTo]?.name : userData[callFrom]?.name; return ( diff --git a/src/routes/chat/VoiceChat/VoiceManagement.tsx b/src/routes/chat/VoiceChat/VoiceManagement.tsx index c897e178..ddc86511 100644 --- a/src/routes/chat/VoiceChat/VoiceManagement.tsx +++ b/src/routes/chat/VoiceChat/VoiceManagement.tsx @@ -4,7 +4,7 @@ import clsx from "clsx"; import { updateChannelVisibleAside } from "@/app/slices/footprint"; import Operations from "@/components/Voice/Operations"; -import { playAgoraVideo } from "@/utils"; +import { playLivekitVideo } from "@/utils"; import IconFullscreen from "@/assets/icons/fullscreen.svg"; import IconMicOff from "@/assets/icons/mic.off.svg"; import IconMic from "@/assets/icons/mic.on.svg"; @@ -26,7 +26,7 @@ const VoiceManagement = ({ id, context, info }: Props) => { useEffect(() => { const ids = voicingMembers.ids; ids.forEach((id) => { - playAgoraVideo(id); + playLivekitVideo(id); }); }, [voicingMembers.ids]); const handleFullscreen = (uid?: number) => { @@ -50,7 +50,7 @@ const VoiceManagement = ({ id, context, info }: Props) => {
    ); } - if (info.connectionState == "RECONNECTING") { + if (info.connectionState == "reconnecting") { return (
    Reconnecting... diff --git a/src/routes/chat/VoiceChat/index.tsx b/src/routes/chat/VoiceChat/index.tsx index 26239b81..3d7194fa 100644 --- a/src/routes/chat/VoiceChat/index.tsx +++ b/src/routes/chat/VoiceChat/index.tsx @@ -2,19 +2,16 @@ import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { shallowEqual, useDispatch } from "react-redux"; -import { useGetAgoraStatusQuery, useGetVocespaceConfigQuery } from "@/app/services/server"; +import { useGetLivekitStatusQuery, useSignalLivekitCallMutation } from "@/app/services/server"; import { updateChannelVisibleAside, updateDMVisibleAside } from "@/app/slices/footprint"; import { updateCallInfo } from "@/app/slices/voice"; import { useAppSelector } from "@/app/store"; import { ChatContext } from "@/types/common"; import Tooltip from "@/components/Tooltip"; import { useVoice } from "@/components/Voice"; -import { compareVersion, isInIframe } from "@/utils"; +import { isInIframe } from "@/utils"; import IconHeadphone from "@/assets/icons/headphone.svg"; -import { useEffect, useMemo, useState } from "react"; -import useSendMessage from "@/hooks/useSendMessage"; import { useNavigate } from "react-router-dom"; -import { VocespaceConfig } from "@/types/server"; type Props = { context?: ChatContext; @@ -23,42 +20,19 @@ type Props = { const isIframe = isInIframe(); const VoiceChat = ({ id, context = "channel" }: Props) => { - const [chatType, setChatType] = useState<"agora" | "vocespace">("vocespace"); - const currentVersion = useAppSelector((store) => store.server.version, shallowEqual); - const showVoceSpace = useMemo(() => { - return compareVersion(currentVersion, "0.5.8") >= 0; - }, [currentVersion]); - const { joinVoice, joined, joining = false, joinedAtThisContext } = useVoice({ id, context }); + const [signalLivekitCall] = useSignalLivekitCallMutation(); const dispatch = useDispatch(); const loginUid = useAppSelector((store) => store.authData.user?.uid ?? 0, shallowEqual); const isAdmin = useAppSelector((store) => store.authData.user?.is_admin ?? false, shallowEqual); const visibleAside = useAppSelector( - (store) => (context == "channel" ? "voice" : null), + (store) => (context == "channel" ? store.footprint.channelAsides[id] : store.footprint.dmAsides[id]), shallowEqual ); const voiceList = useAppSelector((store) => store.voice.list, shallowEqual); - const { data: enabled } = useGetAgoraStatusQuery(); - const [vocespaceConfig, setVoceSpaceConfig] = useState(undefined); + const { data: enabled } = useGetLivekitStatusQuery(); const { t } = useTranslation("chat"); - const { data: conf } = showVoceSpace ? useGetVocespaceConfigQuery() : { data: undefined }; - - useEffect(() => { - if (!showVoceSpace) { - setChatType("agora"); - } else { - if (conf && conf.enabled) { - setVoceSpaceConfig(conf); - setChatType("vocespace"); - } else if (conf && !conf.enabled) { - setVoceSpaceConfig(conf); - setChatType("agora"); - } else { - setChatType("agora"); - } - } - }, [showVoceSpace, enabled, conf]); const toggleDashboard = () => { const data = { @@ -67,12 +41,13 @@ const VoiceChat = ({ id, context = "channel" }: Props) => { }; dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data)); }; - const handleJoin = () => { + const handleJoin = async () => { if (joining || joined) { alert("You have joined another channel, please leave first!"); return; } - joinVoice(); + const joinedLivekit = await joinVoice(); + if (!joinedLivekit) return; const data = { id, aside: "voice" as const, @@ -81,6 +56,7 @@ const VoiceChat = ({ id, context = "channel" }: Props) => { // 实时显示calling box if (!joinedAtThisContext && context == "dm") { dispatch(updateCallInfo({ from: loginUid, to: id, calling: false })); + signalLivekitCall({ uid: id, action: "invite" }); } }; const handleInIframe = () => { @@ -91,75 +67,18 @@ const VoiceChat = ({ id, context = "channel" }: Props) => { const visible = visibleAside == "voice"; const memberCount = voiceList.find((v) => v.context == context && v.id == id)?.memberCount ?? 0; const badgeClass = `absolute -top-2 -right-2 w-4 h-4 rounded-full bg-primary-400 text-white `; - const { sendMessage } = useSendMessage({ context, from: loginUid, to: id }); - const replying_mid = useAppSelector( - (store) => store.message.replying[`${context}_${id}`], - shallowEqual - ); const navigate = useNavigate(); - - const sendVoceSpaceMsg = async (url: string) => { - await sendMessage({ - reply_mid: replying_mid, - type: "text", - content: `Join Vocespace Meeting: ${url}`, - from_uid: loginUid, - }); - }; - - const handleSendVocespaceRequest = async () => { - const genUUID = () => - "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => - (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16) - ); - if (vocespaceConfig && vocespaceConfig.state && vocespaceConfig.state === "undeployed") { - await sendVoceSpaceMsg( - `https://vocespace.com/${context}_${id}_${encodeURIComponent(genUUID())}` - ); - } else if (vocespaceConfig && vocespaceConfig?.enabled && vocespaceConfig.state === "success") { - // 优先使用 additional_domains 中健康检查通过的域名 - let domain = vocespaceConfig.url; - if (vocespaceConfig.additional_domains && vocespaceConfig.additional_domains instanceof Map) { - const healthyDomain = Array.from(vocespaceConfig.additional_domains.entries()).find( - ([_, isHealthy]) => isHealthy - ); - if (healthyDomain) { - domain = healthyDomain[0]; - } - } - - let url = domain; - if (url.includes(":7880")) { - url = `https://${url.replace(":7880", ":3008")}/${context}_${id}`; - } else { - url = `https://${url}/${context}_${id}`; - } - await sendVoceSpaceMsg(url); - } else { - if (!showVoceSpace) { - await sendVoceSpaceMsg( - `https://vocespace.com/${context}_${id}_${encodeURIComponent(genUUID())}` - ); - } else { - if (isAdmin) { - // 跳转到/setting/video - navigate("/setting/video"); - } else { - await sendVoceSpaceMsg( - `https://vocespace.com/${context}_${id}_${encodeURIComponent(genUUID())}` - ); - } - } - } - }; const handleOnClick = async () => { - if (chatType === "agora" && enabled) { - return isIframe ? handleInIframe() : joinedAtThisContext ? toggleDashboard() : handleJoin(); - } else { - // 向当前频道中发送Vocespace链接 - await handleSendVocespaceRequest(); + if (!enabled) { + if (isAdmin) { + navigate("/setting/video"); + } else { + toast.error("LiveKit is not configured. Contact the admin."); + } + return; } + return isIframe ? handleInIframe() : joinedAtThisContext ? toggleDashboard() : handleJoin(); }; return ( diff --git a/src/routes/chat/VoiceFullscreen.tsx b/src/routes/chat/VoiceFullscreen.tsx index f4f766a1..5cb903ac 100644 --- a/src/routes/chat/VoiceFullscreen.tsx +++ b/src/routes/chat/VoiceFullscreen.tsx @@ -11,7 +11,7 @@ import { useAppSelector } from "../../app/store"; import Avatar from "../../components/Avatar"; import Tooltip from "../../components/Tooltip"; import { ChatContext } from "../../types/common"; -import { playAgoraVideo } from "../../utils"; +import { playLivekitVideo } from "../../utils"; type Props = { context: ChatContext; @@ -30,7 +30,7 @@ const VoiceFullscreen = ({ id, context }: Props) => { useEffect(() => { const ids = voicingMembers.ids; ids.forEach((id) => { - playAgoraVideo(id); + playLivekitVideo(id); const { speakingVolume = 0 } = voicingMembers.byId[id]; const speaking = speakingVolume > 50; if (speaking) { diff --git a/src/routes/chat/index.tsx b/src/routes/chat/index.tsx index 40c81768..328cbb0f 100644 --- a/src/routes/chat/index.tsx +++ b/src/routes/chat/index.tsx @@ -8,6 +8,7 @@ import ChannelModal from "@/components/ChannelModal"; import ErrorCatcher from "@/components/ErrorCatcher"; import Server from "@/components/Server"; import UsersModal from "@/components/UsersModal"; +import DMCalling from "@/components/Voice/DMCalling"; import useVirtualKeyboard from "@/hooks/useVirtualKeyboard"; import { isIOS } from "@/utils"; import ChannelChat from "./ChannelChat"; @@ -29,6 +30,7 @@ function ChatPage() { const [usersModalVisible, setUsersModalVisible] = useState(false); const { channel_id = 0, user_id = 0 } = useParams(); const callingTo = useAppSelector((store) => store.voice.callingTo, shallowEqual); + const callingFrom = useAppSelector((store) => store.voice.callingFrom, shallowEqual); const aside = useAppSelector( (store) => channel_id @@ -76,6 +78,7 @@ function ChatPage() { )} {usersModalVisible && } +
    void; - // domain: string; -} - -const getExpireDay = (sub_dur: PriceSubscriptionDuration) => { - const currDate = dayjs(); - // 默认10年 - let res = currDate; - switch (sub_dur) { - case "year": - res = currDate.add(100, "year"); - break; - case "month": - res = currDate.add(1, "month"); - break; - case "quarter": - res = currDate.add(3, "month"); - break; - default: - break; - } - return res.format("YYYY-MM-DD"); -}; -const LicensePriceList = getLicensePriceList(); -const LicensePriceListModal: FC = ({ closeModal }) => { - const { t } = useTranslation("setting"); - const { t: ct } = useTranslation(); - const [getUrl, { isLoading, isSuccess }] = useGetLicensePaymentUrlMutation(); - const [host, setHost] = useState(location.hostname); - const [popUpVisible, setPopUpVisible] = useState(false); - const [selectPrice, setSelectPrice] = useState( - `${LicensePriceList[0].pid}|${LicensePriceList[0].limit}|${LicensePriceList[0].type}|${ - LicensePriceList[0].sub_dur || "" - }` - ); - const handleRenew = async () => { - const hostPrefixed = `https://${host}`; - if (!linkify.test(hostPrefixed)) { - toast.error("Invalid Host"); - return; - } - if (new URL(hostPrefixed).port !== "" || host.endsWith(":443")) { - toast.error(t("license.tip_port")); - return; - } - const [priceId, user_limit, type, sub_dur = "month"] = selectPrice.split("|") as [ - string, - string, - PriceType, - PriceSubscriptionDuration - ]; - const metadata = { - user_limit: Number(user_limit), - expire: type == "subscription" ? getExpireDay(sub_dur) : getExpireDay("year"), - // 本地,则*通配符 - domain: host.startsWith("localhost") ? "*" : host, - }; - console.log(metadata); - // return; - - const resp = await getUrl({ - type, - priceId, - metadata, - cancel_url: location.href, - success_url: `${location.origin}/#/cb/payment_success`, - }); - if ("error" in resp) { - toast.error("Payment link initialized failed!"); - return; - } - console.log("aaaa", resp.data); - // todo - location.href = resp.data.session_url; - }; - const handlePriceSelect = (price: string) => { - console.log(price); - setSelectPrice(price); - }; - const handleUpdateHost = (evt: ChangeEvent) => { - setHost(evt.target.value); - }; - const togglePopUpVisible = () => { - setPopUpVisible((prev) => !prev); - }; - const handleTalk = () => { - window.open("https://buy.stripe.com/bJeaEX9ex2PUer2aLe6c00O", "_blank"); - }; - const isBooking = selectPrice.includes("booking"); - - return ( - - - {t("vocespace.prerequisite.4")} -
    - } - // className="!min-w-[480px]" - title={t("license.renew")} - description={t("license.renew_select")} - buttons={ - <> - - {isBooking ? ( - - ) : ( - -
    {t("license.tip_domain")}
    - -
    - - {" "} - {t("license.tip_port")} - -
    - - -
    -
    -
    - } - > - - - )} - - } - > - - `${title} ${desc ? `[${desc}]` : ""}${price ? `[${price}]` : ""}` - )} - values={LicensePriceList.map( - ({ pid, limit, type = "payment", sub_dur = "month" }) => - `${pid}|${limit}|${type}|${sub_dur}` - )} - value={selectPrice} - onChange={(v) => { - handlePriceSelect(v); - }} - /> - - - ); -}; - -export default LicensePriceListModal; diff --git a/src/routes/setting/License/UpdateLicenseModal.tsx b/src/routes/setting/License/UpdateLicenseModal.tsx index 3c184861..069c40a7 100644 --- a/src/routes/setting/License/UpdateLicenseModal.tsx +++ b/src/routes/setting/License/UpdateLicenseModal.tsx @@ -20,7 +20,7 @@ const UpdateLicenseModal: FC = ({ closeModal, updateLicense, updating, up const { t } = useTranslation("setting"); const { t: ct } = useTranslation(); - const handleRenew = async () => { + const handleUpdate = async () => { const updateSuccess = await updateLicense(value); if (typeof updateSuccess == "boolean" && !updateSuccess) { toast.error("Check License Invalid!"); @@ -44,7 +44,7 @@ const UpdateLicenseModal: FC = ({ closeModal, updateLicense, updating, up - diff --git a/src/routes/setting/License/index.tsx b/src/routes/setting/License/index.tsx index 3e6341b0..bb835396 100644 --- a/src/routes/setting/License/index.tsx +++ b/src/routes/setting/License/index.tsx @@ -5,7 +5,6 @@ import dayjs from "dayjs"; import Button from "@/components/styled/Button"; import useLicense from "@/hooks/useLicense"; -import LicensePriceListModal from "./LicensePriceListModal"; import UpdateLicenseModal from "./UpdateLicenseModal"; interface ItemProps extends HTMLAttributes { @@ -37,15 +36,10 @@ const Item = ({ label, data, foldable = false, ...rest }: ItemProps) => { ); }; export default function License() { - const { t, i18n } = useTranslation("setting"); - // const { t: ct } = useTranslation(); + const { t } = useTranslation("setting"); const { license: licenseInfo, reachLimit, upsertLicense, upserting, upserted } = useLicense(true); - const [modalVisible, setModalVisible] = useState(false); const [updateVisible, setUpdateVisible] = useState(false); const [base58Fold, setBase58Fold] = useState(true); - const toggleModalVisible = () => { - setModalVisible((prev) => !prev); - }; const toggleUpdateModalVisible = () => { setUpdateVisible((prev) => !prev); }; @@ -87,34 +81,11 @@ export default function License() { />
    -
    -
    - {modalVisible && } {updateVisible && ( store.server.version, shallowEqual); - const showVoceSpace = useMemo(() => { - return compareVersion(currentVersion, "0.5.6") >= 0; - }, [currentVersion]); - useEffect(() => { - if (showVoceSpace) { - setIsAgora(false); - } else { - setIsAgora(true); - } - }, [showVoceSpace]); - const { changed, reset, values, setValues, toggleEnable, updateConfig } = useConfig("agora"); - const getClass = (selected: boolean) => - clsx( - `cursor-pointer p-2 box-border flex-center`, - selected && `border border-solid border-primary-400 shadow rounded-lg` - ); - const handleUpdate = () => { - // const { token_url, description } = values; - const _values = values as AgoraConfig; - if (!_values.url) { - _values.url = "https://api.agora.io"; - } - updateConfig(_values); - }; - - const handleChange = (evt: ChangeEvent) => { - const newValue = evt.target.value; - const { type = "" } = evt.target.dataset; - setValues((prev) => { - if (!prev) return prev; - return { ...prev, [type]: newValue }; - }); - }; - - if (!values) return null; - const { - url, - project_id, - app_id, - app_certificate, - customer_id, - customer_secret, - enabled = false, - } = values as AgoraConfig; - const _url = url || "https://api.agora.io"; - return ( -
    - {showVoceSpace && ( -
      -
    • setIsAgora(false)}> - -
    • -
    • setIsAgora(true)}> - -
    • -
    - )} - - {isAgora ? ( - <> - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - {changed && } - - ) : ( - - )} -
    - ); -} diff --git a/src/routes/setting/config/Livekit.tsx b/src/routes/setting/config/Livekit.tsx new file mode 100644 index 00000000..1a6f960c --- /dev/null +++ b/src/routes/setting/config/Livekit.tsx @@ -0,0 +1,88 @@ +import { ChangeEvent } from "react"; + +import SaveTip from "@/components/SaveTip"; +import { ConfigTip } from "@/components/ConfigTip"; +import Input from "@/components/styled/Input"; +import Label from "@/components/styled/Label"; +import Toggle from "@/components/styled/Toggle"; +import useConfig from "@/hooks/useConfig"; +import { LivekitConfig } from "@/types/server"; + +export default function ConfigLivekit() { + const { changed, reset, values, setValues, toggleEnable, updateConfig } = useConfig("livekit"); + + const handleUpdate = () => { + const next = values as LivekitConfig; + updateConfig({ + ...next, + url: next.url.trim().replace(/\/+$/, ""), + api_key: next.api_key.trim(), + api_secret: next.api_secret.trim(), + }); + }; + + const handleChange = (evt: ChangeEvent) => { + const newValue = evt.target.value; + const { type = "" } = evt.target.dataset; + setValues((prev) => { + if (!prev) return prev; + return { ...prev, [type]: newValue }; + }); + }; + + if (!values) return null; + const { enabled = false, url = "", api_key = "", api_secret = "" } = values as LivekitConfig; + + return ( +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + {changed && } +
    + ); +} diff --git a/src/routes/setting/config/Vocespace.tsx b/src/routes/setting/config/Vocespace.tsx deleted file mode 100644 index af3961b5..00000000 --- a/src/routes/setting/config/Vocespace.tsx +++ /dev/null @@ -1,396 +0,0 @@ -import { ConfigTip } from "@/components/ConfigTip"; -import { useTranslation } from "react-i18next"; -import HowToTip from "./HowToTip"; -import Toggle from "@/components/styled/Toggle"; -import Input from "@/components/styled/Input"; -import Label from "@/components/styled/Label"; -import SaveTip from "@/components/SaveTip"; -import useConfig from "@/hooks/useConfig"; -import { VocespaceConfig } from "@/types/server"; -import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react"; -import Radio from "@/components/styled/Radio"; -import { useAppSelector } from "@/app/store"; -import { compareVersion } from "@/utils"; -import { shallowEqual } from "react-redux"; -import Button from "@/components/styled/Button"; -import { useHealthCheckVocespaceMutation } from "@/app/services/server"; -import toast from "react-hot-toast"; -import { m } from "framer-motion"; - -export function ConfigVocespace() { - const { t } = useTranslation("setting", { keyPrefix: "vocespace" }); - const [domains, setDomains] = useState>(new Map()); - const currentVersion = useAppSelector((store) => store.server.version, shallowEqual); - const showVoceSpace = useMemo(() => { - return compareVersion(currentVersion, "0.5.8") >= 0; - }, [currentVersion]); - const [healthCheckVocespace] = useHealthCheckVocespaceMutation(); - - if (!showVoceSpace) { - return ( -
    - -
    - ); - } - - const { changed, reset, values, setValues, toggleEnable, updateConfig, refetch } = - useConfig("vocespace"); - - const handleUpdate = async () => { - const _values = values as VocespaceConfig; - if (_values.url.trim() === "" && _values.enabled) { - alert("Custom domain is required when enabling Vocespace."); - return; - } - - if (!_values.server_type) { - _values.server_type = "vps"; - } - - // 将 Map 转换为普通对象以便 JSON 序列化 - const submitValues = { - ..._values, - additional_domains: - _values.additional_domains instanceof Map - ? Object.fromEntries(_values.additional_domains) - : _values.additional_domains, - }; - - // 后端快速返回结果,只要检测环境可以执行就立即返回成功,不然会一直pending等待,导致前端超时 - const res = await updateConfig(submitValues as any); - - if (res.error) { - toast.error(`Auto Deploy Failed: ${res.error}`); - } else { - let { data } = res; - - if ((data as unknown as string) === "deployed") { - return; - } else { - // 生成 sh文件让用户下载 - const el = document.createElement("a"); - let shell = data as unknown as string; - const file = new Blob([shell], { type: "text/plain" }); - el.href = URL.createObjectURL(file); - el.download = "deploy_vocespace.sh"; - document.body.appendChild(el); // Required for this to work in FireFox - el.click(); - } - } - }; - - const handleChange = (evt: ChangeEvent) => { - const newValue = evt.target.value; - const { type = "" } = evt.target.dataset; - setValues((prev) => { - if (!prev) return prev; - return { ...prev, [type]: newValue }; - }); - }; - - const handleChangeDomains = ( - evt: ChangeEvent, - index: number - ) => { - const newValue = evt.target.value; - setDomains((prev) => { - const newDomains = prev instanceof Map ? new Map(prev) : new Map(); - let i = 0; - for (let key of newDomains.keys()) { - if (i === index) { - newDomains.delete(key); - newDomains.set(newValue, false); - break; - } - i++; - } - // also update to values, - setValues((v) => { - if (!v) return v; - return { ...v, additional_domains: newDomains }; - }); - return newDomains; - }); - }; - - const healthCheck = async (domain: string) => { - const res = await healthCheckVocespace({ url: domain }); - const check = (res.data as unknown as any)?.check || false; - if (check) { - // 如果domain是url就更新状态为成功 - if (domain === (values as VocespaceConfig).url) { - setValues((prev) => { - if (!prev) return prev; - return { ...prev, state: "success" }; - }); - } else { - // 更新additional_domains状态,将map的value设置为true - setDomains((prev) => { - const newDomains = new Map(prev); - if (newDomains.has(domain)) { - newDomains.set(domain, true); - } - // also update to values, - setValues((v) => { - if (!v) return v; - return { ...v, additional_domains: newDomains }; - }); - return newDomains; - }); - } - } else { - toast.error(`${domain} ${t("failed")}`); - } - }; - - // add new input field for vocespace domains - const addInput = () => { - setDomains((prev) => { - const newDomains = prev instanceof Map ? new Map(prev) : new Map(); - newDomains.set("", false); - // also update to values, - setValues((v) => { - if (!v) return v; - return { ...v, additional_domains: newDomains }; - }); - return newDomains; - }); - }; - - useEffect(() => { - if (!values) return; - - const { additional_domains } = values as VocespaceConfig; - console.warn("vocespace additional_domains:", additional_domains); - if (additional_domains) { - // 将普通对象转换为 Map - const domainsMap = - additional_domains instanceof Map - ? additional_domains - : new Map(Object.entries(additional_domains)); - setDomains(domainsMap as Map); - } - }, [values]); - - if (!values) return null; - const { - url, - license, - enabled = false, - password, - state, - server_type, - additional_domains, - } = values as VocespaceConfig; - - return ( -
    - -
    -
    - - - -
    -
    - -
      -
    1. {t("prerequisite.1")}
    2. -
    3. {t("prerequisite.2")}
    4. -
    5. {t("prerequisite.3")}
    6. -
    7. - VoceSpace Doc:{" "} - - doc.vocespace.com - -
    8. -
    -
    - -
    -
    - -
    - - - { - setValues((prev) => { - if (!prev) return prev; - return { ...prev, server_type: v as "nas" | "vps" | "other" }; - }); - }} - /> - -
    - -

    {t("domain_desc")}

    -
    - -
    - - - - -
    - {domains && - domains.size > 0 && - domains.entries().map(([domain, checked], index) => ( -
    - handleChangeDomains(evt, index)} - value={domain} - // name="url" - placeholder={`video.${window.location.hostname} (Your domain pointing to the current server IP)`} - /> - -
    - ))} -
    -
    - - -
    -
    -
    - {state && ( -
    - {state === "success" ? ( - - ) : ( - <> - - {enabled && ( - - please run the deploy_vocespace.sh in your vps or server

    {" "} -
    chmod +x deploy_vocespace.sh && sh ./deploy_vocespace.sh
    -
    - )} - - )} -
    - )} -
    - {t("prerequisite.4")} -
    -
    - {changed && } - {/* */} -
    - ); -} - -function StateDot({ color, className, word }: { color: string; className?: string; word: string }) { - return ( -
    -
    - {word} -
    - ); -} diff --git a/src/routes/setting/navs.tsx b/src/routes/setting/navs.tsx index 25838370..4555c34d 100644 --- a/src/routes/setting/navs.tsx +++ b/src/routes/setting/navs.tsx @@ -7,8 +7,8 @@ import APIConfig from "./APIConfig"; import APIDocument from "./APIDocument"; import BotConfig from "./BotConfig"; import Cloudflared from "./Cloudflared"; -import ConfigAgora from "./config/Agora"; import ConfigFirebase from "./config/Firebase"; +import ConfigLivekit from "./config/Livekit"; import Logins from "./config/Logins"; import ConfigSMTP from "./config/SMTP"; import DataManagement from "./DataManagement"; @@ -18,7 +18,6 @@ import NotificationSettings from "./NotificationSettings"; import Overview from "./Overview"; import Widget from "./Widget"; import { shallowEqual } from "react-redux"; -import { ConfigVocespace } from "./config/Vocespace"; import AdminNotificationChannels from "./AdminNotificationChannels"; const dataManagementNav = { @@ -73,7 +72,7 @@ const navs = [ }, { name: "video", - component: , + component: , }, { name: "smtp", @@ -117,7 +116,6 @@ const navs = [ const useNavs = () => { const { t } = useTranslation("setting"); const loginUser = useAppSelector((store) => store.authData.user, shallowEqual); - const upgraded = useAppSelector((store) => store.server.upgraded, shallowEqual); const filteredNavs = loginUser?.is_admin ? navs : navs @@ -163,13 +161,7 @@ const useNavs = () => { if (loginUser?.is_admin) { return true; } else { - // about 特殊处理下 - // if (nav.name == "about") { - // // 有付费,但是普通用户,则不显示 about - // return !upgraded; - // } else { return !nav.admin; - // } } }); }; diff --git a/src/types/common.ts b/src/types/common.ts index 653f2539..bff12b8b 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -4,15 +4,4 @@ export interface EntityId { export type ChatContext = "dm" | "channel"; export type Theme = "auto" | "dark" | "light"; export type AuthType = "register" | "login"; -export type PriceType = "subscription" | "payment" | "booking"; -export type PriceSubscriptionDuration = "month" | "quarter" | "year"; -export type Price = { - title?: string; - price?: string; - limit?: number; - pid?: string; - desc?: string; - type: PriceType; - sub_dur?: PriceSubscriptionDuration; -}; export type IPData = { error?: boolean; country: string }; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 9a30cefc..e51e4179 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,12 +1,10 @@ -import { - IAgoraRTCClient, - ICameraVideoTrack, - ILocalAudioTrack, - ILocalVideoTrack, - IMicrophoneAudioTrack, - IRemoteAudioTrack, - IRemoteVideoTrack -} from "agora-rtc-sdk-ng"; +import type { + LocalAudioTrack, + LocalVideoTrack, + RemoteAudioTrack, + RemoteVideoTrack, + Room, +} from "livekit-client"; interface BeforeInstallPromptEvent extends Event { /** * Returns an array of DOMString items containing the platforms on which the event was dispatched. @@ -48,12 +46,12 @@ export declare global { __WB_MANIFEST: Array; skipWaiting: () => void; CACHE: { [key: string]: typeof localforage | undefined }; - VOICE_CLIENT?: IAgoraRTCClient; + VOICE_CLIENT?: Room; VOICE_TRACK_MAP: { - [key: number]: IRemoteAudioTrack | IMicrophoneAudioTrack | undefined | null; + [key: number]: RemoteAudioTrack | LocalAudioTrack | undefined | null; }; VIDEO_TRACK_MAP: { - [key: number]: IRemoteVideoTrack | ICameraVideoTrack | ShareScreenTrack | undefined | null; + [key: number]: RemoteVideoTrack | LocalVideoTrack | undefined | null; }; } interface WindowEventMap { @@ -63,4 +61,3 @@ export declare global { scrollIntoViewIfNeeded?: any; } } -export type ShareScreenTrack = ILocalVideoTrack | [ILocalVideoTrack, ILocalAudioTrack]; diff --git a/src/types/server.ts b/src/types/server.ts index 431c9509..9f4e2ec3 100644 --- a/src/types/server.ts +++ b/src/types/server.ts @@ -1,5 +1,4 @@ // call `organization` in backend -import { PriceType } from "./common"; import { User } from "./user"; export interface Server extends SystemCommon { @@ -35,53 +34,23 @@ export interface FirebaseConfig { private_key: string; client_email: string; } -export interface AgoraConfig { +export interface LivekitConfig { enabled: boolean; url: string; - project_id: string; - app_id: string; - app_certificate: string; - customer_id: string; - customer_secret: string; + api_key: string; + api_secret: string; } - -export interface VocespaceConfig { - enabled: boolean; - password: string; +export interface LivekitTokenResponse { + token: string; url: string; - license: string; - state: "success" | "undeployed" | ""; - server_type?: "nas" | "vps" | "other"; - /** - * use additional_domains - * - key: domain - * - value: is deployed - */ - additional_domains?: Map; -} - -export interface AgoraVoicingListResponse { - success: boolean; - data: { - channels: { channel_name: string; user_count: number }[]; - total_size: number; - }; -} -export interface AgoraChannelUsersResponse { - success: boolean; - data: { - channel_exist: boolean; - mode?: number; - total?: number; - users?: number[]; - }; -} -export interface AgoraTokenResponse { - agora_token: string; - uid: number; - channel_name: string; + room: string; + identity: string; expired_in: number; - app_id: string; +} +export type LivekitCallAction = "invite" | "cancel"; +export interface LivekitCallRequest { + uid: number; + action: LivekitCallAction; } export interface SMTPConfig { enabled: boolean; @@ -126,21 +95,6 @@ export interface LicenseResponse { base58: string; user_limit: number; } -export interface LicenseMetadata { - expire: string; - user_limit: number; - domain: string | string[]; -} -export interface RenewLicense { - type: PriceType; - priceId: string; - metadata: LicenseMetadata; - cancel_url: string; - success_url: string; -} -export interface RenewLicenseResponse { - session_url: string; -} export interface CreateAdminDTO extends Pick { password: string; } diff --git a/src/types/sse.ts b/src/types/sse.ts index 867cb996..a84da3e9 100644 --- a/src/types/sse.ts +++ b/src/types/sse.ts @@ -228,6 +228,13 @@ interface UserCallEvent { target: number; uid: number; } +interface RtcCallEvent { + type: "rtc_call"; + from_uid: number; + to_uid: number; + room: string; + action: "invite" | "cancel"; +} interface MessageClearedEvent { type: "message_cleared"; latest_deleted_mid: number; @@ -267,6 +274,7 @@ export type ServerEvent = | HeartbeatEvent | GroupClearEvent | UserCallEvent + | RtcCallEvent | ServerConfigChangedEvent | MessageClearedEvent | UserRemark diff --git a/src/utils.tsx b/src/utils.tsx index 72e7906e..d16695c2 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -1,4 +1,4 @@ -import { ICameraVideoTrack, ILocalAudioTrack, ILocalVideoTrack } from "agora-rtc-sdk-ng"; +import type { LocalVideoTrack, RemoteVideoTrack } from "livekit-client"; import dayjs from "dayjs"; import AudioMsgSound from "@/assets/msg.sound.wav"; import BASE_URL, { @@ -445,22 +445,19 @@ export const fromNowTime = (ts?: number) => { const currTS = +new Date(); return dayjs(ts > currTS ? currTS : ts).fromNow(); }; -export const playAgoraVideo = (uid: number, videoTrack?: ICameraVideoTrack | null) => { - if (!videoTrack && !window.VIDEO_TRACK_MAP[uid]) return; +type RtcVideoTrack = LocalVideoTrack | RemoteVideoTrack; + +export const playLivekitVideo = (uid: number, videoTrack?: RtcVideoTrack | null) => { + const track = videoTrack ?? window.VIDEO_TRACK_MAP[uid]; + if (!track) return; const playerEle = document.querySelector(`#CAMERA_${uid}`) as HTMLElement; if (playerEle) { playerEle.classList.add("h-[120px]"); - if (videoTrack) { - videoTrack.play(playerEle); - } else { - if (!window.VIDEO_TRACK_MAP[uid]) return; - if ("play" in (window.VIDEO_TRACK_MAP[uid] ?? {})) { - window.VIDEO_TRACK_MAP[uid]?.play(playerEle); - } else { - const tracks = window.VIDEO_TRACK_MAP[uid] as [ILocalVideoTrack, ILocalAudioTrack]; - tracks[0].play(playerEle); - } - } + playerEle.replaceChildren(); + const mediaElement = track.attach(); + mediaElement.className = "w-full h-full object-cover"; + mediaElement.setAttribute("playsinline", "true"); + playerEle.appendChild(mediaElement); } };