mirror of
https://github.com/zkitter/ui.git
synced 2026-01-09 21:28:05 -05:00
implement notification UI (#54)
* wip * ui wip * finish notification for invite, like, repost, and reply * add notification for accepting invite * add notification for mentions * fix build and test * fix: add missing html-entities dep * add notification for chat messages (#68) * add notification for chat messages * add chat notifications per chat id * pin gun to v0.2020.1232 * update pnpm-locl * Search (#70) * wip * add search input * add ui for search results * add keydown control and user search to GlobalSearchInput * fix mobile onClick timing * reset selected index on blur * add zindex * notif: store last read timestamp in backend (#71) * notif: store last read timestamp in backend * store last read timestamp of chat in backend * fix typo * remove console.log * use enum for notification types * use ContextReplacementPlugin * fix prettier and unit test * bump chat when unread Co-authored-by: r1oga <38692952+r1oga@users.noreply.github.com>
This commit is contained in:
37704
package-lock.json
generated
Normal file
37704
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@
|
||||
"ethereum-blockies-base64": "^1.0.2",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"gun": "^0.2020.1238",
|
||||
"gun": "0.2020.1232",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"magnet-uri": "^6.2.0",
|
||||
@@ -48,6 +48,7 @@
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"react": "~16",
|
||||
"react-dom": "~16",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-qr-code": "^2.0.8",
|
||||
"react-qr-reader": "^2.2.0",
|
||||
"react-redux": "^7.2.3",
|
||||
@@ -85,6 +86,7 @@
|
||||
"@types/redux-logger": "^3.0.9",
|
||||
"@types/sinon": "^10.0.13",
|
||||
"@types/webtorrent": "^0.109.3",
|
||||
"assert": "^2.0.0",
|
||||
"browserify": "^17.0.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
@@ -92,6 +94,7 @@
|
||||
"css-loader": "^6.7.2",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-entities": "^2.3.3",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"husky": "^8.0.2",
|
||||
|
||||
155
pnpm-lock.yaml
generated
155
pnpm-lock.yaml
generated
@@ -29,6 +29,7 @@ specifiers:
|
||||
'@types/webtorrent': ^0.109.3
|
||||
'@zk-kit/identity': ^1.4.1
|
||||
'@zk-kit/protocols': ^1.11.1
|
||||
assert: ^2.0.0
|
||||
bn.js: ^5.2.1
|
||||
browserify: ^17.0.0
|
||||
classnames: ^2.3.2
|
||||
@@ -46,7 +47,8 @@ specifiers:
|
||||
fake-indexeddb: ^4.0.0
|
||||
fast-deep-equal: ^3.1.3
|
||||
file-loader: ^6.2.0
|
||||
gun: ^0.2020.1238
|
||||
gun: 0.2020.1232
|
||||
html-entities: ^2.3.3
|
||||
html-webpack-plugin: ^5.5.0
|
||||
https-browserify: ^1.0.0
|
||||
husky: ^8.0.2
|
||||
@@ -69,6 +71,7 @@ specifiers:
|
||||
pretty-bytes: ^6.0.0
|
||||
react: ~16
|
||||
react-dom: ~16
|
||||
react-popper: ^2.3.0
|
||||
react-qr-code: ^2.0.8
|
||||
react-qr-reader: ^2.2.0
|
||||
react-redux: ^7.2.3
|
||||
@@ -118,7 +121,7 @@ dependencies:
|
||||
ethereum-blockies-base64: 1.0.2
|
||||
eventemitter2: 6.4.9
|
||||
fast-deep-equal: 3.1.3
|
||||
gun: 0.2020.1238
|
||||
gun: 0.2020.1232
|
||||
isomorphic-fetch: 3.0.0
|
||||
lodash.debounce: 4.0.8
|
||||
magnet-uri: 6.2.0
|
||||
@@ -128,6 +131,7 @@ dependencies:
|
||||
pretty-bytes: 6.0.0
|
||||
react: 16.14.0
|
||||
react-dom: 16.14.0_react@16.14.0
|
||||
react-popper: 2.3.0_wcqkhtmu7mswc6yz4uyexck3ty
|
||||
react-qr-code: 2.0.8_react@16.14.0
|
||||
react-qr-reader: 2.2.1_wcqkhtmu7mswc6yz4uyexck3ty
|
||||
react-redux: 7.2.9_wcqkhtmu7mswc6yz4uyexck3ty
|
||||
@@ -141,7 +145,7 @@ dependencies:
|
||||
redux-thunk: 2.4.2_redux@4.2.0
|
||||
remarkable: 2.0.1
|
||||
web3: 1.8.1
|
||||
web3modal: 1.9.10_react-is@18.2.0
|
||||
web3modal: 1.9.10
|
||||
webtorrent: 1.9.4
|
||||
|
||||
devDependencies:
|
||||
@@ -163,6 +167,7 @@ devDependencies:
|
||||
'@types/redux-logger': 3.0.9
|
||||
'@types/sinon': 10.0.13
|
||||
'@types/webtorrent': 0.109.3
|
||||
assert: 2.0.0
|
||||
browserify: 17.0.0
|
||||
concurrently: 7.6.0
|
||||
copy-webpack-plugin: 11.0.0_webpack@5.75.0
|
||||
@@ -170,6 +175,7 @@ devDependencies:
|
||||
css-loader: 6.7.2_webpack@5.75.0
|
||||
fake-indexeddb: 4.0.0
|
||||
file-loader: 6.2.0_webpack@5.75.0
|
||||
html-entities: 2.3.3
|
||||
html-webpack-plugin: 5.5.0_webpack@5.75.0
|
||||
https-browserify: 1.0.0
|
||||
husky: 8.0.2
|
||||
@@ -177,7 +183,7 @@ devDependencies:
|
||||
jest: 29.3.1_@types+node@18.11.9
|
||||
jest-environment-jsdom: 29.3.1
|
||||
jest-silent-reporter: 0.5.0
|
||||
jsdom-worker: 0.3.0_node-fetch@2.6.7
|
||||
jsdom-worker: 0.3.0
|
||||
node-loader: 2.0.0_webpack@5.75.0
|
||||
node-sass: 8.0.0
|
||||
os-browserify: 0.3.0
|
||||
@@ -188,7 +194,7 @@ devDependencies:
|
||||
stream-browserify: 3.0.0
|
||||
stream-http: 3.2.0
|
||||
style-loader: 3.3.1_webpack@5.75.0
|
||||
ts-jest: 29.0.3_6crhf7ajeizammv76u753sn6i4
|
||||
ts-jest: 29.0.3_4f6uxrzmuwipl5rr3bcogf6k74
|
||||
ts-loader: 9.4.1_vfotqvx6lgcbf3upbs6hgaza4q
|
||||
typescript: 4.9.3
|
||||
webpack: 5.75.0_webpack-cli@5.0.0
|
||||
@@ -2218,6 +2224,11 @@ packages:
|
||||
resolution: {integrity: sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg==}
|
||||
dev: false
|
||||
|
||||
/addressparser/0.3.2:
|
||||
resolution: {integrity: sha512-fDlslCJpojuY1cnb7tY7COAriA7cdSzDiWyrWNdFn7Cjd+jrEgZavqkOgD/wg+eH765YPnQjqlS88OL/Q0Qtkg==}
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/aes-js/3.0.0:
|
||||
resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==}
|
||||
dev: false
|
||||
@@ -2250,10 +2261,8 @@ packages:
|
||||
indent-string: 4.0.0
|
||||
dev: true
|
||||
|
||||
/ajv-formats/2.1.1_ajv@8.11.2:
|
||||
/ajv-formats/2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
ajv: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
ajv:
|
||||
optional: true
|
||||
@@ -2430,6 +2439,15 @@ packages:
|
||||
util: 0.10.3
|
||||
dev: true
|
||||
|
||||
/assert/2.0.0:
|
||||
resolution: {integrity: sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==}
|
||||
dependencies:
|
||||
es6-object-assign: 1.1.0
|
||||
is-nan: 1.3.2
|
||||
object-is: 1.1.5
|
||||
util: 0.12.5
|
||||
dev: true
|
||||
|
||||
/async-foreach/0.1.3:
|
||||
resolution: {integrity: sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==}
|
||||
dev: true
|
||||
@@ -2518,7 +2536,7 @@ packages:
|
||||
babel-plugin-syntax-jsx: 6.18.0
|
||||
lodash: 4.17.21
|
||||
picomatch: 2.3.1
|
||||
styled-components: 5.3.6_gzjxbbqgwmhffys5rngtiuy3yq
|
||||
styled-components: 5.3.6_wcqkhtmu7mswc6yz4uyexck3ty
|
||||
dev: false
|
||||
|
||||
/babel-plugin-syntax-jsx/6.18.0:
|
||||
@@ -4127,6 +4145,14 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/define-properties/1.1.4:
|
||||
resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
has-property-descriptors: 1.0.0
|
||||
object-keys: 1.1.1
|
||||
dev: true
|
||||
|
||||
/defined/1.0.1:
|
||||
resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==}
|
||||
dev: true
|
||||
@@ -4390,6 +4416,29 @@ packages:
|
||||
minimalistic-assert: 1.0.1
|
||||
minimalistic-crypto-utils: 1.0.1
|
||||
|
||||
/emailjs-base64/1.1.4:
|
||||
resolution: {integrity: sha512-4h0xp1jgVTnIQBHxSJWXWanNnmuc5o+k4aHEpcLXSToN8asjB5qbXAexs7+PEsUKcEyBteNYsSvXUndYT2CGGA==}
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/emailjs-mime-codec/2.0.9:
|
||||
resolution: {integrity: sha512-7qJo4pFGcKlWh/kCeNjmcgj34YoJWY0ekZXEHYtluWg4MVBnXqGM4CRMtZQkfYwitOhUgaKN5EQktJddi/YIDQ==}
|
||||
dependencies:
|
||||
emailjs-base64: 1.1.4
|
||||
ramda: 0.26.1
|
||||
text-encoding: 0.7.0
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/emailjs/2.2.0:
|
||||
resolution: {integrity: sha512-J9HNx13GA5DnJma10YxsSqYCErTyB0KoVflTddPTyKlEVHM0MckZXn/zDqovdacwWkHCxqC9AKVY8GMPaGvaGQ==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
addressparser: 0.3.2
|
||||
emailjs-mime-codec: 2.0.9
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/emittery/0.13.1:
|
||||
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -4484,6 +4533,10 @@ packages:
|
||||
es6-symbol: 3.1.3
|
||||
dev: false
|
||||
|
||||
/es6-object-assign/1.1.0:
|
||||
resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==}
|
||||
dev: true
|
||||
|
||||
/es6-promise/4.2.8:
|
||||
resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
|
||||
dev: false
|
||||
@@ -5560,16 +5613,19 @@ packages:
|
||||
/graceful-fs/4.2.10:
|
||||
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
|
||||
|
||||
/gun/0.2020.1238:
|
||||
resolution: {integrity: sha512-8rKaAN74o3RTc2/a3CZca3Rx6rWnYpB1nS7mCtWH+LC7wvt2tzPR1yfRCq61haoR/Vtbl45qdq8iyoz9lAY8mg==}
|
||||
/gun/0.2020.1232:
|
||||
resolution: {integrity: sha512-/LAooFgiIwbHt6geaY76uqgrsAS0LmSVvynw7gsaJL/Irn8pZh/Irxoq0iUTcrUR2TzAK81LKLgeM/5pLQZ8Wg==}
|
||||
engines: {node: '>=0.8.4'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ^5.0.2
|
||||
dependencies:
|
||||
ws: 7.5.9
|
||||
optionalDependencies:
|
||||
'@peculiar/webcrypto': 1.4.1
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
buffer: 5.7.1
|
||||
emailjs: 2.2.0
|
||||
text-encoding: 0.7.0
|
||||
dev: false
|
||||
|
||||
/handle-thing/2.0.1:
|
||||
@@ -5603,6 +5659,12 @@ packages:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/has-property-descriptors/1.0.0:
|
||||
resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
|
||||
dependencies:
|
||||
get-intrinsic: 1.1.3
|
||||
dev: true
|
||||
|
||||
/has-symbol-support-x/1.4.2:
|
||||
resolution: {integrity: sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==}
|
||||
dev: true
|
||||
@@ -6296,6 +6358,14 @@ packages:
|
||||
resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==}
|
||||
dev: true
|
||||
|
||||
/is-nan/1.3.2:
|
||||
resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
dev: true
|
||||
|
||||
/is-natural-number/4.0.1:
|
||||
resolution: {integrity: sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==}
|
||||
dev: true
|
||||
@@ -6978,13 +7048,12 @@ packages:
|
||||
resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
|
||||
dev: false
|
||||
|
||||
/jsdom-worker/0.3.0_node-fetch@2.6.7:
|
||||
/jsdom-worker/0.3.0:
|
||||
resolution: {integrity: sha512-nlPmN0i93+e6vxzov8xqLMR+MBs/TAYeSviehivzqovHH0AgooVx9pQ/otrygASppPvdR+V9Jqx5SMe8+FcADg==}
|
||||
peerDependencies:
|
||||
node-fetch: '*'
|
||||
dependencies:
|
||||
mitt: 3.0.0
|
||||
node-fetch: 2.6.7
|
||||
uuid-v4: 0.1.0
|
||||
dev: true
|
||||
|
||||
@@ -7947,6 +8016,7 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: false
|
||||
|
||||
/node-forge/1.3.1:
|
||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||
@@ -8135,6 +8205,19 @@ packages:
|
||||
/object-inspect/1.12.2:
|
||||
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
|
||||
|
||||
/object-is/1.1.5:
|
||||
resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
dev: true
|
||||
|
||||
/object-keys/1.1.1:
|
||||
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dev: true
|
||||
|
||||
/oboe/2.1.5:
|
||||
resolution: {integrity: sha512-zRFWiF+FoicxEs3jNI/WYUrVEgA7DeET/InK0XQuudGHRg8iIob3cNPrJTKaz4004uaA9Pbe+Dwa8iluhjLZWA==}
|
||||
dependencies:
|
||||
@@ -8864,6 +8947,11 @@ packages:
|
||||
ffjavascript: 0.2.55
|
||||
dev: false
|
||||
|
||||
/ramda/0.26.1:
|
||||
resolution: {integrity: sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==}
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/random-access-file/2.2.1:
|
||||
resolution: {integrity: sha512-RGU0xmDqdOyEiynob1KYSeh8+9c9Td1MJ74GT1viMEYAn8SJ9oBtWCXLsYZukCF46yududHOdM449uRYbzBrZQ==}
|
||||
dependencies:
|
||||
@@ -8943,6 +9031,7 @@ packages:
|
||||
|
||||
/react-is/18.2.0:
|
||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||
dev: true
|
||||
|
||||
/react-popper/2.3.0_ahsyp7paedy7ttpixxrzktxjdi:
|
||||
resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==}
|
||||
@@ -8958,6 +9047,19 @@ packages:
|
||||
warning: 4.0.3
|
||||
dev: false
|
||||
|
||||
/react-popper/2.3.0_wcqkhtmu7mswc6yz4uyexck3ty:
|
||||
resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==}
|
||||
peerDependencies:
|
||||
'@popperjs/core': ^2.0.0
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
react-dom: ^16.8.0 || ^17 || ^18
|
||||
dependencies:
|
||||
react: 16.14.0
|
||||
react-dom: 16.14.0_react@16.14.0
|
||||
react-fast-compare: 3.2.0
|
||||
warning: 4.0.3
|
||||
dev: false
|
||||
|
||||
/react-qr-code/2.0.8_react@16.14.0:
|
||||
resolution: {integrity: sha512-zYO9EAPQU8IIeD6c6uAle7NlKOiVKs8ji9hpbWPTGxO+FLqBN2on+XCXQvnhm91nrRd306RvNXUkUNcXXSfhWA==}
|
||||
peerDependencies:
|
||||
@@ -9485,7 +9587,7 @@ packages:
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.11
|
||||
ajv: 8.11.2
|
||||
ajv-formats: 2.1.1_ajv@8.11.2
|
||||
ajv-formats: 2.1.1
|
||||
ajv-keywords: 5.1.0_ajv@8.11.2
|
||||
dev: true
|
||||
|
||||
@@ -10232,7 +10334,7 @@ packages:
|
||||
webpack: 5.75.0_webpack-cli@5.0.0
|
||||
dev: true
|
||||
|
||||
/styled-components/5.3.6_gzjxbbqgwmhffys5rngtiuy3yq:
|
||||
/styled-components/5.3.6_wcqkhtmu7mswc6yz4uyexck3ty:
|
||||
resolution: {integrity: sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==}
|
||||
engines: {node: '>=10'}
|
||||
requiresBuild: true
|
||||
@@ -10251,7 +10353,6 @@ packages:
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 16.14.0
|
||||
react-dom: 16.14.0_react@16.14.0
|
||||
react-is: 18.2.0
|
||||
shallowequal: 1.1.0
|
||||
supports-color: 5.5.0
|
||||
dev: false
|
||||
@@ -10438,6 +10539,12 @@ packages:
|
||||
deprecated: testrpc has been renamed to ganache-cli, please use this package from now on.
|
||||
dev: false
|
||||
|
||||
/text-encoding/0.7.0:
|
||||
resolution: {integrity: sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==}
|
||||
deprecated: no longer maintained
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/thirty-two/1.0.2:
|
||||
resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==}
|
||||
engines: {node: '>=0.2.6'}
|
||||
@@ -10557,6 +10664,7 @@ packages:
|
||||
|
||||
/tr46/0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
dev: false
|
||||
|
||||
/tr46/2.1.0:
|
||||
resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==}
|
||||
@@ -10598,7 +10706,7 @@ packages:
|
||||
resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==}
|
||||
dev: false
|
||||
|
||||
/ts-jest/29.0.3_6crhf7ajeizammv76u753sn6i4:
|
||||
/ts-jest/29.0.3_4f6uxrzmuwipl5rr3bcogf6k74:
|
||||
resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
hasBin: true
|
||||
@@ -10619,7 +10727,6 @@ packages:
|
||||
esbuild:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.20.2
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
jest: 29.3.1_@types+node@18.11.9
|
||||
@@ -11378,14 +11485,14 @@ packages:
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/web3modal/1.9.10_react-is@18.2.0:
|
||||
/web3modal/1.9.10:
|
||||
resolution: {integrity: sha512-gRByp+toRiADwkJLLGRXsnIVbLS1aJB71sJyryS6C7cF6jJ3cRN1LbPYEMObMyJkyjOZonx0CNZVAYGiD099aA==}
|
||||
dependencies:
|
||||
detect-browser: 5.3.0
|
||||
prop-types: 15.8.1
|
||||
react: 16.14.0
|
||||
react-dom: 16.14.0_react@16.14.0
|
||||
styled-components: 5.3.6_gzjxbbqgwmhffys5rngtiuy3yq
|
||||
styled-components: 5.3.6_wcqkhtmu7mswc6yz4uyexck3ty
|
||||
tslib: 1.14.1
|
||||
transitivePeerDependencies:
|
||||
- react-is
|
||||
@@ -11404,6 +11511,7 @@ packages:
|
||||
|
||||
/webidl-conversions/3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: false
|
||||
|
||||
/webidl-conversions/4.0.2:
|
||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||
@@ -11694,6 +11802,7 @@ packages:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
dev: false
|
||||
|
||||
/whatwg-url/8.7.0:
|
||||
resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==}
|
||||
|
||||
@@ -84,10 +84,10 @@ export default function Avatar(props: Props): ReactElement {
|
||||
}, [name, address, groupName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (username) {
|
||||
if (username && !user) {
|
||||
dispatch(getUser(username));
|
||||
}
|
||||
}, [username]);
|
||||
}, [username, user]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
||||
@@ -7,6 +7,8 @@ import classNames from 'classnames';
|
||||
import { useAccount, useGunLoggedIn } from '../../ducks/web3';
|
||||
import { useSelectedLocalId } from '../../ducks/worker';
|
||||
import { fetchNameByAddress } from '../../util/web3';
|
||||
import NotificationIcon from '../NotificationIcon';
|
||||
import ChatNavIcon from '../ChatNavIcon';
|
||||
|
||||
export default function BottomNav(): ReactElement {
|
||||
const loggedIn = useGunLoggedIn();
|
||||
@@ -31,8 +33,9 @@ export default function BottomNav(): ReactElement {
|
||||
<div className="bottom-nav">
|
||||
<BottomNavIcon fa="fas fa-home" pathname="/home" disabled={!loggedIn} />
|
||||
{/*<BottomNavIcon fa="fas fa-envelope" pathname={`/${ensName || address}/`} disabled={!loggedIn} />*/}
|
||||
<BottomNavIcon fa="fas fa-envelope" pathname={`/chat`} disabled={!selectedLocalId} />
|
||||
<ChatNavIcon fa="fas fa-envelope" pathname={`/chat`} disabled={!selectedLocalId} />
|
||||
<BottomNavIcon fa="fas fa-globe-asia" pathname="/explore" />
|
||||
<NotificationIcon fa="fas fa-bell" pathname="/notifications" />
|
||||
<Web3Button className="bottom-nav__web3-icon" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, {
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import classNames from 'classnames';
|
||||
@@ -16,7 +17,13 @@ import Avatar, { Username } from '../Avatar';
|
||||
import Textarea from '../Textarea';
|
||||
import { generateZkIdentityFromHex, sha256, signWithP256 } from '../../util/crypto';
|
||||
import { FromNow } from '../ChatMenu';
|
||||
import { useChatId, useChatMessage, useMessagesByChatId, zkchat } from '../../ducks/chats';
|
||||
import {
|
||||
setLastReadForChatId,
|
||||
useChatId,
|
||||
useChatMessage,
|
||||
useMessagesByChatId,
|
||||
zkchat,
|
||||
} from '../../ducks/chats';
|
||||
import Icon from '../Icon';
|
||||
import SpinnerGIF from '../../../static/icons/spinner.gif';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -30,6 +37,7 @@ export default function ChatContent(): ReactElement {
|
||||
const messages = useMessagesByChatId(chatId);
|
||||
const chat = useChatId(chatId);
|
||||
const params = useParams<{ chatId: string }>();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (!chat) return;
|
||||
@@ -40,6 +48,10 @@ export default function ChatContent(): ReactElement {
|
||||
loadMore();
|
||||
}, [loadMore]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setLastReadForChatId(chatId));
|
||||
}, [chatId, messages]);
|
||||
|
||||
if (!chat) return <></>;
|
||||
|
||||
return (
|
||||
@@ -97,6 +109,7 @@ function ChatHeader(): ReactElement {
|
||||
}
|
||||
|
||||
function ChatEditor(): ReactElement {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { chatId } = useParams<{ chatId: string }>();
|
||||
const selected = useSelectedLocalId();
|
||||
const [content, setContent] = useState('');
|
||||
@@ -179,11 +192,11 @@ function ChatEditor(): ReactElement {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setSending(false);
|
||||
e.currentTarget.focus();
|
||||
textareaRef.current?.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
[submitMessage]
|
||||
[submitMessage, textareaRef]
|
||||
);
|
||||
|
||||
const onChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
@@ -199,6 +212,7 @@ function ChatEditor(): ReactElement {
|
||||
<div className="flex flex-row w-full">
|
||||
<div className="chat-content__editor ml-2">
|
||||
<Textarea
|
||||
_ref={textareaRef}
|
||||
key={chatId}
|
||||
className="text-light border mr-2 my-2"
|
||||
// ref={(el) => el?.focus()}
|
||||
|
||||
@@ -21,6 +21,7 @@ import chats, {
|
||||
useChatId,
|
||||
useChatIds,
|
||||
useLastNMessages,
|
||||
useUnreadChatMessages,
|
||||
zkchat,
|
||||
} from '../../ducks/chats';
|
||||
import Icon from '../Icon';
|
||||
@@ -35,9 +36,6 @@ import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
export default function ChatMenu(): ReactElement {
|
||||
const selected = useSelectedLocalId();
|
||||
const selecteduser = useUser(selected?.address);
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const chatIds = useChatIds();
|
||||
const [showingCreateChat, setShowingCreateChat] = useState(false);
|
||||
const [selectedNewConvo, selectNewConvo] = useState<Chat | null>(null);
|
||||
@@ -45,18 +43,6 @@ export default function ChatMenu(): ReactElement {
|
||||
const [searchResults, setSearchResults] = useState<Chat[] | null>(null);
|
||||
const params = useParams<{ chatId: string }>();
|
||||
|
||||
useEffect(() => {
|
||||
if (selecteduser?.ecdh && selected?.type === 'gun') {
|
||||
setTimeout(() => {
|
||||
dispatch(fetchChats(selecteduser.ecdh));
|
||||
}, 500);
|
||||
} else if (selected?.type === 'interrep' || selected?.type === 'taz') {
|
||||
setTimeout(() => {
|
||||
dispatch(fetchChats(selected.identityCommitment));
|
||||
}, 500);
|
||||
}
|
||||
}, [selected, selecteduser]);
|
||||
|
||||
const onSearchNewChatChange = useCallback(async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchParam(e.target.value);
|
||||
const res = await fetch(`${config.indexerAPI}/v1/zkchat/chats/search/${e.target.value}`);
|
||||
@@ -218,6 +204,7 @@ function ChatMenuItem(props: {
|
||||
const history = useHistory();
|
||||
const [last] = useLastNMessages(props.chatId, 1);
|
||||
const theme = useThemeContext();
|
||||
const unreads = useUnreadChatMessages(props.chatId);
|
||||
|
||||
const isSelected = props.chatId === params.chatId;
|
||||
|
||||
@@ -301,11 +288,18 @@ function ChatMenuItem(props: {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!props.hideLastChat && (
|
||||
<div className={classNames('flex-grow-0 flex-shrink-0 mt-1 text-gray-500')}>
|
||||
{last?.timestamp && <FromNow className="text-xs" timestamp={last.timestamp} />}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-end justify-center">
|
||||
{!!unreads && (
|
||||
<div className="flex flex-row items-center justify-center bg-red-500 text-white text-xs rounded-full w-4 h-4">
|
||||
{unreads}
|
||||
</div>
|
||||
)}
|
||||
{!props.hideLastChat && (
|
||||
<div className={classNames('flex-grow-0 flex-shrink-0 mt-1 text-gray-500')}>
|
||||
{last?.timestamp && <FromNow className="text-xs" timestamp={last.timestamp} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
51
src/components/ChatNavIcon/index.tsx
Normal file
51
src/components/ChatNavIcon/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
import { useUnreadChatMessagesAll } from '../../ducks/chats';
|
||||
|
||||
export default function ChatNavIcon(props: {
|
||||
fa: string;
|
||||
pathname: string;
|
||||
disabled?: boolean;
|
||||
isTop?: boolean;
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const count = useUnreadChatMessagesAll();
|
||||
|
||||
const iconClass = props.isTop ? 'top-nav__icon' : 'bottom-nav__icon';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
props.className,
|
||||
iconClass,
|
||||
{
|
||||
[`${iconClass}--selected`]: pathname === props.pathname,
|
||||
[`${iconClass}--disabled`]: props.disabled,
|
||||
}
|
||||
)}>
|
||||
<Icon
|
||||
className="relative"
|
||||
onClick={
|
||||
pathname !== props.pathname && !props.disabled
|
||||
? () => history.push(props.pathname)
|
||||
: undefined
|
||||
}
|
||||
fa={props.fa}
|
||||
size={1.125}>
|
||||
{count > 0 && (
|
||||
<div className="bg-red-500 text-white rounded-full text-xs notification-icon__counter">
|
||||
{count}
|
||||
</div>
|
||||
)}
|
||||
</Icon>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -58,6 +58,7 @@ export function UserRow(props: {
|
||||
name?: string;
|
||||
group?: string;
|
||||
onClick?: () => void;
|
||||
highlight?: boolean;
|
||||
}): ReactElement {
|
||||
const history = useHistory();
|
||||
const [username, setUsername] = useState('');
|
||||
@@ -95,6 +96,8 @@ export function UserRow(props: {
|
||||
{
|
||||
'hover:bg-gray-200': theme !== 'dark',
|
||||
'hover:bg-gray-800': theme === 'dark',
|
||||
'bg-gray-200': props.highlight && theme !== 'dark',
|
||||
'bg-gray-800': props.highlight && theme === 'dark',
|
||||
}
|
||||
)}
|
||||
onClick={props.onClick || onClick}>
|
||||
|
||||
61
src/components/GlobalSearchInput/global-search.scss
Normal file
61
src/components/GlobalSearchInput/global-search.scss
Normal file
@@ -0,0 +1,61 @@
|
||||
@import '../../util/variable';
|
||||
|
||||
.global-search {
|
||||
@extend %row-nowrap;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 1rem;
|
||||
padding: 0 1rem;
|
||||
|
||||
.input-group {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: $primary-color;
|
||||
|
||||
.icon {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
&--has-results {
|
||||
border-bottom: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&__results {
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
max-height: 22.5rem;
|
||||
left: 0;
|
||||
border: 1px solid $primary-color;
|
||||
border-top: 0;
|
||||
z-index: 300;
|
||||
}
|
||||
|
||||
&__result {
|
||||
&--selected {
|
||||
background-color: rgba($black, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.global-search {
|
||||
&__result {
|
||||
&--selected {
|
||||
background-color: rgba($white, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.global-search {
|
||||
width: 100%;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
138
src/components/GlobalSearchInput/index.tsx
Normal file
138
src/components/GlobalSearchInput/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { KeyboardEvent, ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import Input from '../Input';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
import './global-search.scss';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { useHistory } from 'react-router';
|
||||
import { searchUsers } from '../../ducks/users';
|
||||
import store from '../../store/configureAppStore';
|
||||
import { UserRow } from '../DiscoverUserPanel';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const debouncedSearchUsers = debounce(query => store.dispatch(searchUsers(query)), 100);
|
||||
|
||||
export default function GlobalSearchInput(props: Props): ReactElement {
|
||||
const theme = useThemeContext();
|
||||
const history = useHistory();
|
||||
const [query, setQuery] = useState(props.defaultValue || '');
|
||||
const [userResults, setUserResults] = useState<string[]>([]);
|
||||
const [focused, setFocus] = useState(false);
|
||||
const [selectedIndex, selectIndex] = useState(-1);
|
||||
|
||||
const search = useCallback(async () => {
|
||||
const list: any = await debouncedSearchUsers(query);
|
||||
setUserResults(list.map(({ address }: any) => address));
|
||||
}, [query]);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
setUserResults([]);
|
||||
setFocus(false);
|
||||
selectIndex(-1);
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const onFocus = useCallback(() => {
|
||||
setFocus(true);
|
||||
search();
|
||||
}, [search]);
|
||||
|
||||
const onChange = useCallback(async (e: any) => {
|
||||
setQuery(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onEnter = useCallback(
|
||||
async (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (selectedIndex <= 0) {
|
||||
history.push(`/search?q=${encodeURIComponent(query)}`);
|
||||
} else if (selectedIndex <= userResults.length) {
|
||||
history.push(`/${userResults[selectedIndex - 1]}/`);
|
||||
}
|
||||
}
|
||||
},
|
||||
[query, selectedIndex, userResults]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
const len = userResults.length;
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
selectIndex(Math.min(selectedIndex + 1, len));
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
selectIndex(Math.max(selectedIndex - 1, 0));
|
||||
break;
|
||||
}
|
||||
},
|
||||
[userResults, query, selectedIndex]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
if (!query) {
|
||||
setUserResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
search();
|
||||
})();
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<div className={classNames('flex flex-col relative', props.className)}>
|
||||
<div
|
||||
className={classNames('global-search', props.className, {
|
||||
'bg-gray-100 focus:bg-white': theme !== 'dark',
|
||||
'bg-gray-900 focus:bg-black': theme === 'dark',
|
||||
'global-search--has-results': !!query && focused,
|
||||
})}>
|
||||
<Icon fa="fas fa-search" className="text-gray-500" />
|
||||
<Input
|
||||
type="text"
|
||||
className="text-sm"
|
||||
placeholder="Search Zkitter"
|
||||
onChange={onChange}
|
||||
onKeyPress={onEnter}
|
||||
defaultValue={props.defaultValue}
|
||||
value={query}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
{!!query && focused && (
|
||||
<div
|
||||
className={classNames('absolute global-search__results', {
|
||||
'bg-white': theme !== 'dark',
|
||||
'bg-black': theme === 'dark',
|
||||
})}>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap px-4 py-2',
|
||||
'cursor-pointer items-center transition',
|
||||
{
|
||||
'global-search__result--selected': selectedIndex === 0,
|
||||
'hover:bg-gray-200': theme !== 'dark',
|
||||
'hover:bg-gray-800': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
onClick={() => history.push(`/search?q=${encodeURIComponent(query)}`)}>
|
||||
{`Search for "${query}"`}
|
||||
</div>
|
||||
{userResults.map((address, i) => (
|
||||
<UserRow key={address} name={address} highlight={selectedIndex === i + 1} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,12 +6,13 @@ import PostModerationPanel from '../PostModerationPanel';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
import GroupMembersPanel from '../GroupMembersPanel';
|
||||
import GlobalSearchInput from '../GlobalSearchInput';
|
||||
|
||||
export default function MetaPanel(props: { className?: string }): ReactElement {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/explore">
|
||||
<DefaultMetaPanels className={props.className} />
|
||||
<ExploreMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/:name/status/:hash">
|
||||
<PostMetaPanels className={props.className} />
|
||||
@@ -23,9 +24,11 @@ export default function MetaPanel(props: { className?: string }): ReactElement {
|
||||
<DefaultMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/home">
|
||||
<DefaultMetaPanels className={props.className} />
|
||||
<ExploreMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/notifications">
|
||||
<ExploreMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/notifications" />
|
||||
<Route path="/settings" />
|
||||
<Route path="/create-local-backup" />
|
||||
<Route path="/onboarding/interrep" />
|
||||
@@ -49,6 +52,17 @@ function DefaultMetaPanels(props: { className?: string }): ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
function ExploreMetaPanels(props: { className?: string }): ReactElement {
|
||||
return (
|
||||
<div className={classNames('app__meta-content', props.className)}>
|
||||
<GlobalSearchInput className="mt-2" />
|
||||
<DiscoverUserPanel key="discover-user" />
|
||||
<DiscoverTagPanel key="discover-tag" />
|
||||
<AppFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileMetaPanels(props: { className?: string }): ReactElement {
|
||||
return (
|
||||
<div className={classNames('app__meta-content', props.className)}>
|
||||
|
||||
52
src/components/NotificationIcon/index.tsx
Normal file
52
src/components/NotificationIcon/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
import './notification-icon.scss';
|
||||
import { useUnreadCounts } from '../../ducks/app';
|
||||
|
||||
export default function NotificationIcon(props: {
|
||||
fa: string;
|
||||
pathname: string;
|
||||
disabled?: boolean;
|
||||
isTop?: boolean;
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const count = useUnreadCounts();
|
||||
|
||||
const iconClass = props.isTop ? 'top-nav__icon' : 'bottom-nav__icon';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
props.className,
|
||||
iconClass,
|
||||
{
|
||||
[`${iconClass}--selected`]: pathname === props.pathname,
|
||||
[`${iconClass}--disabled`]: props.disabled,
|
||||
}
|
||||
)}>
|
||||
<Icon
|
||||
className="relative"
|
||||
onClick={
|
||||
pathname !== props.pathname && !props.disabled
|
||||
? () => history.push(props.pathname)
|
||||
: undefined
|
||||
}
|
||||
fa={props.fa}
|
||||
size={1.125}>
|
||||
{count > 0 && (
|
||||
<div className="bg-red-500 text-white rounded-full text-xs notification-icon__counter">
|
||||
{count}
|
||||
</div>
|
||||
)}
|
||||
</Icon>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
src/components/NotificationIcon/notification-icon.scss
Normal file
17
src/components/NotificationIcon/notification-icon.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
@import '../../util/variable';
|
||||
|
||||
.notification-icon {
|
||||
&__counter {
|
||||
position: absolute;
|
||||
top: -0.375rem;
|
||||
right: -0.375rem;
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { useHistory } from 'react-router';
|
||||
import { convertMarkdownToDraft, DraftEditor } from '../DraftEditor';
|
||||
import { fetchLikersByPost, useMeta, usePost, useZKGroupFromPost } from '../../ducks/posts';
|
||||
import { useUser } from '../../ducks/users';
|
||||
import { PostMessageSubType } from '../../util/message';
|
||||
import { MessageType, PostMessageSubType } from '../../util/message';
|
||||
import { getHandle, getUsername } from '../../util/user';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
@@ -61,6 +61,7 @@ export default function ExpandedPost(
|
||||
address={user?.address}
|
||||
incognito={post.creator === ''}
|
||||
group={zkGroup}
|
||||
twitterUsername={post.type === MessageType._TWEET ? post.creator : undefined}
|
||||
/>
|
||||
<div className="flex flex-col flex-nowrap items-start text-light w-full cursor-pointer">
|
||||
<div className="font-bold text-base mr-1 hover:underline" onClick={gotoUserProfile}>
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function RegularPost(
|
||||
<div>
|
||||
<Avatar
|
||||
className="mr-3 w-12 h-12"
|
||||
address={user?.username}
|
||||
address={user?.address}
|
||||
incognito={post.creator === ''}
|
||||
group={zkGroup}
|
||||
twitterUsername={post.type === MessageType._TWEET ? post.creator : undefined}
|
||||
|
||||
@@ -4,7 +4,7 @@ import './textarea.scss';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
type Props = {
|
||||
ref?: LegacyRef<HTMLTextAreaElement>;
|
||||
_ref?: LegacyRef<HTMLTextAreaElement>;
|
||||
label?: string;
|
||||
errorMessage?: string;
|
||||
} & TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
@@ -23,7 +23,7 @@ export default function Textarea(props: Props): ReactElement {
|
||||
'focus-within:border-gray-600 border-gray-800': !textareaProps.readOnly && theme === 'dark',
|
||||
})}>
|
||||
{label && <div className="textarea-group__label">{label}</div>}
|
||||
<textarea ref={props.ref} {...textareaProps} />
|
||||
<textarea ref={props._ref} {...textareaProps} />
|
||||
{errorMessage && <small className="error-message">{errorMessage}</small>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,9 @@ import Modal from '../Modal';
|
||||
import MetaPanel from '../MetaPanel';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
import config from '../../util/config';
|
||||
import NotificationIcon from '../NotificationIcon';
|
||||
import ChatNavIcon from '../ChatNavIcon';
|
||||
import GlobalSearchInput from '../GlobalSearchInput';
|
||||
|
||||
export default function TopNav(): ReactElement {
|
||||
const theme = useThemeContext();
|
||||
@@ -37,6 +40,7 @@ export default function TopNav(): ReactElement {
|
||||
)}>
|
||||
<div className={classNames('flex flex-row flex-nowrap items-center flex-grow flex-shrink-0')}>
|
||||
<Switch>
|
||||
<Route path="/search" component={SearchHeaderGroup} />
|
||||
<Route path="/explore" component={GlobalHeaderGroup} />
|
||||
<Route path="/home" component={GlobalHeaderGroup} />
|
||||
<Route path="/tag/:tagName" component={TagHeaderGroup} />
|
||||
@@ -46,7 +50,7 @@ export default function TopNav(): ReactElement {
|
||||
<Route path="/onboarding/interrep" component={DefaultHeaderGroup} />
|
||||
<Route path="/connect/twitter" component={DefaultHeaderGroup} />
|
||||
<Route path="/signup" component={DefaultHeaderGroup} />
|
||||
<Route path="/notification" component={DefaultHeaderGroup} />
|
||||
<Route path="/notifications" component={DefaultHeaderGroup} />
|
||||
<Route path="/chat/:chatId?" component={ChatHeaderGroup} />
|
||||
<Route path="/settings" component={SettingHeaderGroup} />
|
||||
<Route path="/:name" component={UserProfileHeaderGroup} />
|
||||
@@ -101,9 +105,9 @@ function NavIconRow() {
|
||||
}
|
||||
)}>
|
||||
<TopNavIcon fa="fas fa-home" pathname="/home" disabled={!loggedIn} />
|
||||
<TopNavIcon fa="fas fa-envelope" pathname={`/chat`} disabled={!selectedLocalId} />
|
||||
<ChatNavIcon isTop fa="fas fa-envelope" pathname="/chat" disabled={!selectedLocalId} />
|
||||
<TopNavIcon fa="fas fa-globe-asia" pathname="/explore" />
|
||||
{/*<TopNavIcon fa="fas fa-bell" pathname="/notifications" />*/}
|
||||
<NotificationIcon isTop fa="fas fa-bell" pathname="/notifications" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -138,6 +142,50 @@ function DefaultHeaderGroup() {
|
||||
);
|
||||
}
|
||||
|
||||
function SearchHeaderGroup() {
|
||||
const loggedIn = useGunLoggedIn();
|
||||
const account = useAccount();
|
||||
const selectedLocalId = useSelectedLocalId();
|
||||
const history = useHistory();
|
||||
const [ensName, setEnsName] = useState('');
|
||||
const params = new URLSearchParams(location.search);
|
||||
const query = params.get('q') || undefined;
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
if (history.action !== 'POP') return history.goBack();
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
|
||||
let address = '';
|
||||
|
||||
if (loggedIn) {
|
||||
address = selectedLocalId?.address || account;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const ens = await fetchNameByAddress(address);
|
||||
setEnsName(ens);
|
||||
})();
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap flex-grow items-center flex-shrink-0',
|
||||
'p-1 mx-4'
|
||||
)}>
|
||||
<Icon
|
||||
className="w-8 h-8 flex flex-row items-center justify-center top-nav__back-icon"
|
||||
fa="fas fa-chevron-left"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<GlobalSearchInput defaultValue={query} />
|
||||
<TopNavContextButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GlobalHeaderGroup() {
|
||||
const loggedIn = useGunLoggedIn();
|
||||
const account = useAccount();
|
||||
|
||||
@@ -37,12 +37,21 @@ export enum Item {
|
||||
const useItem = (item: Item, id: string) => {
|
||||
if (item === Item.Like) return { fetch: fetchLikersByPost, count: useMeta(id).likeCount };
|
||||
if (item === Item.Follower)
|
||||
return { fetch: fetchUserFollowers, count: useUser(id)?.meta.followerCount };
|
||||
return {
|
||||
fetch: fetchUserFollowers,
|
||||
count: useUser(id)?.meta?.followerCount || 0,
|
||||
};
|
||||
|
||||
if (item === Item.Following)
|
||||
return { fetch: fetchUserFollowings, count: useUser(id)?.meta.followingCount };
|
||||
return {
|
||||
fetch: fetchUserFollowings,
|
||||
count: useUser(id)?.meta?.followingCount || 0,
|
||||
};
|
||||
|
||||
return { fetch: fetchRetweetsByPost, count: useMeta(id).repostCount };
|
||||
return {
|
||||
fetch: fetchRetweetsByPost,
|
||||
count: useMeta(id).repostCount,
|
||||
};
|
||||
};
|
||||
|
||||
function MaybePlural(props: { users: string[]; text: Item }): ReactElement {
|
||||
|
||||
@@ -388,7 +388,7 @@ function CurrentUserItem(props: {
|
||||
const gotoProfile = useCallback(() => {
|
||||
if (!selectedUser) return;
|
||||
const { ens, name, address } = selectedUser;
|
||||
history.push(`/${ens || address}`);
|
||||
history.push(`/${ens || address}/`);
|
||||
props.closePopup();
|
||||
}, [selectedUser]);
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppRootState } from '../store/configureAppStore';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import config from '../util/config';
|
||||
|
||||
const THEME_LS_KEY = 'theme';
|
||||
|
||||
enum ActionTypes {
|
||||
SET_THEME = 'app/setTheme',
|
||||
UPDATE_NOTIFICATIONS = 'app/updateNotifications',
|
||||
UPDATE_LAST_READ = 'app/updateLastRead',
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
@@ -17,6 +20,7 @@ type Action<payload> = {
|
||||
|
||||
type State = {
|
||||
theme: string;
|
||||
notifications: number;
|
||||
};
|
||||
|
||||
const getTheme = () => {
|
||||
@@ -26,8 +30,9 @@ const getTheme = () => {
|
||||
return 'light';
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
const initialState: State = {
|
||||
theme: getTheme(),
|
||||
notifications: 0,
|
||||
};
|
||||
|
||||
export const setTheme = (theme: 'dark' | 'light') => {
|
||||
@@ -38,6 +43,49 @@ export const setTheme = (theme: 'dark' | 'light') => {
|
||||
};
|
||||
};
|
||||
|
||||
export const updateLastReadTimestamp =
|
||||
() => async (dispatch: any, getState: () => AppRootState) => {
|
||||
const {
|
||||
worker: { selected },
|
||||
} = getState();
|
||||
if (selected?.type !== 'gun') return;
|
||||
const { address } = selected;
|
||||
const res = await fetch(`${config.indexerAPI}/v1/lastread/${address}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
lastread: Date.now(),
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.payload);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.UPDATE_NOTIFICATIONS,
|
||||
payload: 0,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateNotifications = () => async (dispatch: any, getState: () => AppRootState) => {
|
||||
const {
|
||||
worker: { selected },
|
||||
} = getState();
|
||||
if (selected?.type !== 'gun') return;
|
||||
const { address } = selected;
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/${address}/notifications/unread`);
|
||||
const json = await resp.json();
|
||||
|
||||
if (json.error || !json.payload?.TOTAL) return 0;
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.UPDATE_NOTIFICATIONS,
|
||||
payload: json.payload.TOTAL || 0,
|
||||
});
|
||||
};
|
||||
|
||||
export default function app(state = initialState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_THEME:
|
||||
@@ -45,6 +93,11 @@ export default function app(state = initialState, action: Action<any>): State {
|
||||
...state,
|
||||
theme: action.payload,
|
||||
};
|
||||
case ActionTypes.UPDATE_NOTIFICATIONS:
|
||||
return {
|
||||
...state,
|
||||
notifications: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -57,3 +110,9 @@ export const useSetting = () => {
|
||||
};
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useUnreadCounts = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.app.notifications;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,16 @@ export const zkchat = new ZKChatClient({
|
||||
sse.on('NEW_CHAT_MESSAGE', async (payload: any) => {
|
||||
const message = await zkchat.inflateMessage(payload);
|
||||
zkchat.prependMessage(message);
|
||||
const chat: Chat = {
|
||||
type: 'DIRECT',
|
||||
receiver: '',
|
||||
receiverECDH: message.receiver.ecdh!,
|
||||
senderECDH: message.sender.ecdh!,
|
||||
senderHash: message.sender.hash,
|
||||
};
|
||||
const chatId = zkchat.deriveChatId(chat);
|
||||
// @ts-ignore
|
||||
store.dispatch(incrementUnreadForChatId(chatId));
|
||||
});
|
||||
|
||||
const onNewMessage = (message: ChatMessage) => {
|
||||
@@ -33,6 +43,9 @@ zkchat.on(EVENTS.MESSAGE_PREPENDED, onNewMessage);
|
||||
zkchat.on(EVENTS.CHAT_CREATED, (chat: Chat) => {
|
||||
store.dispatch(addChat(chat));
|
||||
});
|
||||
zkchat.on(EVENTS.IDENTITY_CHANGED, () => {
|
||||
store.dispatch(setChats(zkchat.activeChats));
|
||||
});
|
||||
|
||||
enum ActionTypes {
|
||||
SET_CHATS = 'chats/setChats',
|
||||
@@ -40,11 +53,12 @@ enum ActionTypes {
|
||||
ADD_CHAT = 'chats/addChat',
|
||||
SET_CHAT_NICKNAME = 'chats/setChatNickname',
|
||||
SET_MESSAGE = 'chats/SET_MESSAGE',
|
||||
SET_UNREAD = 'chats/SET_UNREAD',
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionTypes;
|
||||
payload?: payload;
|
||||
payload: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
};
|
||||
@@ -56,6 +70,9 @@ type State = {
|
||||
[chatId: string]: InflatedChat;
|
||||
};
|
||||
};
|
||||
unreads: {
|
||||
[chatId: string]: number;
|
||||
};
|
||||
messages: {
|
||||
[messageId: string]: ChatMessage;
|
||||
};
|
||||
@@ -67,6 +84,7 @@ export type InflatedChat = Chat & {
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
unreads: {},
|
||||
chats: {
|
||||
order: [],
|
||||
map: {},
|
||||
@@ -104,6 +122,14 @@ const setMessage = (msg: ChatMessage): Action<ChatMessage> => ({
|
||||
payload: msg,
|
||||
});
|
||||
|
||||
const setUnread = (
|
||||
chatId: string,
|
||||
unreads: number
|
||||
): Action<{ chatId: string; unreads: number }> => ({
|
||||
type: ActionTypes.SET_UNREAD,
|
||||
payload: { chatId, unreads },
|
||||
});
|
||||
|
||||
const setMessagesForChat = (
|
||||
chatId: string,
|
||||
messages: string[]
|
||||
@@ -112,10 +138,62 @@ const setMessagesForChat = (
|
||||
payload: { chatId, messages },
|
||||
});
|
||||
|
||||
export const fetchChats =
|
||||
(address: string) => async (dispatch: Dispatch, getState: () => AppRootState) => {
|
||||
await zkchat.fetchActiveChats(address);
|
||||
dispatch(setChats(zkchat.activeChats));
|
||||
export const setLastReadForChatId =
|
||||
(chatId: string) => async (dispatch: Dispatch, getState: () => AppRootState) => {
|
||||
const {
|
||||
chats: {
|
||||
chats: { map },
|
||||
},
|
||||
} = getState();
|
||||
|
||||
const chat = map[chatId];
|
||||
|
||||
if (!zkchat.identity || !chat) return;
|
||||
|
||||
const res = await fetch(
|
||||
`${config.indexerAPI}/v1/lastread/${zkchat.identity.ecdh.pub}/${chat.receiverECDH}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
lastread: Date.now(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.payload);
|
||||
}
|
||||
|
||||
dispatch(setUnread(chatId, 0));
|
||||
};
|
||||
|
||||
export const fetchChats = (address: string) => async (dispatch: Dispatch) => {
|
||||
await zkchat.fetchActiveChats(address);
|
||||
dispatch(setChats(zkchat.activeChats));
|
||||
};
|
||||
|
||||
export const fetchUnreads = () => async (dispatch: Dispatch, getState: () => AppRootState) => {
|
||||
for (const chat of Object.values(zkchat.activeChats)) {
|
||||
const { senderECDH, receiverECDH } = chat || {};
|
||||
const chatId = zkchat.deriveChatId(chat);
|
||||
const resp = await fetch(
|
||||
`${config.indexerAPI}/v1/zkchat/chat-messages/dm/${receiverECDH}/${senderECDH}/unread`
|
||||
);
|
||||
const json = await resp.json();
|
||||
if (!json.error) {
|
||||
dispatch(setUnread(chatId, json.payload));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const incrementUnreadForChatId =
|
||||
(chatId: string) => async (dispatch: Dispatch, getState: () => AppRootState) => {
|
||||
const {
|
||||
chats: { unreads },
|
||||
} = getState();
|
||||
dispatch(setUnread(chatId, (unreads[chatId] || 0) + 1));
|
||||
};
|
||||
|
||||
export default function chats(state = initialState, action: Action<any>): State {
|
||||
@@ -128,6 +206,8 @@ export default function chats(state = initialState, action: Action<any>): State
|
||||
return handleSetMessagesForChats(state, action);
|
||||
case ActionTypes.SET_CHAT_NICKNAME:
|
||||
return handeSetNickname(state, action);
|
||||
case ActionTypes.SET_UNREAD:
|
||||
return handleSetUnread(state, action);
|
||||
case ActionTypes.SET_MESSAGE:
|
||||
return {
|
||||
...state,
|
||||
@@ -244,9 +324,31 @@ function handleSetMessagesForChats(
|
||||
};
|
||||
}
|
||||
|
||||
function handleSetUnread(state: State, action: Action<{ chatId: string; unreads: number }>): State {
|
||||
const { chatId, unreads } = action.payload;
|
||||
const { chats } = state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
chats: {
|
||||
...chats,
|
||||
order: unreads ? [chatId].concat(chats.order.filter(id => id !== chatId)) : chats.order,
|
||||
},
|
||||
unreads: {
|
||||
...state.unreads,
|
||||
[chatId]: unreads,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const useChatIds = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.chats.chats.order;
|
||||
const {
|
||||
chats: {
|
||||
chats: { order },
|
||||
},
|
||||
} = state;
|
||||
return order;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
@@ -286,3 +388,19 @@ export const useChatMessage = (messageId: string) => {
|
||||
return state.chats.messages[messageId];
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useUnreadChatMessagesAll = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
const {
|
||||
chats: { order },
|
||||
unreads,
|
||||
} = state.chats;
|
||||
return order.reduce((sum, id) => (sum += unreads[id] || 0), 0);
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useUnreadChatMessages = (chatId: string) => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.chats.unreads[chatId] || 0;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,8 @@ import config from '../util/config';
|
||||
import { Dispatch } from 'redux';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useCallback } from 'react';
|
||||
import { EditorState } from 'draft-js';
|
||||
import { convertMarkdownToDraft } from '../components/DraftEditor';
|
||||
|
||||
enum ActionTypes {
|
||||
SET_POSTS = 'posts/setPosts',
|
||||
@@ -205,6 +207,16 @@ export const fetchPosts =
|
||||
return json.payload.map((post: any) => post.messageId);
|
||||
};
|
||||
|
||||
export const fetchNotifications =
|
||||
(address: string, limit = 10, offset = 0) =>
|
||||
async () => {
|
||||
const resp = await fetch(
|
||||
`${config.indexerAPI}/v1/${address}/notifications?limit=${limit}&offset=${offset}`
|
||||
);
|
||||
const json = await resp.json();
|
||||
return json.payload;
|
||||
};
|
||||
|
||||
export const fetchLikedBy =
|
||||
(creator?: string, limit = 10, offset = 0) =>
|
||||
async (dispatch: ThunkDispatch<any, any, any>, getState: () => AppRootState) => {
|
||||
@@ -343,6 +355,21 @@ const processPosts = (posts: any[]) => async (dispatch: Dispatch) => {
|
||||
}, 0);
|
||||
};
|
||||
|
||||
export const searchPosts =
|
||||
(query: string, limit = 20, offset = 0) =>
|
||||
async (dispatch: ThunkDispatch<any, any, any>, getState: () => AppRootState) => {
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/posts/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, limit, offset }),
|
||||
});
|
||||
const json = await resp.json();
|
||||
|
||||
if (json.error) throw new Error(json.payload);
|
||||
|
||||
return json.payload.map((p: any) => p.messageId);
|
||||
};
|
||||
|
||||
export const fetchHomeFeed =
|
||||
(limit = 10, offset = 0) =>
|
||||
async (dispatch: ThunkDispatch<any, any, any>, getState: () => AppRootState) => {
|
||||
@@ -501,6 +528,16 @@ export const usePost = (messageId?: string): Post | null => {
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const usePostContent = (messageId?: string): string => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
const post = state.posts.map[messageId || ''];
|
||||
|
||||
if (!post) return '';
|
||||
|
||||
return post.payload.content.slice(0, 512);
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useMeta = (messageId = '') => {
|
||||
return useSelector((state: AppRootState): PostMeta => {
|
||||
return (
|
||||
@@ -518,7 +555,7 @@ export const useZKGroupFromPost = (messageId?: string) => {
|
||||
return useSelector((state: AppRootState): string | undefined => {
|
||||
if (!messageId) return;
|
||||
const post = state.posts.meta[messageId];
|
||||
if (!post) return undefined;
|
||||
if (!post?.interepProvider) return undefined;
|
||||
|
||||
return post.interepProvider === 'taz'
|
||||
? 'semaphore_taz_members'
|
||||
|
||||
@@ -19,7 +19,7 @@ import { connectZKPR } from '../../ducks/zkpr';
|
||||
import SettingView from '../SettingView';
|
||||
import MetaPanel from '../../components/MetaPanel';
|
||||
import ChatView from '../ChatView';
|
||||
import { zkchat } from '../../ducks/chats';
|
||||
import { fetchUnreads, zkchat } from '../../ducks/chats';
|
||||
import {
|
||||
generateECDHKeyPairFromhex,
|
||||
generateZkIdentityFromHex,
|
||||
@@ -32,6 +32,9 @@ import ThemeContext from '../../components/ThemeContext';
|
||||
import classNames from 'classnames';
|
||||
import { Identity } from '@semaphore-protocol/identity';
|
||||
import TazModal from '../../components/TazModal';
|
||||
import NotificationView from '../NotificationView';
|
||||
import { updateNotifications } from '../../ducks/app';
|
||||
import SearchResultsView from '../SearchResultsView';
|
||||
|
||||
export default function App(): ReactElement {
|
||||
const dispatch = useDispatch();
|
||||
@@ -94,6 +97,8 @@ export default function App(): ReactElement {
|
||||
zk: zkIdentity,
|
||||
ecdh: keyPair,
|
||||
});
|
||||
await dispatch(fetchUnreads());
|
||||
await dispatch(updateNotifications());
|
||||
})();
|
||||
} else if (selected?.type === 'interrep') {
|
||||
(async () => {
|
||||
@@ -111,6 +116,7 @@ export default function App(): ReactElement {
|
||||
zk: zkIdentity,
|
||||
ecdh: keyPair,
|
||||
});
|
||||
await dispatch(fetchUnreads());
|
||||
})();
|
||||
} else if (selected?.type === 'taz') {
|
||||
(async () => {
|
||||
@@ -123,6 +129,7 @@ export default function App(): ReactElement {
|
||||
zk: zkIdentity,
|
||||
ecdh: keyPair,
|
||||
});
|
||||
await dispatch(fetchUnreads());
|
||||
})();
|
||||
}
|
||||
}, [selected]);
|
||||
@@ -167,7 +174,9 @@ export default function App(): ReactElement {
|
||||
<AuthRoute path="/home">
|
||||
<HomeFeed />
|
||||
</AuthRoute>
|
||||
<Route path="/notifications" />
|
||||
<Route path="/notifications">
|
||||
<NotificationView />
|
||||
</Route>
|
||||
<Route path="/create-local-backup">
|
||||
<SignupView viewType={ViewType.localBackup} />
|
||||
</Route>
|
||||
@@ -192,6 +201,9 @@ export default function App(): ReactElement {
|
||||
<Route path="/taz">
|
||||
<GlobalFeed />
|
||||
</Route>
|
||||
<Route path="/search">
|
||||
<SearchResultsView />
|
||||
</Route>
|
||||
<Route path="/:name">
|
||||
<ProfileView />
|
||||
</Route>
|
||||
|
||||
188
src/pages/NotificationView/index.tsx
Normal file
188
src/pages/NotificationView/index.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useSelectedLocalId } from '../../ducks/worker';
|
||||
import { useHistory } from 'react-router';
|
||||
import { parseMessageId, PostMessageSubType } from '../../util/message';
|
||||
import { getUser, useUser } from '../../ducks/users';
|
||||
import { useThemeContext } from '../../components/ThemeContext';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import './notification.scss';
|
||||
import Avatar from '../../components/Avatar';
|
||||
import { getName } from '../../util/user';
|
||||
import Icon from '../../components/Icon';
|
||||
import Post from '../../components/Post';
|
||||
import {
|
||||
fetchMeta,
|
||||
fetchNotifications,
|
||||
fetchPost,
|
||||
fetchPosts,
|
||||
useGoToPost,
|
||||
usePost,
|
||||
usePostContent,
|
||||
} from '../../ducks/posts';
|
||||
import InfiniteScrollable from '../../components/InfiniteScrollable';
|
||||
import { updateLastReadTimestamp } from '../../ducks/app';
|
||||
import { NotificationType } from '../../util/notifications';
|
||||
|
||||
export default function NotificationView(): ReactElement {
|
||||
const [limit, setLimit] = useState(20);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const selected = useSelectedLocalId();
|
||||
const selectedUser = useUser(selected?.address);
|
||||
const dispatch = useDispatch();
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const theme = useThemeContext();
|
||||
|
||||
const fetchMore = useCallback(
|
||||
async (reset = false) => {
|
||||
if (!selected?.address) return;
|
||||
|
||||
if (reset) {
|
||||
const payload: any = await dispatch(fetchNotifications(selected.address, 20, 0));
|
||||
setOffset(20);
|
||||
setNotifications(payload);
|
||||
} else {
|
||||
if (notifications.length % limit) return;
|
||||
const payload: any = await dispatch(fetchNotifications(selected.address, limit, offset));
|
||||
setOffset(offset + limit);
|
||||
setNotifications(notifications.concat(payload));
|
||||
}
|
||||
},
|
||||
[limit, offset, selected?.address, notifications]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
if (selected?.type === 'gun') {
|
||||
fetchMore(true);
|
||||
dispatch(updateLastReadTimestamp());
|
||||
} else {
|
||||
setNotifications([]);
|
||||
setOffset(0);
|
||||
}
|
||||
})();
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected?.address && !selectedUser?.ecdh) {
|
||||
dispatch(getUser(selected.address));
|
||||
}
|
||||
}, [selectedUser?.ecdh, selected?.address]);
|
||||
|
||||
return (
|
||||
<InfiniteScrollable
|
||||
className={classNames('notifications', 'border-l border-r', 'mx-4 py-2', {
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}
|
||||
bottomOffset={128}
|
||||
onScrolledToBottom={fetchMore}>
|
||||
{notifications.map((data: any) => {
|
||||
const { message_id, type } = data;
|
||||
|
||||
switch (type) {
|
||||
case NotificationType.DIRECT:
|
||||
return null;
|
||||
case NotificationType.LIKE:
|
||||
case NotificationType.REPOST:
|
||||
case NotificationType.MEMBER_INVITE:
|
||||
case NotificationType.MEMBER_ACCEPT:
|
||||
return <IncomingReactionRow type={type} messageId={message_id} />;
|
||||
case NotificationType.REPLY:
|
||||
case NotificationType.MENTION:
|
||||
return <IncomingReplyRow messageId={message_id} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</InfiniteScrollable>
|
||||
);
|
||||
}
|
||||
|
||||
function IncomingReactionRow(props: {
|
||||
messageId: string;
|
||||
type: 'LIKE' | 'REPOST' | 'MEMBER_INVITE' | 'MEMBER_ACCEPT';
|
||||
}): ReactElement {
|
||||
const { creator, hash } = parseMessageId(props.messageId);
|
||||
const user = useUser(creator);
|
||||
const history = useHistory();
|
||||
const originalPost = usePost(props.messageId);
|
||||
const referencedPost = usePost(originalPost?.payload.reference);
|
||||
const messageId =
|
||||
originalPost?.subtype === PostMessageSubType.Repost
|
||||
? originalPost.payload.reference
|
||||
: props.messageId;
|
||||
const post = originalPost?.subtype === PostMessageSubType.Repost ? referencedPost : originalPost;
|
||||
const dispatch = useDispatch();
|
||||
const content = usePostContent(messageId);
|
||||
|
||||
const goto = useCallback(() => {
|
||||
if (props.type === 'MEMBER_INVITE' || props.type === 'MEMBER_ACCEPT') {
|
||||
history.push(`/${creator}`);
|
||||
} else {
|
||||
history.push(`/${creator}/status/${hash}`);
|
||||
}
|
||||
}, [creator, hash]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!post) {
|
||||
dispatch(fetchPost(messageId));
|
||||
dispatch(fetchMeta(messageId));
|
||||
}
|
||||
}, [messageId, post]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row cursor-pointer notification-row px-4 py-3" onClick={goto}>
|
||||
<Icon
|
||||
className="flex flex-row mr-4 notification-row__icon"
|
||||
fa={classNames({
|
||||
'fas fa-heart text-red-500 ': props.type === 'LIKE',
|
||||
'fas fa-retweet text-green-500': props.type === 'REPOST',
|
||||
'fas fa-user-plus text-blue-500': props.type === 'MEMBER_INVITE',
|
||||
'fas fa-user-check text-blue-500': props.type === 'MEMBER_ACCEPT',
|
||||
})}
|
||||
size={props.type === 'MEMBER_INVITE' || props.type === 'MEMBER_ACCEPT' ? 1.6125 : 2}
|
||||
/>
|
||||
<div className="flex flex-col items-start">
|
||||
<Avatar className="w-8 h-8" address={creator} incognito={!creator} />
|
||||
{props.type === 'MEMBER_INVITE' && (
|
||||
<div className="mt-2 text-sm">
|
||||
<span className="mr-1">You received an invitation to join</span>
|
||||
<span className="font-semibold">{getName(user)}</span>
|
||||
</div>
|
||||
)}
|
||||
{props.type === 'MEMBER_ACCEPT' && (
|
||||
<div className="mt-2 text-sm">
|
||||
<span className="font-semibold">{getName(user)}</span>
|
||||
<span className="ml-1">accepted your invitation</span>
|
||||
</div>
|
||||
)}
|
||||
{props.type === 'LIKE' && (
|
||||
<div className="mt-2 text-sm">
|
||||
<span className="font-semibold">{creator ? getName(user) : 'Someone'}</span>
|
||||
<span className="ml-1">liked your post</span>
|
||||
</div>
|
||||
)}
|
||||
{props.type === 'REPOST' && (
|
||||
<div className="mt-2 text-sm">
|
||||
<span className="font-semibold">{creator ? getName(user) : 'Someone'}</span>
|
||||
<span className="ml-1">shared your post</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-gray-500 mt-2">{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IncomingReplyRow(props: { messageId: string }): ReactElement {
|
||||
const gotoPost = useGoToPost();
|
||||
|
||||
return (
|
||||
<Post
|
||||
className="notification-row cursor-pointer"
|
||||
messageId={props.messageId}
|
||||
onClick={() => gotoPost(props.messageId)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
35
src/pages/NotificationView/notification.scss
Normal file
35
src/pages/NotificationView/notification.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
@import '../../util/variable';
|
||||
|
||||
.notifications {
|
||||
width: 36.25rem;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0; /* Remove scrollbar space */
|
||||
background: transparent; /* Optional: just make scrollbar invisible */
|
||||
}
|
||||
}
|
||||
|
||||
.notification-row {
|
||||
//padding: 1rem 2rem;
|
||||
|
||||
&__icon {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-row {
|
||||
border-bottom: 1px solid $gray-100;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.notification-row {
|
||||
border-bottom: 1px solid $gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.notifications {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
72
src/pages/SearchResultsView/index.tsx
Normal file
72
src/pages/SearchResultsView/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import InfiniteScrollable from '../../components/InfiniteScrollable';
|
||||
import { useThemeContext } from '../../components/ThemeContext';
|
||||
import { useLocation } from 'react-router';
|
||||
import './search-view.scss';
|
||||
import { searchPosts, useGoToPost } from '../../ducks/posts';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Post from '../../components/Post';
|
||||
|
||||
export default function SearchResultsView(): ReactElement {
|
||||
const [limit, setLimit] = useState(20);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [order, setOrder] = useState<string[]>([]);
|
||||
const theme = useThemeContext();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const gotoPost = useGoToPost();
|
||||
const params = new URLSearchParams(location.search);
|
||||
const query = params.get('q');
|
||||
|
||||
const fetchMore = useCallback(
|
||||
async (reset = false) => {
|
||||
if (!query) return;
|
||||
|
||||
if (reset) {
|
||||
const messageIds: any = await dispatch(searchPosts(query, 20, 0));
|
||||
console.log({ messageIds });
|
||||
setOffset(20);
|
||||
setOrder(messageIds);
|
||||
} else {
|
||||
if (order.length % limit) return;
|
||||
const messageIds: any = await dispatch(searchPosts(query, limit, offset));
|
||||
setOffset(offset + limit);
|
||||
setOrder(order.concat(messageIds));
|
||||
}
|
||||
},
|
||||
[limit, offset, order, query]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMore(true);
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<InfiniteScrollable
|
||||
className={classNames('search-view', 'mx-4 py-2', {
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}
|
||||
bottomOffset={128}
|
||||
onScrolledToBottom={fetchMore}>
|
||||
{!order.length && (
|
||||
<div className="text-sm text-gray-500 text-center mt-2">No matching results</div>
|
||||
)}
|
||||
{order.map((messageId, i) => {
|
||||
return (
|
||||
<Post
|
||||
key={messageId}
|
||||
// key={i}
|
||||
className={classNames('rounded-xl transition-colors mb-1 cursor-pointer border', {
|
||||
'hover:border-gray-300 border-gray-200': theme !== 'dark',
|
||||
'hover:border-gray-700 border-gray-800': theme === 'dark',
|
||||
})}
|
||||
messageId={messageId}
|
||||
onClick={() => gotoPost(messageId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</InfiniteScrollable>
|
||||
);
|
||||
}
|
||||
17
src/pages/SearchResultsView/search-view.scss
Normal file
17
src/pages/SearchResultsView/search-view.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
@import '../../util/variable';
|
||||
|
||||
.search-view {
|
||||
width: 36.25rem;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0; /* Remove scrollbar space */
|
||||
background: transparent; /* Optional: just make scrollbar invisible */
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.search-view {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ test('store - should initialize', async () => {
|
||||
mods: { posts: {} },
|
||||
app: {
|
||||
theme: 'dark',
|
||||
notifications: 0,
|
||||
},
|
||||
chats: {
|
||||
chats: {
|
||||
@@ -39,6 +40,7 @@ test('store - should initialize', async () => {
|
||||
order: [],
|
||||
},
|
||||
messages: {},
|
||||
unreads: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
9
src/util/notifications.ts
Normal file
9
src/util/notifications.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum NotificationType {
|
||||
DIRECT = 'DIRECT',
|
||||
LIKE = 'LIKE',
|
||||
REPOST = 'REPOST',
|
||||
MEMBER_INVITE = 'MEMBER_INVITE',
|
||||
MEMBER_ACCEPT = 'MEMBER_ACCEPT',
|
||||
REPLY = 'REPLY',
|
||||
MENTION = 'MENTION',
|
||||
}
|
||||
@@ -47,16 +47,16 @@ class SSE extends EventEmitter2 {
|
||||
}
|
||||
});
|
||||
|
||||
this.updateTopics(topics);
|
||||
this.updateTopics([...new Set(topics)]);
|
||||
|
||||
for (const chatId in zkchat.activeChats) {
|
||||
const chat = zkchat.activeChats[chatId];
|
||||
if (chat.type === 'DIRECT') {
|
||||
if (chat.senderHash && chat.senderECDH) {
|
||||
await this.updateTopics([`ecdh:${chat.senderECDH}`]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// for (const chatId in zkchat.activeChats) {
|
||||
// const chat = zkchat.activeChats[chatId];
|
||||
// if (chat.type === 'DIRECT') {
|
||||
// if (chat.senderHash && chat.senderECDH) {
|
||||
// await this.updateTopics([`ecdh:${chat.senderECDH}`]);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if (this.pulse) {
|
||||
clearInterval(this.pulse);
|
||||
@@ -79,6 +79,9 @@ class SSE extends EventEmitter2 {
|
||||
case 'NEW_CHAT_MESSAGE':
|
||||
this.emit(data.type, data.message);
|
||||
return;
|
||||
case 'UPDATE_UNREAD':
|
||||
this.emit(data.type, data.message);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ export const defaultENS = new ENS({
|
||||
});
|
||||
|
||||
const cachedName: any = {};
|
||||
const fetchPromises: any = {};
|
||||
|
||||
export const fetchNameByAddress = async (address: string) => {
|
||||
if (typeof cachedName[address] !== 'undefined') {
|
||||
return cachedName[address];
|
||||
@@ -33,11 +35,21 @@ export const fetchAddressByName = async (ens: string) => {
|
||||
if (typeof cachedName[ens] !== 'undefined') {
|
||||
return cachedName[ens];
|
||||
}
|
||||
const address = await defaultENS.name(ens).getAddress();
|
||||
|
||||
if (address) {
|
||||
cachedName[ens] = address || null;
|
||||
if (fetchPromises[ens]) {
|
||||
return fetchPromises[ens];
|
||||
}
|
||||
|
||||
return address;
|
||||
const fetchPromise = new Promise(async resolve => {
|
||||
const address = await defaultENS.name(ens).getAddress();
|
||||
if (address) {
|
||||
cachedName[ens] = address || null;
|
||||
}
|
||||
delete fetchPromises[ens];
|
||||
resolve(address);
|
||||
});
|
||||
|
||||
fetchPromises[ens] = fetchPromise;
|
||||
|
||||
return fetchPromise;
|
||||
};
|
||||
|
||||
@@ -94,6 +94,7 @@ export type Chat =
|
||||
| {
|
||||
type: 'PUBLIC_ROOM';
|
||||
receiver: string;
|
||||
receiverECDH?: string;
|
||||
senderECDH: string;
|
||||
senderHash?: string;
|
||||
};
|
||||
@@ -241,6 +242,7 @@ export class ZKChatClient extends EventEmitter2 {
|
||||
MESSAGE_PREPENDED: 'MESSAGE_PREPENDED',
|
||||
MESSAGE_APPENDED: 'MESSAGE_APPENDED',
|
||||
CHAT_CREATED: 'CHAT_CREATED',
|
||||
IDENTITY_CHANGED: 'IDENTITY_CHANGED',
|
||||
};
|
||||
|
||||
activeChats: {
|
||||
@@ -375,11 +377,13 @@ export class ZKChatClient extends EventEmitter2 {
|
||||
const validChats = await this._validateConvos(bucket.activeChats);
|
||||
|
||||
this.activeChats = validChats;
|
||||
await this.fetchActiveChats(this.identity!.ecdh.pub);
|
||||
}
|
||||
|
||||
async importIdentity(identity: ZKChatIdentity) {
|
||||
this.identity = identity;
|
||||
await this._load();
|
||||
this.emit(ZKChatClient.EVENTS.IDENTITY_CHANGED);
|
||||
}
|
||||
|
||||
createDM = async (receiver: string, receiverECDH: string, isAnon?: boolean): Promise<Chat> => {
|
||||
|
||||
@@ -79,6 +79,10 @@ module.exports = [
|
||||
app: path.join(__dirname, 'src', 'app.tsx'),
|
||||
serviceWorker: path.join(__dirname, 'src', 'serviceWorkers', 'index.ts'),
|
||||
},
|
||||
// ignoreWarnings: [
|
||||
// { module: /node_modules\/gun\/gun.js/ },
|
||||
// { module: /node_modules\/gun\/sea.js/ },
|
||||
// ],
|
||||
// [
|
||||
// ...(isProd ? [] : devServerEntries),
|
||||
// `./src/app.tsx`,
|
||||
@@ -100,6 +104,7 @@ module.exports = [
|
||||
os: require.resolve('os-browserify/browser'),
|
||||
http: require.resolve('stream-http'),
|
||||
https: require.resolve('https-browserify'),
|
||||
assert: require.resolve('assert/'),
|
||||
constants: false,
|
||||
fs: false,
|
||||
},
|
||||
@@ -137,6 +142,7 @@ module.exports = [
|
||||
title: process.env.APP_TITLE || 'Zkitter',
|
||||
inject: true,
|
||||
}),
|
||||
new webpack.ContextReplacementPlugin(/gun/),
|
||||
],
|
||||
stats: 'minimal',
|
||||
devServer: {
|
||||
|
||||
Reference in New Issue
Block a user