feat: migrate to LiveKit, remove Agora/payment, add domain certs for Caddy
Sync to CNB / sync (push) Has been cancelled

This commit is contained in:
2026-07-04 07:15:55 +08:00
parent f3f18140f5
commit 7a9e004cbc
89 changed files with 1067 additions and 2762 deletions
+3
View File
@@ -21,6 +21,8 @@ const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"
const createEnvironmentHash = require("./webpack/persistentCache/createEnvironmentHash"); const createEnvironmentHash = require("./webpack/persistentCache/createEnvironmentHash");
// Source maps are resource heavy and can cause out of memory issue for large source files. // Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false"; 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 reactRefreshRuntimeEntry = require.resolve("react-refresh/runtime");
const reactRefreshWebpackPluginRuntimeEntry = require.resolve( const reactRefreshWebpackPluginRuntimeEntry = require.resolve(
@@ -404,6 +406,7 @@ module.exports = function (webpackEnv) {
isEnvProduction && isEnvProduction &&
!process.env.REACT_APP_RELEASE && !process.env.REACT_APP_RELEASE &&
!process.env.REACT_APP_OFFICIAL_DEMO && !process.env.REACT_APP_OFFICIAL_DEMO &&
shouldAnalyzeBundle &&
new BundleAnalyzerPlugin({ new BundleAnalyzerPlugin({
openAnalyzer: false openAnalyzer: false
}), }),
+1 -1
View File
@@ -24,7 +24,6 @@
"@udecode/plate-paragraph": "^33.0.0", "@udecode/plate-paragraph": "^33.0.0",
"@udecode/plate-trailing-block": "^33.0.0", "@udecode/plate-trailing-block": "^33.0.0",
"@uiball/loaders": "^1.3.1", "@uiball/loaders": "^1.3.1",
"agora-rtc-sdk-ng": "^4.24.0",
"broadcast-channel": "^7.1.0", "broadcast-channel": "^7.1.0",
"browserslist": "^4.25.1", "browserslist": "^4.25.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -45,6 +44,7 @@
"linkify-react": "^4.3.2", "linkify-react": "^4.3.2",
"linkify-string": "^4.3.2", "linkify-string": "^4.3.2",
"linkifyjs": "^4.3.2", "linkifyjs": "^4.3.2",
"livekit-client": "^2.20.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"localforage-setitems": "^1.4.0", "localforage-setitems": "^1.4.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
+79 -147
View File
@@ -79,9 +79,6 @@ importers:
'@uiball/loaders': '@uiball/loaders':
specifier: ^1.3.1 specifier: ^1.3.1
version: 1.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.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: broadcast-channel:
specifier: ^7.1.0 specifier: ^7.1.0
version: 7.1.0 version: 7.1.0
@@ -142,6 +139,9 @@ importers:
linkifyjs: linkifyjs:
specifier: ^4.3.2 specifier: ^4.3.2
version: 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: localforage:
specifier: ^1.10.0 specifier: ^1.10.0
version: 1.10.0 version: 1.10.0
@@ -353,15 +353,6 @@ importers:
packages: 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': '@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1053,6 +1044,9 @@ packages:
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@bufbuild/protobuf@1.10.1':
resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==}
'@discoveryjs/json-ext@0.5.7': '@discoveryjs/json-ext@0.5.7':
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@@ -1409,6 +1403,12 @@ packages:
'@leichtgewicht/ip-codec@2.0.5': '@leichtgewicht/ip-codec@2.0.5':
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} 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': '@metamask/onboarding@1.0.1':
resolution: {integrity: sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ==} resolution: {integrity: sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ==}
@@ -1918,6 +1918,9 @@ packages:
'@types/debug@4.1.12': '@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} 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': '@types/eslint-scope@3.7.7':
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
@@ -2370,12 +2373,6 @@ packages:
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
engines: {node: '>= 10.0.0'} 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: ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies: peerDependencies:
@@ -2463,9 +2460,6 @@ packages:
async@3.2.6: async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
at-least-node@1.0.0: at-least-node@1.0.0:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'} engines: {node: '>= 4.0.0'}
@@ -2481,9 +2475,6 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
axios@1.9.0:
resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
babel-loader@9.2.1: babel-loader@9.2.1:
resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==}
engines: {node: '>= 14.15.0'} engines: {node: '>= 14.15.0'}
@@ -2667,10 +2658,6 @@ packages:
colorette@2.0.20: colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} 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: comma-separated-tokens@1.0.8:
resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==}
@@ -2950,10 +2937,6 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
depd@1.1.2: depd@1.1.2:
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -3257,10 +3240,6 @@ packages:
resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
engines: {node: '>=0.8.0'} 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: file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@@ -3356,18 +3335,10 @@ packages:
vue-template-compiler: vue-template-compiler:
optional: true optional: true
form-data@4.0.1:
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
engines: {node: '>= 6'}
format@0.2.2: format@0.2.2:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'} 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: forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -3938,6 +3909,9 @@ packages:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true hasBin: true
jose@6.2.3:
resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
jotai-optics@0.3.2: jotai-optics@0.3.2:
resolution: {integrity: sha512-RH6SvqU5hmkVqnHmaqf9zBXvIAs4jLxkDHS4fr5ljuBKHs8+HQ02v+9hX7ahTppxx6dUb0GGUE80jQKJ0kFTLw==} resolution: {integrity: sha512-RH6SvqU5hmkVqnHmaqf9zBXvIAs4jLxkDHS4fr5ljuBKHs8+HQ02v+9hX7ahTppxx6dUb0GGUE80jQKJ0kFTLw==}
peerDependencies: peerDependencies:
@@ -4070,6 +4044,11 @@ packages:
linkifyjs@4.3.2: linkifyjs@4.3.2:
resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} 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: loader-runner@4.3.0:
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
engines: {node: '>=6.11.5'} engines: {node: '>=6.11.5'}
@@ -4124,6 +4103,10 @@ packages:
lodash@4.17.21: lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
loglevel@1.9.2:
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
engines: {node: '>= 0.6.0'}
long@5.2.5: long@5.2.5:
resolution: {integrity: sha512-e0r9YBBgNCq1D1o5Dp8FMH0N5hsFtXDBiVa0qoJPHpakvZkmDKPRoGffZJII/XsHvj9An9blm+cRJ01yQqU+Dw==} resolution: {integrity: sha512-e0r9YBBgNCq1D1o5Dp8FMH0N5hsFtXDBiVa0qoJPHpakvZkmDKPRoGffZJII/XsHvj9An9blm+cRJ01yQqU+Dw==}
@@ -4373,11 +4356,6 @@ packages:
no-case@3.0.4: no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} 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: node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0} engines: {node: 4.x || >=6.0.0}
@@ -4527,9 +4505,6 @@ packages:
package-json-from-dist@1.0.1: package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
param-case@3.0.4: param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
@@ -4940,9 +4915,6 @@ packages:
proxy-compare@2.6.0: proxy-compare@2.6.0:
resolution: {integrity: sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==} resolution: {integrity: sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -5289,6 +5261,9 @@ packages:
run-parallel@1.2.0: run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
safe-array-concat@1.1.3: safe-array-concat@1.1.3:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
@@ -5328,6 +5303,10 @@ packages:
scroll-into-view-if-needed@3.1.0: scroll-into-view-if-needed@3.1.0:
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
sdp-transform@2.15.0:
resolution: {integrity: sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==}
hasBin: true
sdp@3.2.0: sdp@3.2.0:
resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==} resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==}
@@ -5491,6 +5470,7 @@ packages:
source-map@0.8.0-beta.0: source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'} 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: sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
@@ -5750,6 +5730,9 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
typed-emitter@2.1.0:
resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==}
typescript-eslint@8.39.0: typescript-eslint@8.39.0:
resolution: {integrity: sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==} resolution: {integrity: sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -5762,10 +5745,6 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
ua-parser-js@0.7.40:
resolution: {integrity: sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==}
hasBin: true
unbox-primitive@1.1.0: unbox-primitive@1.1.0:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -5975,10 +5954,6 @@ packages:
web-namespaces@2.0.1: web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} 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: web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
@@ -6039,8 +6014,8 @@ packages:
webpack-cli: webpack-cli:
optional: true optional: true
webrtc-adapter@8.2.0: webrtc-adapter@9.0.5:
resolution: {integrity: sha512-umxCMgedPAVq4Pe/jl3xmelLXLn4XZWFEMR5Iipb5wJ+k1xMX0yC4ZY9CueZUU1MjapFxai1tFGE7R/kotH6Ww==} resolution: {integrity: sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==}
engines: {node: '>=6.0.0', npm: '>=3.10.0'} engines: {node: '>=6.0.0', npm: '>=3.10.0'}
websocket-driver@0.7.4: websocket-driver@0.7.4:
@@ -6251,30 +6226,6 @@ packages:
snapshots: 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': {} '@alloc/quick-lru@5.2.0': {}
'@ampproject/remapping@2.3.0': '@ampproject/remapping@2.3.0':
@@ -7162,6 +7113,8 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1 '@babel/helper-validator-identifier': 7.27.1
'@bufbuild/protobuf@1.10.1': {}
'@discoveryjs/json-ext@0.5.7': {} '@discoveryjs/json-ext@0.5.7': {}
'@emoji-mart/data@1.2.1': {} '@emoji-mart/data@1.2.1': {}
@@ -7642,6 +7595,12 @@ snapshots:
'@leichtgewicht/ip-codec@2.0.5': {} '@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': '@metamask/onboarding@1.0.1':
dependencies: dependencies:
bowser: 2.11.0 bowser: 2.11.0
@@ -8122,6 +8081,8 @@ snapshots:
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/dom-mediacapture-record@1.0.22': {}
'@types/eslint-scope@3.7.7': '@types/eslint-scope@3.7.7':
dependencies: dependencies:
'@types/eslint': 9.6.1 '@types/eslint': 9.6.1
@@ -8691,22 +8652,6 @@ snapshots:
address@1.2.2: {} 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): ajv-formats@2.1.1(ajv@8.17.1):
optionalDependencies: optionalDependencies:
ajv: 8.17.1 ajv: 8.17.1
@@ -8786,8 +8731,6 @@ snapshots:
async@3.2.6: {} async@3.2.6: {}
asynckit@0.4.0: {}
at-least-node@1.0.0: {} at-least-node@1.0.0: {}
autoprefixer@10.4.21(postcss@8.5.6): autoprefixer@10.4.21(postcss@8.5.6):
@@ -8804,14 +8747,6 @@ snapshots:
dependencies: dependencies:
possible-typed-array-names: 1.1.0 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): babel-loader@9.2.1(@babel/core@7.28.0)(webpack@5.101.0):
dependencies: dependencies:
'@babel/core': 7.28.0 '@babel/core': 7.28.0
@@ -9040,10 +8975,6 @@ snapshots:
colorette@2.0.20: {} colorette@2.0.20: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
comma-separated-tokens@1.0.8: {} comma-separated-tokens@1.0.8: {}
comma-separated-tokens@2.0.3: {} comma-separated-tokens@2.0.3: {}
@@ -9323,8 +9254,6 @@ snapshots:
has-property-descriptors: 1.0.2 has-property-descriptors: 1.0.2
object-keys: 1.1.1 object-keys: 1.1.1
delayed-stream@1.0.0: {}
depd@1.1.2: {} depd@1.1.2: {}
depd@2.0.0: {} depd@2.0.0: {}
@@ -9724,11 +9653,6 @@ snapshots:
dependencies: dependencies:
websocket-driver: 0.7.4 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: file-entry-cache@8.0.0:
dependencies: dependencies:
flat-cache: 4.0.1 flat-cache: 4.0.1
@@ -9868,18 +9792,8 @@ snapshots:
optionalDependencies: optionalDependencies:
eslint: 9.32.0(jiti@1.21.7) 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: {} format@0.2.2: {}
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
forwarded@0.2.0: {} forwarded@0.2.0: {}
fraction.js@4.3.7: {} fraction.js@4.3.7: {}
@@ -10488,6 +10402,8 @@ snapshots:
jiti@1.21.7: {} 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): jotai-optics@0.3.2(jotai@2.11.3(@types/react@19.1.9)(react@19.1.1))(optics-ts@2.4.1):
dependencies: dependencies:
jotai: 2.11.3(@types/react@19.1.9)(react@19.1.1) jotai: 2.11.3(@types/react@19.1.9)(react@19.1.1)
@@ -10584,6 +10500,19 @@ snapshots:
linkifyjs@4.3.2: {} 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-runner@4.3.0: {}
loader-utils@3.3.1: {} loader-utils@3.3.1: {}
@@ -10629,6 +10558,8 @@ snapshots:
lodash@4.17.21: {} lodash@4.17.21: {}
loglevel@1.9.2: {}
long@5.2.5: {} long@5.2.5: {}
longest-streak@3.1.0: {} longest-streak@3.1.0: {}
@@ -10959,8 +10890,6 @@ snapshots:
lower-case: 2.0.2 lower-case: 2.0.2
tslib: 2.8.1 tslib: 2.8.1
node-domexception@1.0.0: {}
node-fetch@2.7.0: node-fetch@2.7.0:
dependencies: dependencies:
whatwg-url: 5.0.0 whatwg-url: 5.0.0
@@ -11097,8 +11026,6 @@ snapshots:
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
pako@2.1.0: {}
param-case@3.0.4: param-case@3.0.4:
dependencies: dependencies:
dot-case: 3.0.4 dot-case: 3.0.4
@@ -11502,8 +11429,6 @@ snapshots:
proxy-compare@2.6.0: {} proxy-compare@2.6.0: {}
proxy-from-env@1.1.0: {}
punycode@2.3.1: {} punycode@2.3.1: {}
qrcode.react@4.2.0(react@19.1.1): qrcode.react@4.2.0(react@19.1.1):
@@ -11880,6 +11805,11 @@ snapshots:
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
rxjs@7.8.2:
dependencies:
tslib: 2.8.1
optional: true
safe-array-concat@1.1.3: safe-array-concat@1.1.3:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
@@ -11931,6 +11861,8 @@ snapshots:
dependencies: dependencies:
compute-scroll-into-view: 3.1.1 compute-scroll-into-view: 3.1.1
sdp-transform@2.15.0: {}
sdp@3.2.0: {} sdp@3.2.0: {}
select-hose@2.0.0: {} select-hose@2.0.0: {}
@@ -12452,6 +12384,10 @@ snapshots:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10 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): typescript-eslint@8.39.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.2):
dependencies: 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) '@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: {} typescript@5.9.2: {}
ua-parser-js@0.7.40: {}
unbox-primitive@1.1.0: unbox-primitive@1.1.0:
dependencies: dependencies:
call-bound: 1.0.3 call-bound: 1.0.3
@@ -12645,8 +12579,6 @@ snapshots:
web-namespaces@2.0.1: {} web-namespaces@2.0.1: {}
web-streams-polyfill@3.3.3: {}
web-vitals@4.2.4: {} web-vitals@4.2.4: {}
webidl-conversions@3.0.1: {} webidl-conversions@3.0.1: {}
@@ -12770,7 +12702,7 @@ snapshots:
- esbuild - esbuild
- uglify-js - uglify-js
webrtc-adapter@8.2.0: webrtc-adapter@9.0.5:
dependencies: dependencies:
sdp: 3.2.0 sdp: 3.2.0
+1 -9
View File
@@ -11,7 +11,6 @@
"welcome_channel": "Willkommen in {{name}}", "welcome_channel": "Willkommen in {{name}}",
"welcome_desc": "Dies ist der Beginn des Kanals #{{name}}.", "welcome_desc": "Dies ist der Beginn des Kanals #{{name}}.",
"edit_channel": "Kanal bearbeiten", "edit_channel": "Kanal bearbeiten",
"channel_name": "Kanalname", "channel_name": "Kanalname",
"private_channel": "Privater Kanal", "private_channel": "Privater Kanal",
"create_channel": "Neuen Kanal erstellen", "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.", "create_private_channel_desc": "Dies ist ein privater Kanal, nur ausgewählte Mitglieder können beitreten.",
"search_user_placeholder": "Benutzernamen zum Suchen eingeben", "search_user_placeholder": "Benutzernamen zum Suchen eingeben",
"welcome_msg": "Willkommen im Kanal {{name}}", "welcome_msg": "Willkommen im Kanal {{name}}",
"invite_title": "Freunde zu {{name}} einladen", "invite_title": "Freunde zu {{name}} einladen",
"invite_by_email": "Per E-Mail einladen", "invite_by_email": "Per E-Mail einladen",
"enable_smtp": "Zuerst SMTP aktivieren", "enable_smtp": "Zuerst SMTP aktivieren",
@@ -29,23 +27,18 @@
"invite_link_edit": "Einladungslink bearbeiten", "invite_link_edit": "Einladungslink bearbeiten",
"invite_link_setting_tip": "Einladungslink läuft ab in {{expire}}, maximale Nutzungen: {{times}}", "invite_link_setting_tip": "Einladungslink läuft ab in {{expire}}, maximale Nutzungen: {{times}}",
"generate_new_link": "Neuen Link generieren", "generate_new_link": "Neuen Link generieren",
"send_to": "Senden an", "send_to": "Senden an",
"edited": "bearbeitet", "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_title": "Nachricht löschen",
"delete_msg_desc": "Bist du sicher, dass du {{msg}} löschen möchtest?", "delete_msg_desc": "Bist du sicher, dass du {{msg}} löschen möchtest?",
"delete_msg_this": "diese Nachricht", "delete_msg_this": "diese Nachricht",
"delete_msg_these": "diese Nachrichten", "delete_msg_these": "diese Nachrichten",
"new_msg": "{{num}} neue Nachrichten", "new_msg": "{{num}} neue Nachrichten",
"mark_read": "Als gelesen markieren", "mark_read": "Als gelesen markieren",
"pin_chat": "Chat anheften", "pin_chat": "Chat anheften",
"unpin_chat": "Lösen", "unpin_chat": "Lösen",
"reply_msg_del": "Diese Nachricht wurde gelöscht.", "reply_msg_del": "Diese Nachricht wurde gelöscht.",
"file": "Datei", "file": "Datei",
"image": "Bild", "image": "Bild",
"forward": "Weiterleiten", "forward": "Weiterleiten",
@@ -65,7 +58,6 @@
"contact_block_tip": "Dieser Benutzer wurde von dir blockiert", "contact_block_tip": "Dieser Benutzer wurde von dir blockiert",
"file_expired": "Datei abgelaufen", "file_expired": "Datei abgelaufen",
"only_owner_can_send": "Nur der Kanalbesitzer kann Nachrichten senden!", "only_owner_can_send": "Nur der Kanalbesitzer kann Nachrichten senden!",
"remark": "Spitzname hinzufügen", "remark": "Spitzname hinzufügen",
"remark_clear": "Spitzname zurücksetzen", "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.", "remark_intro": "Finde Freunde schneller mit einem persönlichen Spitznamen. Dieser ist nur für dich in deinen Direktnachrichten sichtbar.",
+1 -15
View File
@@ -51,7 +51,6 @@
"install": "Installieren", "install": "Installieren",
"download_origin": "Original herunterladen", "download_origin": "Original herunterladen",
"close": "Schließen", "close": "Schließen",
"upgrade": "Upgrade",
"view_pwd": "Passwort anzeigen", "view_pwd": "Passwort anzeigen",
"change_pwd": "Passwort ändern" "change_pwd": "Passwort ändern"
}, },
@@ -59,7 +58,6 @@
"uploading": "Wird hochgeladen" "uploading": "Wird hochgeladen"
}, },
"tip": { "tip": {
"email_msg_tip": "E-Mail-Benachrichtigungen sind in deiner Region blockiert. Upgrade auf Pro für diese Funktion.",
"update": "Erfolgreich aktualisiert!", "update": "Erfolgreich aktualisiert!",
"delete": "Erfolgreich gelöscht!", "delete": "Erfolgreich gelöscht!",
"reg": "Erfolgreich registriert!", "reg": "Erfolgreich registriert!",
@@ -71,22 +69,10 @@
}, },
"new_version": "<0>Neue Version</0> verfügbar", "new_version": "<0>Neue Version</0> verfügbar",
"mobile_app": "Entdecke unsere <0>Mobile App</0>", "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": { "server_update": {
"version_needed": "Diese Funktion benötigt Server-Version: <0>{{version}}</0> 🚨", "version_needed": "Diese Funktion benötigt Server-Version: <0>{{version}}</0> 🚨",
"current_version": "Aktuelle 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" "howto": "Anleitung zum Update"
}, },
"inactive": { "inactive": {
-2
View File
@@ -12,7 +12,6 @@
"current_pwd": "Aktuelles Passwort", "current_pwd": "Aktuelles Passwort",
"new_pwd": "Neues Passwort", "new_pwd": "Neues Passwort",
"confirm_new_pwd": "Neues Passwort bestätigen", "confirm_new_pwd": "Neues Passwort bestätigen",
"manage_members": "Kontakte verwalten", "manage_members": "Kontakte verwalten",
"manage_tip": "Deaktivieren ermöglicht späteres Wiederherstellen des Kontos.", "manage_tip": "Deaktivieren ermöglicht späteres Wiederherstellen des Kontos.",
"admin": "Admin", "admin": "Admin",
@@ -28,7 +27,6 @@
"roles": "Rollen", "roles": "Rollen",
"set_admin": "Admin", "set_admin": "Admin",
"set_normal": "Benutzer", "set_normal": "Benutzer",
"search_not_found": "Nicht gefunden oder Suchberechtigung fehlt.", "search_not_found": "Nicht gefunden oder Suchberechtigung fehlt.",
"search_by_id": "Nach ID suchen", "search_by_id": "Nach ID suchen",
"search_by_id_ph": "Benutzer-ID eingeben", "search_by_id_ph": "Benutzer-ID eingeben",
+3 -45
View File
@@ -15,7 +15,6 @@
"bot": "Bot & Webhook", "bot": "Bot & Webhook",
"notification_channels": "Benachrichtigungskanäle", "notification_channels": "Benachrichtigungskanäle",
"firebase": "Firebase", "firebase": "Firebase",
"agora": "Agora",
"smtp": "SMTP", "smtp": "SMTP",
"login_method": "Anmeldemethoden", "login_method": "Anmeldemethoden",
"third_app": "Anmeldung über Drittanbieter", "third_app": "Anmeldung über Drittanbieter",
@@ -27,26 +26,6 @@
"video": "Videoanrufe", "video": "Videoanrufe",
"notification": "Benachrichtigungen" "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": { "channel": {
"leave": "Kanal verlassen", "leave": "Kanal verlassen",
"leave_desc": "Bist du sicher, dass du diesen Kanal verlassen möchtest?", "leave_desc": "Bist du sicher, dass du diesen Kanal verlassen möchtest?",
@@ -197,23 +176,8 @@
"expire": "Läuft ab am", "expire": "Läuft ab am",
"create": "Erstellt am", "create": "Erstellt am",
"value": "Lizenzwert", "value": "Lizenzwert",
"renew": "Lizenz verlängern",
"update": "Manuell aktualisieren", "update": "Manuell aktualisieren",
"update_placeholder": "Neuen Lizenzwert eingeben", "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"
}, },
"bot": { "bot": {
"add_api_key": "API-Schlüssel hinzufügen", "add_api_key": "API-Schlüssel hinzufügen",
@@ -273,7 +237,7 @@
"client_email": "Client-E-Mail" "client_email": "Client-E-Mail"
}, },
"smtp": { "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:", "sub_desc": "SMTP-Einstellungen konfigurieren:",
"enable": "Aktivieren", "enable": "Aktivieren",
"host": "Host", "host": "Host",
@@ -284,12 +248,6 @@
"how_to": "SMTP einrichten?", "how_to": "SMTP einrichten?",
"send_test_email": "Test-E-Mail senden" "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": { "third_app": {
"key": "API-Sicherheitsschlüssel", "key": "API-Sicherheitsschlüssel",
"update": "Geheimnis aktualisieren", "update": "Geheimnis aktualisieren",
@@ -352,7 +310,7 @@
"step_1": "Schritt 1: Eigene Server-URL eintragen", "step_1": "Schritt 1: Eigene Server-URL eintragen",
"step_2": "Schritt 2: Login-Token eingeben", "step_2": "Schritt 2: Login-Token eingeben",
"step_2_desc": "Token per X-API-Key-Header abrufen oder unten kopieren:", "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": { "notification": {
"desc": "Benachrichtigungseinstellungen für verschiedene Kanäle konfigurieren", "desc": "Benachrichtigungseinstellungen für verschiedene Kanäle konfigurieren",
+3 -3
View File
@@ -7,8 +7,7 @@
"download": "Mobile Apps herunterladen", "download": "Mobile Apps herunterladen",
"help": "Fragen? Besuche unser Hilfe-Center", "help": "Fragen? Besuche unser Hilfe-Center",
"sign_in_tip": "Zum Senden anmelden", "sign_in_tip": "Zum Senden anmelden",
"vocespace": " VoceSpace Videoanruf", "license": "VoceChat Lizenz",
"license": "VoceChat Upgrade-Lizenz",
"page_editor": { "page_editor": {
"title": "Page Management", "title": "Page Management",
"status_select": "Select State", "status_select": "Select State",
@@ -66,5 +65,6 @@
"tunnel_progress": "Starting tunnel...", "tunnel_progress": "Starting tunnel...",
"tunnel_error": "Tunnel error", "tunnel_error": "Tunnel error",
"tunnel_get_domain": "Get Public Domain" "tunnel_get_domain": "Get Public Domain"
} },
"video": "Video"
} }
+1 -10
View File
@@ -18,7 +18,6 @@
"created_at": "Created", "created_at": "Created",
"updated_at": "Last updated", "updated_at": "Last updated",
"dismiss_announcement": "Dismiss announcement", "dismiss_announcement": "Dismiss announcement",
"channel_name": "Channel Name", "channel_name": "Channel Name",
"private_channel": "Private Channel", "private_channel": "Private Channel",
"create_channel": "Create New 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.", "create_private_channel_desc": "This is a private channel, only select members will be able to join.",
"search_user_placeholder": "Type Username to search", "search_user_placeholder": "Type Username to search",
"welcome_msg": "Welcome to channel {{name}}", "welcome_msg": "Welcome to channel {{name}}",
"invite_title": "Add friends to {{name}}", "invite_title": "Add friends to {{name}}",
"invite_by_email": "Invite by Email", "invite_by_email": "Invite by Email",
"enable_smtp": "Enable SMTP First", "enable_smtp": "Enable SMTP First",
@@ -36,23 +34,18 @@
"invite_link_edit": "Edit invite link", "invite_link_edit": "Edit invite link",
"invite_link_setting_tip": "Invite link expires in {{expire}}, max use times: {{times}}", "invite_link_setting_tip": "Invite link expires in {{expire}}, max use times: {{times}}",
"generate_new_link": "Generate New Link", "generate_new_link": "Generate New Link",
"send_to": "Send To", "send_to": "Send To",
"edited": "edited", "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_title": "Delete Message",
"delete_msg_desc": "Are you sure want to delete {{msg}}?", "delete_msg_desc": "Are you sure want to delete {{msg}}?",
"delete_msg_this": "this message", "delete_msg_this": "this message",
"delete_msg_these": "these messages", "delete_msg_these": "these messages",
"new_msg": "{{num}} new messages", "new_msg": "{{num}} new messages",
"mark_read": "Mark as Read", "mark_read": "Mark as Read",
"pin_chat": "Pin Chat", "pin_chat": "Pin Chat",
"unpin_chat": "Unpin", "unpin_chat": "Unpin",
"reply_msg_del": "This message has been deleted.", "reply_msg_del": "This message has been deleted.",
"file": "file", "file": "file",
"image": "image", "image": "image",
"forward": "forward", "forward": "forward",
@@ -72,12 +65,10 @@
"contact_block_tip": "This user has been blocked by you", "contact_block_tip": "This user has been blocked by you",
"file_expired": "File Expired", "file_expired": "File Expired",
"only_owner_can_send": "only Channel owner can send message!", "only_owner_can_send": "only Channel owner can send message!",
"remark": "Add friend nickname", "remark": "Add friend nickname",
"remark_clear": "Reset 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_intro": "Find a friend faster with a personal nickname. It will only be visible to you in your direct messages.",
"remark_placeholder": "Set Nickname", "remark_placeholder": "Set Nickname",
"guest_mode_warning": { "guest_mode_warning": {
"title": "This Channel Will Be Visible to Guests", "title": "This Channel Will Be Visible to Guests",
"desc": "If you continue, guest users may be able to see messages in this public channel.", "desc": "If you continue, guest users may be able to see messages in this public channel.",
+1 -15
View File
@@ -51,7 +51,6 @@
"install": "Install", "install": "Install",
"download_origin": "Download original", "download_origin": "Download original",
"close": "Close", "close": "Close",
"upgrade": "Upgrade",
"view_pwd": "View Password", "view_pwd": "View Password",
"change_pwd": "Update Password" "change_pwd": "Update Password"
}, },
@@ -59,7 +58,6 @@
"uploading": "Uploading" "uploading": "Uploading"
}, },
"tip": { "tip": {
"email_msg_tip": "Your region has blocked notifications, upgrade to Pro to use email notification.",
"update": "Update Successfully!", "update": "Update Successfully!",
"delete": "Delete Successfully!", "delete": "Delete Successfully!",
"reg": "Register Successfully!", "reg": "Register Successfully!",
@@ -71,22 +69,10 @@
}, },
"new_version": "<0>New Version</0> Available", "new_version": "<0>New Version</0> Available",
"mobile_app": "Check out our <0>Mobile APP</0>", "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": { "server_update": {
"version_needed": "This function needs server version:<0>{{version}}</0> at least 🚨", "version_needed": "This function needs server version:<0>{{version}}</0> at least 🚨",
"current_version": "Your current version:<0>{{version}}</0>", "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" "howto": "How to Update VoceChat Server"
}, },
"inactive": { "inactive": {
-2
View File
@@ -12,7 +12,6 @@
"current_pwd": "Current Password", "current_pwd": "Current Password",
"new_pwd": "New Password", "new_pwd": "New Password",
"confirm_new_pwd": "Confirm New Password", "confirm_new_pwd": "Confirm New Password",
"manage_members": "Manage Contacts", "manage_members": "Manage Contacts",
"manage_tip": "Disabling your account means you can recover it at any time after taking this action.", "manage_tip": "Disabling your account means you can recover it at any time after taking this action.",
"admin": "Admin", "admin": "Admin",
@@ -28,7 +27,6 @@
"roles": "Roles", "roles": "Roles",
"set_admin": "Admin", "set_admin": "Admin",
"set_normal": "User", "set_normal": "User",
"search_not_found": "Not found, or this user is not allowed to be searched.", "search_not_found": "Not found, or this user is not allowed to be searched.",
"search_disabled": "User search has been disabled by the administrator.", "search_disabled": "User search has been disabled by the administrator.",
"add_to_contact": "Add to contact", "add_to_contact": "Add to contact",
+3 -46
View File
@@ -16,8 +16,6 @@
"bot": "Bot & Webhook", "bot": "Bot & Webhook",
"notification_channels": "Notification Channels", "notification_channels": "Notification Channels",
"firebase": "Firebase", "firebase": "Firebase",
"agora": "Agora",
"vocespace": "Video",
"smtp": "SMTP", "smtp": "SMTP",
"login_method": "Login Methods", "login_method": "Login Methods",
"third_app": "Third-party Login", "third_app": "Third-party Login",
@@ -201,23 +199,8 @@
"expire": "Expired At", "expire": "Expired At",
"create": "Created At", "create": "Created At",
"value": "License Value", "value": "License Value",
"renew": "Renew License",
"update": "Update Manually", "update": "Update Manually",
"update_placeholder": "Please input the new license value", "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"
}, },
"bot": { "bot": {
"add_api_key": "Add API Key", "add_api_key": "Add API Key",
@@ -277,7 +260,7 @@
"client_email": "Client Email" "client_email": "Client Email"
}, },
"smtp": { "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.", "sub_desc": "Please set up your SMTP below.",
"enable": "Enable", "enable": "Enable",
"host": "Host", "host": "Host",
@@ -288,32 +271,6 @@
"how_to": "How to setup SMTP?", "how_to": "How to setup SMTP?",
"send_test_email": "Send Test Email" "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": { "third_app": {
"key": "API Secure Key", "key": "API Secure Key",
"update": "Update Secret", "update": "Update Secret",
@@ -480,4 +437,4 @@
"error": "Error" "error": "Error"
} }
} }
} }
+3 -3
View File
@@ -7,8 +7,7 @@
"download": "Download Mobile apps", "download": "Download Mobile apps",
"help": "Got questions? Visit our help center", "help": "Got questions? Visit our help center",
"sign_in_tip": "Please sign in to send message", "sign_in_tip": "Please sign in to send message",
"vocespace": "VoceSpace Video Call", "license": "VoceChat License",
"license": "VoceChat Upgrade License",
"page_editor": { "page_editor": {
"title": "Page Management", "title": "Page Management",
"status_select": "Select State", "status_select": "Select State",
@@ -66,5 +65,6 @@
"tunnel_progress": "Starting tunnel...", "tunnel_progress": "Starting tunnel...",
"tunnel_error": "Tunnel error", "tunnel_error": "Tunnel error",
"tunnel_get_domain": "Get Public Domain" "tunnel_get_domain": "Get Public Domain"
} },
"video": "Video"
} }
+3 -3
View File
@@ -43,9 +43,9 @@
"github_logging_in": "Iniciando sesión con GitHub...", "github_logging_in": "Iniciando sesión con GitHub...",
"github_cb_tip": "Por favor, cierra esta ventana y vuelve a la ventana del widget", "github_cb_tip": "Por favor, cierra esta ventana y vuelve a la ventana del widget",
"magic_link_expire": { "magic_link_expire": {
"title": "Enlace mágico inválido o caducado", "title": "Enlace mágico inválido o caducado",
"desc": "Por favor, solicita un nuevo enlace mágico.", "desc": "Por favor, solicita un nuevo enlace mágico.",
"desc_close": "Puedes cerrar esta ventana ahora." "desc_close": "Puedes cerrar esta ventana ahora."
}, },
"invite_mobile": { "invite_mobile": {
"join": "Únete a nuestro servidor", "join": "Únete a nuestro servidor",
+15 -24
View File
@@ -1,26 +1,23 @@
{ {
"pin": "Fijar", "pin": "Fijar",
"pin_desc": "¿Quieres fijar este mensaje en", "pin_desc": "¿Quieres fijar este mensaje en",
"pinned_msg": "Mensaje Fijado", "pinned_msg": "Mensaje Fijado",
"pin_empty_tip": "Este canal aún no tiene ningún mensaje fijado.", "pin_empty_tip": "Este canal aún no tiene ningún mensaje fijado.",
"fav": "Favorito", "fav": "Favorito",
"fav_msg": "Mensaje Guardado", "fav_msg": "Mensaje Guardado",
"fav_empty_tip": "Este canal aún no tiene ningún mensaje guardado.", "fav_empty_tip": "Este canal aún no tiene ningún mensaje guardado.",
"channel_members": "Miembros del Canal", "channel_members": "Miembros del Canal",
"add_channel_members": "Agregar miembros", "add_channel_members": "Agregar miembros",
"welcome_channel": "Bienvenido a {{name}}", "welcome_channel": "Bienvenido a {{name}}",
"welcome_desc": "Este es el inicio del canal #{{name}}.", "welcome_desc": "Este es el inicio del canal #{{name}}.",
"edit_channel": "Editar Canal" "edit_channel": "Editar Canal",
"channel_name": "Nombre del Canal", "channel_name": "Nombre del Canal",
"private_channel": "Canal Privado", "private_channel": "Canal Privado",
"create_channel": "Crear Nuevo Canal", "create_channel": "Crear Nuevo Canal",
"create_channel_desc": "Este es un canal público, todos en este equipo pueden verlo.", "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.", "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", "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_title": "Agregar amigos a {{name}}",
"invite_by_email": "Invitar por Correo Electrónico", "invite_by_email": "Invitar por Correo Electrónico",
"enable_smtp": "Habilitar SMTP Primero", "enable_smtp": "Habilitar SMTP Primero",
@@ -29,24 +26,19 @@
"invite_link_faq": "¿Enlace de invitación incorrecto?", "invite_link_faq": "¿Enlace de invitación incorrecto?",
"invite_link_edit": "Editar enlace de invitación", "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}}", "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", "send_to": "Enviar a",
"edited": "editado", "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_title": "Eliminar Mensaje",
"delete_msg_desc": "¿Estás seguro de que quieres eliminar {{msg}}?", "delete_msg_desc": "¿Estás seguro de que quieres eliminar {{msg}}?",
"delete_msg_this": "este mensaje", "delete_msg_this": "este mensaje",
"delete_msg_these": "estos mensajes", "delete_msg_these": "estos mensajes",
"new_msg": "{{num}} nuevos mensajes", "new_msg": "{{num}} nuevos mensajes",
"mark_read": "Marcar como Leído", "mark_read": "Marcar como Leído",
"pin_chat": "Fijar Chat", "pin_chat": "Fijar Chat",
"unpin_chat": "Desfijar", "unpin_chat": "Desfijar",
"reply_msg_del": "Este mensaje ha sido eliminado.", "reply_msg_del": "Este mensaje ha sido eliminado.",
"file": "archivo", "file": "archivo",
"image": "imagen", "image": "imagen",
"forward": "reenviar", "forward": "reenviar",
@@ -65,5 +57,4 @@
"contact_tip": "Este usuario no está en tus contactos", "contact_tip": "Este usuario no está en tus contactos",
"contact_block_tip": "Este usuario ha sido bloqueado por ti", "contact_block_tip": "Este usuario ha sido bloqueado por ti",
"file_expired": "Archivo Expirado" "file_expired": "Archivo Expirado"
} }
-12
View File
@@ -67,18 +67,6 @@
}, },
"new_version": "<0>Nueva Versión</0> Disponible", "new_version": "<0>Nueva Versión</0> Disponible",
"mobile_app": "Echa un vistazo a nuestra <0>Aplicación Móvil</0>", "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": { "server_update": {
"version_needed": "Esta función requiere la versión del servidor:<0>{{version}}</0> como mínimo 🚨", "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>", "current_version": "Tu versión actual:<0>{{version}}</0>",
-2
View File
@@ -12,7 +12,6 @@
"current_pwd": "Contraseña actual", "current_pwd": "Contraseña actual",
"new_pwd": "Nueva contraseña", "new_pwd": "Nueva contraseña",
"confirm_new_pwd": "Confirmar nueva contraseña", "confirm_new_pwd": "Confirmar nueva contraseña",
"manage_members": "Gestionar Contactos", "manage_members": "Gestionar Contactos",
"manage_tip": "Desactivar tu cuenta significa que puedes recuperarla en cualquier momento después de tomar esta acción.", "manage_tip": "Desactivar tu cuenta significa que puedes recuperarla en cualquier momento después de tomar esta acción.",
"admin": "Admin", "admin": "Admin",
@@ -28,7 +27,6 @@
"roles": "Roles", "roles": "Roles",
"set_admin": "Admin", "set_admin": "Admin",
"set_normal": "Usuario", "set_normal": "Usuario",
"search_not_found": "No encontrado, o este usuario no está permitido para ser buscado.", "search_not_found": "No encontrado, o este usuario no está permitido para ser buscado.",
"search_by_id": "Buscar por ID", "search_by_id": "Buscar por ID",
"search_by_id_ph": "Ingrese el ID de usuario", "search_by_id_ph": "Ingrese el ID de usuario",
+1 -41
View File
@@ -15,7 +15,6 @@
"bot": "Bot & Webhook", "bot": "Bot & Webhook",
"notification_channels": "Canales de Notificación", "notification_channels": "Canales de Notificación",
"firebase": "Firebase", "firebase": "Firebase",
"agora": "Agora",
"smtp": "SMTP", "smtp": "SMTP",
"login_method": "Métodos de Inicio de Sesión", "login_method": "Métodos de Inicio de Sesión",
"third_app": "Inicio de Sesión de Terceros", "third_app": "Inicio de Sesión de Terceros",
@@ -27,26 +26,6 @@
"video": "Video", "video": "Video",
"notification": "Notificaciones" "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": { "channel": {
"leave": "Abandonar Canal", "leave": "Abandonar Canal",
"leave_desc": "¿Estás seguro de que deseas abandonar este canal?", "leave_desc": "¿Estás seguro de que deseas abandonar este canal?",
@@ -146,23 +125,8 @@
"expire": "Caduca en", "expire": "Caduca en",
"create": "Creado en", "create": "Creado en",
"value": "Valor de la Licencia", "value": "Valor de la Licencia",
"renew": "Renovar Licencia",
"update": "Actualizar Manualmente", "update": "Actualizar Manualmente",
"update_placeholder": "Por favor, introduce el nuevo valor de la licencia", "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"
}, },
"bot": { "bot": {
"add_api_key": "Agregar Clave API", "add_api_key": "Agregar Clave API",
@@ -229,10 +193,6 @@
"how_to": "¿Cómo configurar SMTP?", "how_to": "¿Cómo configurar SMTP?",
"send_test_email": "Enviar Correo Electrónico de Prueba" "send_test_email": "Enviar Correo Electrónico de Prueba"
}, },
"agora": {
"enable": "Habilitar",
"how_to": "¿Cómo configurar Agora?"
},
"third_app": { "third_app": {
"key": "Clave de Seguridad API", "key": "Clave de Seguridad API",
"update": "Actualizar Secreto", "update": "Actualizar Secreto",
+3 -3
View File
@@ -7,8 +7,7 @@
"download": "Descarga las aplicaciones móviles", "download": "Descarga las aplicaciones móviles",
"help": "¿Tienes preguntas? Visita nuestro centro de ayuda", "help": "¿Tienes preguntas? Visita nuestro centro de ayuda",
"sign_in_tip": "Por favor, inicia sesión para enviar mensajes", "sign_in_tip": "Por favor, inicia sesión para enviar mensajes",
"vocespace": "Llamada de video VoceSpace", "license": "Licencia de VoceChat",
"license": "VoceChat Licencia de Actualización",
"page_editor": { "page_editor": {
"title": "Page Management", "title": "Page Management",
"status_select": "Select State", "status_select": "Select State",
@@ -66,5 +65,6 @@
"tunnel_progress": "Starting tunnel...", "tunnel_progress": "Starting tunnel...",
"tunnel_error": "Tunnel error", "tunnel_error": "Tunnel error",
"tunnel_get_domain": "Get Public Domain" "tunnel_get_domain": "Get Public Domain"
} },
"video": "Video"
} }
+74 -74
View File
@@ -1,74 +1,74 @@
{ {
"login": { "login": {
"title": "Connexion à {{name}}", "title": "Connexion à {{name}}",
"google": "Se connecter avec Google", "google": "Se connecter avec Google",
"github": "Se connecter avec GitHub", "github": "Se connecter avec GitHub",
"metamask": "Se connecter avec MetaMask", "metamask": "Se connecter avec MetaMask",
"password": "Se connecter avec Mot de Passe", "password": "Se connecter avec Mot de Passe",
"oidc": "Se connecter avec OIDC", "oidc": "Se connecter avec OIDC",
"no_account": "Vous n'avez pas de compte ?" "no_account": "Vous n'avez pas de compte ?"
}, },
"reg": { "reg": {
"title": "Inscription à {{name}}", "title": "Inscription à {{name}}",
"google": "S'inscrire avec Google", "google": "S'inscrire avec Google",
"github": "S'inscrire avec GitHub", "github": "S'inscrire avec GitHub",
"metamask": "S'inscrire avec MetaMask", "metamask": "S'inscrire avec MetaMask",
"oidc": "S'inscrire avec OIDC", "oidc": "S'inscrire avec OIDC",
"have_account": "Vous avez un compte ?", "have_account": "Vous avez un compte ?",
"input_name": "Quel est votre nom", "input_name": "Quel est votre nom",
"input_name_tip": "Entrez un nom ou un pseudo pour que les gens sachent comment vous souhaitez être appelé. Votre nom ne sera visible que dans les espaces que vous avez rejoints." "input_name_tip": "Entrez un nom ou un pseudo pour que les gens sachent comment vous souhaitez être appelé. Votre nom ne sera visible que dans les espaces que vous avez rejoints."
}, },
"logout": { "logout": {
"title": "Déconnexion", "title": "Déconnexion",
"desc": "Êtes-vous sûr de vouloir vous déconnecter de ce compte ?", "desc": "Êtes-vous sûr de vouloir vous déconnecter de ce compte ?",
"clear_local": "Effacer les données locales", "clear_local": "Effacer les données locales",
"role_changed": "Votre rôle a changé, voulez-vous vous reconnecter pour que cela prenne effet ?", "role_changed": "Votre rôle a changé, voulez-vous vous reconnecter pour que cela prenne effet ?",
"later": "Peut-être plus tard" "later": "Peut-être plus tard"
}, },
"continue": "Continuer avec Email", "continue": "Continuer avec Email",
"placeholder_name": "Entrez votre Nom", "placeholder_name": "Entrez votre Nom",
"placeholder_email": "Entrez votre Email", "placeholder_email": "Entrez votre Email",
"placeholder_pwd": "Entrez le Mot de Passe", "placeholder_pwd": "Entrez le Mot de Passe",
"placeholder_confirm_pwd": "Confirmez le Mot de Passe", "placeholder_confirm_pwd": "Confirmez le Mot de Passe",
"check_email": "Vérifiez votre boîte de réception", "check_email": "Vérifiez votre boîte de réception",
"check_email_desc": "Nous venons d'envoyer un lien magique à <0>{{email}}</0>. Une fois qu'il arrive, il sera valide pendant 15 minutes.", "check_email_desc": "Nous venons d'envoyer un lien magique à <0>{{email}}</0>. Une fois qu'il arrive, il sera valide pendant 15 minutes.",
"back_sign_in": "Retour à la connexion", "back_sign_in": "Retour à la connexion",
"welcome": "Bienvenue sur le serveur {{name}}", "welcome": "Bienvenue sur le serveur {{name}}",
"guest_login_tip": "Veuillez scanner le code QR ou vous connecter pour envoyer un message", "guest_login_tip": "Veuillez scanner le code QR ou vous connecter pour envoyer un message",
"enter": "Se connecter à ", "enter": "Se connecter à ",
"sign_in": "Se Connecter", "sign_in": "Se Connecter",
"sign_up": "S'inscrire", "sign_up": "S'inscrire",
"signing_up": "Inscription en cours", "signing_up": "Inscription en cours",
"github_login_success": "Connexion GitHub réussie !", "github_login_success": "Connexion GitHub réussie !",
"github_logging_in": "Connexion GitHub en cours...", "github_logging_in": "Connexion GitHub en cours...",
"github_cb_tip": "Veuillez fermer cette fenêtre et revenir à la fenêtre du widget", "github_cb_tip": "Veuillez fermer cette fenêtre et revenir à la fenêtre du widget",
"magic_link_expire": { "magic_link_expire": {
"title": "Lien magique invalide ou expiré", "title": "Lien magique invalide ou expiré",
"desc": "Veuillez demander un nouveau lien magique.", "desc": "Veuillez demander un nouveau lien magique.",
"desc_close": "Vous pouvez fermer cette fenêtre maintenant." "desc_close": "Vous pouvez fermer cette fenêtre maintenant."
}, },
"invite_mobile": { "invite_mobile": {
"join": "Rejoignez notre Serveur", "join": "Rejoignez notre Serveur",
"start_download": "Commencez par télécharger l'application mobile VoceChat", "start_download": "Commencez par télécharger l'application mobile VoceChat",
"open": "Ouvrir VoceChat", "open": "Ouvrir VoceChat",
"have_already": "Vous avez déjà l'application ?" "have_already": "Vous avez déjà l'application ?"
}, },
"invite_expire": { "invite_expire": {
"min30": "30 minutes", "min30": "30 minutes",
"h1": "1 heure", "h1": "1 heure",
"h6": "6 heures", "h6": "6 heures",
"h12": "12 heures", "h12": "12 heures",
"d1": "1 jour", "d1": "1 jour",
"d7": "7 jours", "d7": "7 jours",
"d30": "30 jours" "d30": "30 jours"
}, },
"invite_times": { "invite_times": {
"no_limit": "sans limite", "no_limit": "sans limite",
"time1": "1 utilisation", "time1": "1 utilisation",
"times5": "5 utilisations", "times5": "5 utilisations",
"times10": "10 utilisations", "times10": "10 utilisations",
"times25": "25 utilisations", "times25": "25 utilisations",
"times50": "50 utilisations", "times50": "50 utilisations",
"times100": "100 utilisations" "times100": "100 utilisations"
} }
} }
+60 -67
View File
@@ -1,67 +1,60 @@
{ {
"pin": "Épingler", "pin": "Épingler",
"pin_desc": "Voulez-vous épingler ce message à", "pin_desc": "Voulez-vous épingler ce message à",
"pinned_msg": "Message Épinglé", "pinned_msg": "Message Épinglé",
"pin_empty_tip": "Ce canal n'a pas encore de message épinglé.", "pin_empty_tip": "Ce canal n'a pas encore de message épinglé.",
"fav": "Favori", "fav": "Favori",
"fav_msg": "Message Enregistré", "fav_msg": "Message Enregistré",
"fav_empty_tip": "Ce canal n'a pas encore de message enregistré.", "fav_empty_tip": "Ce canal n'a pas encore de message enregistré.",
"channel_members": "Membres du Canal", "channel_members": "Membres du Canal",
"add_channel_members": "Ajouter des membres", "add_channel_members": "Ajouter des membres",
"welcome_channel": "Bienvenue dans {{name}}", "welcome_channel": "Bienvenue dans {{name}}",
"welcome_desc": "Ceci est le début du canal #{{name}}.", "welcome_desc": "Ceci est le début du canal #{{name}}.",
"edit_channel": "Modifier le Canal", "edit_channel": "Modifier le Canal",
"channel_name": "Nom du Canal",
"channel_name": "Nom du Canal", "private_channel": "Canal Privé",
"private_channel": "Canal Privé", "create_channel": "Créer un Nouveau Canal",
"create_channel": "Créer un Nouveau Canal", "create_channel_desc": "Ceci est un canal public, tout le monde dans cette équipe peut le voir.",
"create_channel_desc": "Ceci est un canal public, tout le monde dans cette équipe peut le voir.", "create_private_channel_desc": "Ceci est un canal privé, seuls certains membres pourront y accéder.",
"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",
"search_user_placeholder": "Tapez un Nom d'utilisateur pour chercher", "welcome_msg": "Bienvenue dans le canal {{name}}",
"welcome_msg": "Bienvenue dans le canal {{name}}", "invite_title": "Ajouter des amis à {{name}}",
"invite_by_email": "Inviter par Email",
"invite_title": "Ajouter des amis à {{name}}", "enable_smtp": "Activez SMTP d'abord",
"invite_by_email": "Inviter par Email", "send_invite_link": "Ou envoyez le lien d'invitation à vos amis",
"enable_smtp": "Activez SMTP d'abord", "share_invite_link": "Partagez ce lien pour inviter des personnes à ce serveur.",
"send_invite_link": "Ou envoyez le lien d'invitation à vos amis", "invite_link_faq": "Lien d'invitation incorrect ?",
"share_invite_link": "Partagez ce lien pour inviter des personnes à ce serveur.", "invite_link_edit": "Modifier le lien d'invitation",
"invite_link_faq": "Lien d'invitation incorrect ?", "invite_link_setting_tip": "Le lien d'invitation expire dans {{expire}}, max de fois d'utilisation : {{times}}",
"invite_link_edit": "Modifier le lien d'invitation", "generate_new_link": "Générer un Nouveau Lien",
"invite_link_setting_tip": "Le lien d'invitation expire dans {{expire}}, max de fois d'utilisation : {{times}}", "send_to": "Envoyer À",
"generate_new_link": "Générer un Nouveau Lien", "edited": "modifié",
"license_tip": "La limite d'utilisateurs de la licence actuelle est atteinte. Contactez l'administrateur.",
"send_to": "Envoyer À", "delete_msg_title": "Supprimer le Message",
"edited": "modifié", "delete_msg_desc": "Êtes-vous sûr de vouloir supprimer {{msg}} ?",
"license_tip": "Votre licence a atteint sa limite, veuillez mettre à niveau la Licence ou contacter l'Administrateur !", "delete_msg_this": "ce message",
"delete_msg_these": "ces messages",
"delete_msg_title": "Supprimer le Message", "new_msg": "{{num}} nouveaux messages",
"delete_msg_desc": "Êtes-vous sûr de vouloir supprimer {{msg}} ?", "mark_read": "Marquer comme Lu",
"delete_msg_this": "ce message", "pin_chat": "Épingler la Conversation",
"delete_msg_these": "ces messages", "unpin_chat": "Désépingler",
"reply_msg_del": "Ce message a été supprimé.",
"new_msg": "{{num}} nouveaux messages", "file": "fichier",
"mark_read": "Marquer comme Lu", "image": "image",
"pin_chat": "Épingler la Conversation", "forward": "transférer",
"unpin_chat": "Désépingler", "voice_message": "message vocal",
"voice": "Appel Vocal",
"reply_msg_del": "Ce message a été supprimé.", "deafen": "Se rendre sourd",
"undeafen": "Rendre à nouveau l'ouïe",
"file": "fichier", "mute": "Couper le son",
"image": "image", "unmute": "Réactiver le son",
"forward": "transférer", "camera_on": "Activer la caméra",
"voice_message": "message vocal", "camera_off": "Désactiver la caméra",
"voice": "Appel Vocal", "leave_voice": "Quitter l'Appel Vocal",
"deafen": "Se rendre sourd", "add_contact": "Ajouter aux contacts",
"undeafen": "Rendre à nouveau l'ouïe", "block": "Bloquer",
"mute": "Couper le son", "unblock": "Débloquer",
"unmute": "Réactiver le son", "contact_tip": "Cet utilisateur n'est pas dans vos contacts",
"camera_on": "Activer la caméra", "contact_block_tip": "Cet utilisateur a été bloqué par vous",
"camera_off": "Désactiver la caméra", "file_expired": "Fichier Expiré"
"leave_voice": "Quitter l'Appel Vocal", }
"add_contact": "Ajouter aux contacts",
"block": "Bloquer",
"unblock": "Débloquer",
"contact_tip": "Cet utilisateur n'est pas dans vos contacts",
"contact_block_tip": "Cet utilisateur a été bloqué par vous",
"file_expired": "Fichier Expiré"
}
-12
View File
@@ -67,18 +67,6 @@
}, },
"new_version": "<0>Nouvelle Version</0> Disponible", "new_version": "<0>Nouvelle Version</0> Disponible",
"mobile_app": "Découvrez notre <0>Application Mobile</0>", "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": { "server_update": {
"version_needed": "Cette fonction nécessite la version serveur :<0>{{version}}</0> au minimum 🚨", "version_needed": "Cette fonction nécessite la version serveur :<0>{{version}}</0> au minimum 🚨",
"current_version": "Votre version actuelle :<0>{{version}}</0>", "current_version": "Votre version actuelle :<0>{{version}}</0>",
+6 -6
View File
@@ -1,6 +1,6 @@
{ {
"all_items": "Tous les Éléments", "all_items": "Tous les Éléments",
"image": "Images", "image": "Images",
"video": "Vidéos", "video": "Vidéos",
"audio": "Audio" "audio": "Audio"
} }
+6 -6
View File
@@ -1,6 +1,6 @@
{ {
"from": "De", "from": "De",
"channel": "Canal", "channel": "Canal",
"type": "Type", "type": "Type",
"date": "Date" "date": "Date"
} }
+37 -39
View File
@@ -1,39 +1,37 @@
{ {
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"email": "Email", "email": "Email",
"password": "Mot de Passe", "password": "Mot de Passe",
"delete_account": "Supprimer le Compte", "delete_account": "Supprimer le Compte",
"change_name": "Changer votre nom d'utilisateur", "change_name": "Changer votre nom d'utilisateur",
"change_name_desc": "Entrez un nouveau nom d'utilisateur", "change_name_desc": "Entrez un nouveau nom d'utilisateur",
"change_email": "Changer votre email", "change_email": "Changer votre email",
"change_email_desc": "Entrez un nouvel email.", "change_email_desc": "Entrez un nouvel email.",
"change_pwd": "Changer votre mot de passe", "change_pwd": "Changer votre mot de passe",
"change_pwd_desc": "Entrez le mot de passe actuel et le nouveau mot de passe", "change_pwd_desc": "Entrez le mot de passe actuel et le nouveau mot de passe",
"current_pwd": "Mot de Passe Actuel", "current_pwd": "Mot de Passe Actuel",
"new_pwd": "Nouveau Mot de Passe", "new_pwd": "Nouveau Mot de Passe",
"confirm_new_pwd": "Confirmer le Nouveau Mot de Passe", "confirm_new_pwd": "Confirmer le Nouveau Mot de Passe",
"manage_members": "Gérer les Contacts",
"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.",
"manage_tip": "Désactiver votre compte signifie que vous pouvez le récupérer à tout moment après avoir pris cette action.", "admin": "Administrateur",
"admin": "Administrateur", "user": "Utilisateur",
"user": "Utilisateur", "copy_email": "Copier l'email",
"copy_email": "Copier l'email", "send_msg": "Envoyer un message",
"send_msg": "Envoyer un message", "call": "Appeler",
"call": "Appeler", "remove": "Retirer du serveur",
"remove": "Retirer du serveur", "remove_from_contact": "Retirer des contacts",
"remove_from_contact": "Retirer des contacts", "remove_account": "Retirer le compte",
"remove_account": "Retirer le compte", "remove_account_desc": "Êtes-vous sûr de vouloir retirer ce compte ?",
"remove_account_desc": "Êtes-vous sûr de vouloir retirer ce compte ?", "remove_from_channel": "Retirer du canal",
"remove_from_channel": "Retirer du canal", "roles": "Rôles",
"roles": "Rôles", "set_admin": "Administrateur",
"set_admin": "Administrateur", "set_normal": "Utilisateur",
"set_normal": "Utilisateur", "search_not_found": "Non trouvé, ou cet utilisateur n'est pas autorisé à être recherché.",
"search_by_id": "Rechercher par ID",
"search_not_found": "Non trouvé, ou cet utilisateur n'est pas autorisé à être recherché.", "search_by_id_ph": "Saisir l'ID de l'utilisateur",
"search_by_id": "Rechercher par ID", "search_by_email": "Rechercher par email",
"search_by_id_ph": "Saisir l'ID de l'utilisateur", "search_by_email_ph": "Saisir l'email de l'utilisateur",
"search_by_email": "Rechercher par email", "search_by_name": "Rechercher par nom",
"search_by_email_ph": "Saisir l'email de l'utilisateur", "search_by_name_ph": "Saisir le nom de l'utilisateur"
"search_by_name": "Rechercher par nom", }
"search_by_name_ph": "Saisir le nom de l'utilisateur"
}
+1 -41
View File
@@ -15,7 +15,6 @@
"bot": "Bot & Webhook", "bot": "Bot & Webhook",
"notification_channels": "Canaux de Notification", "notification_channels": "Canaux de Notification",
"firebase": "Firebase", "firebase": "Firebase",
"agora": "Agora",
"smtp": "SMTP", "smtp": "SMTP",
"login_method": "Méthodes de Connexion", "login_method": "Méthodes de Connexion",
"third_app": "Connexion Tiers", "third_app": "Connexion Tiers",
@@ -27,26 +26,6 @@
"video": "Vidéo", "video": "Vidéo",
"notification": "Notifications" "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": { "channel": {
"leave": "Quitter le Canal", "leave": "Quitter le Canal",
"leave_desc": "Êtes-vous sûr de vouloir quitter ce canal ?", "leave_desc": "Êtes-vous sûr de vouloir quitter ce canal ?",
@@ -152,23 +131,8 @@
"expire": "Expiré À", "expire": "Expiré À",
"create": "Créé À", "create": "Créé À",
"value": "Valeur de la Licence", "value": "Valeur de la Licence",
"renew": "Renouveler la Licence",
"update": "Mettre à Jour Manuellement", "update": "Mettre à Jour Manuellement",
"update_placeholder": "Veuillez saisir la nouvelle valeur de la licence", "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"
}, },
"bot": { "bot": {
"add_api_key": "Ajouter une Clé API", "add_api_key": "Ajouter une Clé API",
@@ -235,10 +199,6 @@
"how_to": "Comment configurer SMTP ?", "how_to": "Comment configurer SMTP ?",
"send_test_email": "Envoyer un Email de Test" "send_test_email": "Envoyer un Email de Test"
}, },
"agora": {
"enable": "Activer",
"how_to": "Comment configurer Agora ?"
},
"third_app": { "third_app": {
"key": "Clé Sécurisée API", "key": "Clé Sécurisée API",
"update": "Mettre à Jour le Secret", "update": "Mettre à Jour le Secret",
+3 -3
View File
@@ -7,8 +7,7 @@
"download": "Téléchargez les applications mobiles", "download": "Téléchargez les applications mobiles",
"help": "Vous avez des questions ? Visitez notre centre d'aide", "help": "Vous avez des questions ? Visitez notre centre d'aide",
"sign_in_tip": "Veuillez vous connecter pour envoyer un message", "sign_in_tip": "Veuillez vous connecter pour envoyer un message",
"vocespace": "Appel Vidéo VoceSpace", "license": "Licence VoceChat",
"license": "VoceChat Licence de Mise à Niveau",
"page_editor": { "page_editor": {
"title": "Page Management", "title": "Page Management",
"status_select": "Select State", "status_select": "Select State",
@@ -66,5 +65,6 @@
"tunnel_progress": "Starting tunnel...", "tunnel_progress": "Starting tunnel...",
"tunnel_error": "Tunnel error", "tunnel_error": "Tunnel error",
"tunnel_get_domain": "Get Public Domain" "tunnel_get_domain": "Get Public Domain"
} },
"video": "Vidéo"
} }
+5 -5
View File
@@ -1,5 +1,5 @@
{ {
"welcome": "👋 Bonjour, Ravi de vous rencontrer !", "welcome": "👋 Bonjour, Ravi de vous rencontrer !",
"placeholder_send": "Tapez et appuyez sur entrer", "placeholder_send": "Tapez et appuyez sur entrer",
"start_chat": "Commencer le Chat" "start_chat": "Commencer le Chat"
} }
+2 -5
View File
@@ -11,22 +11,19 @@
"welcome_channel": " {{name}} チャンネルへようこそ", "welcome_channel": " {{name}} チャンネルへようこそ",
"welcome_desc": " #{{name}} の最初のところ", "welcome_desc": " #{{name}} の最初のところ",
"edit_channel": "チャネルを編集する", "edit_channel": "チャネルを編集する",
"channel_name": "チャネル名", "channel_name": "チャネル名",
"private_channel": "プライベートチャネル", "private_channel": "プライベートチャネル",
"create_channel": "チャネルを作る", "create_channel": "チャネルを作る",
"create_channel_desc": "これはオープンチャンネルで、誰もが見ることができます", "create_channel_desc": "これはオープンチャンネルで、誰もが見ることができます",
"create_private_channel_desc": "これはプライベートチャンネルで、指定された人だけが見ることができます", "create_private_channel_desc": "これはプライベートチャンネルで、指定された人だけが見ることができます",
"search_user_placeholder": "ユーザー名を入力してください", "search_user_placeholder": "ユーザー名を入力してください",
"invite_title": "友達を{{name}}に招待する", "invite_title": "友達を{{name}}に招待する",
"invite_by_email": "招待メールを送信する", "invite_by_email": "招待メールを送信する",
"enable_smtp": "始めにSMTPを有効にする", "enable_smtp": "始めにSMTPを有効にする",
"send_invite_link": "もしくは、招待用URLをコピーする", "send_invite_link": "もしくは、招待用URLをコピーする",
"share_invite_link": "このリンクを使って、友達を招待する", "share_invite_link": "このリンクを使って、友達を招待する",
"invite_link_setting_tip": "招待用URLの有効期限は48時間です", "invite_link_setting_tip": "招待用URLの有効期限は48時間です",
"generate_new_link": "新しいリンクを生成する", "generate_new_link": "新しいリンクを生成する",
"send_to": "送信先",
"send_to": "送信先" "license_tip": "現在のライセンスのユーザー上限に達しました。管理者に連絡してください。"
} }
+1 -16
View File
@@ -8,7 +8,6 @@
"more": "もっと", "more": "もっと",
"action": { "action": {
"login": "Sign In", "login": "Sign In",
"logout": "ログアウト", "logout": "ログアウト",
"change_avatar": "アバターを変更", "change_avatar": "アバターを変更",
"cancel": "キャンセル", "cancel": "キャンセル",
@@ -36,7 +35,6 @@
"reply": "返信", "reply": "返信",
"pin": "ピン留め", "pin": "ピン留め",
"unpin": "ピン留め解除", "unpin": "ピン留め解除",
"send": "送信", "send": "送信",
"send_msg": "メッセージを送る", "send_msg": "メッセージを送る",
"copy": "コピー", "copy": "コピー",
@@ -50,7 +48,6 @@
"uploading": "アップロード中" "uploading": "アップロード中"
}, },
"tip": { "tip": {
"email_msg_tip": "お住まいの地域では通知がブロックされています。メール通知をご利用いただくには、Pro にアップグレードしてください。",
"update": "更新に成功しました!", "update": "更新に成功しました!",
"delete": "削除に成功しました!", "delete": "削除に成功しました!",
"reg": "登録に成功しました!", "reg": "登録に成功しました!",
@@ -62,22 +59,10 @@
}, },
"new_version": "<0>新バージョン</0>をご利用いただけます", "new_version": "<0>新バージョン</0>をご利用いただけます",
"mobile_app": "<0>モバイルアプリ</0>をご覧ください", "mobile_app": "<0>モバイルアプリ</0>をご覧ください",
"price": {
"pro": {
"title": "VoceChat Pro",
"desc": "",
"price": "$49"
},
"supreme": {
"title": "VoceSpace プライベート導入ビデオ [$499]",
"desc": "",
"price": ""
}
},
"server_update": { "server_update": {
"version_needed": "この機能には、サーバーバージョン<0>{{version}} 🚨以上が必要です", "version_needed": "この機能には、サーバーバージョン<0>{{version}} 🚨以上が必要です",
"current_version": "現在のバージョン: <0>{{version}} 0>", "current_version": "現在のバージョン: <0>{{version}} 0>",
"update_tip": "サーバーをアップグレードしてください!", "update_tip": "サーバーを更新してください!",
"howto": "VoceChatサーバーのアップデート方法" "howto": "VoceChatサーバーのアップデート方法"
}, },
"inactive": { "inactive": {
+1 -33
View File
@@ -12,7 +12,6 @@
"bot": "ボット&Webhook", "bot": "ボット&Webhook",
"notification_channels": "通知チャンネル", "notification_channels": "通知チャンネル",
"firebase": "Firebase", "firebase": "Firebase",
"agora": "Agora",
"smtp": "SMTP", "smtp": "SMTP",
"login_method": "ログイン方法", "login_method": "ログイン方法",
"third_app": "サードパーティー", "third_app": "サードパーティー",
@@ -24,34 +23,12 @@
"video": "ビデオ", "video": "ビデオ",
"notification": "通知設定" "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": { "channel": {
"leave": "チャンネルを離れる", "leave": "チャンネルを離れる",
"leave_desc": "このチャンネルを離れますか?", "leave_desc": "このチャンネルを離れますか?",
"transfer_desc": "離れる前にこのチャンネルのコントロール権を他のユーザーに譲渡しなければならない", "transfer_desc": "離れる前にこのチャンネルのコントロール権を他のユーザーに譲渡しなければならない",
"delete": "チャンネルを削除する", "delete": "チャンネルを削除する",
"delete_desc": "このチャンネルを削除しますか?", "delete_desc": "このチャンネルを削除しますか?",
"name": "チャンネル名", "name": "チャンネル名",
"topic": "トピック", "topic": "トピック",
"topic_placeholder": "チャンネルのトピックを入力してください", "topic_placeholder": "チャンネルのトピックを入力してください",
@@ -63,7 +40,6 @@
"name": "サーバー名", "name": "サーバー名",
"desc": "サーバーの説明", "desc": "サーバーの説明",
"upload_desc": "画像サイズとして、128x128 は必須、512x512 はおすすめ。大きさは 5M を超えないでください。", "upload_desc": "画像サイズとして、128x128 は必須、512x512 はおすすめ。大きさは 5M を超えないでください。",
"sign_up": { "sign_up": {
"title": "登録方法", "title": "登録方法",
"desc": "登録方法を設定してください", "desc": "登録方法を設定してください",
@@ -88,16 +64,8 @@
"expire": "有効期限", "expire": "有効期限",
"create": "作成日時", "create": "作成日時",
"value": "内容", "value": "内容",
"renew": "ライセンスキをリニューアル",
"update": "ライセンスキーをアップロード", "update": "ライセンスキーをアップロード",
"update_placeholder": "ライセンスキーをアップロードしてください", "update_placeholder": "ライセンスキーをアップロードしてください"
"renew_select": "アップグするライセンスを選択してください",
"tip": {
"title": "無料アップグレードを取得するチャンス!",
"user_test": "テストに参加することで、無料アップグレードを取得する機会を得る",
"contact": "ここで予約する:"
}
}, },
"firebase": { "firebase": {
"enable": "有効にする", "enable": "有効にする",
+3 -3
View File
@@ -7,8 +7,7 @@
"download": "アプリをダウンロードしましょう", "download": "アプリをダウンロードしましょう",
"help": "まだ聞きたいことがある? ヘルプにアクセスしましょう", "help": "まだ聞きたいことがある? ヘルプにアクセスしましょう",
"sign_in_tip": "Please sign in to send message", "sign_in_tip": "Please sign in to send message",
"vocespace": " VoceSpace ビデオ通話", "license": "VoceChat ライセンス",
"license": "VoceChat アップグレードライセンス",
"page_editor": { "page_editor": {
"title": "Page Management", "title": "Page Management",
"status_select": "Select State", "status_select": "Select State",
@@ -55,5 +54,6 @@
"admin_account": "管理者アカウント", "admin_account": "管理者アカウント",
"who_sign_up": "登録に関する権限設定", "who_sign_up": "登録に関する権限設定",
"invites": "招待" "invites": "招待"
} },
"video": "ビデオ"
} }
+1 -8
View File
@@ -11,7 +11,6 @@
"welcome_channel": "Welcome to {{name}}", "welcome_channel": "Welcome to {{name}}",
"welcome_desc": "This is the start of the #{{name}} channel.", "welcome_desc": "This is the start of the #{{name}} channel.",
"edit_channel": "Edit Channel", "edit_channel": "Edit Channel",
"channel_name": "Channel Name", "channel_name": "Channel Name",
"private_channel": "Private Channel", "private_channel": "Private Channel",
"create_channel": "Create New 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.", "create_private_channel_desc": "This is a private channel, only select members will be able to join.",
"search_user_placeholder": "Type Username to search", "search_user_placeholder": "Type Username to search",
"welcome_msg": "Welcome to channel {{name}}", "welcome_msg": "Welcome to channel {{name}}",
"invite_title": "Add friends to {{name}}", "invite_title": "Add friends to {{name}}",
"invite_by_email": "Invite by Email", "invite_by_email": "Invite by Email",
"enable_smtp": "Enable SMTP First", "enable_smtp": "Enable SMTP First",
@@ -29,23 +27,18 @@
"invite_link_edit": "Edit invite link", "invite_link_edit": "Edit invite link",
"invite_link_setting_tip": "Invite link expires in {{expire}}, max use times: {{times}}", "invite_link_setting_tip": "Invite link expires in {{expire}}, max use times: {{times}}",
"generate_new_link": "Generate New Link", "generate_new_link": "Generate New Link",
"send_to": "Send To", "send_to": "Send To",
"edited": "edited", "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_title": "Delete Message",
"delete_msg_desc": "Are you sure want to delete {{msg}}?", "delete_msg_desc": "Are you sure want to delete {{msg}}?",
"delete_msg_this": "this message", "delete_msg_this": "this message",
"delete_msg_these": "these messages", "delete_msg_these": "these messages",
"new_msg": "{{num}} new messages", "new_msg": "{{num}} new messages",
"mark_read": "Mark as Read", "mark_read": "Mark as Read",
"pin_chat": "Pin Chat", "pin_chat": "Pin Chat",
"unpin_chat": "Unpin", "unpin_chat": "Unpin",
"reply_msg_del": "This message has been deleted.", "reply_msg_del": "This message has been deleted.",
"file": "file", "file": "file",
"image": "image", "image": "image",
"forward": "forward", "forward": "forward",
+1 -13
View File
@@ -67,22 +67,10 @@
}, },
"new_version": "<0>New Version</0> Available", "new_version": "<0>New Version</0> Available",
"mobile_app": "Check out our <0>Mobile APP</0>", "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": { "server_update": {
"version_needed": "This function needs server version:<0>{{version}}</0> at least 🚨", "version_needed": "This function needs server version:<0>{{version}}</0> at least 🚨",
"current_version": "Your current version:<0>{{version}}</0>", "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" "howto": "How to Update VoceChat Server"
}, },
"inactive": { "inactive": {
-2
View File
@@ -12,7 +12,6 @@
"current_pwd": "Senha Atual", "current_pwd": "Senha Atual",
"new_pwd": "Nova Senha", "new_pwd": "Nova Senha",
"confirm_new_pwd": "Confirmar Nova Senha", "confirm_new_pwd": "Confirmar Nova Senha",
"manage_members": "Gerenciar Contatos", "manage_members": "Gerenciar Contatos",
"manage_tip": "Disabling your account means you can recover it at any time after taking this action.", "manage_tip": "Disabling your account means you can recover it at any time after taking this action.",
"admin": "Admin", "admin": "Admin",
@@ -28,7 +27,6 @@
"roles": "Roles", "roles": "Roles",
"set_admin": "Admin", "set_admin": "Admin",
"set_normal": "User", "set_normal": "User",
"search_not_found": "Not found, or this user is not allowed to be searched.", "search_not_found": "Not found, or this user is not allowed to be searched.",
"search_by_id": "Search by ID", "search_by_id": "Search by ID",
"search_by_id_ph": "Input user ID", "search_by_id_ph": "Input user ID",
+1 -41
View File
@@ -15,7 +15,6 @@
"bot": "Bot & Webhook", "bot": "Bot & Webhook",
"notification_channels": "Canais de Notificação", "notification_channels": "Canais de Notificação",
"firebase": "Firebase", "firebase": "Firebase",
"agora": "Agora",
"smtp": "SMTP", "smtp": "SMTP",
"login_method": "Login Methods", "login_method": "Login Methods",
"third_app": "Third-party Login", "third_app": "Third-party Login",
@@ -27,26 +26,6 @@
"video": "Video", "video": "Video",
"notification": "Notificações" "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": { "channel": {
"leave": "Leave Channel", "leave": "Leave Channel",
"leave_desc": "Are you sure want to leave this channel?", "leave_desc": "Are you sure want to leave this channel?",
@@ -145,23 +124,8 @@
"expire": "Expired At", "expire": "Expired At",
"create": "Created At", "create": "Created At",
"value": "License Value", "value": "License Value",
"renew": "Renew License",
"update": "Update Manually", "update": "Update Manually",
"update_placeholder": "Please input the new license value", "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"
}, },
"bot": { "bot": {
"add_api_key": "Add API Key", "add_api_key": "Add API Key",
@@ -228,10 +192,6 @@
"how_to": "How to setup SMTP?", "how_to": "How to setup SMTP?",
"send_test_email": "Send Test Email" "send_test_email": "Send Test Email"
}, },
"agora": {
"enable": "Enable",
"how_to": "How to setup Agora?"
},
"third_app": { "third_app": {
"key": "API Secure Key", "key": "API Secure Key",
"update": "Update Secret", "update": "Update Secret",
+3 -3
View File
@@ -7,8 +7,7 @@
"download": "Download Mobile apps", "download": "Download Mobile apps",
"help": "Tem perguntas? Visite nossa central de ajuda", "help": "Tem perguntas? Visite nossa central de ajuda",
"sign_in_tip": "Faça login para enviar mensagem", "sign_in_tip": "Faça login para enviar mensagem",
"vocespace": "Chamada de vídeo VoceSpace", "license": "Licença VoceChat",
"license": "VoceChat Licença de Atualização",
"page_editor": { "page_editor": {
"title": "Page Management", "title": "Page Management",
"status_select": "Select State", "status_select": "Select State",
@@ -66,5 +65,6 @@
"tunnel_progress": "Starting tunnel...", "tunnel_progress": "Starting tunnel...",
"tunnel_error": "Tunnel error", "tunnel_error": "Tunnel error",
"tunnel_get_domain": "Get Public Domain" "tunnel_get_domain": "Get Public Domain"
} },
"video": "Vídeo"
} }
+1 -8
View File
@@ -11,7 +11,6 @@
"welcome_channel": "Добро пожаловать в {{name}}", "welcome_channel": "Добро пожаловать в {{name}}",
"welcome_desc": "Это начало #{{name}} канала.", "welcome_desc": "Это начало #{{name}} канала.",
"edit_channel": "Редактировать канал", "edit_channel": "Редактировать канал",
"channel_name": "Имя канала", "channel_name": "Имя канала",
"private_channel": "Частный канал", "private_channel": "Частный канал",
"create_channel": "Создать новый канал", "create_channel": "Создать новый канал",
@@ -19,7 +18,6 @@
"create_private_channel_desc": "Это закрытый канал, присоединиться к которому смогут только избранные участники.", "create_private_channel_desc": "Это закрытый канал, присоединиться к которому смогут только избранные участники.",
"search_user_placeholder": "Введите имя пользователя для поиска", "search_user_placeholder": "Введите имя пользователя для поиска",
"welcome_msg": "Добро пожаловать на канал {{name}}", "welcome_msg": "Добро пожаловать на канал {{name}}",
"invite_title": "Добавить друзей в {{name}}", "invite_title": "Добавить друзей в {{name}}",
"invite_by_email": "Пригласить по электронной почте", "invite_by_email": "Пригласить по электронной почте",
"enable_smtp": "Сначала включите SMTP", "enable_smtp": "Сначала включите SMTP",
@@ -29,23 +27,18 @@
"invite_link_edit": "Изменить ссылку приглашения", "invite_link_edit": "Изменить ссылку приглашения",
"invite_link_setting_tip": "Срок действия ссылки-приглашения истекает через {{expire}}, максимальное количество использований: {{times}}", "invite_link_setting_tip": "Срок действия ссылки-приглашения истекает через {{expire}}, максимальное количество использований: {{times}}",
"generate_new_link": "Создать новую ссылку", "generate_new_link": "Создать новую ссылку",
"send_to": "Отправить", "send_to": "Отправить",
"edited": "отредактировано", "edited": "отредактировано",
"license_tip": "Ваша лицензия достигла лимита, обновите лицензию или обратитесь к администратору!", "license_tip": "Достигнут лимит пользователей текущей лицензии. Обратитесь к администратору.",
"delete_msg_title": "Удалить сообщение", "delete_msg_title": "Удалить сообщение",
"delete_msg_desc": "Вы уверены, что хотите удалить {{msg}}?", "delete_msg_desc": "Вы уверены, что хотите удалить {{msg}}?",
"delete_msg_this": "это сообщение", "delete_msg_this": "это сообщение",
"delete_msg_these": "эти сообщения", "delete_msg_these": "эти сообщения",
"new_msg": "{{num}} новое сообщение", "new_msg": "{{num}} новое сообщение",
"mark_read": "Отметить как прочитанное", "mark_read": "Отметить как прочитанное",
"pin_chat": "Закрепить чат", "pin_chat": "Закрепить чат",
"unpin_chat": "Открепить", "unpin_chat": "Открепить",
"reply_msg_del": "Это сообщение было удалено.", "reply_msg_del": "Это сообщение было удалено.",
"file": "Файл", "file": "Файл",
"image": "Изображение", "image": "Изображение",
"forward": "Вперед", "forward": "Вперед",
-14
View File
@@ -51,7 +51,6 @@
"install": "Установка", "install": "Установка",
"download_origin": "Скачать оригинал", "download_origin": "Скачать оригинал",
"close": "Закрыть", "close": "Закрыть",
"upgrade": "Обновление",
"view_pwd": "Посмотреть пароль", "view_pwd": "Посмотреть пароль",
"change_pwd": "Обновить пароль" "change_pwd": "Обновить пароль"
}, },
@@ -59,7 +58,6 @@
"uploading": "Загрузка" "uploading": "Загрузка"
}, },
"tip": { "tip": {
"email_msg_tip": "В вашем регионе заблокированы уведомления. Перейдите на версию Pro, чтобы использовать уведомления по электронной почте.",
"update": "Обновление успешно выполнено!", "update": "Обновление успешно выполнено!",
"delete": "Удаление успешно выполнено!", "delete": "Удаление успешно выполнено!",
"reg": "Регистрация прошла успешно!", "reg": "Регистрация прошла успешно!",
@@ -71,18 +69,6 @@
}, },
"new_version": "<0>New Version</0> Available", "new_version": "<0>New Version</0> Available",
"mobile_app": "Check out our <0>Mobile APP</0>", "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": { "server_update": {
"version_needed": "Для этой функции требуется серверная версия:<0>{{version}}</0> по крайней мере 🚨 ", "version_needed": "Для этой функции требуется серверная версия:<0>{{version}}</0> по крайней мере 🚨 ",
"current_version": "Ваша текущая версия:<0>{{version}}</0>", "current_version": "Ваша текущая версия:<0>{{version}}</0>",
-2
View File
@@ -12,7 +12,6 @@
"current_pwd": "Текущий пароль", "current_pwd": "Текущий пароль",
"new_pwd": "Новый пароль", "new_pwd": "Новый пароль",
"confirm_new_pwd": "Подтвердите новый пароль", "confirm_new_pwd": "Подтвердите новый пароль",
"manage_members": "Управление контактами", "manage_members": "Управление контактами",
"manage_tip": "Отключение вашей учетной записи означает, что вы сможете восстановить ее в любое время после выполнения этого действия.", "manage_tip": "Отключение вашей учетной записи означает, что вы сможете восстановить ее в любое время после выполнения этого действия.",
"admin": "Администратор", "admin": "Администратор",
@@ -28,7 +27,6 @@
"roles": "Роли", "roles": "Роли",
"set_admin": "Администратор", "set_admin": "Администратор",
"set_normal": "Пользователь", "set_normal": "Пользователь",
"search_not_found": "Не найдено, или поиск этого пользователя запрещен.", "search_not_found": "Не найдено, или поиск этого пользователя запрещен.",
"search_by_id": "Поиск по идентификатору", "search_by_id": "Поиск по идентификатору",
"search_by_id_ph": "Введите идентификатор пользователя", "search_by_id_ph": "Введите идентификатор пользователя",
+2 -44
View File
@@ -15,7 +15,6 @@
"bot": "Bot & Webhook", "bot": "Bot & Webhook",
"notification_channels": "Каналы уведомлений", "notification_channels": "Каналы уведомлений",
"firebase": "Firebase", "firebase": "Firebase",
"agora": "Agora",
"smtp": "SMTP", "smtp": "SMTP",
"login_method": "Методы входа", "login_method": "Методы входа",
"third_app": "Вход третьей стороны", "third_app": "Вход третьей стороны",
@@ -27,26 +26,6 @@
"video": "Видео", "video": "Видео",
"notification": "Уведомления" "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": { "channel": {
"leave": "Покинуть канал", "leave": "Покинуть канал",
"leave_desc": "Вы уверены, что хотите покинуть этот канал?", "leave_desc": "Вы уверены, что хотите покинуть этот канал?",
@@ -191,23 +170,8 @@
"expire": "Истек срок действия", "expire": "Истек срок действия",
"create": "Создано в", "create": "Создано в",
"value": "Стоимость лицензии", "value": "Стоимость лицензии",
"renew": "Продлить лицензию",
"update": "Обновить вручную", "update": "Обновить вручную",
"update_placeholder": "Введите новое значение лицензии.", "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": "Начать оплату"
}, },
"bot": { "bot": {
"add_api_key": "Добавить API-ключ", "add_api_key": "Добавить API-ключ",
@@ -267,7 +231,7 @@
"client_email": "Электронная почта клиента" "client_email": "Электронная почта клиента"
}, },
"smtp": { "smtp": {
"desc": "Для нового сообщения Уведомление по электронной почте (расширенная функция, обновите до <0>VoceChat Pro</0>) и для подтверждения адреса электронной почты при новой регистрации мы используем SMTP. ", "desc": "Для уведомлений о новых сообщениях по электронной почте и подтверждения электронной почты при регистрации мы используем SMTP.",
"sub_desc": "Пожалуйста, настройте ваш SMTP ниже.", "sub_desc": "Пожалуйста, настройте ваш SMTP ниже.",
"enable": "Давать возможность", "enable": "Давать возможность",
"host": "Хост", "host": "Хост",
@@ -278,12 +242,6 @@
"how_to": "Как настроить SMTP?", "how_to": "Как настроить SMTP?",
"send_test_email": "Отправить тестовое письмо" "send_test_email": "Отправить тестовое письмо"
}, },
"agora": {
"desc": "Для видео- и аудиозвонков мы используем Agora API. ",
"sub_desc": "Каждый проект Agora получит 10 000 минут бесплатного использования API в месяц. Создайте проект самостоятельно. Следуйте инструкциям ниже, чтобы правильно настроить Agora.",
"enable": "Давать возможность",
"how_to": "Как настроить Agora?"
},
"third_app": { "third_app": {
"key": "Безопасный ключ API", "key": "Безопасный ключ API",
"update": "Обновление секрета", "update": "Обновление секрета",
+3 -3
View File
@@ -7,8 +7,7 @@
"download": "Загрузить мобильные приложения", "download": "Загрузить мобильные приложения",
"help": "Есть вопросы? Посетите наш справочный центр", "help": "Есть вопросы? Посетите наш справочный центр",
"sign_in_tip": "Пожалуйста, войдите, чтобы отправить сообщение", "sign_in_tip": "Пожалуйста, войдите, чтобы отправить сообщение",
"vocespace": "Видеозвонок VoceSpace", "license": "Лицензия VoceChat",
"license": "VoceChat Лицензия на обновление",
"page_editor": { "page_editor": {
"title": "Page Management", "title": "Page Management",
"status_select": "Select State", "status_select": "Select State",
@@ -66,5 +65,6 @@
"tunnel_progress": "Starting tunnel...", "tunnel_progress": "Starting tunnel...",
"tunnel_error": "Tunnel error", "tunnel_error": "Tunnel error",
"tunnel_get_domain": "Get Public Domain" "tunnel_get_domain": "Get Public Domain"
} },
"video": "Видео"
} }
+1 -8
View File
@@ -11,7 +11,6 @@
"welcome_channel": "{{name}} kanalına hoşgeldiniz!", "welcome_channel": "{{name}} kanalına hoşgeldiniz!",
"welcome_desc": "Burası #{{name}} kanal'ının başlangıcıdır.", "welcome_desc": "Burası #{{name}} kanal'ının başlangıcıdır.",
"edit_channel": "Kanal'ı düzenle", "edit_channel": "Kanal'ı düzenle",
"channel_name": "Kanal Adı", "channel_name": "Kanal Adı",
"private_channel": "Özel Kanal", "private_channel": "Özel Kanal",
"create_channel": "Yeni Kanal Yarat", "create_channel": "Yeni Kanal Yarat",
@@ -19,7 +18,6 @@
"create_private_channel_desc": "Bu özel bir kanaldır, sadece seçilen üyeler görebilir.", "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", "search_user_placeholder": "Kullanıcı araması için birşeyler yazın",
"welcome_msg": "{{name}} kanalına hoşgeldiniz", "welcome_msg": "{{name}} kanalına hoşgeldiniz",
"invite_title": "{{name}} kullanıcısını arkadaş olarak ekle", "invite_title": "{{name}} kullanıcısını arkadaş olarak ekle",
"invite_by_email": "E-posta ile davet et", "invite_by_email": "E-posta ile davet et",
"enable_smtp": "Öncelikle SMTP'ye izin verin ", "enable_smtp": "Öncelikle SMTP'ye izin verin ",
@@ -28,21 +26,16 @@
"invite_link_faq": "Hatalı davet bağlantısı mı?", "invite_link_faq": "Hatalı davet bağlantısı mı?",
"invite_link_setting_tip": "Davet bağlantısı 48 saat geçerli olacaktır.", "invite_link_setting_tip": "Davet bağlantısı 48 saat geçerli olacaktır.",
"generate_new_link": "Yeni bağlantı yarat", "generate_new_link": "Yeni bağlantı yarat",
"send_to": "Şuna gönder", "send_to": "Şuna gönder",
"edited": "düzenlenmiş", "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_title": "İletiyi Sil",
"delete_msg_desc": "{{msg}} iletisini silmek istediğinize emin misiniz?", "delete_msg_desc": "{{msg}} iletisini silmek istediğinize emin misiniz?",
"delete_msg_this": "bu ileti", "delete_msg_this": "bu ileti",
"delete_msg_these": "bu iletiler", "delete_msg_these": "bu iletiler",
"new_msg": "{{num}} yeni ileti", "new_msg": "{{num}} yeni ileti",
"mark_read": "Okundu olarak işaretle", "mark_read": "Okundu olarak işaretle",
"reply_msg_del": "Bu ileti silinmiş.", "reply_msg_del": "Bu ileti silinmiş.",
"file": "dosya", "file": "dosya",
"image": "resim", "image": "resim",
"forward": "ilet" "forward": "ilet"
-12
View File
@@ -65,18 +65,6 @@
}, },
"new_version": "<0>Yeni Sürüm</0> Mevcut", "new_version": "<0>Yeni Sürüm</0> Mevcut",
"mobile_app": "Yeni <0>Mobil Uygulama</0>'yı deneyin", "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": { "server_update": {
"version_needed": "Bu yöntem için sunucu sürümü en az <0>{{version}}</0> olmalı 🚨", "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>", "current_version": "Sizin mevcut sürümünüz:<0>{{version}}</0>",
-1
View File
@@ -12,7 +12,6 @@
"current_pwd": "Mevcut Parola", "current_pwd": "Mevcut Parola",
"new_pwd": "Yeni Parola", "new_pwd": "Yeni Parola",
"confirm_new_pwd": "Yeni Parola Onayı", "confirm_new_pwd": "Yeni Parola Onayı",
"manage_members": "Üyeleri Yönet", "manage_members": "Üyeleri Yönet",
"manage_tip": "Hesabınızı etkisizleştirmeniz durumunda istediğiniz zaman kaldığınız yerden devam edebilirsiniz.", "manage_tip": "Hesabınızı etkisizleştirmeniz durumunda istediğiniz zaman kaldığınız yerden devam edebilirsiniz.",
"admin": "Sistem Yöneticisi", "admin": "Sistem Yöneticisi",
+1 -37
View File
@@ -14,7 +14,6 @@
"bot": "Bot & Webhook", "bot": "Bot & Webhook",
"notification_channels": "Bildirim Kanalları", "notification_channels": "Bildirim Kanalları",
"firebase": "Firebase", "firebase": "Firebase",
"agora": "Agora",
"smtp": "SMTP", "smtp": "SMTP",
"login_method": "Giriş Yöntemleri", "login_method": "Giriş Yöntemleri",
"third_app": "Third-party Girişler", "third_app": "Third-party Girişler",
@@ -28,26 +27,6 @@
"video": "Video", "video": "Video",
"notification": "Bildirimler" "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": { "channel": {
"leave": "Kanalı Terket", "leave": "Kanalı Terket",
"leave_desc": "Bu kanalı terketmek istediğinize emin misiniz?", "leave_desc": "Bu kanalı terketmek istediğinize emin misiniz?",
@@ -104,23 +83,8 @@
"expire": "Süre Dolum Tarihi", "expire": "Süre Dolum Tarihi",
"create": "Yaratılma Tarihi", "create": "Yaratılma Tarihi",
"value": "Lisans değeri", "value": "Lisans değeri",
"renew": "Lisansı Yenile",
"update": "El ile Yenile", "update": "El ile Yenile",
"update_placeholder": "Lütfen yeni lisans değerini girin", "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"
}, },
"bot": { "bot": {
"add_api_key": "API Anahtarı Ekleyin", "add_api_key": "API Anahtarı Ekleyin",
+3 -3
View File
@@ -7,8 +7,7 @@
"download": "Mobil uygulamaları indirin", "download": "Mobil uygulamaları indirin",
"help": "Sorunuz var mı? Yardım merkezimizi ziyaret edin", "help": "Sorunuz var mı? Yardım merkezimizi ziyaret edin",
"sign_in_tip": "İleti göndermek için lütfen oturum açın", "sign_in_tip": "İleti göndermek için lütfen oturum açın",
"vocespace": " VoceSpace Video Görüşmesi", "license": "VoceChat Lisansı",
"license": "VoceChat Yükseltme Lisansı",
"page_editor": { "page_editor": {
"title": "Page Management", "title": "Page Management",
"status_select": "Select State", "status_select": "Select State",
@@ -66,5 +65,6 @@
"tunnel_progress": "Starting tunnel...", "tunnel_progress": "Starting tunnel...",
"tunnel_error": "Tunnel error", "tunnel_error": "Tunnel error",
"tunnel_get_domain": "Get Public Domain" "tunnel_get_domain": "Get Public Domain"
} },
"video": "Video"
} }
+1 -9
View File
@@ -18,7 +18,6 @@
"created_at": "创建时间", "created_at": "创建时间",
"updated_at": "最后更新", "updated_at": "最后更新",
"dismiss_announcement": "关闭公告", "dismiss_announcement": "关闭公告",
"channel_name": "频道名称", "channel_name": "频道名称",
"private_channel": "私密频道", "private_channel": "私密频道",
"create_channel": "创建新频道", "create_channel": "创建新频道",
@@ -26,7 +25,6 @@
"create_private_channel_desc": "这是一个私密频道,只有被选择的人才能看到", "create_private_channel_desc": "这是一个私密频道,只有被选择的人才能看到",
"search_user_placeholder": "请输入用户名", "search_user_placeholder": "请输入用户名",
"welcome_msg": "欢迎来到 {{name}} 频道", "welcome_msg": "欢迎来到 {{name}} 频道",
"invite_title": "邀请朋友加入{{name}}", "invite_title": "邀请朋友加入{{name}}",
"invite_by_email": "邮件邀请", "invite_by_email": "邮件邀请",
"enable_smtp": "首先开启 SMTP", "enable_smtp": "首先开启 SMTP",
@@ -36,22 +34,18 @@
"invite_link_edit": "编辑邀请链接", "invite_link_edit": "编辑邀请链接",
"invite_link_setting_tip": "邀请链接有效期:{{expire}},最多使用次数:{{times}}", "invite_link_setting_tip": "邀请链接有效期:{{expire}},最多使用次数:{{times}}",
"generate_new_link": "生成新邀请链接", "generate_new_link": "生成新邀请链接",
"send_to": "发送至", "send_to": "发送至",
"edited": "已编辑", "edited": "已编辑",
"license_tip": "您的使用人数已达证书限制上限,请升级证书或者联系管理员", "license_tip": "您的使用人数已达当前证书限制上限,请联系管理员处理。",
"delete_msg_title": "删除消息", "delete_msg_title": "删除消息",
"delete_msg_desc": "确定删除{{msg}}", "delete_msg_desc": "确定删除{{msg}}",
"delete_msg_this": "此消息", "delete_msg_this": "此消息",
"delete_msg_these": "这些消息", "delete_msg_these": "这些消息",
"new_msg": "{{num}} 条新消息", "new_msg": "{{num}} 条新消息",
"mark_read": "设为已读", "mark_read": "设为已读",
"pin_chat": "置顶", "pin_chat": "置顶",
"unpin_chat": "取消置顶", "unpin_chat": "取消置顶",
"reply_msg_del": "被回复的消息已删除!", "reply_msg_del": "被回复的消息已删除!",
"file": "文件", "file": "文件",
"image": "图片", "image": "图片",
"forward": "转发", "forward": "转发",
@@ -71,12 +65,10 @@
"contact_block_tip": "该用户已被你屏蔽", "contact_block_tip": "该用户已被你屏蔽",
"file_expired": "文件已过期", "file_expired": "文件已过期",
"only_owner_can_send": "只有频道创建者才能发消息!", "only_owner_can_send": "只有频道创建者才能发消息!",
"remark": "备注", "remark": "备注",
"remark_clear": "重置备注", "remark_clear": "重置备注",
"remark_intro": "给联系人备注,方便日后查找,备注仅您可见", "remark_intro": "给联系人备注,方便日后查找,备注仅您可见",
"remark_placeholder": "备注该用户", "remark_placeholder": "备注该用户",
"guest_mode_warning": { "guest_mode_warning": {
"title": "该频道将对访客可见", "title": "该频道将对访客可见",
"desc": "继续创建后,访客用户可能会看到该公共频道中的消息。", "desc": "继续创建后,访客用户可能会看到该公共频道中的消息。",
-15
View File
@@ -38,7 +38,6 @@
"reply": "回复", "reply": "回复",
"pin": "置顶", "pin": "置顶",
"unpin": "取消置顶", "unpin": "取消置顶",
"send": "发送", "send": "发送",
"send_msg": "发消息", "send_msg": "发消息",
"copy": "复制", "copy": "复制",
@@ -51,7 +50,6 @@
"install": "安装", "install": "安装",
"download_origin": "下载原图", "download_origin": "下载原图",
"close": "关闭", "close": "关闭",
"upgrade": "升级",
"view_pwd": "查看密码", "view_pwd": "查看密码",
"change_pwd": "修改密码" "change_pwd": "修改密码"
}, },
@@ -59,7 +57,6 @@
"uploading": "上传中" "uploading": "上传中"
}, },
"tip": { "tip": {
"email_msg_tip": "您的地区收不到消息提醒,升级到 Pro 版本可以使用邮件提醒功能",
"update": "更新成功!", "update": "更新成功!",
"delete": "删除成功!", "delete": "删除成功!",
"reg": "注册成功!", "reg": "注册成功!",
@@ -71,18 +68,6 @@
}, },
"new_version": "有<0>新版本</0>", "new_version": "有<0>新版本</0>",
"mobile_app": "欢迎下载使用<0>手机 APP</0>", "mobile_app": "欢迎下载使用<0>手机 APP</0>",
"price": {
"pro": {
"title": "VoceChat 专业版",
"desc": "",
"price": "$49"
},
"supreme": {
"title": "VoceSpace 私有部署音视频[499$]",
"desc": "",
"price": ""
}
},
"server_update": { "server_update": {
"version_needed": "支持该特性的最低 Server 版本:<0>{{version}}</0>", "version_needed": "支持该特性的最低 Server 版本:<0>{{version}}</0>",
"current_version": "当前的 Server 版本:<0>{{version}}</0>", "current_version": "当前的 Server 版本:<0>{{version}}</0>",
-1
View File
@@ -27,7 +27,6 @@
"roles": "设置角色", "roles": "设置角色",
"set_admin": "管理员", "set_admin": "管理员",
"set_normal": "普通用户", "set_normal": "普通用户",
"search_not_found": "用户不存在,或者不允许搜索", "search_not_found": "用户不存在,或者不允许搜索",
"search_disabled": "管理员已关闭用户搜索功能", "search_disabled": "管理员已关闭用户搜索功能",
"add_to_contact": "加为联系人", "add_to_contact": "加为联系人",
+3 -45
View File
@@ -15,7 +15,6 @@
"bot": "机器人&Webhook", "bot": "机器人&Webhook",
"notification_channels": "通知渠道", "notification_channels": "通知渠道",
"firebase": "Firebase", "firebase": "Firebase",
"agora": "Agora",
"smtp": "SMTP", "smtp": "SMTP",
"login_method": "登录设置", "login_method": "登录设置",
"third_app": "第三方认证", "third_app": "第三方认证",
@@ -198,23 +197,8 @@
"expire": "过期时间", "expire": "过期时间",
"create": "创建时间", "create": "创建时间",
"value": "证书内容", "value": "证书内容",
"renew": "升级证书",
"update": "手动更新", "update": "手动更新",
"update_placeholder": "请输入新的证书内容", "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": "开始支付"
}, },
"bot": { "bot": {
"add_api_key": "新增 API Key", "add_api_key": "新增 API Key",
@@ -274,7 +258,7 @@
"client_email": "客户端邮箱" "client_email": "客户端邮箱"
}, },
"smtp": { "smtp": {
"desc": "对于新消息电子邮件通知(高级功能,升级到 <0>VoceChat Pro</0>和新注册电子邮件验证,我们使用 SMTP。", "desc": "对于新消息电子邮件通知和新注册电子邮件验证,我们使用 SMTP。",
"sub_desc": "请在下方设置您的 SMTP。", "sub_desc": "请在下方设置您的 SMTP。",
"enable": "开启", "enable": "开启",
"host": "主机", "host": "主机",
@@ -285,32 +269,6 @@
"how_to": "如何设置 SMTP", "how_to": "如何设置 SMTP",
"send_test_email": "发送测试邮件" "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": { "third_app": {
"key": "API 安全密钥", "key": "API 安全密钥",
"update": "更新安全密钥", "update": "更新安全密钥",
@@ -477,4 +435,4 @@
"error": "错误" "error": "错误"
} }
} }
} }
+3 -3
View File
@@ -7,8 +7,7 @@
"download": "下载移动端APP", "download": "下载移动端APP",
"help": "还有问题?访问我们的帮助中心", "help": "还有问题?访问我们的帮助中心",
"sign_in_tip": "请登录后发消息", "sign_in_tip": "请登录后发消息",
"vocespace": " VoceSpace 视频通话", "license": "VoceChat 证书",
"license": "VoceChat 升级证书",
"page_editor": { "page_editor": {
"title": "页面管理", "title": "页面管理",
"status_select": "状态选择", "status_select": "状态选择",
@@ -66,5 +65,6 @@
"tunnel_progress": "正在启动 Tunnel...", "tunnel_progress": "正在启动 Tunnel...",
"tunnel_error": "Tunnel 出错", "tunnel_error": "Tunnel 出错",
"tunnel_get_domain": "获取公网域名" "tunnel_get_domain": "获取公网域名"
} },
"video": "视频通话"
} }
-64
View File
@@ -1,16 +1,5 @@
import i18n from "../i18n"; 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 official_dev = `https://dev.voce.chat`;
const local_dev = `http://localhost:3000`; const local_dev = `http://localhost:3000`;
// const local_dev = `https://dev.voce.chat`; // 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; export const IS_OFFICIAL_DEMO = BASE_ORIGIN === official_dev;
const BASE_URL = `${BASE_ORIGIN}/api`; 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 CACHE_VERSION = `0.4.3`;
export const WIDGET_USER_PWD = `123123`; export const WIDGET_USER_PWD = `123123`;
export const GuestRoutes = ["/", "/chat", "/chat/channel/:channel_id"]; export const GuestRoutes = ["/", "/chat", "/chat/channel/:channel_id"];
-3
View File
@@ -27,15 +27,12 @@ const whiteList = [
"createAdmin", "createAdmin",
"getBotRelatedChannels", "getBotRelatedChannels",
"sendMessageByBot", "sendMessageByBot",
"getAgoraVoicingList",
"preCheckFileFromUrl", "preCheckFileFromUrl",
"passkeyLoginStart", "passkeyLoginStart",
"passkeyLoginFinish", "passkeyLoginFinish",
"getAutoTunnelInfo", "getAutoTunnelInfo",
]; ];
const whiteList401 = [ const whiteList401 = [
"getAgoraVoicingList",
"getAgoraChannels",
"getGoogleAuthConfig", "getGoogleAuthConfig",
"getGithubAuthConfig", "getGithubAuthConfig",
"getAutoTunnelInfo", "getAutoTunnelInfo",
+22 -108
View File
@@ -3,10 +3,6 @@ import { createApi } from "@reduxjs/toolkit/query/react";
import { Channel } from "@/types/channel"; import { Channel } from "@/types/channel";
import { ContentTypeKey } from "@/types/message"; import { ContentTypeKey } from "@/types/message";
import { import {
AgoraChannelUsersResponse,
AgoraConfig,
AgoraTokenResponse,
AgoraVoicingListResponse,
AutoTunnelInfo, AutoTunnelInfo,
CloudflaredStatus, CloudflaredStatus,
CreateAdminDTO, CreateAdminDTO,
@@ -14,14 +10,14 @@ import {
GithubAuthConfig, GithubAuthConfig,
GoogleAuthConfig, GoogleAuthConfig,
LicenseResponse, LicenseResponse,
LivekitCallRequest,
LivekitConfig,
LivekitTokenResponse,
LoginConfig, LoginConfig,
RenewLicense,
RenewLicenseResponse,
Server, Server,
SMTPConfig, SMTPConfig,
SystemCommon, SystemCommon,
TestEmailDTO, TestEmailDTO,
VocespaceConfig,
} from "@/types/server"; } from "@/types/server";
import { User } from "@/types/user"; import { User } from "@/types/user";
import { compareVersion, encodeBase64 } from "@/utils"; import { compareVersion, encodeBase64 } from "@/utils";
@@ -29,11 +25,8 @@ import BASE_URL, {
ContentTypes, ContentTypes,
IS_OFFICIAL_DEMO, IS_OFFICIAL_DEMO,
KEY_SERVER_VERSION, KEY_SERVER_VERSION,
PAYMENT_URL_PREFIX,
} from "../config"; } from "../config";
import { updateInfo } from "../slices/server"; import { updateInfo } from "../slices/server";
import { updateCallInfo, upsertVoiceList } from "../slices/voice";
import { RootState } from "../store";
import baseQuery from "./base.query"; import baseQuery from "./base.query";
import { GetFilesDTO, VoceChatFile } from "@/types/resource"; import { GetFilesDTO, VoceChatFile } from "@/types/resource";
import { GroupAnnouncement } from "@/types/sse"; import { GroupAnnouncement } from "@/types/sse";
@@ -130,89 +123,29 @@ export const serverApi = createApi({
body: data, body: data,
}), }),
}), }),
getVocespaceConfig: builder.query<VocespaceConfig, void>({ getLivekitConfig: builder.query<LivekitConfig, void>({
query: () => ({ url: `/admin/vocespace/config` }), query: () => ({ url: `/admin/livekit/config` }),
}), }),
getAgoraConfig: builder.query<AgoraConfig, void>({ updateLivekitConfig: builder.mutation<void, LivekitConfig>({
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>({
query: (data) => ({ query: (data) => ({
url: `/admin/agora/config`, url: `/admin/livekit/config`,
method: "POST", method: "POST",
body: data, 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) => ({ query: (data) => ({
url: `/admin/vocespace/config`, url: `/admin/livekit/token`,
method: "POST", method: "POST",
body: data, body: data,
}), }),
}), }),
healthCheckVocespace: builder.mutation<boolean, { url: string }>({ signalLivekitCall: builder.mutation<void, LivekitCallRequest>({
query: (data) => ({ query: (data) => ({
url: `/admin/vocespace/health/${encodeURIComponent(data.url)}`, url: `/admin/livekit/call`,
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`,
method: "POST", method: "POST",
body: data, body: data,
}), }),
@@ -364,14 +297,14 @@ export const serverApi = createApi({
// vocechat 官方 demo 则忽略 // vocechat 官方 demo 则忽略
if (IS_OFFICIAL_DEMO) return; if (IS_OFFICIAL_DEMO) return;
const rootStore = getState() as RootState; const rootStore = getState() as RootState;
const { upgraded: prevValue } = rootStore.server; const { licensed: prevValue } = rootStore.server;
try { try {
const { const {
data: { user_limit }, data: { user_limit },
} = await queryFulfilled; } = await queryFulfilled;
const currValue = user_limit > 20; const currValue = user_limit > 20;
if (prevValue !== currValue) { if (prevValue !== currValue) {
dispatch(updateInfo({ upgraded: currValue })); dispatch(updateInfo({ licensed: currValue }));
} }
} catch { } catch {
console.error("get license failed "); 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>({ checkLicense: builder.mutation<LicenseResponse, string>({
query: (license) => ({ query: (license) => ({
url: "/license/check", url: "/license/check",
@@ -543,14 +464,14 @@ export const {
useUpdateFirebaseConfigMutation, useUpdateFirebaseConfigMutation,
useGetFirebaseConfigQuery, useGetFirebaseConfigQuery,
useLazyGetFirebaseConfigQuery, useLazyGetFirebaseConfigQuery,
useLazyGetAgoraConfigQuery, useLazyGetLivekitConfigQuery,
useLazyGetSMTPConfigQuery, useLazyGetSMTPConfigQuery,
useLazyGetLoginConfigQuery, useLazyGetLoginConfigQuery,
useGetLoginConfigQuery, useGetLoginConfigQuery,
useUpdateLoginConfigMutation, useUpdateLoginConfigMutation,
useGetSMTPConfigQuery, useGetSMTPConfigQuery,
useUpdateSMTPConfigMutation, useUpdateSMTPConfigMutation,
useUpdateAgoraConfigMutation, useUpdateLivekitConfigMutation,
useGetServerQuery, useGetServerQuery,
useLazyGetServerQuery, useLazyGetServerQuery,
useUpdateServerMutation, useUpdateServerMutation,
@@ -561,27 +482,20 @@ export const {
useUpsertLicenseMutation, useUpsertLicenseMutation,
useCheckLicenseMutation, useCheckLicenseMutation,
useGetLicenseQuery, useGetLicenseQuery,
useGetLicensePaymentUrlMutation,
useLazyGetGeneratedLicenseQuery,
useLazyGetBotRelatedChannelsQuery, useLazyGetBotRelatedChannelsQuery,
useSendMessageByBotMutation, useSendMessageByBotMutation,
useUpdateFrontendUrlMutation, useUpdateFrontendUrlMutation,
useGetFrontendUrlQuery, useGetFrontendUrlQuery,
useGetAgoraConfigQuery, useGetLivekitConfigQuery,
useGetAgoraStatusQuery, useGetLivekitStatusQuery,
useGetAgoraChannelsQuery,
useUpdateSystemCommonMutation, useUpdateSystemCommonMutation,
useLazyGetSystemCommonQuery, useLazyGetSystemCommonQuery,
useGetSystemCommonQuery, useGetSystemCommonQuery,
useGenerateAgoraTokenMutation, useGenerateLivekitTokenMutation,
useLazyGetAgoraUsersByChannelQuery, useSignalLivekitCallMutation,
useLazyClearAllFilesQuery, useLazyClearAllFilesQuery,
useLazyClearAllMessagesQuery, useLazyClearAllMessagesQuery,
useLazyGetFilesQuery, useLazyGetFilesQuery,
useGetVocespaceConfigQuery,
useLazyGetVocespaceConfigQuery,
useUpdateVocespaceConfigMutation,
useHealthCheckVocespaceMutation,
useGetGroupAnnouncementQuery, useGetGroupAnnouncementQuery,
useCreateOrUpdateGroupAnnouncementMutation, useCreateOrUpdateGroupAnnouncementMutation,
useDeleteGroupAnnouncementMutation, useDeleteGroupAnnouncementMutation,
+4 -4
View File
@@ -3,7 +3,7 @@ import { LoginConfig, Server } from "@/types/server";
export interface StoredServer extends Server { export interface StoredServer extends Server {
version: string; version: string;
upgraded: boolean; licensed: boolean;
logo: string; logo: string;
inviteLink?: { inviteLink?: {
link: string; link: string;
@@ -13,7 +13,7 @@ export interface StoredServer extends Server {
} }
const initialState: StoredServer = { const initialState: StoredServer = {
version: "", version: "",
upgraded: false, licensed: false,
name: "", name: "",
description: "", description: "",
logo: "", logo: "",
@@ -45,7 +45,7 @@ const serverSlice = createSlice({
fillServer(state, action: PayloadAction<StoredServer>) { fillServer(state, action: PayloadAction<StoredServer>) {
const { const {
version, version,
upgraded, licensed,
inviteLink = { inviteLink = {
link: "", link: "",
expire: 0 expire: 0
@@ -67,7 +67,7 @@ const serverSlice = createSlice({
} = action.payload || {}; } = action.payload || {};
return { return {
version: state.version || version, version: state.version || version,
upgraded: state.upgraded || upgraded, licensed: Boolean(state.licensed || licensed),
name: state.name || name, name: state.name || name,
logo: state.logo || logo, logo: state.logo || logo,
description: state.description || description, description: state.description || description,
+8 -9
View File
@@ -1,5 +1,4 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ConnectionState } from "agora-rtc-sdk-ng";
import { ChatContext } from "@/types/common"; import { ChatContext } from "@/types/common";
import { KEY_UID } from "../config"; import { KEY_UID } from "../config";
import { resetAuthData } from "./auth.data"; import { resetAuthData } from "./auth.data";
@@ -13,7 +12,7 @@ export type VoiceBasicInfo = {
export type VoicingInfo = { export type VoicingInfo = {
downlinkNetworkQuality?: number; downlinkNetworkQuality?: number;
joining?: boolean; joining?: boolean;
connectionState?: ConnectionState; connectionState?: string;
} & VoiceBasicInfo & } & VoiceBasicInfo &
VoicingMemberInfo; VoicingMemberInfo;
@@ -168,7 +167,7 @@ const voiceSlice = createSlice({
}; };
} }
}, },
updateConnectionState(state, { payload }: PayloadAction<ConnectionState>) { updateConnectionState(state, { payload }: PayloadAction<string>) {
if (state.voicing) { if (state.voicing) {
state.voicing.connectionState = payload; state.voicing.connectionState = payload;
} }
@@ -230,18 +229,18 @@ const voiceSlice = createSlice({
builder.addCase(resetAuthData, (state) => { builder.addCase(resetAuthData, (state) => {
// reset voicing info // reset voicing info
if (window.VOICE_CLIENT) { if (window.VOICE_CLIENT) {
window.VOICE_CLIENT.leave(); window.VOICE_CLIENT.disconnect();
Object.entries(window.VOICE_TRACK_MAP).forEach(([uid, track]) => { Object.entries(window.VOICE_TRACK_MAP).forEach(([uid, track]) => {
if (track && "close" in track) { if (track && "detach" in track) {
track.close(); track.detach().forEach((el) => el.remove());
} }
delete window.VOICE_TRACK_MAP[+uid]; delete window.VOICE_TRACK_MAP[+uid];
}); });
Object.entries(window.VIDEO_TRACK_MAP).forEach(([uid, track]) => { Object.entries(window.VIDEO_TRACK_MAP).forEach(([uid, track]) => {
if (track && "close" in track) { if (track && "detach" in track) {
track?.close(); track.detach().forEach((el) => el.remove());
} }
delete window.VOICE_TRACK_MAP[+uid]; delete window.VIDEO_TRACK_MAP[+uid];
}); });
} }
state.voicing = null; state.voicing = null;
+8 -21
View File
@@ -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 { useTranslation } from "react-i18next";
import { NavLink, useNavigate } from "react-router-dom"; import { NavLink, useNavigate } from "react-router-dom";
import Linkify from "linkify-react"; import Linkify from "linkify-react";
@@ -10,7 +10,6 @@ import { BASE_ORIGIN } from "../app/config";
import { compareVersion } from "../utils"; import { compareVersion } from "../utils";
import { useLazyGetPageHtmlQuery, useUploadPageHtmlMutation, useResetPageHtmlMutation, useGeneratePageApiKeyMutation } from "@/app/services/server"; import { useLazyGetPageHtmlQuery, useUploadPageHtmlMutation, useResetPageHtmlMutation, useGeneratePageApiKeyMutation } from "@/app/services/server";
import useServerExtSetting from "../hooks/useServerExtSetting"; import useServerExtSetting from "../hooks/useServerExtSetting";
import useLicense from "@/hooks/useLicense";
import ChannelModal from "./ChannelModal"; import ChannelModal from "./ChannelModal";
import InviteModal from "./InviteModal"; import InviteModal from "./InviteModal";
import UsersModal from "./UsersModal"; import UsersModal from "./UsersModal";
@@ -49,8 +48,7 @@ const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const useIframe = compareVersion(server.version, IFRAME_MIN_VERSION) >= 0; const useIframe = compareVersion(server.version, IFRAME_MIN_VERSION) >= 0;
const { license: licenseInfo } = useLicense(true); const showVideoSettings = Boolean(isAdmin);
const showVoceSpace = useMemo(() => compareVersion(server.version, "0.5.6") >= 0, [server.version]);
const [inviteModalVisible, setInviteModalVisible] = useState(false); const [inviteModalVisible, setInviteModalVisible] = useState(false);
const [createChannelVisible, setCreateChannelVisible] = 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) { if (!useIframe) {
const chatTip = type === "chat" ? t("start_by_channel") : t("start_by_dm"); const chatTip = type === "chat" ? t("start_by_channel") : t("start_by_dm");
const chatHandler = type === "chat" ? () => setCreateChannelVisible(true) : () => setUserListVisible(true); const chatHandler = type === "chat" ? () => setCreateChannelVisible(true) : () => setUserListVisible(true);
const toLicensePage = () => navigate("/setting/license");
return ( return (
<> <>
<div className="flex flex-col gap-8 -mt-[50px] dark:bg-gray-700"> <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> <div className={legacyClasses.boxTip}>{t("invite")}</div>
</button> </button>
)} )}
{licenseInfo?.user_limit == 20 && showVoceSpace ? ( <button onClick={chatHandler} className={legacyClasses.box}>
<button onClick={toLicensePage} className={legacyClasses.box}> <IconChat className={legacyClasses.boxIcon} />
<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"> <div className={legacyClasses.boxTip}>{chatTip}</div>
<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> </button>
<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> {showVideoSettings ? (
</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 ? (
<NavLink to={"/setting/video"} className={legacyClasses.box}> <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"> <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)" /> <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> </linearGradient>
</defs> </defs>
</svg> </svg>
<div className={legacyClasses.boxTip}>{t("vocespace")}</div> <div className={legacyClasses.boxTip}>{t("video")}</div>
</NavLink> </NavLink>
) : ( ) : (
<a href={"https://voce.chat#download"} target={"_blank"} rel="noreferrer" className={legacyClasses.box}> <a href={"https://voce.chat#download"} target={"_blank"} rel="noreferrer" className={legacyClasses.box}>
+4 -4
View File
@@ -4,7 +4,7 @@ import { NavLink } from "react-router-dom";
import Tippy from "@tippyjs/react"; import Tippy from "@tippyjs/react";
import clsx from "clsx"; import clsx from "clsx";
import { useGetAgoraStatusQuery } from "@/app/services/server"; import { useGetLivekitStatusQuery } from "@/app/services/server";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import useUserOperation from "@/hooks/useUserOperation"; import useUserOperation from "@/hooks/useUserOperation";
import IconCall from "@/assets/icons/call.svg"; import IconCall from "@/assets/icons/call.svg";
@@ -24,7 +24,7 @@ interface Props {
const Profile: FC<Props> = ({ uid, type = "embed", cid }) => { const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
const [remarkVisible, setRemarkVisible] = useState(false); const [remarkVisible, setRemarkVisible] = useState(false);
const { data: agoraEnabled } = useGetAgoraStatusQuery(); const { data: livekitEnabled } = useGetLivekitStatusQuery();
const { t } = useTranslation("member"); const { t } = useTranslation("member");
const { t: chatTrans } = useTranslation("chat"); const { t: chatTrans } = useTranslation("chat");
const { t: ct } = useTranslation(); const { t: ct } = useTranslation();
@@ -91,7 +91,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
<span>{t("send_msg")}</span> <span>{t("send_msg")}</span>
</li> </li>
</NavLink> </NavLink>
{agoraEnabled && type == "embed" && ( {livekitEnabled && type == "embed" && (
<li role="button" onClick={startCall} className={`${iconClass} icon chat`}> <li role="button" onClick={startCall} className={`${iconClass} icon chat`}>
<IconCall className="fill-primary-400" /> <IconCall className="fill-primary-400" />
<span>{t("call")}</span> <span>{t("call")}</span>
@@ -112,7 +112,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
title: chatTrans("remark"), title: chatTrans("remark"),
handler: setRemarkVisible.bind(null, true), handler: setRemarkVisible.bind(null, true),
}, },
agoraEnabled && livekitEnabled &&
type == "card" && { type == "card" && {
title: t("call"), title: t("call"),
handler: startCall, handler: startCall,
+14 -10
View File
@@ -7,11 +7,12 @@ import { Waveform } from "@uiball/loaders";
import clsx from "clsx"; import clsx from "clsx";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useSignalLivekitCallMutation } from "../../app/services/server";
import IconCallOff from "@/assets/icons/call.off.svg"; import IconCallOff from "@/assets/icons/call.off.svg";
import IconCallAnswer from "@/assets/icons/call.svg"; import IconCallAnswer from "@/assets/icons/call.svg";
import { updateCalling } from "../../app/slices/voice"; import { updateCalling } from "../../app/slices/voice";
import { useAppSelector } from "../../app/store"; import { useAppSelector } from "../../app/store";
import { playAgoraVideo } from "../../utils"; import { playLivekitVideo } from "../../utils";
import Avatar from "../Avatar"; import Avatar from "../Avatar";
import Tooltip from "../Tooltip"; import Tooltip from "../Tooltip";
import useVoice from "./useVoice"; import useVoice from "./useVoice";
@@ -25,38 +26,41 @@ const DMCalling = ({ from, to = 0 }: Props) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { pathname } = useLocation(); const { pathname } = useLocation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { leave, joinVoice, joining } = useVoice({ id: to, context: "dm" });
const containerRef = useRef(null); const containerRef = useRef(null);
const loginUid = useAppSelector((store) => store.authData.user?.uid, shallowEqual); 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 calling = useAppSelector((store) => store.voice.calling, shallowEqual);
const voicingMembers = useAppSelector((store) => store.voice.voicingMembers, shallowEqual); const voicingMembers = useAppSelector((store) => store.voice.voicingMembers, shallowEqual);
const fromUser = useAppSelector((store) => store.users.byId[from], shallowEqual); const fromUser = useAppSelector((store) => store.users.byId[from], shallowEqual);
const toUser = useAppSelector((store) => store.users.byId[to], shallowEqual); const toUser = useAppSelector((store) => store.users.byId[to], shallowEqual);
const sendByMe = loginUid !== toUser.uid;
useEffect(() => { useEffect(() => {
const ids = voicingMembers.ids; const ids = voicingMembers.ids;
ids.forEach((id) => { ids.forEach((id) => {
playAgoraVideo(id); playLivekitVideo(id);
}); });
}, [voicingMembers.ids]); }, [voicingMembers.ids]);
const handleCancel = () => { const handleCancel = () => {
console.log("cancel"); signalLivekitCall({ uid: peerUid, action: "cancel" });
if (sendByMe || voicingMembers.ids.length == 2) { if (sendByMe || voicingMembers.ids.length == 2) {
leave(); leave();
} }
// 拒绝 // 拒绝
dispatch(updateCalling(false)); dispatch(updateCalling(false));
}; };
const handleAnswer = () => { const handleAnswer = async () => {
joinVoice(); const joinedLivekit = await joinVoice();
navigate(`/chat/dm/${from}`); if (!joinedLivekit) return;
navigate(`/chat/dm/${peerUid}`);
}; };
if (!fromUser || !toUser) return null;
const sendByMe = loginUid !== toUser.uid;
const { name, avatar } = sendByMe ? toUser : fromUser; const { name, avatar } = sendByMe ? toUser : fromUser;
const connected = voicingMembers.ids.length == 2; const connected = voicingMembers.ids.length == 2;
console.log("dm calling", !fromUser, !toUser, connected);
const atChatPath = sendByMe ? pathname == `/chat/dm/${to}` : pathname == `/chat/dm/${from}`; 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 ( return (
<div <div
+6 -43
View File
@@ -2,7 +2,6 @@ import { Dispatch, MouseEvent, SetStateAction, useEffect, useState } from "react
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { shallowEqual, useDispatch } from "react-redux"; import { shallowEqual, useDispatch } from "react-redux";
import Tippy from "@tippyjs/react"; import Tippy from "@tippyjs/react";
import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from "agora-rtc-sdk-ng";
import clsx from "clsx"; import clsx from "clsx";
// import { updateChannelVisibleAside, updateDMVisibleAside } from '@/app/slices/footprint'; // import { updateChannelVisibleAside, updateDMVisibleAside } from '@/app/slices/footprint';
@@ -44,7 +43,6 @@ const DeviceList = ({
selected: string; selected: string;
}[]; }[];
}) => { }) => {
const loginUid = useAppSelector((store) => store.authData.user?.uid ?? 0, shallowEqual);
const dispatch = useDispatch(); const dispatch = useDispatch();
// const { t } = useTranslation("chat"); // const { t } = useTranslation("chat");
const toggleVisible = (evt: MouseEvent<HTMLDivElement>) => { const toggleVisible = (evt: MouseEvent<HTMLDivElement>) => {
@@ -55,46 +53,10 @@ const DeviceList = ({
// evt.stopPropagation(); // evt.stopPropagation();
const { deviceId = "", kind, selected = "" } = evt.currentTarget.dataset; const { deviceId = "", kind, selected = "" } = evt.currentTarget.dataset;
if (selected == deviceId || !kind) return; if (selected == deviceId || !kind) return;
switch (kind as MediaDeviceKind) { window.VOICE_CLIENT?.switchActiveDevice(kind as MediaDeviceKind, deviceId).catch((err) => {
case "audiooutput": console.log("switch active device error", err);
case "audioinput": });
{ dispatch(updateSelectDeviceId({ kind: kind as MediaDeviceKind, value: deviceId }));
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;
}
}; };
const handleBlockClick = (evt: MouseEvent<HTMLDivElement>) => { const handleBlockClick = (evt: MouseEvent<HTMLDivElement>) => {
evt.stopPropagation(); evt.stopPropagation();
@@ -189,7 +151,8 @@ const Operations = ({ id, context, mode = "channel" }: Props) => {
} = useVoice({ id, context }); } = useVoice({ id, context });
const { t } = useTranslation("chat"); const { t } = useTranslation("chat");
useEffect(() => { useEffect(() => {
AgoraRTC.getDevices() navigator.mediaDevices
.enumerateDevices()
.then((devices) => { .then((devices) => {
console.log("devices", devices); console.log("devices", devices);
dispatch( dispatch(
+176 -172
View File
@@ -1,208 +1,212 @@
import { memo, useEffect } from "react"; import { memo, useEffect } from "react";
import { shallowEqual, useDispatch } from "react-redux"; import { shallowEqual, useDispatch } from "react-redux";
import AgoraRTC from "agora-rtc-sdk-ng";
import { import {
useGetAgoraChannelsQuery, Participant,
useGetAgoraStatusQuery, RemoteAudioTrack,
useLazyGetAgoraUsersByChannelQuery RemoteTrack,
} from "../../app/services/server"; RemoteTrackPublication,
Room,
RoomEvent,
Track,
} from "livekit-client";
import { import {
addVoiceMember, addVoiceMember,
removeVoiceMember, removeVoiceMember,
updateCallInfo,
updateConnectionState, updateConnectionState,
updateVoicingMember, updateVoicingMember,
updateVoicingNetworkQuality upsertVoiceList,
} from "../../app/slices/voice"; } from "../../app/slices/voice";
import { useAppSelector } from "../../app/store"; import { useAppSelector } from "../../app/store";
import { isInIframe, playAgoraVideo } from "../../utils"; import { isInIframe, playLivekitVideo } from "../../utils";
import DMCalling from "./DMCalling";
import useVoice from "./useVoice";
AgoraRTC.setLogLevel(process.env.NODE_ENV === "development" ? 0 : 4);
window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {}; window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {};
window.VIDEO_TRACK_MAP = window.VIDEO_TRACK_MAP ?? {}; window.VIDEO_TRACK_MAP = window.VIDEO_TRACK_MAP ?? {};
const inIframe = isInIframe(); 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 Voice = () => {
const loginUid = useAppSelector((store) => store.authData.user?.uid, shallowEqual); 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 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(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
const initializeAgoraClient = async () => { if (inIframe || window.VOICE_CLIENT) return;
// 创建agora客户端实例 window.VOICE_CLIENT = new Room({
const agoraEngine = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" }); adaptiveStream: true,
// 无论频道内是否有人说话,都会每两秒返回提示音量 dynacast: true,
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 (label?.includes("screen")) { useEffect(() => {
// // 远端用户共享屏幕 const room = window.VOICE_CLIENT;
// // const screenTrack = user.videoTrack as ShareScreenTrack; if (!room || inIframe) return;
// // 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);
};
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) => { const handlePageUnload = (evt: BeforeUnloadEvent) => {
console.log("unload"); if (window.VOICE_CLIENT?.state === "connected") {
if (window.VOICE_CLIENT?.connectionState === "CONNECTED") {
evt.preventDefault(); evt.preventDefault();
return (evt.returnValue = ""); return (evt.returnValue = "");
} }
}; };
if (!window.VOICE_CLIENT && !inIframe) { window.addEventListener("beforeunload", handlePageUnload, { capture: true });
initializeAgoraClient();
window.addEventListener("beforeunload", handlePageUnload, { capture: true });
}
return () => { 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 }); 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; return null;
}; };
export { useVoice }; export { default as useVoice } from "./useVoice";
export default memo(Voice); export default memo(Voice);
+208 -209
View File
@@ -1,25 +1,73 @@
import toast from "react-hot-toast";
import { shallowEqual, useDispatch } from "react-redux"; 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 AudioJoin from "@/assets/join.wav";
import { useGenerateAgoraTokenMutation } from "../../app/services/server";
import { updateChannelVisibleAside, updateDMVisibleAside } from "../../app/slices/footprint";
import { import {
addVoiceMember, useGenerateLivekitTokenMutation,
updatePin, useSignalLivekitCallMutation,
updateVoicingInfo, } from "../../app/services/server";
upsertVoiceList import { updateChannelVisibleAside, updateDMVisibleAside } from "../../app/slices/footprint";
} from "../../app/slices/voice"; import { addVoiceMember, updatePin, updateVoicingInfo, upsertVoiceList } from "../../app/slices/voice";
import { useAppSelector } from "../../app/store"; import { useAppSelector } from "../../app/store";
import { ChatContext } from "../../types/common"; import { ChatContext } from "../../types/common";
import { ShareScreenTrack } from "../../types/global"; import { playLivekitVideo } from "../../utils";
import { playAgoraVideo } from "../../utils";
type VoiceProps = { type VoiceProps = {
id: number; id: number;
context?: ChatContext; context?: ChatContext;
}; };
const audioJoin = new Audio(AudioJoin); 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 useVoice = ({ id, context = "channel" }: VoiceProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const loginUid = useAppSelector((store) => store.authData.user?.uid ?? 0, shallowEqual); const loginUid = useAppSelector((store) => store.authData.user?.uid ?? 0, shallowEqual);
@@ -55,241 +103,190 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
(store) => store.voice.videoInputDeviceId, (store) => store.voice.videoInputDeviceId,
shallowEqual shallowEqual
); );
const [generateToken] = useGenerateAgoraTokenMutation(); const [generateToken] = useGenerateLivekitTokenMutation();
// const [joining, setJoining] = useState(false); const [signalLivekitCall] = useSignalLivekitCallMutation();
const joinVoice = async () => { const joinVoice = async () => {
// setJoining(true); if (!window.VOICE_CLIENT) {
toast.error("LiveKit client is not ready.");
return false;
}
dispatch( dispatch(
updateVoicingInfo({ updateVoicingInfo({
id, id,
context, context,
joining: true joining: true,
}) })
); );
const resp = await generateToken(context == "channel" ? { gid: id } : { uid: id }); const resp = await generateToken(context == "channel" ? { gid: id } : { uid: id });
if ("error" in resp) { if ("error" in resp) {
console.error("generate agora token error"); console.error("generate livekit token error", resp.error);
dispatch( toast.error("LiveKit is not configured or unavailable.");
updateVoicingInfo({ dispatch(updateVoicingInfo({ joining: false, id, context }));
joining: false, return false;
id, }
context
}) try {
); const { url, token, room } = resp.data;
} else { if (window.VOICE_CLIENT.state !== "disconnected") {
console.table(resp.data); await window.VOICE_CLIENT.disconnect();
const { channel_name, app_id, agora_token, uid } = resp.data; }
if (window.VOICE_CLIENT) { await window.VOICE_CLIENT.connect(url, token, { autoSubscribe: true });
await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid); await setActiveDevice("audioinput", audioInputDeviceId);
// Create a local audio track from the microphone audio. await window.VOICE_CLIENT.localParticipant.setMicrophoneEnabled(true);
const localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack({
microphoneId: audioInputDeviceId const localAudioTrack = getLocalTrackBySource<LocalAudioTrack>(Track.Source.Microphone);
}); if (localAudioTrack) {
// 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));
// 放到全局变量里
window.VOICE_TRACK_MAP[loginUid] = 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"
});
// 取消正在进行的视频 try {
await closeCamera(); await audioJoin.play();
await window.VOICE_CLIENT?.publish(localVideoTrack); } catch (error) {
console.warn("play join sound failed!", error);
}
dispatch( dispatch(
updateVoicingInfo({ updateVoicingInfo({
video: false, deafen: false,
shareScreen: true, muted: false,
joining: false,
connectionState: window.VOICE_CLIENT.state,
id, id,
context context,
}) })
); );
// 放到全局变量里 dispatch(addVoiceMember(loginUid));
window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack; dispatch(updateLocalVoiceList(id, context, room));
playAgoraVideo(loginUid); return true;
// 进入全屏并Pin自己 } 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") { if (context == "channel") {
dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" })); dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" }));
} else { } else {
dispatch(updateDMVisibleAside({ id, aside: "voice_fullscreen" })); dispatch(updateDMVisibleAside({ id, aside: "voice_fullscreen" }));
} }
dispatch(updatePin({ uid: loginUid, action: "pin" })); dispatch(updatePin({ uid: loginUid, action: "pin" }));
// 监听屏幕共享结束事件
if ("close" in localVideoTrack) {
localVideoTrack.getMediaStreamTrack().onended = () => {
stopShareScreen();
};
}
} catch (error) { } 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 leave = async () => {
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; const room = window.VOICE_CLIENT;
const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ICameraVideoTrack; if (!room) return;
if (window.VOICE_CLIENT) { if (context == "dm") {
await window.VOICE_CLIENT.leave(); signalLivekitCall({ uid: id, action: "cancel" });
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
})
);
} }
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( dispatch(
updateVoicingInfo({ upsertVoiceList({
muted: mute,
id, 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; const setMute = async (mute: boolean) => {
if (!localAudioTrack) return; const room = window.VOICE_CLIENT;
if (deafen) { if (!room) return;
localAudioTrack.setMuted(true); await room.localParticipant.setMicrophoneEnabled(!mute);
// 远端音频,全部静音 const localAudioTrack = getLocalTrackBySource<LocalAudioTrack>(Track.Source.Microphone);
Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => { window.VOICE_TRACK_MAP[loginUid] = localAudioTrack ?? null;
audioTrack?.setVolume(0); if (!mute && voicingInfo?.deafen) {
}); Object.values(window.VOICE_TRACK_MAP).forEach((audioTrack) => {
} else { (audioTrack as RemoteAudioTrack | undefined)?.setVolume?.(100);
localAudioTrack.setMuted(false);
// 远端音频,恢复原音
Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => {
audioTrack?.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 })); dispatch(updateVoicingInfo({ deafen, id, context }));
}; };
const enterFullscreen = (uid?: number) => { const enterFullscreen = (uid?: number) => {
if (context == "channel") { if (context == "channel") {
dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" })); dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" }));
@@ -300,6 +297,7 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
dispatch(updatePin({ uid, action: "pin" })); dispatch(updatePin({ uid, action: "pin" }));
} }
}; };
const exitFullscreen = () => { const exitFullscreen = () => {
if (context == "channel") { if (context == "channel") {
dispatch(updateChannelVisibleAside({ id, aside: "voice" })); dispatch(updateChannelVisibleAside({ id, aside: "voice" }));
@@ -307,14 +305,15 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
dispatch(updateDMVisibleAside({ id, aside: null })); dispatch(updateDMVisibleAside({ id, aside: null }));
} }
}; };
const joinedAtThisContext = voicingInfo const joinedAtThisContext = voicingInfo
? voicingInfo.id == id && voicingInfo.context == context ? voicingInfo.id == id && voicingInfo.context == context
: false; : false;
return { return {
setMute, setMute,
setDeafen, setDeafen,
leave, leave,
// canVoice,
voicingInfo, voicingInfo,
joining: voicingInfo ? voicingInfo.joining : undefined, joining: voicingInfo ? voicingInfo.joining : undefined,
joinedAtThisContext, joinedAtThisContext,
@@ -332,7 +331,7 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
videoInputDevices, videoInputDevices,
audioInputDeviceId, audioInputDeviceId,
audioOutputDeviceId, audioOutputDeviceId,
videoInputDeviceId videoInputDeviceId,
}; };
}; };
+17 -37
View File
@@ -4,30 +4,22 @@ import { useTranslation } from "react-i18next";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { import {
useGetAgoraConfigQuery,
useGetFirebaseConfigQuery, useGetFirebaseConfigQuery,
useGetLivekitConfigQuery,
useGetLoginConfigQuery, useGetLoginConfigQuery,
useGetSMTPConfigQuery, useGetSMTPConfigQuery,
useGetVocespaceConfigQuery,
useUpdateAgoraConfigMutation,
useUpdateFirebaseConfigMutation, useUpdateFirebaseConfigMutation,
useUpdateLivekitConfigMutation,
useUpdateLoginConfigMutation, useUpdateLoginConfigMutation,
useUpdateSMTPConfigMutation, useUpdateSMTPConfigMutation,
useUpdateVocespaceConfigMutation,
} from "@/app/services/server"; } from "@/app/services/server";
import { import { FirebaseConfig, LivekitConfig, LoginConfig, SMTPConfig } from "@/types/server";
AgoraConfig,
FirebaseConfig,
LoginConfig,
SMTPConfig,
VocespaceConfig,
} from "@/types/server";
// config: smtp agora login firebase // config: smtp livekit login firebase
type ConfigType = "smtp" | "agora" | "login" | "firebase" | "vocespace"; type ConfigType = "smtp" | "livekit" | "login" | "firebase";
type ConfigMap = Record< type ConfigMap = Record<
ConfigType, ConfigType,
AgoraConfig | FirebaseConfig | LoginConfig | SMTPConfig | VocespaceConfig LivekitConfig | FirebaseConfig | LoginConfig | SMTPConfig
>; >;
type valuesOf<T> = T[keyof T]; type valuesOf<T> = T[keyof T];
let originalValue: valuesOf<ConfigMap> | undefined = undefined; let originalValue: valuesOf<ConfigMap> | undefined = undefined;
@@ -40,21 +32,13 @@ export default function useConfig(config: keyof ConfigMap = "smtp") {
useUpdateLoginConfigMutation(); useUpdateLoginConfigMutation();
const [updateSMTPConfig, { isSuccess: SMTPUpdated, isLoading: SMTPUpdating }] = const [updateSMTPConfig, { isSuccess: SMTPUpdated, isLoading: SMTPUpdating }] =
useUpdateSMTPConfigMutation(); useUpdateSMTPConfigMutation();
const [updateAgoraConfig, { isSuccess: AgoraUpdated, isLoading: AgoraUpdating }] = const [updateLivekitConfig, { isSuccess: LivekitUpdated, isLoading: LivekitUpdating }] =
useUpdateAgoraConfigMutation(); useUpdateLivekitConfigMutation();
const [updateVocespaceConfig, { isSuccess: VocespaceUpdated, isLoading: VocespaceUpdating }] =
useUpdateVocespaceConfigMutation();
const [updateFirebaseConfig, { isSuccess: FirebaseUpdated, isLoading: FirebaseUpdating }] = const [updateFirebaseConfig, { isSuccess: FirebaseUpdated, isLoading: FirebaseUpdating }] =
useUpdateFirebaseConfigMutation(); useUpdateFirebaseConfigMutation();
const { refetch: getAgoraConfig, data: agoraConfig } = useGetAgoraConfigQuery(undefined, { const { refetch: getLivekitConfig, data: livekitConfig } = useGetLivekitConfigQuery(undefined, {
skip: config !== "agora", skip: config !== "livekit",
}); });
const { refetch: getVocespaceConfig, data: vocespaceConfig } = useGetVocespaceConfigQuery(
undefined,
{
skip: config !== "vocespace",
}
);
const { refetch: getLoginConfig, data: loginConfig } = useGetLoginConfigQuery(undefined, { const { refetch: getLoginConfig, data: loginConfig } = useGetLoginConfigQuery(undefined, {
skip: config !== "login", skip: config !== "login",
}); });
@@ -71,30 +55,26 @@ export default function useConfig(config: keyof ConfigMap = "smtp") {
const updateFns = { const updateFns = {
login: updateLoginConfig, login: updateLoginConfig,
smtp: updateSMTPConfig, smtp: updateSMTPConfig,
agora: updateAgoraConfig, livekit: updateLivekitConfig,
firebase: updateFirebaseConfig, firebase: updateFirebaseConfig,
vocespace: updateVocespaceConfig,
}; };
const requests = { const requests = {
smtp: getSMTPConfig, smtp: getSMTPConfig,
agora: getAgoraConfig, livekit: getLivekitConfig,
firebase: getFirebaseConfig, firebase: getFirebaseConfig,
login: getLoginConfig, login: getLoginConfig,
vocespace: getVocespaceConfig,
}; };
const updates = { const updates = {
login: LoginUpdated, login: LoginUpdated,
smtp: SMTPUpdated, smtp: SMTPUpdated,
agora: AgoraUpdated, livekit: LivekitUpdated,
firebase: FirebaseUpdated, firebase: FirebaseUpdated,
vocespace: VocespaceUpdated,
}; };
const updatings = { const updatings = {
login: LoginUpdating, login: LoginUpdating,
smtp: SMTPUpdating, smtp: SMTPUpdating,
agora: AgoraUpdating, livekit: LivekitUpdating,
firebase: FirebaseUpdating, firebase: FirebaseUpdating,
vocespace: VocespaceUpdating,
}; };
const updateConfig = updateFns[config]; const updateConfig = updateFns[config];
const refetch = requests[config]; const refetch = requests[config];
@@ -121,12 +101,12 @@ export default function useConfig(config: keyof ConfigMap = "smtp") {
} }
}, [updated]); }, [updated]);
useEffect(() => { useEffect(() => {
const _config = smtpConfig || firebaseConfig || loginConfig || agoraConfig || vocespaceConfig; const _config = smtpConfig || firebaseConfig || loginConfig || livekitConfig;
if (_config) { if (_config) {
originalValue = _config; originalValue = _config;
setValues(_config); setValues(_config);
} }
}, [smtpConfig, firebaseConfig, loginConfig, agoraConfig, vocespaceConfig]); }, [smtpConfig, firebaseConfig, loginConfig, livekitConfig]);
useEffect(() => { useEffect(() => {
// 空对象 // 空对象
if (!values || Object.keys(values).length == 0) return; if (!values || Object.keys(values).length == 0) return;
@@ -144,7 +124,7 @@ export default function useConfig(config: keyof ConfigMap = "smtp") {
reset, reset,
changed, changed,
updateConfig, updateConfig,
agoraConfig, livekitConfig,
values, values,
setValues, setValues,
toggleEnable, toggleEnable,
+3 -3
View File
@@ -15,7 +15,7 @@ import { updateInfo } from "@/app/slices/server";
const useLicense = (refetchOnMountOrArgChange = false) => { const useLicense = (refetchOnMountOrArgChange = false) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const userCount = useAppSelector((store) => store.users.ids.length, shallowEqual); 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 isGuest = useAppSelector((store) => store.authData.guest, shallowEqual);
const { const {
data: license, data: license,
@@ -49,12 +49,12 @@ const useLicense = (refetchOnMountOrArgChange = false) => {
const lUserLimit = license?.user_limit ?? Number.MAX_SAFE_INTEGER; const lUserLimit = license?.user_limit ?? Number.MAX_SAFE_INTEGER;
useEffect(() => { useEffect(() => {
if (lUserLimit > 20) { if (lUserLimit > 20) {
dispatch(updateInfo({ upgraded: true })); dispatch(updateInfo({ licensed: true }));
} }
}, [lUserLimit]); }, [lUserLimit]);
return { return {
upgraded, licensed,
reachLimit: userCount >= lUserLimit, reachLimit: userCount >= lUserLimit,
license, license,
checked, checked,
+49 -1
View File
@@ -18,6 +18,7 @@ import {
removePinChats, removePinChats,
resetFootprint, resetFootprint,
updateAutoDeleteSetting, updateAutoDeleteSetting,
updateDMVisibleAside,
updateMute, updateMute,
updateReadChannels, updateReadChannels,
updateReadUsers, updateReadUsers,
@@ -37,7 +38,7 @@ import { removeUserSession, resetUserMsg } from "@/app/slices/message.user";
import { updateInfo } from "@/app/slices/server"; import { updateInfo } from "@/app/slices/server";
import { setReady, updateSSEStatus } from "@/app/slices/ui"; import { setReady, updateSSEStatus } from "@/app/slices/ui";
import { updateContactStatus, updateUsersByLogs, updateUsersStatus } from "@/app/slices/users"; 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 { useAppDispatch, useAppSelector } from "@/app/store";
import { import {
ServerEvent, ServerEvent,
@@ -69,6 +70,9 @@ export default function useStreaming() {
const guest = useAppSelector((store) => store.authData.guest, shallowEqual); const guest = useAppSelector((store) => store.authData.guest, shallowEqual);
const readUsers = useAppSelector((store) => store.footprint.readUsers, shallowEqual); const readUsers = useAppSelector((store) => store.footprint.readUsers, shallowEqual);
const readChannels = useAppSelector((store) => store.footprint.readChannels, 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 dispatch = useAppDispatch();
const loginUid = user?.uid || 0; const loginUid = user?.uid || 0;
@@ -78,6 +82,9 @@ export default function useStreaming() {
const readChannelsRef = useRef(readChannels); const readChannelsRef = useRef(readChannels);
const userRef = useRef(user); const userRef = useRef(user);
const guestRef = useRef(guest); const guestRef = useRef(guest);
const voicingRef = useRef(voicing);
const callingFromRef = useRef(callingFrom);
const callingToRef = useRef(callingTo);
// Update refs whenever values change // Update refs whenever values change
useEffect(() => { useEffect(() => {
@@ -100,6 +107,18 @@ export default function useStreaming() {
guestRef.current = guest; guestRef.current = guest;
}, [guest]); }, [guest]);
useEffect(() => {
voicingRef.current = voicing;
}, [voicing]);
useEffect(() => {
callingFromRef.current = callingFrom;
}, [callingFrom]);
useEffect(() => {
callingToRef.current = callingTo;
}, [callingTo]);
const keepAlive = (timeout?: number) => { const keepAlive = (timeout?: number) => {
console.info("debug SSE: start new keepalive"); console.info("debug SSE: start new keepalive");
clearTimeout(aliveInter); clearTimeout(aliveInter);
@@ -243,6 +262,35 @@ export default function useStreaming() {
dispatch(updateCallInfo({ from: uid, to: target, calling: true })); dispatch(updateCallInfo({ from: uid, to: target, calling: true }));
break; 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": { case "users_snapshot": {
const { version } = data; const { version } = data;
+20 -4
View File
@@ -6,6 +6,10 @@ import { shallowEqual, useDispatch } from "react-redux";
import { useMatch, useNavigate } from "react-router-dom"; import { useMatch, useNavigate } from "react-router-dom";
import { hideAll } from "tippy.js"; import { hideAll } from "tippy.js";
import {
useGetLivekitStatusQuery,
useSignalLivekitCallMutation,
} from "@/app/services/server";
import { useRemoveMembersMutation } from "@/app/services/channel"; import { useRemoveMembersMutation } from "@/app/services/channel";
import { import {
useLazyDeleteUserQuery, useLazyDeleteUserQuery,
@@ -34,6 +38,8 @@ const useUserOperation = ({ uid, cid }: IProps) => {
const [passedUid, setPassedUid] = useState<number | undefined>(undefined); const [passedUid, setPassedUid] = useState<number | undefined>(undefined);
const isUserDetailPath = useMatch(`/users/${uid}`); const isUserDetailPath = useMatch(`/users/${uid}`);
const [updateContactStatus] = useUpdateContactStatusMutation(); const [updateContactStatus] = useUpdateContactStatusMutation();
const { data: livekitEnabled } = useGetLivekitStatusQuery();
const [signalLivekitCall] = useSignalLivekitCallMutation();
const [updateUser, { isSuccess: updateUserSuccess }] = useUpdateUserMutation(); const [updateUser, { isSuccess: updateUserSuccess }] = useUpdateUserMutation();
const [removeUser, { isSuccess: removeUserSuccess }] = useLazyDeleteUserQuery(); const [removeUser, { isSuccess: removeUserSuccess }] = useLazyDeleteUserQuery();
const [removeInChannel, { isSuccess: removeSuccess }] = useRemoveMembersMutation(); const [removeInChannel, { isSuccess: removeSuccess }] = useRemoveMembersMutation();
@@ -48,7 +54,7 @@ const useUserOperation = ({ uid, cid }: IProps) => {
shallowEqual shallowEqual
); );
const loginUser = useAppSelector((store) => store.authData.user, 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 ?? {}; const { show_email = true, dm_to_member = true } = channel ?? {};
useEffect(() => { useEffect(() => {
setPassedUid(uid ?? loginUser?.uid); setPassedUid(uid ?? loginUser?.uid);
@@ -93,12 +99,21 @@ const useUserOperation = ({ uid, cid }: IProps) => {
copy(finalEmail || ""); copy(finalEmail || "");
hideAll(); 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) { if (joining || joined) {
alert("You have joined another channel, please leave first!"); alert("You have joined another channel, please leave first!");
return; return;
} }
joinVoice(); const joinedLivekit = await joinVoice();
if (!joinedLivekit || !uid) return;
const data = { const data = {
id: uid ?? 0, id: uid ?? 0,
aside: "voice" as const aside: "voice" as const
@@ -107,6 +122,7 @@ const useUserOperation = ({ uid, cid }: IProps) => {
// 实时显示 calling box // 实时显示 calling box
if (!joinedAtThisContext) { if (!joinedAtThisContext) {
dispatch(updateCallInfo({ from: loginUser?.uid ?? 0, to: uid, calling: false })); dispatch(updateCallInfo({ from: loginUser?.uid ?? 0, to: uid, calling: false }));
signalLivekitCall({ uid, action: "invite" });
} }
navigateTo(`/chat/dm/${uid}`); navigateTo(`/chat/dm/${uid}`);
}; };
@@ -149,7 +165,7 @@ const useUserOperation = ({ uid, cid }: IProps) => {
const canBlock: boolean = loginUid != uid; const canBlock: boolean = loginUid != uid;
const canRemoveFromContact: boolean = loginUid != uid; const canRemoveFromContact: boolean = loginUid != uid;
const canInviteChannel = !!cid && (loginUser?.is_admin || channel?.owner == loginUser?.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; const canUpdatePassword: boolean = !!loginUser?.is_admin && loginUid != uid && uid !== 1;
return { return {
showEmailInChannel: show_email, showEmailInChannel: show_email,
-53
View File
@@ -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;
+1 -14
View File
@@ -2,7 +2,6 @@ import { DOMAttributes, ReactNode } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import GithubCallback, { GithubLoginSource } from "./GithubCallback"; import GithubCallback, { GithubLoginSource } from "./GithubCallback";
import PaymentSuccess from "./PaymentSuccess";
const StyledWrapper = ({ children }: DOMAttributes<HTMLDivElement> & { children?: ReactNode }) => { const StyledWrapper = ({ children }: DOMAttributes<HTMLDivElement> & { children?: ReactNode }) => {
return ( return (
@@ -11,21 +10,9 @@ const StyledWrapper = ({ children }: DOMAttributes<HTMLDivElement> & { children?
</div> </div>
); );
}; };
// type Props = { // This page handles third-party callbacks such as GitHub login.
// type: "payment_success";
// };
// 该页面服务于一些第三方服务的回调,比如 stripe 付款成功的回调,GitHub 登录成功的回调
export default function CallbackPage() { export default function CallbackPage() {
const { type = "", payload = "" } = useParams(); const { type = "", payload = "" } = useParams();
// stripe 付款成功
if (type == "payment_success") {
return (
<StyledWrapper>
<PaymentSuccess sid={payload} />
</StyledWrapper>
);
}
// github 授权成功
if (type == "github") { if (type == "github") {
const query = new URLSearchParams(location.search); const query = new URLSearchParams(location.search);
const code = query.get("code") ?? ""; const code = query.get("code") ?? "";
+17 -9
View File
@@ -2,13 +2,14 @@ import { useEffect } from "react";
import { shallowEqual, useDispatch } from "react-redux"; import { shallowEqual, useDispatch } from "react-redux";
import clsx from "clsx"; import clsx from "clsx";
import { useSignalLivekitCallMutation } from "@/app/services/server";
import { updateCallInfo } from "@/app/slices/voice"; import { updateCallInfo } from "@/app/slices/voice";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import Avatar from "@/components/Avatar"; import Avatar from "@/components/Avatar";
import Tooltip from "@/components/Tooltip"; import Tooltip from "@/components/Tooltip";
import { useVoice } from "@/components/Voice"; import { useVoice } from "@/components/Voice";
import Operations from "@/components/Voice/Operations"; import Operations from "@/components/Voice/Operations";
import { playAgoraVideo } from "@/utils"; import { playLivekitVideo } from "@/utils";
import IconCallOff from "@/assets/icons/call.off.svg"; import IconCallOff from "@/assets/icons/call.off.svg";
import IconCallAnswer from "@/assets/icons/call.svg"; import IconCallAnswer from "@/assets/icons/call.svg";
import IconMicOff from "@/assets/icons/mic.off.svg"; import IconMicOff from "@/assets/icons/mic.off.svg";
@@ -120,21 +121,28 @@ const DMVoice = ({ uid }: Props) => {
(store) => store.voice, (store) => store.voice,
shallowEqual 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(() => { useEffect(() => {
const ids = voicingMembers.ids; const ids = voicingMembers.ids;
ids.forEach((id) => { ids.forEach((id) => {
playAgoraVideo(id); playLivekitVideo(id);
}); });
}, [voicingMembers.ids]); }, [voicingMembers.ids]);
if (![callingFrom, callingTo].includes(uid)) return null; if (![callingFrom, callingTo].includes(uid)) return null;
const { name: fromUsername, avatar: fromAvatar } = userData[callingFrom]; const fromUser = userData[callingFrom];
const { name: toUsername, avatar: toAvatar, uid: toUid } = userData[callingTo]; 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 sendByMe = loginUid !== toUid;
const onlyToSelf = voicingMembers.ids.length == 1 && voicingMembers.ids[0] == callingTo; const onlyToSelf = voicingMembers.ids.length == 1 && voicingMembers.ids[0] == callingTo;
const handleCancel = () => { const handleCancel = () => {
console.log("cancel"); const peerUid = sendByMe ? callingTo : callingFrom;
signalLivekitCall({ uid: peerUid, action: "cancel" });
if (sendByMe || voicingMembers.ids.length == 2 || onlyToSelf) { if (sendByMe || voicingMembers.ids.length == 2 || onlyToSelf) {
leave(); leave();
} }
@@ -143,8 +151,8 @@ const DMVoice = ({ uid }: Props) => {
dispatch(updateCallInfo({ from: 0, to: 0 })); dispatch(updateCallInfo({ from: 0, to: 0 }));
} }
}; };
const handleAnswer = () => { const handleAnswer = async () => {
joinVoice(); await joinVoice();
}; };
const connected = voicingMembers.ids.length == 2; const connected = voicingMembers.ids.length == 2;
// const { muted, shareScreen, video } = voicingInfo ?? {} as VoicingInfo; // const { muted, shareScreen, video } = voicingInfo ?? {} as VoicingInfo;
@@ -211,7 +219,7 @@ const DMVoice = ({ uid }: Props) => {
)} )}
{connected && ( {connected && (
<> <>
<Operations id={callingTo} context="dm" mode="dm" /> <Operations id={peerUid} context="dm" mode="dm" />
</> </>
)} )}
</div> </div>
+3 -14
View File
@@ -1,27 +1,16 @@
// import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import Button from "@/components/styled/Button"; const LicenseOutdatedTip = () => {
// type Props = {};
const LicenseUpgradeTip = () => {
const { t } = useTranslation("chat"); const { t } = useTranslation("chat");
const navigateTo = useNavigate();
const handleRedirect = () => {
navigateTo("/setting/license");
};
return ( 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"> <span className="text-white">
<i className="text-xl mr-2">🚨</i> <i className="text-xl mr-2">🚨</i>
{t("license_tip")} {t("license_tip")}
</span> </span>
<Button onClick={handleRedirect} className="small">{`Upgrade License`}</Button>
</div> </div>
); );
}; };
export default LicenseUpgradeTip; export default LicenseOutdatedTip;
+2 -2
View File
@@ -14,7 +14,7 @@ import IconWarning from "@/assets/icons/warning.svg";
import AddContactTip from "./AddContactTip"; import AddContactTip from "./AddContactTip";
import DMVoice from "./DMVoicing"; import DMVoice from "./DMVoicing";
import DnDTip from "./DnDTip"; import DnDTip from "./DnDTip";
import LicenseUpgradeTip from "./LicenseOutdatedTip"; import LicenseOutdatedTip from "./LicenseOutdatedTip";
import LoginTip from "./LoginTip"; import LoginTip from "./LoginTip";
import Operations from "./Operations"; import Operations from "./Operations";
import VirtualMessageFeed, { VirtualMessageFeedHandle } from "./VirtualMessageFeed"; import VirtualMessageFeed, { VirtualMessageFeedHandle } from "./VirtualMessageFeed";
@@ -109,7 +109,7 @@ const Layout: FC<Props> = ({
{readonly ? ( {readonly ? (
<LoginTip /> <LoginTip />
) : reachLimit ? ( ) : reachLimit ? (
<LicenseUpgradeTip /> <LicenseOutdatedTip />
) : ( ) : (
<div className={clsx(`flex justify-center`, selects && "hidden")}> <div className={clsx(`flex justify-center`, selects && "hidden")}>
<Send id={to} context={context} feedRef={feedRef} /> <Send id={to} context={context} feedRef={feedRef} />
+2 -2
View File
@@ -39,11 +39,11 @@ const RTCWidget = ({ id, context = "channel" }: Props) => {
const callFrom = useAppSelector((store) => store.voice.callingFrom, shallowEqual); const callFrom = useAppSelector((store) => store.voice.callingFrom, shallowEqual);
const callTo = useAppSelector((store) => store.voice.callingTo, shallowEqual); const callTo = useAppSelector((store) => store.voice.callingTo, shallowEqual);
const channelData = useAppSelector((store) => store.channels.byId, 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; if (!loginUser || !voicingInfo || joining) return null;
// const name = voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name; // const name = voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name;
// if (!name) return null; // if (!name) return null;
const isReConnecting = voicingInfo.connectionState == "RECONNECTING"; const isReConnecting = voicingInfo.connectionState == "reconnecting";
const targetDMUsername = const targetDMUsername =
callFrom == loginUser.uid ? userData[callTo]?.name : userData[callFrom]?.name; callFrom == loginUser.uid ? userData[callTo]?.name : userData[callFrom]?.name;
return ( return (
@@ -4,7 +4,7 @@ import clsx from "clsx";
import { updateChannelVisibleAside } from "@/app/slices/footprint"; import { updateChannelVisibleAside } from "@/app/slices/footprint";
import Operations from "@/components/Voice/Operations"; import Operations from "@/components/Voice/Operations";
import { playAgoraVideo } from "@/utils"; import { playLivekitVideo } from "@/utils";
import IconFullscreen from "@/assets/icons/fullscreen.svg"; import IconFullscreen from "@/assets/icons/fullscreen.svg";
import IconMicOff from "@/assets/icons/mic.off.svg"; import IconMicOff from "@/assets/icons/mic.off.svg";
import IconMic from "@/assets/icons/mic.on.svg"; import IconMic from "@/assets/icons/mic.on.svg";
@@ -26,7 +26,7 @@ const VoiceManagement = ({ id, context, info }: Props) => {
useEffect(() => { useEffect(() => {
const ids = voicingMembers.ids; const ids = voicingMembers.ids;
ids.forEach((id) => { ids.forEach((id) => {
playAgoraVideo(id); playLivekitVideo(id);
}); });
}, [voicingMembers.ids]); }, [voicingMembers.ids]);
const handleFullscreen = (uid?: number) => { const handleFullscreen = (uid?: number) => {
@@ -50,7 +50,7 @@ const VoiceManagement = ({ id, context, info }: Props) => {
</div> </div>
); );
} }
if (info.connectionState == "RECONNECTING") { if (info.connectionState == "reconnecting") {
return ( return (
<div className="w-full h-full flex-center flex-col gap-1 p-1 "> <div className="w-full h-full flex-center flex-col gap-1 p-1 ">
<span className="text-red-300">Reconnecting...</span> <span className="text-red-300">Reconnecting...</span>
+17 -98
View File
@@ -2,19 +2,16 @@ import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { shallowEqual, useDispatch } from "react-redux"; 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 { updateChannelVisibleAside, updateDMVisibleAside } from "@/app/slices/footprint";
import { updateCallInfo } from "@/app/slices/voice"; import { updateCallInfo } from "@/app/slices/voice";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import { ChatContext } from "@/types/common"; import { ChatContext } from "@/types/common";
import Tooltip from "@/components/Tooltip"; import Tooltip from "@/components/Tooltip";
import { useVoice } from "@/components/Voice"; import { useVoice } from "@/components/Voice";
import { compareVersion, isInIframe } from "@/utils"; import { isInIframe } from "@/utils";
import IconHeadphone from "@/assets/icons/headphone.svg"; 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 { useNavigate } from "react-router-dom";
import { VocespaceConfig } from "@/types/server";
type Props = { type Props = {
context?: ChatContext; context?: ChatContext;
@@ -23,42 +20,19 @@ type Props = {
const isIframe = isInIframe(); const isIframe = isInIframe();
const VoiceChat = ({ id, context = "channel" }: Props) => { 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 { joinVoice, joined, joining = false, joinedAtThisContext } = useVoice({ id, context });
const [signalLivekitCall] = useSignalLivekitCallMutation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const loginUid = useAppSelector((store) => store.authData.user?.uid ?? 0, shallowEqual); const loginUid = useAppSelector((store) => store.authData.user?.uid ?? 0, shallowEqual);
const isAdmin = useAppSelector((store) => store.authData.user?.is_admin ?? false, shallowEqual); const isAdmin = useAppSelector((store) => store.authData.user?.is_admin ?? false, shallowEqual);
const visibleAside = useAppSelector( const visibleAside = useAppSelector(
(store) => (context == "channel" ? "voice" : null), (store) => (context == "channel" ? store.footprint.channelAsides[id] : store.footprint.dmAsides[id]),
shallowEqual shallowEqual
); );
const voiceList = useAppSelector((store) => store.voice.list, shallowEqual); const voiceList = useAppSelector((store) => store.voice.list, shallowEqual);
const { data: enabled } = useGetAgoraStatusQuery(); const { data: enabled } = useGetLivekitStatusQuery();
const [vocespaceConfig, setVoceSpaceConfig] = useState<VocespaceConfig | undefined>(undefined);
const { t } = useTranslation("chat"); 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 toggleDashboard = () => {
const data = { const data = {
@@ -67,12 +41,13 @@ const VoiceChat = ({ id, context = "channel" }: Props) => {
}; };
dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data)); dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data));
}; };
const handleJoin = () => { const handleJoin = async () => {
if (joining || joined) { if (joining || joined) {
alert("You have joined another channel, please leave first!"); alert("You have joined another channel, please leave first!");
return; return;
} }
joinVoice(); const joinedLivekit = await joinVoice();
if (!joinedLivekit) return;
const data = { const data = {
id, id,
aside: "voice" as const, aside: "voice" as const,
@@ -81,6 +56,7 @@ const VoiceChat = ({ id, context = "channel" }: Props) => {
// 实时显示calling box // 实时显示calling box
if (!joinedAtThisContext && context == "dm") { if (!joinedAtThisContext && context == "dm") {
dispatch(updateCallInfo({ from: loginUid, to: id, calling: false })); dispatch(updateCallInfo({ from: loginUid, to: id, calling: false }));
signalLivekitCall({ uid: id, action: "invite" });
} }
}; };
const handleInIframe = () => { const handleInIframe = () => {
@@ -91,75 +67,18 @@ const VoiceChat = ({ id, context = "channel" }: Props) => {
const visible = visibleAside == "voice"; const visible = visibleAside == "voice";
const memberCount = voiceList.find((v) => v.context == context && v.id == id)?.memberCount ?? 0; 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 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 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 () => { const handleOnClick = async () => {
if (chatType === "agora" && enabled) { if (!enabled) {
return isIframe ? handleInIframe() : joinedAtThisContext ? toggleDashboard() : handleJoin(); if (isAdmin) {
} else { navigate("/setting/video");
// 向当前频道中发送Vocespace链接 } else {
await handleSendVocespaceRequest(); toast.error("LiveKit is not configured. Contact the admin.");
}
return;
} }
return isIframe ? handleInIframe() : joinedAtThisContext ? toggleDashboard() : handleJoin();
}; };
return ( return (
+2 -2
View File
@@ -11,7 +11,7 @@ import { useAppSelector } from "../../app/store";
import Avatar from "../../components/Avatar"; import Avatar from "../../components/Avatar";
import Tooltip from "../../components/Tooltip"; import Tooltip from "../../components/Tooltip";
import { ChatContext } from "../../types/common"; import { ChatContext } from "../../types/common";
import { playAgoraVideo } from "../../utils"; import { playLivekitVideo } from "../../utils";
type Props = { type Props = {
context: ChatContext; context: ChatContext;
@@ -30,7 +30,7 @@ const VoiceFullscreen = ({ id, context }: Props) => {
useEffect(() => { useEffect(() => {
const ids = voicingMembers.ids; const ids = voicingMembers.ids;
ids.forEach((id) => { ids.forEach((id) => {
playAgoraVideo(id); playLivekitVideo(id);
const { speakingVolume = 0 } = voicingMembers.byId[id]; const { speakingVolume = 0 } = voicingMembers.byId[id];
const speaking = speakingVolume > 50; const speaking = speakingVolume > 50;
if (speaking) { if (speaking) {
+3
View File
@@ -8,6 +8,7 @@ import ChannelModal from "@/components/ChannelModal";
import ErrorCatcher from "@/components/ErrorCatcher"; import ErrorCatcher from "@/components/ErrorCatcher";
import Server from "@/components/Server"; import Server from "@/components/Server";
import UsersModal from "@/components/UsersModal"; import UsersModal from "@/components/UsersModal";
import DMCalling from "@/components/Voice/DMCalling";
import useVirtualKeyboard from "@/hooks/useVirtualKeyboard"; import useVirtualKeyboard from "@/hooks/useVirtualKeyboard";
import { isIOS } from "@/utils"; import { isIOS } from "@/utils";
import ChannelChat from "./ChannelChat"; import ChannelChat from "./ChannelChat";
@@ -29,6 +30,7 @@ function ChatPage() {
const [usersModalVisible, setUsersModalVisible] = useState(false); const [usersModalVisible, setUsersModalVisible] = useState(false);
const { channel_id = 0, user_id = 0 } = useParams(); const { channel_id = 0, user_id = 0 } = useParams();
const callingTo = useAppSelector((store) => store.voice.callingTo, shallowEqual); const callingTo = useAppSelector((store) => store.voice.callingTo, shallowEqual);
const callingFrom = useAppSelector((store) => store.voice.callingFrom, shallowEqual);
const aside = useAppSelector( const aside = useAppSelector(
(store) => (store) =>
channel_id channel_id
@@ -76,6 +78,7 @@ function ChatPage() {
<ChannelModal closeModal={toggleChannelModalVisible} personal={true} /> <ChannelModal closeModal={toggleChannelModalVisible} personal={true} />
)} )}
{usersModalVisible && <UsersModal closeModal={toggleUsersModalVisible} />} {usersModalVisible && <UsersModal closeModal={toggleUsersModalVisible} />}
<DMCalling from={callingFrom} to={callingTo} />
<div <div
className={clsx( className={clsx(
`flex ${mobileHeight} md:h-full md:pt-2 md:pb-2.5 md:pr-1`, `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 } = useTranslation("setting");
const { t: ct } = useTranslation(); const { t: ct } = useTranslation();
const handleRenew = async () => { const handleUpdate = async () => {
const updateSuccess = await updateLicense(value); const updateSuccess = await updateLicense(value);
if (typeof updateSuccess == "boolean" && !updateSuccess) { if (typeof updateSuccess == "boolean" && !updateSuccess) {
toast.error("Check License Invalid!"); toast.error("Check License Invalid!");
@@ -44,7 +44,7 @@ const UpdateLicenseModal: FC<Props> = ({ closeModal, updateLicense, updating, up
<Button onClick={closeModal} className="ghost"> <Button onClick={closeModal} className="ghost">
{ct("action.cancel")} {ct("action.cancel")}
</Button> </Button>
<Button disabled={updating || updated || !value} onClick={handleRenew}> <Button disabled={updating || updated || !value} onClick={handleUpdate}>
{updating ? "Updating" : updated ? "Update Successfully" : t("license.update")} {updating ? "Updating" : updated ? "Update Successfully" : t("license.update")}
</Button> </Button>
</> </>
+1 -30
View File
@@ -5,7 +5,6 @@ import dayjs from "dayjs";
import Button from "@/components/styled/Button"; import Button from "@/components/styled/Button";
import useLicense from "@/hooks/useLicense"; import useLicense from "@/hooks/useLicense";
import LicensePriceListModal from "./LicensePriceListModal";
import UpdateLicenseModal from "./UpdateLicenseModal"; import UpdateLicenseModal from "./UpdateLicenseModal";
interface ItemProps extends HTMLAttributes<HTMLSpanElement> { interface ItemProps extends HTMLAttributes<HTMLSpanElement> {
@@ -37,15 +36,10 @@ const Item = ({ label, data, foldable = false, ...rest }: ItemProps) => {
); );
}; };
export default function License() { export default function License() {
const { t, i18n } = useTranslation("setting"); const { t } = useTranslation("setting");
// const { t: ct } = useTranslation();
const { license: licenseInfo, reachLimit, upsertLicense, upserting, upserted } = useLicense(true); const { license: licenseInfo, reachLimit, upsertLicense, upserting, upserted } = useLicense(true);
const [modalVisible, setModalVisible] = useState(false);
const [updateVisible, setUpdateVisible] = useState(false); const [updateVisible, setUpdateVisible] = useState(false);
const [base58Fold, setBase58Fold] = useState(true); const [base58Fold, setBase58Fold] = useState(true);
const toggleModalVisible = () => {
setModalVisible((prev) => !prev);
};
const toggleUpdateModalVisible = () => { const toggleUpdateModalVisible = () => {
setUpdateVisible((prev) => !prev); setUpdateVisible((prev) => !prev);
}; };
@@ -87,34 +81,11 @@ export default function License() {
/> />
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button onClick={toggleModalVisible}>{t("license.renew")}</Button>
<Button onClick={toggleUpdateModalVisible} className="ghost"> <Button onClick={toggleUpdateModalVisible} className="ghost">
{t("license.update")} {t("license.update")}
</Button> </Button>
</div> </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> </div>
{modalVisible && <LicensePriceListModal closeModal={toggleModalVisible} />}
{updateVisible && ( {updateVisible && (
<UpdateLicenseModal <UpdateLicenseModal
updated={upserted} updated={upserted}
-174
View File
@@ -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>
);
}
+88
View File
@@ -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>
);
}
-396
View File
@@ -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>
);
}
+2 -10
View File
@@ -7,8 +7,8 @@ import APIConfig from "./APIConfig";
import APIDocument from "./APIDocument"; import APIDocument from "./APIDocument";
import BotConfig from "./BotConfig"; import BotConfig from "./BotConfig";
import Cloudflared from "./Cloudflared"; import Cloudflared from "./Cloudflared";
import ConfigAgora from "./config/Agora";
import ConfigFirebase from "./config/Firebase"; import ConfigFirebase from "./config/Firebase";
import ConfigLivekit from "./config/Livekit";
import Logins from "./config/Logins"; import Logins from "./config/Logins";
import ConfigSMTP from "./config/SMTP"; import ConfigSMTP from "./config/SMTP";
import DataManagement from "./DataManagement"; import DataManagement from "./DataManagement";
@@ -18,7 +18,6 @@ import NotificationSettings from "./NotificationSettings";
import Overview from "./Overview"; import Overview from "./Overview";
import Widget from "./Widget"; import Widget from "./Widget";
import { shallowEqual } from "react-redux"; import { shallowEqual } from "react-redux";
import { ConfigVocespace } from "./config/Vocespace";
import AdminNotificationChannels from "./AdminNotificationChannels"; import AdminNotificationChannels from "./AdminNotificationChannels";
const dataManagementNav = { const dataManagementNav = {
@@ -73,7 +72,7 @@ const navs = [
}, },
{ {
name: "video", name: "video",
component: <ConfigAgora />, component: <ConfigLivekit />,
}, },
{ {
name: "smtp", name: "smtp",
@@ -117,7 +116,6 @@ const navs = [
const useNavs = () => { const useNavs = () => {
const { t } = useTranslation("setting"); const { t } = useTranslation("setting");
const loginUser = useAppSelector((store) => store.authData.user, shallowEqual); const loginUser = useAppSelector((store) => store.authData.user, shallowEqual);
const upgraded = useAppSelector((store) => store.server.upgraded, shallowEqual);
const filteredNavs = loginUser?.is_admin const filteredNavs = loginUser?.is_admin
? navs ? navs
: navs : navs
@@ -163,13 +161,7 @@ const useNavs = () => {
if (loginUser?.is_admin) { if (loginUser?.is_admin) {
return true; return true;
} else { } else {
// about 特殊处理下
// if (nav.name == "about") {
// // 有付费,但是普通用户,则不显示 about
// return !upgraded;
// } else {
return !nav.admin; return !nav.admin;
// }
} }
}); });
}; };
-11
View File
@@ -4,15 +4,4 @@ export interface EntityId {
export type ChatContext = "dm" | "channel"; export type ChatContext = "dm" | "channel";
export type Theme = "auto" | "dark" | "light"; export type Theme = "auto" | "dark" | "light";
export type AuthType = "register" | "login"; 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 }; export type IPData = { error?: boolean; country: string };
+10 -13
View File
@@ -1,12 +1,10 @@
import { import type {
IAgoraRTCClient, LocalAudioTrack,
ICameraVideoTrack, LocalVideoTrack,
ILocalAudioTrack, RemoteAudioTrack,
ILocalVideoTrack, RemoteVideoTrack,
IMicrophoneAudioTrack, Room,
IRemoteAudioTrack, } from "livekit-client";
IRemoteVideoTrack
} from "agora-rtc-sdk-ng";
interface BeforeInstallPromptEvent extends Event { interface BeforeInstallPromptEvent extends Event {
/** /**
* Returns an array of DOMString items containing the platforms on which the event was dispatched. * 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>; __WB_MANIFEST: Array<PrecacheEntry | string>;
skipWaiting: () => void; skipWaiting: () => void;
CACHE: { [key: string]: typeof localforage | undefined }; CACHE: { [key: string]: typeof localforage | undefined };
VOICE_CLIENT?: IAgoraRTCClient; VOICE_CLIENT?: Room;
VOICE_TRACK_MAP: { VOICE_TRACK_MAP: {
[key: number]: IRemoteAudioTrack | IMicrophoneAudioTrack | undefined | null; [key: number]: RemoteAudioTrack | LocalAudioTrack | undefined | null;
}; };
VIDEO_TRACK_MAP: { VIDEO_TRACK_MAP: {
[key: number]: IRemoteVideoTrack | ICameraVideoTrack | ShareScreenTrack | undefined | null; [key: number]: RemoteVideoTrack | LocalVideoTrack | undefined | null;
}; };
} }
interface WindowEventMap { interface WindowEventMap {
@@ -63,4 +61,3 @@ export declare global {
scrollIntoViewIfNeeded?: any; scrollIntoViewIfNeeded?: any;
} }
} }
export type ShareScreenTrack = ILocalVideoTrack | [ILocalVideoTrack, ILocalAudioTrack];
+12 -58
View File
@@ -1,5 +1,4 @@
// call `organization` in backend // call `organization` in backend
import { PriceType } from "./common";
import { User } from "./user"; import { User } from "./user";
export interface Server extends SystemCommon { export interface Server extends SystemCommon {
@@ -35,53 +34,23 @@ export interface FirebaseConfig {
private_key: string; private_key: string;
client_email: string; client_email: string;
} }
export interface AgoraConfig { export interface LivekitConfig {
enabled: boolean; enabled: boolean;
url: string; url: string;
project_id: string; api_key: string;
app_id: string; api_secret: string;
app_certificate: string;
customer_id: string;
customer_secret: string;
} }
export interface LivekitTokenResponse {
export interface VocespaceConfig { token: string;
enabled: boolean;
password: string;
url: string; url: string;
license: string; room: string;
state: "success" | "undeployed" | ""; identity: string;
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;
expired_in: number; expired_in: number;
app_id: string; }
export type LivekitCallAction = "invite" | "cancel";
export interface LivekitCallRequest {
uid: number;
action: LivekitCallAction;
} }
export interface SMTPConfig { export interface SMTPConfig {
enabled: boolean; enabled: boolean;
@@ -126,21 +95,6 @@ export interface LicenseResponse {
base58: string; base58: string;
user_limit: number; 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"> { export interface CreateAdminDTO extends Pick<User, "email" | "name" | "gender"> {
password: string; password: string;
} }
+8
View File
@@ -228,6 +228,13 @@ interface UserCallEvent {
target: number; target: number;
uid: number; uid: number;
} }
interface RtcCallEvent {
type: "rtc_call";
from_uid: number;
to_uid: number;
room: string;
action: "invite" | "cancel";
}
interface MessageClearedEvent { interface MessageClearedEvent {
type: "message_cleared"; type: "message_cleared";
latest_deleted_mid: number; latest_deleted_mid: number;
@@ -267,6 +274,7 @@ export type ServerEvent =
| HeartbeatEvent | HeartbeatEvent
| GroupClearEvent | GroupClearEvent
| UserCallEvent | UserCallEvent
| RtcCallEvent
| ServerConfigChangedEvent | ServerConfigChangedEvent
| MessageClearedEvent | MessageClearedEvent
| UserRemark | UserRemark
+11 -14
View File
@@ -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 dayjs from "dayjs";
import AudioMsgSound from "@/assets/msg.sound.wav"; import AudioMsgSound from "@/assets/msg.sound.wav";
import BASE_URL, { import BASE_URL, {
@@ -445,22 +445,19 @@ export const fromNowTime = (ts?: number) => {
const currTS = +new Date(); const currTS = +new Date();
return dayjs(ts > currTS ? currTS : ts).fromNow(); return dayjs(ts > currTS ? currTS : ts).fromNow();
}; };
export const playAgoraVideo = (uid: number, videoTrack?: ICameraVideoTrack | null) => { type RtcVideoTrack = LocalVideoTrack | RemoteVideoTrack;
if (!videoTrack && !window.VIDEO_TRACK_MAP[uid]) return;
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; const playerEle = document.querySelector(`#CAMERA_${uid}`) as HTMLElement;
if (playerEle) { if (playerEle) {
playerEle.classList.add("h-[120px]"); playerEle.classList.add("h-[120px]");
if (videoTrack) { playerEle.replaceChildren();
videoTrack.play(playerEle); const mediaElement = track.attach();
} else { mediaElement.className = "w-full h-full object-cover";
if (!window.VIDEO_TRACK_MAP[uid]) return; mediaElement.setAttribute("playsinline", "true");
if ("play" in (window.VIDEO_TRACK_MAP[uid] ?? {})) { playerEle.appendChild(mediaElement);
window.VIDEO_TRACK_MAP[uid]?.play(playerEle);
} else {
const tracks = window.VIDEO_TRACK_MAP[uid] as [ILocalVideoTrack, ILocalAudioTrack];
tracks[0].play(playerEle);
}
}
} }
}; };