users in channels, kindof
parent
e775e4a240
commit
c0289d92e3
|
|
@ -21,7 +21,9 @@
|
||||||
"eslint-plugin-react": "^7.30.1",
|
"eslint-plugin-react": "^7.30.1",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"get-port": "^6.1.2",
|
"get-port": "^6.1.2",
|
||||||
|
"local-storage": "^2.0.0",
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
|
"peerjs": "^1.4.6",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
@ -928,6 +930,14 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@swc/helpers": {
|
||||||
|
"version": "0.3.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz",
|
||||||
|
"integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@szmarczak/http-timer": {
|
"node_modules/@szmarczak/http-timer": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
||||||
|
|
@ -4929,6 +4939,11 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||||
|
},
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||||
|
|
@ -6929,6 +6944,11 @@
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/local-storage": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/local-storage/-/local-storage-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-/0sRoeijw7yr/igbVVygDuq6dlYCmtsuTmmpnweVlVtl/s10pf5BCq8LWBxW/AMyFJ3MhMUuggMZiYlx6qr9tw=="
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
||||||
|
|
@ -8084,6 +8104,29 @@
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/peerjs": {
|
||||||
|
"version": "1.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.4.6.tgz",
|
||||||
|
"integrity": "sha512-0XA105/9yBFGxfpyCjlI1bcBiyPmXHs8+UDvO2j1WGnY+FXilMn35+P/3t8HzKnUnR1SX0PRkDSk8kM17ciNxA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.3.13",
|
||||||
|
"eventemitter3": "^4.0.7",
|
||||||
|
"peerjs-js-binarypack": "1.0.1",
|
||||||
|
"webrtc-adapter": "^7.7.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/peer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/peerjs-js-binarypack": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/peerjs-js-binarypack/-/peerjs-js-binarypack-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-N6aeia3NhdpV7kiGxJV5xQiZZCVEEVjRz2T2C6UZQiBkHWHzUv/oWA4myQLcwBwO8LUoR1KWW5oStvwVesmfCg=="
|
||||||
|
},
|
||||||
"node_modules/pend": {
|
"node_modules/pend": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
|
@ -9044,6 +9087,18 @@
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rtcpeerconnection-shim": {
|
||||||
|
"version": "1.2.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
|
||||||
|
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
|
||||||
|
"dependencies": {
|
||||||
|
"sdp": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0",
|
||||||
|
"npm": ">=3.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/run-async": {
|
"node_modules/run-async": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
||||||
|
|
@ -9115,6 +9170,11 @@
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sdp": {
|
||||||
|
"version": "2.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
|
||||||
|
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.3.7",
|
"version": "7.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||||
|
|
@ -9932,10 +9992,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.3.1",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
|
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tsutils": {
|
"node_modules/tsutils": {
|
||||||
"version": "3.21.0",
|
"version": "3.21.0",
|
||||||
|
|
@ -10574,6 +10633,19 @@
|
||||||
"integrity": "sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==",
|
"integrity": "sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/webrtc-adapter": {
|
||||||
|
"version": "7.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz",
|
||||||
|
"integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==",
|
||||||
|
"dependencies": {
|
||||||
|
"rtcpeerconnection-shim": "^1.2.15",
|
||||||
|
"sdp": "^2.12.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0",
|
||||||
|
"npm": ">=3.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/whatwg-encoding": {
|
"node_modules/whatwg-encoding": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||||
|
|
@ -11545,6 +11617,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
||||||
"integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
|
"integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
|
||||||
},
|
},
|
||||||
|
"@swc/helpers": {
|
||||||
|
"version": "0.3.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz",
|
||||||
|
"integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@szmarczak/http-timer": {
|
"@szmarczak/http-timer": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
||||||
|
|
@ -14519,6 +14599,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
|
||||||
},
|
},
|
||||||
|
"eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||||
|
},
|
||||||
"execa": {
|
"execa": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||||
|
|
@ -16032,6 +16117,11 @@
|
||||||
"integrity": "sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==",
|
"integrity": "sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"local-storage": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/local-storage/-/local-storage-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-/0sRoeijw7yr/igbVVygDuq6dlYCmtsuTmmpnweVlVtl/s10pf5BCq8LWBxW/AMyFJ3MhMUuggMZiYlx6qr9tw=="
|
||||||
|
},
|
||||||
"locate-path": {
|
"locate-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
||||||
|
|
@ -16889,6 +16979,22 @@
|
||||||
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
|
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"peerjs": {
|
||||||
|
"version": "1.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.4.6.tgz",
|
||||||
|
"integrity": "sha512-0XA105/9yBFGxfpyCjlI1bcBiyPmXHs8+UDvO2j1WGnY+FXilMn35+P/3t8HzKnUnR1SX0PRkDSk8kM17ciNxA==",
|
||||||
|
"requires": {
|
||||||
|
"@swc/helpers": "^0.3.13",
|
||||||
|
"eventemitter3": "^4.0.7",
|
||||||
|
"peerjs-js-binarypack": "1.0.1",
|
||||||
|
"webrtc-adapter": "^7.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peerjs-js-binarypack": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/peerjs-js-binarypack/-/peerjs-js-binarypack-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-N6aeia3NhdpV7kiGxJV5xQiZZCVEEVjRz2T2C6UZQiBkHWHzUv/oWA4myQLcwBwO8LUoR1KWW5oStvwVesmfCg=="
|
||||||
|
},
|
||||||
"pend": {
|
"pend": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
|
@ -17599,6 +17705,14 @@
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rtcpeerconnection-shim": {
|
||||||
|
"version": "1.2.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
|
||||||
|
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
|
||||||
|
"requires": {
|
||||||
|
"sdp": "^2.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"run-async": {
|
"run-async": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
||||||
|
|
@ -17653,6 +17767,11 @@
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sdp": {
|
||||||
|
"version": "2.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
|
||||||
|
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
|
||||||
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "7.3.7",
|
"version": "7.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||||
|
|
@ -18283,10 +18402,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.3.1",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
|
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"tsutils": {
|
"tsutils": {
|
||||||
"version": "3.21.0",
|
"version": "3.21.0",
|
||||||
|
|
@ -18717,6 +18835,15 @@
|
||||||
"integrity": "sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==",
|
"integrity": "sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"webrtc-adapter": {
|
||||||
|
"version": "7.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz",
|
||||||
|
"integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==",
|
||||||
|
"requires": {
|
||||||
|
"rtcpeerconnection-shim": "^1.2.15",
|
||||||
|
"sdp": "^2.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"whatwg-encoding": {
|
"whatwg-encoding": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,9 @@
|
||||||
"eslint-plugin-react": "^7.30.1",
|
"eslint-plugin-react": "^7.30.1",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"get-port": "^6.1.2",
|
"get-port": "^6.1.2",
|
||||||
|
"local-storage": "^2.0.0",
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
|
"peerjs": "^1.4.6",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ import { useContext, useEffect } from "react";
|
||||||
import ServerConnection from "./components/ServerConnection";
|
import ServerConnection from "./components/ServerConnection";
|
||||||
import Sidebar from "./components/Sidebar";
|
import Sidebar from "./components/Sidebar";
|
||||||
import TwoPanel from "./components/TwoPanel";
|
import TwoPanel from "./components/TwoPanel";
|
||||||
|
import Voice from "./components/Voice";
|
||||||
import { SettingsContext } from "./contexts/EphemeralState/EphemeralState";
|
import { SettingsContext } from "./contexts/EphemeralState/EphemeralState";
|
||||||
import useHomeServer from "./contexts/PersistentState/useHomeServerNative";
|
import useHomeServer from "./contexts/PersistentState/useHomeServerNative";
|
||||||
|
import useChannel from "./hooks/useChannel";
|
||||||
import useClientId from "./hooks/useClientId";
|
import useClientId from "./hooks/useClientId";
|
||||||
import useSessionToken from "./hooks/useSessionToken";
|
import useSessionToken from "./hooks/useSessionToken";
|
||||||
import Channels from "./pages/Channels";
|
import Channels from "./pages/Channels";
|
||||||
|
|
@ -22,6 +24,7 @@ export default function Router(props: RouterProps) {
|
||||||
const { sessionToken } = useSessionToken();
|
const { sessionToken } = useSessionToken();
|
||||||
const { homeServer } = useHomeServer();
|
const { homeServer } = useHomeServer();
|
||||||
const { isSettingsOpen, closeSettings } = useContext(SettingsContext);
|
const { isSettingsOpen, closeSettings } = useContext(SettingsContext);
|
||||||
|
const { voice, text } = useChannel();
|
||||||
|
|
||||||
const configured =
|
const configured =
|
||||||
homeServer !== null &&
|
homeServer !== null &&
|
||||||
|
|
@ -43,7 +46,27 @@ export default function Router(props: RouterProps) {
|
||||||
sidebar={300}
|
sidebar={300}
|
||||||
>
|
>
|
||||||
<Sidebar></Sidebar>
|
<Sidebar></Sidebar>
|
||||||
<Chat></Chat>
|
{voice ? (
|
||||||
|
<div style={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
height: '50%',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
|
<Voice></Voice>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
height: '50%',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
|
<Chat></Chat>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Chat></Chat>
|
||||||
|
)}
|
||||||
</TwoPanel>
|
</TwoPanel>
|
||||||
)}
|
)}
|
||||||
</ServerConnection>
|
</ServerConnection>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { useCallback, useContext } from "react"
|
||||||
|
import { PeerContext } from "../contexts/EphemeralState/PeerState";
|
||||||
|
import useChannel from "../hooks/useChannel";
|
||||||
|
import { useApi } from "../lib/useApi";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function Voice(props: any) {
|
||||||
|
const { uid } = props;
|
||||||
|
const { connected, peerId } = useContext(PeerContext);
|
||||||
|
const { channel } = useChannel();
|
||||||
|
|
||||||
|
const { send } = useApi({
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const joinCall = useCallback(() => {
|
||||||
|
if(peerId === null || connected === false) return;
|
||||||
|
send('voice:join', { peerId, channelId: channel })
|
||||||
|
}, [connected, peerId, channel])
|
||||||
|
|
||||||
|
return <div style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '500px',
|
||||||
|
margin: '0px auto',
|
||||||
|
}}>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Peer Info</legend>
|
||||||
|
connected: {connected ? 'true' : 'false'}<br></br>
|
||||||
|
PeerId: {peerId}<br></br>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Actions</legend>
|
||||||
|
<button onClick={joinCall}>Join Call</button>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
import { createContext, useState, useMemo, useEffect } from "react";
|
import { createContext, useState, useMemo, useEffect } from "react";
|
||||||
|
import UserMediaState from "./UserMediaState";
|
||||||
|
import PeerState from "./PeerState";
|
||||||
|
|
||||||
|
export type ChannelType = 'text' | 'voice';
|
||||||
|
|
||||||
export const ChannelContext = createContext<{
|
export const ChannelContext = createContext<{
|
||||||
channel: string | null,
|
channel: string | null,
|
||||||
setChannel: (uid: string) => void
|
text: boolean,
|
||||||
|
voice: boolean,
|
||||||
|
setChannel: (uid: string, type: ChannelType) => void
|
||||||
}>({
|
}>({
|
||||||
channel: null,
|
channel: null,
|
||||||
setChannel: () => {},
|
setChannel: () => {},
|
||||||
|
text: false,
|
||||||
|
voice: false,
|
||||||
});
|
});
|
||||||
export const TransparencyContext = createContext<(transparent: boolean) => void>(() => {});
|
export const TransparencyContext = createContext<(transparent: boolean) => void>(() => {});
|
||||||
export const SettingsContext = createContext<{
|
export const SettingsContext = createContext<{
|
||||||
|
|
@ -24,12 +32,38 @@ export default function EphemeralState(props: {
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const [channel, setChannel] = useState<string | null>(null);
|
const [channel, setChannel] = useState<string | null>(null);
|
||||||
|
const [voice, setVoice] = useState(false);
|
||||||
|
const [text, setText] = useState(false);
|
||||||
const [transparent, setTransparent] = useState(false);
|
const [transparent, setTransparent] = useState(false);
|
||||||
|
|
||||||
const [settings, setSettings] = useState(false);
|
const [settings, setSettings] = useState(false);
|
||||||
|
|
||||||
const channelContextValue = useMemo(() => {
|
const channelContextValue = useMemo(() => {
|
||||||
return { channel, setChannel };
|
return {
|
||||||
|
channel,
|
||||||
|
setChannel: (uid: string, channelType: ChannelType) => {
|
||||||
|
setChannel(uid);
|
||||||
|
switch(channelType) {
|
||||||
|
case 'text': {
|
||||||
|
setVoice(false);
|
||||||
|
setText(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'voice': {
|
||||||
|
setVoice(true);
|
||||||
|
setText(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
setVoice(false);
|
||||||
|
setText(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
voice,
|
||||||
|
text
|
||||||
|
};
|
||||||
}, [channel, setChannel]);
|
}, [channel, setChannel]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -46,7 +80,11 @@ export default function EphemeralState(props: {
|
||||||
closeSettings: () => setSettings(false),
|
closeSettings: () => setSettings(false),
|
||||||
isSettingsOpen: settings,
|
isSettingsOpen: settings,
|
||||||
}}>
|
}}>
|
||||||
{props.children}
|
<UserMediaState>
|
||||||
|
<PeerState>
|
||||||
|
{props.children}
|
||||||
|
</PeerState>
|
||||||
|
</UserMediaState>
|
||||||
</SettingsContext.Provider>
|
</SettingsContext.Provider>
|
||||||
</TransparencyContext.Provider>
|
</TransparencyContext.Provider>
|
||||||
</ChannelContext.Provider>
|
</ChannelContext.Provider>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { Peer, MediaConnection } from "peerjs";
|
||||||
|
import { UserMediaContext } from "./UserMediaState";
|
||||||
|
|
||||||
|
export const PeerContext = createContext<{
|
||||||
|
connected: boolean;
|
||||||
|
peerId: string | null
|
||||||
|
}>({
|
||||||
|
connected: false,
|
||||||
|
peerId: null
|
||||||
|
});
|
||||||
|
|
||||||
|
function useCurrent<T>(thing: T) {
|
||||||
|
const thingRef = useRef<T>(thing);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
thingRef.current = thing;
|
||||||
|
}, [thing]);
|
||||||
|
|
||||||
|
return thingRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PeerState(props: any) {
|
||||||
|
|
||||||
|
const { mediaStream } = useContext(UserMediaContext);
|
||||||
|
// TODO ability to disable until needed
|
||||||
|
// const [enabled, setEnabled] = useState(true);
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
const [peer, setPeer] = useState<Peer | null>(null);
|
||||||
|
const [peerId, setPeerId] = useState<string | null>(null);
|
||||||
|
const [incomingCalls, setIncomingCalls] = useState<MediaConnection[]>([]);
|
||||||
|
|
||||||
|
const addCall = useCurrent(useCallback((call: MediaConnection) => {
|
||||||
|
// HACK lookout for possible timing issues here.
|
||||||
|
// if we get two incomming calls before a re-render
|
||||||
|
// then our state could be out of date?!
|
||||||
|
// a possible solution is to cache the
|
||||||
|
// append to the list, and if the cache and
|
||||||
|
// state disagree, add to the cache, and set state
|
||||||
|
// to the cached value.
|
||||||
|
setIncomingCalls([...incomingCalls, call]);
|
||||||
|
}, [incomingCalls]))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(connected) return;
|
||||||
|
|
||||||
|
const peer = new Peer();
|
||||||
|
setPeer(peer);
|
||||||
|
|
||||||
|
peer.on('open', (id: string) => {
|
||||||
|
setConnected(true);
|
||||||
|
setPeerId(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
peer.on('close', () => {
|
||||||
|
setConnected(false);
|
||||||
|
setPeerId(null);
|
||||||
|
setPeer(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
peer.on('call', (call: MediaConnection) => {
|
||||||
|
addCall(call);
|
||||||
|
});
|
||||||
|
}, [connected]);
|
||||||
|
|
||||||
|
const dial = useCallback((id: string) => {
|
||||||
|
if(peer === null) return;
|
||||||
|
if(mediaStream === null) return;
|
||||||
|
peer.call(id, mediaStream);
|
||||||
|
}, [peer, mediaStream])
|
||||||
|
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
connected,
|
||||||
|
peerId,
|
||||||
|
}), [connected, peerId]);
|
||||||
|
|
||||||
|
return <PeerContext.Provider value={value}>
|
||||||
|
{props.children}
|
||||||
|
</PeerContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { createContext, useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
export const UserMediaContext = createContext<{
|
||||||
|
enabled: boolean;
|
||||||
|
mediaStream: MediaStream | null;
|
||||||
|
enable: () => void;
|
||||||
|
disable: () => void;
|
||||||
|
}>({
|
||||||
|
enabled: false,
|
||||||
|
mediaStream: null,
|
||||||
|
enable: () => {},
|
||||||
|
disable: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function UserMediaState(props: any) {
|
||||||
|
|
||||||
|
const [mediaStream, setMediaStream] = useState<MediaStream | null>(null);
|
||||||
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
|
||||||
|
const enable = useCallback(async () => {
|
||||||
|
const newStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: true,
|
||||||
|
video: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
setMediaStream(newStream);
|
||||||
|
setEnabled(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const disable = useCallback(async () => {
|
||||||
|
if(mediaStream === null) return;
|
||||||
|
|
||||||
|
for(const track of mediaStream?.getTracks()) {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
setMediaStream(null);
|
||||||
|
setEnabled(false);
|
||||||
|
}, [mediaStream])
|
||||||
|
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
enabled: false,
|
||||||
|
mediaStream: null,
|
||||||
|
enable,
|
||||||
|
disable
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return <UserMediaContext.Provider value={value}>
|
||||||
|
{props.children}
|
||||||
|
</UserMediaContext.Provider>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
import { PeerContext } from "./PeerState";
|
||||||
|
import { useApi } from "/@/lib/useApi";
|
||||||
|
|
||||||
|
interface RemotePeer {
|
||||||
|
mediaStream: MediaStream | null;
|
||||||
|
peerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VoiceChannelContext = createContext<{
|
||||||
|
voiceChannelId: string | null;
|
||||||
|
setVoiceChannelId: (channelId: string | null) => void
|
||||||
|
}>({
|
||||||
|
voiceChannelId: null,
|
||||||
|
setVoiceChannelId: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function VoiceChannelState(props: any) {
|
||||||
|
|
||||||
|
const [voiceChannelId, setVoiceChannelId] = useState<string | null>(null);
|
||||||
|
const { peerId, incommingCalls } = useContext(PeerContext);
|
||||||
|
const [peers, setPeers] = useState<RemotePeer[]>([]);
|
||||||
|
|
||||||
|
const { send } = useApi({
|
||||||
|
'voice:list'() {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
}, [voiceChannelId])
|
||||||
|
|
||||||
|
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
voiceChannelId,
|
||||||
|
setVoiceChannelId,
|
||||||
|
}), [voiceChannelId, setVoiceChannelId])
|
||||||
|
|
||||||
|
return <VoiceChannelContext.Provider value={value}>
|
||||||
|
{props.children}
|
||||||
|
</VoiceChannelContext.Provider>
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,25 @@ export function connectApi(url: string) {
|
||||||
let connectionAttempts = 0;
|
let connectionAttempts = 0;
|
||||||
let destroy = false;
|
let destroy = false;
|
||||||
let routers: any[] = [];
|
let routers: any[] = [];
|
||||||
|
let keepalive: NodeJS.Timer | null = null;
|
||||||
|
|
||||||
|
function startKeepalive() {
|
||||||
|
keepalive = setInterval(() => {
|
||||||
|
if(socket !== null) {
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
action: 'up',
|
||||||
|
data: {}
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
stopKeepalive();
|
||||||
|
}
|
||||||
|
}, 30_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopKeepalive() {
|
||||||
|
if(keepalive !== null)
|
||||||
|
clearInterval(keepalive);
|
||||||
|
}
|
||||||
|
|
||||||
const connect = async () => {
|
const connect = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -25,6 +44,7 @@ export function connectApi(url: string) {
|
||||||
if(socket === null) return;
|
if(socket === null) return;
|
||||||
connectionAttempts = 0;
|
connectionAttempts = 0;
|
||||||
console.log('connected to', url);
|
console.log('connected to', url);
|
||||||
|
startKeepalive();
|
||||||
// socket.send('Hello Server!');
|
// socket.send('Hello Server!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -34,14 +54,16 @@ export function connectApi(url: string) {
|
||||||
const routeFound = routers
|
const routeFound = routers
|
||||||
.map(router => router(action, data))
|
.map(router => router(action, data))
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
if(routeFound === 0) {
|
if(routeFound === 0 && action !== 'up') {
|
||||||
console.warn(`route <${action}> not found`);
|
console.warn(`route <${action}> not found`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener('close', () => {
|
socket.addEventListener('close', () => {
|
||||||
if(destroy) return;
|
stopKeepalive();
|
||||||
socket = null;
|
socket = null;
|
||||||
|
if(destroy) return;
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import * as preload from '#preload';
|
import * as preload from '#preload';
|
||||||
|
|
||||||
// console.log('#preload', preload);
|
// console.log('#preload', preload);
|
||||||
|
|
||||||
|
function ls(key: string, value?: string) {
|
||||||
|
if(value === undefined) {
|
||||||
|
return localStorage.getItem(key);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const functions: any = (function() {
|
const functions: any = (function() {
|
||||||
const electron = !!preload.versions;
|
const electron = !!preload.versions;
|
||||||
const cordova = 'cordova' in globalThis;
|
const cordova = 'cordova' in globalThis;
|
||||||
|
|
@ -13,17 +20,23 @@ const functions: any = (function() {
|
||||||
let homeServer: string | null = null;
|
let homeServer: string | null = null;
|
||||||
return {
|
return {
|
||||||
getClientId() {
|
getClientId() {
|
||||||
return cid;
|
return ls('clientId');
|
||||||
},
|
},
|
||||||
setClientId(id: any) {
|
setClientId(id: any) {
|
||||||
cid = id;
|
ls('clientId', id);
|
||||||
},
|
},
|
||||||
getHomeServer() {
|
getHomeServer() {
|
||||||
return homeServer;
|
return ls('homeServer');
|
||||||
},
|
},
|
||||||
setHomeServer(str: string) {
|
setHomeServer(str: string) {
|
||||||
homeServer = str;
|
ls('homeServer', str);
|
||||||
}
|
},
|
||||||
|
getSessionToken() {
|
||||||
|
return ls('sessionToken');
|
||||||
|
},
|
||||||
|
setSessionToken(str: string) {
|
||||||
|
ls('sessionToken', str);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,54 @@
|
||||||
import { CgHashtag } from "react-icons/cg";
|
import { CgHashtag } from "react-icons/cg";
|
||||||
|
import { MdVolumeUp } from "react-icons/md";
|
||||||
|
import { BsQuestionLg } from 'react-icons/bs';
|
||||||
|
import { ChannelType } from "../contexts/EphemeralState/EphemeralState";
|
||||||
import useChannel from "../hooks/useChannel";
|
import useChannel from "../hooks/useChannel";
|
||||||
import useHover from "../hooks/useHover";
|
import useHover from "../hooks/useHover";
|
||||||
|
import { useApi } from "../lib/useApi";
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { VoiceChannelContext } from "../contexts/EphemeralState/VoiceChannelState";
|
||||||
|
|
||||||
interface ChannelProps {
|
interface ChannelProps {
|
||||||
unread: number;
|
unread: number;
|
||||||
uid: string;
|
uid: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
type: ChannelType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Channel(props: ChannelProps) {
|
export default function Channel(props: ChannelProps) {
|
||||||
const { channel, setChannel } = useChannel();
|
const { channel, setChannel } = useChannel();
|
||||||
const { unread, uid, name } = props;
|
const { unread, uid, name, type } = props;
|
||||||
const [ref, hover] = useHover<HTMLDivElement>();
|
const [ref, hover] = useHover<HTMLDivElement>();
|
||||||
const selected = channel === uid;
|
const selected = channel === uid;
|
||||||
|
const { voiceChannelId } = useContext(VoiceChannelContext);
|
||||||
|
|
||||||
|
const [participants, setParticipants] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const { send } = useApi({
|
||||||
|
'voice:join'(data: any) {
|
||||||
|
if(type !== 'voice' || data.channelId !== uid) return;
|
||||||
|
setParticipants([...participants, {
|
||||||
|
clientId: data.clientId,
|
||||||
|
peerId: data.peerId,
|
||||||
|
channelId: data.channelId
|
||||||
|
}])
|
||||||
|
console.log('JOIN', data);
|
||||||
|
},
|
||||||
|
'voice:list'(data: any) {
|
||||||
|
if(type !== 'voice') return;
|
||||||
|
console.log('CURRENTS', data);
|
||||||
|
},
|
||||||
|
'voice:leave'(data: any) {
|
||||||
|
console.log(data);
|
||||||
|
},
|
||||||
|
}, [uid, type, participants])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(type !== 'voice') return;
|
||||||
|
|
||||||
|
setParticipants([]);
|
||||||
|
send('voice:list', { uid });
|
||||||
|
}, [uid]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -30,19 +66,41 @@ export default function Channel(props: ChannelProps) {
|
||||||
transition: 'background 300ms, color 300ms',
|
transition: 'background 300ms, color 300ms',
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setChannel(uid);
|
setChannel(uid, type);
|
||||||
}}
|
}}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<CgHashtag color={
|
{(type === 'text') ? (
|
||||||
selected ? 'var(--neutral-9)' :
|
<CgHashtag color={
|
||||||
hover ? 'var(--neutral-7)' :
|
selected ? 'var(--neutral-9)' :
|
||||||
'var(--neutral-7)'
|
hover ? 'var(--neutral-7)' :
|
||||||
} size={24} style={{
|
'var(--neutral-7)'
|
||||||
margin: '4px',
|
} size={24} style={{
|
||||||
transition: 'background 300ms, color 300ms',
|
margin: '4px',
|
||||||
transform:'skew(-5deg, 0deg)',
|
transition: 'background 300ms, color 300ms',
|
||||||
}}></CgHashtag>
|
transform:'skew(-5deg, 0deg)',
|
||||||
|
}}></CgHashtag>
|
||||||
|
) : ((type === 'voice') ? (
|
||||||
|
<MdVolumeUp color={
|
||||||
|
selected ? 'var(--neutral-9)' :
|
||||||
|
hover ? 'var(--neutral-7)' :
|
||||||
|
'var(--neutral-7)'
|
||||||
|
} size={24} style={{
|
||||||
|
margin: '4px',
|
||||||
|
transition: 'background 300ms, color 300ms',
|
||||||
|
transform:'skew(20deg, 0deg)',
|
||||||
|
}}></MdVolumeUp>
|
||||||
|
) : (
|
||||||
|
<BsQuestionLg color={
|
||||||
|
selected ? 'var(--neutral-9)' :
|
||||||
|
hover ? 'var(--neutral-7)' :
|
||||||
|
'var(--neutral-7)'
|
||||||
|
} size={24} style={{
|
||||||
|
margin: '4px',
|
||||||
|
transition: 'background 300ms, color 300ms',
|
||||||
|
transform:'skew(20deg, 0deg)',
|
||||||
|
}}></BsQuestionLg>
|
||||||
|
))}
|
||||||
<div style={{
|
<div style={{
|
||||||
lineHeight: '32px',
|
lineHeight: '32px',
|
||||||
color: selected ? 'var(--neutral-9)' :
|
color: selected ? 'var(--neutral-9)' :
|
||||||
|
|
@ -66,6 +124,12 @@ export default function Channel(props: ChannelProps) {
|
||||||
marginLeft: '8px',
|
marginLeft: '8px',
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
}} href="#" onClick={() => {}}>Delete</a> */}
|
}} href="#" onClick={() => {}}>Delete</a> */}
|
||||||
|
<br></br>
|
||||||
|
{participants.map(participant => (
|
||||||
|
<div key={participant.clientId}>
|
||||||
|
{participant.clientId}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -10,10 +10,12 @@ import useChannel from '../hooks/useChannel';
|
||||||
import useClientId from '../hooks/useClientId';
|
import useClientId from '../hooks/useClientId';
|
||||||
import useHomeServer from '../contexts/PersistentState/useHomeServerNative';
|
import useHomeServer from '../contexts/PersistentState/useHomeServerNative';
|
||||||
import Channel from './Channel';
|
import Channel from './Channel';
|
||||||
|
import { ChannelType } from '../contexts/EphemeralState/EphemeralState';
|
||||||
|
|
||||||
interface IChannel {
|
interface IChannel {
|
||||||
uid: string;
|
uid: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
type: ChannelType;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUnreads {
|
interface IUnreads {
|
||||||
|
|
@ -25,12 +27,12 @@ export default function Channels() {
|
||||||
const [channels, setChannels] = useState<IChannel[]>([]);
|
const [channels, setChannels] = useState<IChannel[]>([]);
|
||||||
const [unreads, setUnreads] = useState<IUnreads>({});
|
const [unreads, setUnreads] = useState<IUnreads>({});
|
||||||
|
|
||||||
const { channel, setChannel } = useChannel()
|
const { channel, setChannel } = useChannel();
|
||||||
const { clientId } = useClientId()
|
const { clientId } = useClientId();
|
||||||
|
|
||||||
const { send } = useApi({
|
const { send } = useApi({
|
||||||
'channels:list'(data: IChannel[]) {
|
'channels:list'(data: any) {
|
||||||
setChannels(data);
|
setChannels(data.channels);
|
||||||
},
|
},
|
||||||
'channel:add'(channel: IChannel) {
|
'channel:add'(channel: IChannel) {
|
||||||
setChannels([...channels, channel]);
|
setChannels([...channels, channel]);
|
||||||
|
|
@ -53,7 +55,7 @@ export default function Channels() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(channels.length === 0) return;
|
if(channels.length === 0) return;
|
||||||
if(channel !== null) return;
|
if(channel !== null) return;
|
||||||
setChannel(channels[0].uid);
|
setChannel(channels[1].uid, channels[1].type);
|
||||||
}, [channel, channels]);
|
}, [channel, channels]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -66,7 +68,7 @@ export default function Channels() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(clientId === null) return;
|
if(clientId === null) return;
|
||||||
send('client:get', { clientId });
|
// send('client:get', { clientId });
|
||||||
}, [clientId]);
|
}, [clientId]);
|
||||||
|
|
||||||
const textbox = useRef<HTMLInputElement>(null);
|
const textbox = useRef<HTMLInputElement>(null);
|
||||||
|
|
@ -90,6 +92,7 @@ export default function Channels() {
|
||||||
<Channel
|
<Channel
|
||||||
key={c.uid}
|
key={c.uid}
|
||||||
uid={c.uid}
|
uid={c.uid}
|
||||||
|
type={c.type}
|
||||||
unread={unreads[c.uid] ?? 0}
|
unread={unreads[c.uid] ?? 0}
|
||||||
name={c.name}
|
name={c.name}
|
||||||
></Channel>
|
></Channel>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ const config = {
|
||||||
},
|
},
|
||||||
base: '',
|
base: '',
|
||||||
server: {
|
server: {
|
||||||
|
host: true,
|
||||||
fs: {
|
fs: {
|
||||||
strict: true,
|
strict: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import router from './lib/router';
|
import router from './lib/router';
|
||||||
import { expose } from './lib/WebSocketServer';
|
import { expose, reply } from './lib/WebSocketServer';
|
||||||
|
|
||||||
import message from './routers/message';
|
import message from './routers/message';
|
||||||
import channel from './routers/channel';
|
import channel from './routers/channel';
|
||||||
|
|
@ -8,7 +8,9 @@ import totp from './routers/totp';
|
||||||
|
|
||||||
const api = router({
|
const api = router({
|
||||||
up() {
|
up() {
|
||||||
console.log(Date.now());
|
return reply({
|
||||||
|
time: Date.now()
|
||||||
|
});
|
||||||
},
|
},
|
||||||
message: message,
|
message: message,
|
||||||
messages: message,
|
messages: message,
|
||||||
|
|
@ -19,6 +21,7 @@ const api = router({
|
||||||
totp: totp,
|
totp: totp,
|
||||||
session: session,
|
session: session,
|
||||||
sessions: session,
|
sessions: session,
|
||||||
|
voice: voice
|
||||||
});
|
});
|
||||||
|
|
||||||
expose(api, 3000);
|
expose(api, 3000);
|
||||||
|
|
@ -27,6 +30,7 @@ expose(api, 3000);
|
||||||
|
|
||||||
import { update } from './db/migrate';
|
import { update } from './db/migrate';
|
||||||
import session from './routers/session';
|
import session from './routers/session';
|
||||||
|
import voice from './routers/voice';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
update();
|
update();
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,21 @@ import add from '../db/snippets/channel/new.sql';
|
||||||
import { broadcast, reply } from '../lib/WebSocketServer';
|
import { broadcast, reply } from '../lib/WebSocketServer';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
export const mockVoiceChannels = [
|
||||||
|
{
|
||||||
|
uid: v4(),
|
||||||
|
name: 'Voice Test',
|
||||||
|
type: 'voice'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
export default router({
|
export default router({
|
||||||
async list() {
|
async list() {
|
||||||
const res = await query(list);
|
const res = await query(list);
|
||||||
return reply(res ?? undefined);
|
if(res === null) return;
|
||||||
|
return reply({
|
||||||
|
channels: [...(res.map(v => ({...v, type: 'text'}))), ...mockVoiceChannels]
|
||||||
|
});
|
||||||
},
|
},
|
||||||
async add(channel: any) {
|
async add(channel: any) {
|
||||||
const name = channel.name;
|
const name = channel.name;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import router from "../lib/router";
|
||||||
|
import { broadcast, reply } from "../lib/WebSocketServer";
|
||||||
|
|
||||||
|
function filterInPlace<T>(a: T[], condition: (v: T, i: number, a: T[]) => boolean) {
|
||||||
|
let i = 0, j = 0;
|
||||||
|
|
||||||
|
let copy = [...a];
|
||||||
|
let removed = [];
|
||||||
|
|
||||||
|
while (i < a.length) {
|
||||||
|
const val = a[i];
|
||||||
|
if (condition(val, i, copy)) a[j++] = val;
|
||||||
|
else removed.push(val);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.length = j;
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClientChannelRelationship {
|
||||||
|
clientId: string;
|
||||||
|
channelId: string;
|
||||||
|
peerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const participants: ClientChannelRelationship[] = [];
|
||||||
|
|
||||||
|
export default router({
|
||||||
|
async join(data: any) {
|
||||||
|
const { $clientId, channelId, peerId } = data;
|
||||||
|
// TODO validate channel exists
|
||||||
|
if(participants
|
||||||
|
.filter(v => v.clientId === $clientId)
|
||||||
|
.length !== 0) {
|
||||||
|
// TODO REMOVE USER FROM THIS PLACE
|
||||||
|
}
|
||||||
|
|
||||||
|
const user_channel = {
|
||||||
|
clientId: $clientId,
|
||||||
|
peerId,
|
||||||
|
channelId
|
||||||
|
};
|
||||||
|
|
||||||
|
participants.push(user_channel);
|
||||||
|
|
||||||
|
return broadcast(user_channel)
|
||||||
|
},
|
||||||
|
async list(data: any) {
|
||||||
|
const { uid } = data;
|
||||||
|
return reply({
|
||||||
|
uid,
|
||||||
|
participants: participants.filter(v => uid === v.channelId)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async leave(data: any) {
|
||||||
|
const { $clientId } = data;
|
||||||
|
const removed = filterInPlace(participants, (v) => v.clientId !== $clientId);
|
||||||
|
console.log('removed', removed);
|
||||||
|
return broadcast(removed[0]);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -8,11 +8,31 @@
|
||||||
#tracks {
|
#tracks {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: row;
|
||||||
|
}
|
||||||
|
#tracks fieldset {
|
||||||
|
width: '50%'
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="tracks"></div>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Call</legend>
|
||||||
|
<div id="call_status"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<label>Audio</label>
|
||||||
|
<select id="in_audio">
|
||||||
|
</select>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<label>Video</label>
|
||||||
|
<select id="in_video">
|
||||||
|
</select>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Peer Connection</legend>
|
<legend>Peer Connection</legend>
|
||||||
<div>Peer ID: <span id="pid"></span></div>
|
<div>Peer ID: <span id="pid"></span></div>
|
||||||
|
|
@ -31,25 +51,8 @@
|
||||||
<div id="in_status"></div>
|
<div id="in_status"></div>
|
||||||
<button disabled type="submit" id="answer_btn">ANSWER</button>
|
<button disabled type="submit" id="answer_btn">ANSWER</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
|
||||||
<legend>Call</legend>
|
|
||||||
<div id="call_status"></div>
|
|
||||||
|
|
||||||
|
|
||||||
<label>Audio</label>
|
|
||||||
<select id="in_audio">
|
|
||||||
</select>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<label>Video</label>
|
|
||||||
<select id="in_video">
|
|
||||||
</select>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div id="tracks"></div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const eStatus = document.getElementById('status');
|
const eStatus = document.getElementById('status');
|
||||||
const ePid = document.getElementById('pid');
|
const ePid = document.getElementById('pid');
|
||||||
|
|
@ -70,6 +73,19 @@
|
||||||
let incommingCall = null;
|
let incommingCall = null;
|
||||||
let currentVideoTrackId = null;
|
let currentVideoTrackId = null;
|
||||||
let currentAudioTrackId = null;
|
let currentAudioTrackId = null;
|
||||||
|
let selfBoxRemove = null;
|
||||||
|
let ctx = null;
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
if(ctx === null) return;
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.fillRect(0, 0, 160, 90);
|
||||||
|
setTimeout(() => {
|
||||||
|
if(ctx === null) return;
|
||||||
|
ctx.fillStyle = 'red';
|
||||||
|
ctx.fillRect(0, 0, 160, 90);
|
||||||
|
}, 100);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
function closeLocalMediaStream() {
|
function closeLocalMediaStream() {
|
||||||
for(const track of localMediaStream.getTracks()) {
|
for(const track of localMediaStream.getTracks()) {
|
||||||
|
|
@ -78,6 +94,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createLocalMediaStream() {
|
async function createLocalMediaStream() {
|
||||||
|
console.log('creating stream');
|
||||||
if(localMediaStream !== null) {
|
if(localMediaStream !== null) {
|
||||||
closeLocalMediaStream();
|
closeLocalMediaStream();
|
||||||
}
|
}
|
||||||
|
|
@ -100,22 +117,22 @@
|
||||||
width: 160,
|
width: 160,
|
||||||
height: 90
|
height: 90
|
||||||
});
|
});
|
||||||
canvas.getContext("2d").fillRect(0, 0, 160, 90);
|
ctx = canvas.getContext("2d");
|
||||||
const blankStream = canvas.captureStream();
|
const blankStream = canvas.captureStream();
|
||||||
const videoTrack = blankStream.getVideoTracks()[0];
|
const videoTrack = blankStream.getVideoTracks()[0];
|
||||||
console.log('empty tracks', blankStream, videoTrack);
|
|
||||||
localMediaStream.addTrack(videoTrack);
|
localMediaStream.addTrack(videoTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOutgoing();
|
updateOutgoing();
|
||||||
};
|
};
|
||||||
|
|
||||||
console.dir(eAudioOptions);
|
|
||||||
|
|
||||||
async function updateOutgoing() {
|
async function updateOutgoing() {
|
||||||
await createLocalMediaStream();
|
|
||||||
for(const peerId in connections) {
|
for(const peerId in connections) {
|
||||||
const { call } = connections[peerId];
|
const { call, completed } = connections[peerId];
|
||||||
|
if(!completed) continue;
|
||||||
|
|
||||||
|
if(call.peerConnection === undefined) debugger;
|
||||||
|
|
||||||
const audioTrack = localMediaStream.getTracks()
|
const audioTrack = localMediaStream.getTracks()
|
||||||
.filter(track => track.kind === 'audio')[0];
|
.filter(track => track.kind === 'audio')[0];
|
||||||
const videoTrack = localMediaStream.getTracks()
|
const videoTrack = localMediaStream.getTracks()
|
||||||
|
|
@ -128,12 +145,21 @@
|
||||||
call.peerConnection.getSenders()
|
call.peerConnection.getSenders()
|
||||||
.filter(sender => sender.track.kind === 'video')[0]
|
.filter(sender => sender.track.kind === 'video')[0]
|
||||||
.replaceTrack(videoTrack)
|
.replaceTrack(videoTrack)
|
||||||
|
}
|
||||||
|
|
||||||
|
selfBoxRemove?.();
|
||||||
|
|
||||||
|
if(Object.keys(connections.filter(c => c.completed)).length === 0) {
|
||||||
|
selfBoxRemove = addVideoBox(localMediaStream, 'Self', () => {
|
||||||
|
alert('DISCO ALL CONNS');
|
||||||
|
}).remove;
|
||||||
|
} else {
|
||||||
|
selfBoxRemove = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
eAudioOptions.onchange = updateOutgoing;
|
eAudioOptions.onchange = createLocalMediaStream;
|
||||||
eVideoOptions.onchange = updateOutgoing;
|
eVideoOptions.onchange = createLocalMediaStream;
|
||||||
|
|
||||||
// get permissions and enumerate devices
|
// get permissions and enumerate devices
|
||||||
(async function() {
|
(async function() {
|
||||||
|
|
@ -145,7 +171,6 @@
|
||||||
track.stop();
|
track.stop();
|
||||||
}
|
}
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
console.log(devices);
|
|
||||||
|
|
||||||
for(const device of devices.filter(v => v.kind === 'audioinput')) {
|
for(const device of devices.filter(v => v.kind === 'audioinput')) {
|
||||||
const elem = document.createElement('option');
|
const elem = document.createElement('option');
|
||||||
|
|
@ -236,24 +261,7 @@
|
||||||
onCall(call.peer);
|
onCall(call.peer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCall(peerId) {
|
function addVideoBox(stream, name, end) {
|
||||||
const { call, conn, mediaStream, completed } = connections[peerId];
|
|
||||||
if(!call || !conn || !mediaStream) return;
|
|
||||||
if(completed) return;
|
|
||||||
connections[peerId].completed = true;
|
|
||||||
|
|
||||||
console.log('fully connected!');
|
|
||||||
|
|
||||||
eOutgoingStatus.innerText = 'Inactive';
|
|
||||||
eCallBtn.disabled = false;
|
|
||||||
eCallPid.value = '';
|
|
||||||
eIncommingStatus.innerText = '';
|
|
||||||
eAnswerBtn.disabled = true;
|
|
||||||
|
|
||||||
// while(eTracks.firstChild) {
|
|
||||||
// eTracks.removeChild(eTracks.firstChild);
|
|
||||||
// }
|
|
||||||
|
|
||||||
const root = document.createElement('fieldset');
|
const root = document.createElement('fieldset');
|
||||||
const elem = document.createElement('video');
|
const elem = document.createElement('video');
|
||||||
const legend = document.createElement('legend');
|
const legend = document.createElement('legend');
|
||||||
|
|
@ -262,15 +270,12 @@
|
||||||
elem.autoplay = true;
|
elem.autoplay = true;
|
||||||
elem.controls = true;
|
elem.controls = true;
|
||||||
elem.style.width = '100%';
|
elem.style.width = '100%';
|
||||||
elem.srcObject = mediaStream;
|
elem.srcObject = stream;
|
||||||
|
|
||||||
legend.innerText = peerId;
|
legend.innerText = name;
|
||||||
|
|
||||||
endBtn.innerText = "END";
|
endBtn.innerText = "END";
|
||||||
endBtn.addEventListener('click', () => {
|
endBtn.addEventListener('click', end);
|
||||||
conn.close();
|
|
||||||
call.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
// root.appendChild(document.createElement('br'));
|
// root.appendChild(document.createElement('br'));
|
||||||
root.appendChild(legend);
|
root.appendChild(legend);
|
||||||
|
|
@ -280,14 +285,44 @@
|
||||||
|
|
||||||
eTracks.appendChild(root);
|
eTracks.appendChild(root);
|
||||||
|
|
||||||
conn.on('close', () => {
|
return {
|
||||||
while(root.firstChild) {
|
remove() {
|
||||||
root.removeChild(root.firstChild);
|
while(root.firstChild) {
|
||||||
|
root.removeChild(root.firstChild);
|
||||||
|
}
|
||||||
|
root.remove();
|
||||||
}
|
}
|
||||||
root.remove();
|
}
|
||||||
delete connections[peerId];
|
}
|
||||||
if(Object.keys(connections).length === 0) closeLocalMediaStream();
|
|
||||||
});
|
function onCall(peerId) {
|
||||||
|
const { call, conn, mediaStream, completed } = connections[peerId];
|
||||||
|
if(!call || !conn || !mediaStream) return;
|
||||||
|
if(completed) return;
|
||||||
|
connections[peerId].completed = true;
|
||||||
|
|
||||||
|
eOutgoingStatus.innerText = 'Inactive';
|
||||||
|
eCallBtn.disabled = false;
|
||||||
|
eCallPid.value = '';
|
||||||
|
eIncommingStatus.innerText = '';
|
||||||
|
eAnswerBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRemoteBoxes() {
|
||||||
|
for(const peerId in connections.filter(c => c.completed)) {
|
||||||
|
const { mediaStream, call, conn } = connections[peerId];
|
||||||
|
|
||||||
|
const { remove } = addVideoBox(mediaStream, peerId.split('-')[0], () => {
|
||||||
|
conn.close();
|
||||||
|
call.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.on('close', () => {
|
||||||
|
remove();
|
||||||
|
delete connections[peerId];
|
||||||
|
if(Object.keys(connections).length === 0) closeLocalMediaStream();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eEndBtn.addEventListener('click', () => {
|
// eEndBtn.addEventListener('click', () => {
|
||||||
|
|
|
||||||
Reference in New Issue