feat: migrate to LiveKit, remove Agora/payment, add domain certs for Caddy
Sync to CNB / sync (push) Has been cancelled
Sync to CNB / sync (push) Has been cancelled
This commit is contained in:
@@ -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
|
||||
}),
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
Generated
+79
-147
@@ -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
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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</0> verfügbar",
|
||||
"mobile_app": "Entdecke unsere <0>Mobile App</0>",
|
||||
"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}}</0> 🚨",
|
||||
"current_version": "Aktuelle Version: <0>{{version}}</0>",
|
||||
"update_tip": "Bitte Server upgraden!",
|
||||
"update_tip": "Bitte Server aktualisieren!",
|
||||
"howto": "Anleitung zum Update"
|
||||
},
|
||||
"inactive": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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</0> Available",
|
||||
"mobile_app": "Check out our <0>Mobile APP</0>",
|
||||
"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}}</0> at least 🚨",
|
||||
"current_version": "Your current version:<0>{{version}}</0>",
|
||||
"update_tip": "Please upgrade the Server!",
|
||||
"update_tip": "Please update the Server!",
|
||||
"howto": "How to Update VoceChat Server"
|
||||
},
|
||||
"inactive": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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</0>) 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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
+15
-24
@@ -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"
|
||||
|
||||
}
|
||||
|
||||
@@ -67,18 +67,6 @@
|
||||
},
|
||||
"new_version": "<0>Nueva Versión</0> Disponible",
|
||||
"mobile_app": "Echa un vistazo a nuestra <0>Aplicación Móvil</0>",
|
||||
"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}}</0> como mínimo 🚨",
|
||||
"current_version": "Tu versión actual:<0>{{version}}</0>",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"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",
|
||||
@@ -19,7 +18,6 @@
|
||||
"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",
|
||||
@@ -29,23 +27,18 @@
|
||||
"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 !",
|
||||
|
||||
"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",
|
||||
|
||||
@@ -67,18 +67,6 @@
|
||||
},
|
||||
"new_version": "<0>Nouvelle Version</0> Disponible",
|
||||
"mobile_app": "Découvrez notre <0>Application Mobile</0>",
|
||||
"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}}</0> au minimum 🚨",
|
||||
"current_version": "Votre version actuelle :<0>{{version}}</0>",
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"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",
|
||||
@@ -28,7 +27,6 @@
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "現在のライセンスのユーザー上限に達しました。管理者に連絡してください。"
|
||||
}
|
||||
|
||||
@@ -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>新バージョン</0>をご利用いただけます",
|
||||
"mobile_app": "<0>モバイルアプリ</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": {
|
||||
|
||||
@@ -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": "有効にする",
|
||||
|
||||
@@ -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": "ビデオ"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -67,22 +67,10 @@
|
||||
},
|
||||
"new_version": "<0>New Version</0> Available",
|
||||
"mobile_app": "Check out our <0>Mobile APP</0>",
|
||||
"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}}</0> at least 🚨",
|
||||
"current_version": "Your current version:<0>{{version}}</0>",
|
||||
"update_tip": "Please upgrade the Server!",
|
||||
"update_tip": "Please update the Server!",
|
||||
"howto": "How to Update VoceChat Server"
|
||||
},
|
||||
"inactive": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Вперед",
|
||||
|
||||
@@ -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</0> Available",
|
||||
"mobile_app": "Check out our <0>Mobile APP</0>",
|
||||
"price": {
|
||||
"pro": {
|
||||
"title": "VoceChat Pro",
|
||||
"desc": "",
|
||||
"price": "$49"
|
||||
},
|
||||
"supreme": {
|
||||
"title": "Видеоинструкция по развертыванию VoceSpace Private [499$]",
|
||||
"desc": "",
|
||||
"price": ""
|
||||
}
|
||||
},
|
||||
"server_update": {
|
||||
"version_needed": "Для этой функции требуется серверная версия:<0>{{version}}</0> по крайней мере 🚨 ",
|
||||
"current_version": "Ваша текущая версия:<0>{{version}}</0>",
|
||||
|
||||
@@ -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": "Введите идентификатор пользователя",
|
||||
|
||||
@@ -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</0>) и для подтверждения адреса электронной почты при новой регистрации мы используем 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": "Обновление секрета",
|
||||
|
||||
@@ -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": "Видео"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -65,18 +65,6 @@
|
||||
},
|
||||
"new_version": "<0>Yeni Sürüm</0> Mevcut",
|
||||
"mobile_app": "Yeni <0>Mobil Uygulama</0>'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}}</0> olmalı 🚨",
|
||||
"current_version": "Sizin mevcut sürümünüz:<0>{{version}}</0>",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "继续创建后,访客用户可能会看到该公共频道中的消息。",
|
||||
|
||||
@@ -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>新版本</0>",
|
||||
"mobile_app": "欢迎下载使用<0>手机 APP</0>",
|
||||
"price": {
|
||||
"pro": {
|
||||
"title": "VoceChat 专业版",
|
||||
"desc": "",
|
||||
"price": "$49"
|
||||
},
|
||||
"supreme": {
|
||||
"title": "VoceSpace 私有部署音视频[499$]",
|
||||
"desc": "",
|
||||
"price": ""
|
||||
}
|
||||
},
|
||||
"server_update": {
|
||||
"version_needed": "支持该特性的最低 Server 版本:<0>{{version}}</0>",
|
||||
"current_version": "当前的 Server 版本:<0>{{version}}</0>",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"roles": "设置角色",
|
||||
"set_admin": "管理员",
|
||||
"set_normal": "普通用户",
|
||||
|
||||
"search_not_found": "用户不存在,或者不允许搜索",
|
||||
"search_disabled": "管理员已关闭用户搜索功能",
|
||||
"add_to_contact": "加为联系人",
|
||||
|
||||
@@ -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</0>)和新注册电子邮件验证,我们使用 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": "更新安全密钥",
|
||||
|
||||
@@ -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": "视频通话"
|
||||
}
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -27,15 +27,12 @@ const whiteList = [
|
||||
"createAdmin",
|
||||
"getBotRelatedChannels",
|
||||
"sendMessageByBot",
|
||||
"getAgoraVoicingList",
|
||||
"preCheckFileFromUrl",
|
||||
"passkeyLoginStart",
|
||||
"passkeyLoginFinish",
|
||||
"getAutoTunnelInfo",
|
||||
];
|
||||
const whiteList401 = [
|
||||
"getAgoraVoicingList",
|
||||
"getAgoraChannels",
|
||||
"getGoogleAuthConfig",
|
||||
"getGithubAuthConfig",
|
||||
"getAutoTunnelInfo",
|
||||
|
||||
+22
-108
@@ -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<VocespaceConfig, void>({
|
||||
query: () => ({ url: `/admin/vocespace/config` }),
|
||||
getLivekitConfig: builder.query<LivekitConfig, void>({
|
||||
query: () => ({ url: `/admin/livekit/config` }),
|
||||
}),
|
||||
getAgoraConfig: builder.query<AgoraConfig, void>({
|
||||
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<number[], string>({
|
||||
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<void, AgoraConfig>({
|
||||
updateLivekitConfig: builder.mutation<void, LivekitConfig>({
|
||||
query: (data) => ({
|
||||
url: `/admin/agora/config`,
|
||||
url: `/admin/livekit/config`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
}),
|
||||
updateVocespaceConfig: builder.mutation<void, VocespaceConfig>({
|
||||
getLivekitStatus: builder.query<boolean, void>({
|
||||
query: () => ({ url: `/admin/livekit/enabled` }),
|
||||
}),
|
||||
generateLivekitToken: builder.mutation<LivekitTokenResponse, { uid: number } | { gid: number }>({
|
||||
query: (data) => ({
|
||||
url: `/admin/vocespace/config`,
|
||||
url: `/admin/livekit/token`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
}),
|
||||
healthCheckVocespace: builder.mutation<boolean, { url: string }>({
|
||||
signalLivekitCall: builder.mutation<void, LivekitCallRequest>({
|
||||
query: (data) => ({
|
||||
url: `/admin/vocespace/health/${encodeURIComponent(data.url)}`,
|
||||
method: "GET",
|
||||
}),
|
||||
}),
|
||||
getAgoraStatus: builder.query<boolean, void>({
|
||||
query: () => ({ url: `/admin/agora/enabled` }),
|
||||
}),
|
||||
generateAgoraToken: builder.mutation<AgoraTokenResponse, { uid: number } | { gid: number }>({
|
||||
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<RenewLicenseResponse, RenewLicense>({
|
||||
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<LicenseResponse, string>({
|
||||
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,
|
||||
|
||||
@@ -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<StoredServer>) {
|
||||
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,
|
||||
|
||||
@@ -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<ConnectionState>) {
|
||||
updateConnectionState(state, { payload }: PayloadAction<string>) {
|
||||
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;
|
||||
|
||||
@@ -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<Props> = ({ 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 (
|
||||
<>
|
||||
<div className="flex flex-col gap-8 -mt-[50px] dark:bg-gray-700">
|
||||
@@ -338,21 +335,11 @@ Inform me of the preview URL, then ask your clarifying questions and wait for my
|
||||
<div className={legacyClasses.boxTip}>{t("invite")}</div>
|
||||
</button>
|
||||
)}
|
||||
{licenseInfo?.user_limit == 20 && showVoceSpace ? (
|
||||
<button onClick={toLicensePage} className={legacyClasses.box}>
|
||||
<svg viewBox="0 0 1028 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="39.5" height="39">
|
||||
<path d="M625.669087 392.461597l-47.150172-6.847535c-6.651891 0-13.499427-6.651891-16.825373-13.499427L538.020634 324.964463c-6.651891-10.173481-13.499427-16.825373-23.672908-16.825372s-16.825373 6.651891-20.151318 13.499426l-23.672908 47.150172c-3.325946 6.651891-10.173481 10.173481-16.825372 13.499427l-47.150172 6.651892c-10.173481 0-16.825373 6.847535-20.151319 16.825372-3.325946 10.173481 0 16.825373 6.651892 23.672908l37.172335 40.498281c3.325946 3.325946 6.651891 10.173481 6.651891 13.499426v3.325946l-10.173481 57.323653c0 10.173481 3.325946 16.825373 10.173481 23.672908 6.651891 6.651891 16.825373 6.651891 26.998854 0l40.49828-23.672908h10.173481c3.325946 0 6.847535 0 10.173481 3.325946l43.824227 23.672908c3.325946 3.325946 6.651891 3.325946 10.173481 3.325946 3.325946 0 10.173481 0 13.499426-3.325946 6.847535-6.651891 10.173481-13.499427 10.173482-23.672908l-10.173482-53.997707v-3.325946c0-6.651891 3.325946-10.173481 6.847536-16.825373l37.172335-40.49828c6.651891-6.651891 6.651891-16.825373 6.651891-23.672908-0.391288-9.977837-10.564769-16.629729-17.21666-16.629729z" fill="#FFDB5D"></path>
|
||||
<path d="M512 0C229.294612 0 0 229.294612 0 512s229.294612 512 512 512 512-229.294612 512-512S794.901032 0 512 0z m-116.212457 798.618265s-33.650745-37.172335-63.975545-63.975544c-43.824226-13.499427-97.821934-23.672908-97.821933-23.672908l67.497134-114.647306c33.650745 47.150172 97.821934 84.322507 155.145587 97.821933l-60.845243 104.473825z m-97.821934-350.59381c0-114.647306 101.147879-222.447077 215.795186-222.447076 114.647306 0 215.795185 101.147879 215.795185 215.795185 0 114.647306-101.147879 215.795185-215.795185 215.795185-114.647306-0.195644-215.795185-94.495988-215.795186-209.143294z m401.069928 279.77073c-30.324799 30.324799-67.497134 70.82308-67.497134 70.82308l-60.649599-104.473825c57.323653-13.499427 114.647306-53.997707 148.298051-97.821933l70.82308 114.647306c0.195644-3.325946-47.150172 3.325946-90.974398 16.825372z" fill="#FFDB5D"></path>
|
||||
</svg>
|
||||
<div className={legacyClasses.boxTip}>{t("license")}</div>
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={chatHandler} className={legacyClasses.box}>
|
||||
<IconChat className={legacyClasses.boxIcon} />
|
||||
<div className={legacyClasses.boxTip}>{chatTip}</div>
|
||||
</button>
|
||||
)}
|
||||
{showVoceSpace ? (
|
||||
<button onClick={chatHandler} className={legacyClasses.box}>
|
||||
<IconChat className={legacyClasses.boxIcon} />
|
||||
<div className={legacyClasses.boxTip}>{chatTip}</div>
|
||||
</button>
|
||||
{showVideoSettings ? (
|
||||
<NavLink to={"/setting/video"} className={legacyClasses.box}>
|
||||
<svg width="39" height="39" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M29.2884 20.4182C29.7499 19.0293 29.9998 17.5438 29.9998 16C29.9998 8.26806 23.7319 2.00008 15.9999 2.00008C8.26798 2.00008 2 8.26806 2 16C2 17.5744 2.25987 19.088 2.73906 20.5005C3.09861 20.5088 3.43836 20.5246 3.74422 20.539C4.1999 20.5603 4.55678 20.5768 4.8465 20.571L4.85764 20.5707L4.9685 18.3288L4.96959 18.3067L4.97155 18.2847L5.09066 16.9465L5.0947 16.9011L5.1024 16.8562L5.23684 16.0725C5.376 15.2613 5.76653 14.5144 6.35326 13.9372C6.851 13.4475 7.44688 13.1183 8.07693 12.9487C7.43026 12.6087 6.98926 11.9304 6.98926 11.1491C6.98926 10.027 7.8989 9.11737 9.021 9.11737C10.1431 9.11737 11.0527 10.027 11.0527 11.1491C11.0527 11.9091 10.6355 12.5717 10.0175 12.9201C10.5584 13.0484 11.0773 13.2921 11.5345 13.6506C11.7371 13.3464 11.9714 13.0622 12.2347 12.8031C13.0209 12.0296 13.9873 11.55 14.9979 11.3626C14.1354 10.9827 13.5332 10.1204 13.5332 9.11741C13.5332 7.76315 14.631 6.6653 15.9853 6.6653C17.3396 6.6653 18.4374 7.76315 18.4374 9.11741C18.4374 10.1214 17.834 10.9845 16.9701 11.3638C17.9716 11.5504 18.9301 12.0227 19.7133 12.7789C19.9844 13.0407 20.2261 13.3293 20.4354 13.6391C20.8836 13.2519 21.3992 12.9818 21.9412 12.8296C21.3295 12.4794 20.9173 11.8204 20.9173 11.065C20.9173 9.94295 21.827 9.0333 22.9491 9.0333C24.0712 9.0333 24.9808 9.94295 24.9808 11.065C24.9808 11.8041 24.5862 12.4511 23.9961 12.8066C24.6416 12.9728 25.2529 13.3056 25.7617 13.8061C26.3485 14.3833 26.739 15.1303 26.8782 15.9415L27.0126 16.7252L27.0206 16.7717L27.0246 16.8187L27.1858 18.6893L27.1866 18.6993L27.1873 18.7094L27.3044 20.4304C27.337 20.4326 27.3723 20.4346 27.4104 20.4364C27.7961 20.4543 28.2307 20.4435 28.7617 20.4304C28.9274 20.4263 29.1025 20.422 29.2884 20.4182ZM28.3028 22.6866C27.9281 22.6925 27.586 22.6933 27.3064 22.6803C26.9334 22.663 26.476 22.6195 26.0838 22.4527C25.8728 22.3629 25.6172 22.2115 25.4138 21.9497C25.2432 21.7301 25.1474 21.4816 25.1142 21.2295L25.1073 21.23L24.9468 18.872L24.7906 17.0585L24.6642 16.3213C24.6046 15.9742 24.4375 15.6545 24.1864 15.4075C23.5405 14.7721 22.4925 14.7669 21.833 15.4036C21.6016 15.6271 21.4381 15.9122 21.3619 16.2213L21.4801 16.916L21.4824 16.93L21.4845 16.9441L21.7168 18.5553L21.7173 18.5587L21.8892 19.7777L21.9002 19.8558V19.9346V21.1257H21.9004C21.9004 21.6008 21.7303 22.0667 21.3626 22.4051C21.0107 22.7289 20.5693 22.8509 20.1747 22.8361C19.3976 22.807 18.5481 22.2065 18.5333 21.1537H18.5331V19.9907V19.9124L18.5439 19.8348L18.7541 18.3355L18.7541 18.3355L18.7553 18.327L18.9865 16.7647L18.9883 16.7527L18.9903 16.7407L19.081 16.2079L19.0664 16.122C18.9549 15.4666 18.6331 14.8586 18.1529 14.3948C16.9386 13.2223 15.0059 13.2279 13.8102 14.4043C13.3861 14.8216 13.0918 15.3511 12.9606 15.9291C12.9724 15.9842 12.9831 16.0396 12.9926 16.0953L13.1246 16.8717L13.1314 16.9113L13.1353 16.9513L13.2876 18.5165L13.4604 19.8436L13.4698 19.9158V19.9886V21.2518H13.4722C13.4722 21.7247 13.3041 22.1899 12.938 22.5288C12.587 22.8538 12.1455 22.9772 11.7497 22.9623C10.968 22.9328 10.1268 22.3267 10.1121 21.2798H10.1047V19.9486V19.8724L10.115 19.7969L10.2831 18.5639L10.2831 18.5638L10.5057 16.9316L10.5083 16.9124L10.5115 16.8934L10.6538 16.0641C10.5653 15.867 10.4393 15.6866 10.2819 15.5347C9.62251 14.898 8.57449 14.9031 7.92862 15.5385C7.67752 15.7855 7.51039 16.1052 7.45084 16.4524L7.32411 17.1911L7.21101 18.4618L7.08736 20.9622V21.2427H7.07764C7.06269 21.5177 6.97921 21.8038 6.79469 22.0616C6.59679 22.3381 6.33952 22.5026 6.11592 22.6007C5.70919 22.7791 5.24297 22.8098 4.89175 22.8169C4.54388 22.8239 4.14601 22.8065 3.75287 22.7882C6.14194 27.0893 10.731 29.9999 15.9999 29.9999C21.3102 29.9999 25.9299 27.0434 28.3028 22.6866ZM15.9853 8.81264C15.817 8.81264 15.6805 8.94909 15.6805 9.11741C15.6805 9.28573 15.817 9.42218 15.9853 9.42218C16.1536 9.42218 16.2901 9.28573 16.2901 9.11741C16.2901 8.94909 16.1536 8.81264 15.9853 8.81264ZM22.9491 10.8096C22.808 10.8096 22.6936 10.924 22.6936 11.065C22.6936 11.2061 22.808 11.3205 22.9491 11.3205C23.0901 11.3205 23.2045 11.2061 23.2045 11.065C23.2045 10.924 23.0901 10.8096 22.9491 10.8096ZM27.3471 21.0237H27.3448L27.3369 20.9082C27.3439 20.9486 27.3471 20.9875 27.3471 21.0237ZM4.84102 21.1507V21.0613C4.83587 21.1 4.83379 21.1247 4.83379 21.1507H4.84102ZM8.76556 11.1491C8.76556 11.008 8.87993 10.8937 9.021 10.8937C9.16208 10.8937 9.27644 11.008 9.27644 11.1491C9.27644 11.2902 9.16208 11.4046 9.021 11.4046C8.87993 11.4046 8.76556 11.2902 8.76556 11.1491Z" fill="url(#paint0_linear_1_116)" />
|
||||
@@ -363,7 +350,7 @@ Inform me of the preview URL, then ask your clarifying questions and wait for my
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<div className={legacyClasses.boxTip}>{t("vocespace")}</div>
|
||||
<div className={legacyClasses.boxTip}>{t("video")}</div>
|
||||
</NavLink>
|
||||
) : (
|
||||
<a href={"https://voce.chat#download"} target={"_blank"} rel="noreferrer" className={legacyClasses.box}>
|
||||
|
||||
@@ -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<Props> = ({ 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<Props> = ({ uid, type = "embed", cid }) => {
|
||||
<span>{t("send_msg")}</span>
|
||||
</li>
|
||||
</NavLink>
|
||||
{agoraEnabled && type == "embed" && (
|
||||
{livekitEnabled && type == "embed" && (
|
||||
<li role="button" onClick={startCall} className={`${iconClass} icon chat`}>
|
||||
<IconCall className="fill-primary-400" />
|
||||
<span>{t("call")}</span>
|
||||
@@ -112,7 +112,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
|
||||
title: chatTrans("remark"),
|
||||
handler: setRemarkVisible.bind(null, true),
|
||||
},
|
||||
agoraEnabled &&
|
||||
livekitEnabled &&
|
||||
type == "card" && {
|
||||
title: t("call"),
|
||||
handler: startCall,
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Dispatch, MouseEvent, SetStateAction, useEffect, useState } from "react
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { shallowEqual, useDispatch } from "react-redux";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from "agora-rtc-sdk-ng";
|
||||
import clsx from "clsx";
|
||||
|
||||
// import { updateChannelVisibleAside, updateDMVisibleAside } from '@/app/slices/footprint';
|
||||
@@ -44,7 +43,6 @@ const DeviceList = ({
|
||||
selected: string;
|
||||
}[];
|
||||
}) => {
|
||||
const loginUid = useAppSelector((store) => store.authData.user?.uid ?? 0, shallowEqual);
|
||||
const dispatch = useDispatch();
|
||||
// const { t } = useTranslation("chat");
|
||||
const toggleVisible = (evt: MouseEvent<HTMLDivElement>) => {
|
||||
@@ -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<HTMLDivElement>) => {
|
||||
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(
|
||||
|
||||
+176
-172
@@ -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 <DMCalling uid={1} sendByMe={calling !== loginUid} />;
|
||||
if (from !== 0) return <DMCalling from={from} to={to} />;
|
||||
return null;
|
||||
};
|
||||
|
||||
export { useVoice };
|
||||
export { default as useVoice } from "./useVoice";
|
||||
export default memo(Voice);
|
||||
|
||||
+208
-209
@@ -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 = <T,>(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<LocalAudioTrack>(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<LocalVideoTrack>(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<LocalVideoTrack>(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<LocalVideoTrack>(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<LocalVideoTrack>(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<LocalAudioTrack>(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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+17
-37
@@ -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> = T[keyof T];
|
||||
let originalValue: valuesOf<ConfigMap> | 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<number | undefined>(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,
|
||||
|
||||
@@ -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 (
|
||||
<section className="flex flex-col items-center bg-slate-100 dark:bg-slate-800 rounded-2xl w-4/5 md:w-[512px] p-6">
|
||||
<img
|
||||
className="w-28 h-28"
|
||||
src="https://s.voce.chat/web_client/assets/img/check.png"
|
||||
alt="check icon"
|
||||
/>
|
||||
<h1 className="font-bold text-3xl pt-5">{t("payment_success")}</h1>
|
||||
<p className="text-lg pb-7 mt-2 text-gray-400 dark:text-gray-600">
|
||||
{upserting ? t("tip_renewing") : ""}
|
||||
{upserted ? t("tip_renewed") : ""}
|
||||
{isError ? t("tip_renew_error") : ""}
|
||||
</p>
|
||||
<Button disabled={isLoading || upserting} className="back" onClick={handleBack}>
|
||||
{t("back_home")}
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentSuccess;
|
||||
@@ -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<HTMLDivElement> & { children?: ReactNode }) => {
|
||||
return (
|
||||
@@ -11,21 +10,9 @@ const StyledWrapper = ({ children }: DOMAttributes<HTMLDivElement> & { children?
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// 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 (
|
||||
<StyledWrapper>
|
||||
<PaymentSuccess sid={payload} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
// github 授权成功
|
||||
if (type == "github") {
|
||||
const query = new URLSearchParams(location.search);
|
||||
const code = query.get("code") ?? "";
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
<Operations id={callingTo} context="dm" mode="dm" />
|
||||
<Operations id={peerUid} context="dm" mode="dm" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex items-center justify-between bg-red-600 rounded-md w-full py-3 px-4">
|
||||
<div className="flex items-center justify-start bg-red-600 rounded-md w-full py-3 px-4">
|
||||
<span className="text-white">
|
||||
<i className="text-xl mr-2">🚨</i>
|
||||
{t("license_tip")}
|
||||
</span>
|
||||
<Button onClick={handleRedirect} className="small">{`Upgrade License`}</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LicenseUpgradeTip;
|
||||
export default LicenseOutdatedTip;
|
||||
|
||||
@@ -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<Props> = ({
|
||||
{readonly ? (
|
||||
<LoginTip />
|
||||
) : reachLimit ? (
|
||||
<LicenseUpgradeTip />
|
||||
<LicenseOutdatedTip />
|
||||
) : (
|
||||
<div className={clsx(`flex justify-center`, selects && "hidden")}>
|
||||
<Send id={to} context={context} feedRef={feedRef} />
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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) => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (info.connectionState == "RECONNECTING") {
|
||||
if (info.connectionState == "reconnecting") {
|
||||
return (
|
||||
<div className="w-full h-full flex-center flex-col gap-1 p-1 ">
|
||||
<span className="text-red-300">Reconnecting...</span>
|
||||
|
||||
@@ -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<VocespaceConfig | undefined>(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 (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
<ChannelModal closeModal={toggleChannelModalVisible} personal={true} />
|
||||
)}
|
||||
{usersModalVisible && <UsersModal closeModal={toggleUsersModalVisible} />}
|
||||
<DMCalling from={callingFrom} to={callingTo} />
|
||||
<div
|
||||
className={clsx(
|
||||
`flex ${mobileHeight} md:h-full md:pt-2 md:pb-2.5 md:pr-1`,
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import { ChangeEvent, FC, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import dayjs from "dayjs";
|
||||
import * as linkify from "linkifyjs";
|
||||
|
||||
import { getLicensePriceList } from "@/app/config";
|
||||
import { useGetLicensePaymentUrlMutation } from "@/app/services/server";
|
||||
import { PriceSubscriptionDuration, PriceType } from "@/types/common";
|
||||
import Modal from "@/components/Modal";
|
||||
import Button from "@/components/styled/Button";
|
||||
import Input from "@/components/styled/Input";
|
||||
import StyledModal from "@/components/styled/Modal";
|
||||
import StyledRadio from "@/components/styled/Radio";
|
||||
|
||||
// import { LicenseMetadata, RenewLicense } from "@/types/server";
|
||||
|
||||
interface Props {
|
||||
closeModal: () => 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<Props> = ({ 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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<Modal id="modal-modal">
|
||||
<StyledModal
|
||||
footer={
|
||||
<div className="text-sm text-gray-400 dark:text-gray-100 mt-3">
|
||||
{t("vocespace.prerequisite.4")}
|
||||
</div>
|
||||
}
|
||||
// className="!min-w-[480px]"
|
||||
title={t("license.renew")}
|
||||
description={t("license.renew_select")}
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal} className="ghost">
|
||||
{ct("action.cancel")}
|
||||
</Button>
|
||||
{isBooking ? (
|
||||
<Button
|
||||
className="text-sm text-white bg-primary-400 break-keep shadow rounded-lg px-3.5 py-2.5 md:hover:bg-primary-500 active:bg-primary-500 disabled:bg-gray-300"
|
||||
onClick={handleTalk}
|
||||
>
|
||||
{t("license.renew")}
|
||||
</Button>
|
||||
) : (
|
||||
<Tippy
|
||||
visible={popUpVisible}
|
||||
interactive
|
||||
placement="top-end"
|
||||
offset={[-50, -40]}
|
||||
trigger="click"
|
||||
content={
|
||||
<div className="p-3 rounded-lg border border-solid border-gray-200 dark:border-gray-900 flex flex-col items-start gap-3 w-[430px] bg-white dark:bg-gray-800 shadow shadow-gray-200 dark:shadow-gray-900 drop-shadow-xl">
|
||||
<div className="text-gray-500 text-sm">{t("license.tip_domain")}</div>
|
||||
<Input value={host} onChange={handleUpdateHost} />
|
||||
<div className="flex justify-between items-center w-full mt-4">
|
||||
<span className="text-xs text-orange-500 text-left">
|
||||
{" "}
|
||||
{t("license.tip_port")}
|
||||
</span>
|
||||
<div className="flex gap-3 whitespace-nowrap">
|
||||
<Button className="mini cancel" onClick={togglePopUpVisible}>
|
||||
{ct("action.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
className="mini"
|
||||
disabled={isLoading || isSuccess}
|
||||
onClick={handleRenew}
|
||||
>
|
||||
{isLoading
|
||||
? "Initialize Payment URL"
|
||||
: isSuccess
|
||||
? "Redirecting"
|
||||
: t("license.tip_confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={togglePopUpVisible}
|
||||
className="text-sm text-white bg-primary-400 break-keep shadow rounded-lg px-3.5 py-2.5 md:hover:bg-primary-500 active:bg-primary-500 disabled:bg-gray-300"
|
||||
>
|
||||
{" "}
|
||||
{t("license.renew")}
|
||||
</button>
|
||||
</Tippy>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StyledRadio
|
||||
options={LicensePriceList.map(
|
||||
({ title, desc, price }) =>
|
||||
`${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);
|
||||
}}
|
||||
/>
|
||||
</StyledModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default LicensePriceListModal;
|
||||
@@ -20,7 +20,7 @@ const UpdateLicenseModal: FC<Props> = ({ 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<Props> = ({ closeModal, updateLicense, updating, up
|
||||
<Button onClick={closeModal} className="ghost">
|
||||
{ct("action.cancel")}
|
||||
</Button>
|
||||
<Button disabled={updating || updated || !value} onClick={handleRenew}>
|
||||
<Button disabled={updating || updated || !value} onClick={handleUpdate}>
|
||||
{updating ? "Updating" : updated ? "Update Successfully" : t("license.update")}
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -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<HTMLSpanElement> {
|
||||
@@ -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() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={toggleModalVisible}>{t("license.renew")}</Button>
|
||||
<Button onClick={toggleUpdateModalVisible} className="ghost">
|
||||
{t("license.update")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 bg-primary-500 text-white rounded drop-shadow-xl p-5">
|
||||
<h2 className="text-2xl font-bold">{t("license.tip.title")} 🎁</h2>
|
||||
<p className="flex flex-col">
|
||||
<span>{t("license.tip.user_test")}</span>
|
||||
<span>
|
||||
{t("license.tip.contact")}
|
||||
{i18n.language.startsWith("zh") ? (
|
||||
"Privoce"
|
||||
) : (
|
||||
<a
|
||||
className="underline text-lg text-green-200"
|
||||
href="https://calendly.com/hansu/han-meeting"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
https://calendly.com/hansu/han-meeting
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{modalVisible && <LicensePriceListModal closeModal={toggleModalVisible} />}
|
||||
{updateVisible && (
|
||||
<UpdateLicenseModal
|
||||
updated={upserted}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import { ChangeEvent, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AgoraConfig } from "@/types/server";
|
||||
import SaveTip from "@/components/SaveTip";
|
||||
import Input from "@/components/styled/Input";
|
||||
import Label from "@/components/styled/Label";
|
||||
import Toggle from "@/components/styled/Toggle";
|
||||
import useConfig from "@/hooks/useConfig";
|
||||
import HowToTip from "./HowToTip";
|
||||
import { ConfigTip } from "@/components/ConfigTip";
|
||||
import { ConfigVocespace } from "./Vocespace";
|
||||
import clsx from "clsx";
|
||||
import { shallowEqual } from "react-redux";
|
||||
import { useAppSelector } from "@/app/store";
|
||||
import { compareVersion } from "@/utils";
|
||||
|
||||
export default function ConfigAgora() {
|
||||
const { t } = useTranslation("setting", { keyPrefix: "agora" });
|
||||
const [isAgora, setIsAgora] = useState(false);
|
||||
const currentVersion = useAppSelector((store) => 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<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
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 (
|
||||
<div className="setting-container">
|
||||
{showVoceSpace && (
|
||||
<ul
|
||||
className={`hidden md:flex border border-solid dark:border-gray-400 shadow rounded-lg box-border`}
|
||||
style={{ width: "fit-content" }}
|
||||
>
|
||||
<li className={getClass(!isAgora)} data-view={"item"} onClick={() => setIsAgora(false)}>
|
||||
<Label>VoceSpace</Label>
|
||||
</li>
|
||||
<li className={getClass(isAgora)} data-view={"grid"} onClick={() => setIsAgora(true)}>
|
||||
<Label>Agora</Label>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{isAgora ? (
|
||||
<>
|
||||
<ConfigTip title={t("desc")} desc={t("sub_desc")} />
|
||||
<div className="inputs">
|
||||
<div className="input row">
|
||||
<Label className="flex items-center gap-2">
|
||||
Enable
|
||||
<HowToTip link="https://doc.voce.chat/setting/setting-agora" text={t("how_to")} />
|
||||
</Label>
|
||||
<Toggle onClick={toggleEnable} checked={enabled}></Toggle>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="url">Agora URL</Label>
|
||||
<Input
|
||||
disabled={!enabled}
|
||||
data-type="url"
|
||||
onChange={handleChange}
|
||||
value={_url}
|
||||
name="url"
|
||||
placeholder="Agora URL"
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="project_id">Project ID</Label>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
disabled={!enabled}
|
||||
data-type="project_id"
|
||||
onChange={handleChange}
|
||||
value={project_id}
|
||||
name="project_id"
|
||||
placeholder="Project ID"
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="app_id">App ID</Label>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
disabled={!enabled}
|
||||
data-type="app_id"
|
||||
onChange={handleChange}
|
||||
value={app_id}
|
||||
name="app_id"
|
||||
placeholder="APP ID"
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="app_certificate">APP Certificate</Label>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
disabled={!enabled}
|
||||
data-type="app_certificate"
|
||||
onChange={handleChange}
|
||||
value={app_certificate}
|
||||
name="app_certificate"
|
||||
placeholder="APP Certificate"
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="customer_id">Customer ID</Label>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
disabled={!enabled}
|
||||
data-type="customer_id"
|
||||
onChange={handleChange}
|
||||
value={customer_id}
|
||||
name="customer_id"
|
||||
placeholder="Customer ID for RESTful API"
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="customer_secret">Customer Secret</Label>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
disabled={!enabled}
|
||||
data-type="customer_secret"
|
||||
onChange={handleChange}
|
||||
value={customer_secret}
|
||||
name="customer_secret"
|
||||
placeholder="Customer Secret for RESTful API"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={reset} />}
|
||||
</>
|
||||
) : (
|
||||
<ConfigVocespace></ConfigVocespace>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className="setting-container">
|
||||
<ConfigTip
|
||||
title="LiveKit"
|
||||
desc="Use your private LiveKit server for voice, video, and screen sharing."
|
||||
/>
|
||||
<div className="inputs">
|
||||
<div className="input row">
|
||||
<Label>Enable</Label>
|
||||
<Toggle onClick={toggleEnable} checked={enabled}></Toggle>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="url">LiveKit URL</Label>
|
||||
<Input
|
||||
disabled={!enabled}
|
||||
data-type="url"
|
||||
onChange={handleChange}
|
||||
value={url}
|
||||
name="url"
|
||||
placeholder="https://rtc.example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="api_key">API Key</Label>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
disabled={!enabled}
|
||||
data-type="api_key"
|
||||
onChange={handleChange}
|
||||
value={api_key}
|
||||
name="api_key"
|
||||
placeholder="LiveKit API key"
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="api_secret">API Secret</Label>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
disabled={!enabled}
|
||||
data-type="api_secret"
|
||||
onChange={handleChange}
|
||||
value={api_secret}
|
||||
name="api_secret"
|
||||
placeholder="LiveKit API secret"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={reset} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<Map<string, boolean>>(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 (
|
||||
<div className="setting-container" style={{ paddingBottom: 172, height: "fit-content" }}>
|
||||
<Label>{t("old")}</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const newValue = evt.target.value;
|
||||
const { type = "" } = evt.target.dataset;
|
||||
setValues((prev) => {
|
||||
if (!prev) return prev;
|
||||
return { ...prev, [type]: newValue };
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeDomains = (
|
||||
evt: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
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<string, boolean>);
|
||||
}
|
||||
}, [values]);
|
||||
|
||||
if (!values) return null;
|
||||
const {
|
||||
url,
|
||||
license,
|
||||
enabled = false,
|
||||
password,
|
||||
state,
|
||||
server_type,
|
||||
additional_domains,
|
||||
} = values as VocespaceConfig;
|
||||
|
||||
return (
|
||||
<div className="setting-container" style={{ paddingBottom: 172, height: "fit-content" }}>
|
||||
<ConfigTip title={t("desc")} desc={`${t("sub_desc")}`} />
|
||||
<div className="inputs">
|
||||
<div className="input row">
|
||||
<Label style={{ color: "#fff", display: "flex", flexWrap: "wrap" }}>
|
||||
<Label style={{ display: "inline", width: "100%" }}>Enable</Label>
|
||||
{/* <HowToTip link="https://doc.vocespace.com/zh/doc/guide/overview" text={t("how_to")} /> */}
|
||||
<div
|
||||
className="text-gray-400"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{t("how_to")}
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Toggle onClick={toggleEnable} checked={enabled}></Toggle>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="list-disc list-inside text-sm" style={{ fontWeight: 700 }}>
|
||||
{t("prerequisite.0")}
|
||||
</Label>
|
||||
<ol
|
||||
className="list-disc list-inside text-sm text-gray-400 mb-4"
|
||||
style={{ fontSize: 12, margin: 0 }}
|
||||
>
|
||||
<li>{t("prerequisite.1")}</li>
|
||||
<li>{t("prerequisite.2")}</li>
|
||||
<li>{t("prerequisite.3")}</li>
|
||||
<li>
|
||||
VoceSpace Doc:{" "}
|
||||
<a href="https://doc.vocespace.com/" target="_blank" className="underline">
|
||||
doc.vocespace.com
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="input">
|
||||
<div className="flex flex-col text-sm">
|
||||
<Label htmlFor="url" style={{ position: "relative", width: "100%" }}>
|
||||
Server Type
|
||||
</Label>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
style={{ fontSize: 12 }}
|
||||
className="mb-2 block text-gray-400 underline"
|
||||
href={`https://vocespace.com/local_deploy/${
|
||||
server_type === "nas" ? "nas.mp4" : "vps.mp4"
|
||||
}`}
|
||||
target="_blank"
|
||||
>
|
||||
{`https://vocespace.com/local_deploy/${
|
||||
server_type === "nas" ? "nas.mp4" : "vps.mp4"
|
||||
}`}
|
||||
</a>
|
||||
<video
|
||||
src={`https://vocespace.com/local_deploy/${
|
||||
server_type === "nas" ? "nas.mp4" : "vps.mp4"
|
||||
}`}
|
||||
controls
|
||||
style={{ width: "100%", borderRadius: 8, backgroundColor: "#ddd" }}
|
||||
></video>
|
||||
</div>
|
||||
|
||||
<Radio
|
||||
disabled={!enabled}
|
||||
options={["NAS", "VPS", "Other"]}
|
||||
values={["nas", "vps", "other"]}
|
||||
value={server_type || "vps"}
|
||||
onChange={(v) => {
|
||||
setValues((prev) => {
|
||||
if (!prev) return prev;
|
||||
return { ...prev, server_type: v as "nas" | "vps" | "other" };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col text-sm">
|
||||
<Label htmlFor="url" style={{ position: "relative", width: "100%" }}>
|
||||
{url.trim() === "" && enabled && (
|
||||
<span
|
||||
style={{ color: "red", position: "absolute", top: -4, left: -8, fontSize: 16 }}
|
||||
>
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
Custom domain for VoceSpace
|
||||
</Label>
|
||||
<p className="text-gray-400 text-xs">{t("domain_desc")}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2" style={{ width: "100%" }}>
|
||||
<Input
|
||||
className="flex-1"
|
||||
disabled={!enabled}
|
||||
data-type="url"
|
||||
onChange={handleChange}
|
||||
value={url}
|
||||
name="url"
|
||||
placeholder={`video.${window.location.hostname} (Your domain pointing to the current server IP)`}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{ height: 38, backgroundColor: state === "success" ? "#00ff00dd" : undefined }}
|
||||
onClick={async () => await healthCheck(url)}
|
||||
disabled={!enabled}
|
||||
>
|
||||
{state === "success" ? t("checked") : t("check")}
|
||||
</Button>
|
||||
<Button style={{ height: 38 }} onClick={addInput} disabled={!enabled}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
</div>
|
||||
{domains &&
|
||||
domains.size > 0 &&
|
||||
domains.entries().map(([domain, checked], index) => (
|
||||
<div key={index} className="flex gap-2 mt-2" style={{ width: "100%" }}>
|
||||
<Input
|
||||
className="flex-1"
|
||||
disabled={!enabled}
|
||||
data-type="url"
|
||||
onChange={(evt) => handleChangeDomains(evt, index)}
|
||||
value={domain}
|
||||
// name="url"
|
||||
placeholder={`video.${window.location.hostname} (Your domain pointing to the current server IP)`}
|
||||
/>
|
||||
<Button
|
||||
style={{ height: 38, backgroundColor: checked ? "#00ff00dd" : undefined }}
|
||||
onClick={async () => await healthCheck(domain)}
|
||||
disabled={!enabled}
|
||||
>
|
||||
{checked ? t("checked") : t("check")}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="password">Redis Password (Optional)</Label>
|
||||
<Input
|
||||
disabled={!enabled}
|
||||
data-type="password"
|
||||
onChange={handleChange}
|
||||
value={password}
|
||||
name="password"
|
||||
placeholder="This is optional field"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{state && (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
{state === "success" ? (
|
||||
<StateDot
|
||||
color="#00ff00"
|
||||
className="text-green-400"
|
||||
word="VoceSpace deployed successfully."
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<StateDot
|
||||
color="gray"
|
||||
className="text-gray-400"
|
||||
word="VoceSpace is not deployed yet or failed."
|
||||
/>
|
||||
{enabled && (
|
||||
<span className="text-red-500">
|
||||
please run the deploy_vocespace.sh in your vps or server <br></br>{" "}
|
||||
<pre>chmod +x deploy_vocespace.sh && sh ./deploy_vocespace.sh</pre>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="list-disc list-inside text-sm text-gray-400 mb-4"
|
||||
style={{ fontSize: 12, margin: 0, paddingTop: 8 }}
|
||||
>
|
||||
{t("prerequisite.4")}
|
||||
</div>
|
||||
</div>
|
||||
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={reset} />}
|
||||
{/* <button onClick={handleUpdate} className="btn">update</button> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StateDot({ color, className, word }: { color: string; className?: string; word: string }) {
|
||||
return (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12 }}
|
||||
className={className}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-block",
|
||||
height: 12,
|
||||
width: 12,
|
||||
backgroundColor: color,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
></div>
|
||||
{word}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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: <ConfigAgora />,
|
||||
component: <ConfigLivekit />,
|
||||
},
|
||||
{
|
||||
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;
|
||||
// }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Vendored
+10
-13
@@ -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<PrecacheEntry | string>;
|
||||
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];
|
||||
|
||||
+12
-58
@@ -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<string, boolean>;
|
||||
}
|
||||
|
||||
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<User, "email" | "name" | "gender"> {
|
||||
password: string;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
-14
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user