feat: update ui and add compose dialog
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@phosphor-icons/react": "^2.1.7",
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@tanstack/query-sync-storage-persister": "^5.51.15",
|
"@tanstack/query-sync-storage-persister": "^5.51.15",
|
||||||
"@tanstack/react-query": "^5.51.15",
|
"@tanstack/react-query": "^5.51.15",
|
||||||
|
|||||||
291
pnpm-lock.yaml
generated
291
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
|||||||
'@radix-ui/react-avatar':
|
'@radix-ui/react-avatar':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
version: 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-dialog':
|
||||||
|
specifier: ^1.1.1
|
||||||
|
version: 1.1.1(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
||||||
'@radix-ui/react-scroll-area':
|
'@radix-ui/react-scroll-area':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
version: 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
||||||
@@ -521,6 +524,19 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-dialog@1.1.1':
|
||||||
|
resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-direction@1.1.0':
|
'@radix-ui/react-direction@1.1.0':
|
||||||
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
|
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -530,6 +546,63 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-dismissable-layer@1.1.0':
|
||||||
|
resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-focus-guards@1.1.0':
|
||||||
|
resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-focus-scope@1.1.0':
|
||||||
|
resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-id@1.1.0':
|
||||||
|
resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-portal@1.1.1':
|
||||||
|
resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-presence@1.1.0':
|
'@radix-ui/react-presence@1.1.0':
|
||||||
resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
|
resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -587,6 +660,24 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-use-controllable-state@1.1.0':
|
||||||
|
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-use-escape-keydown@1.1.0':
|
||||||
|
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-use-layout-effect@1.1.0':
|
'@radix-ui/react-use-layout-effect@1.1.0':
|
||||||
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
|
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -906,6 +997,10 @@ packages:
|
|||||||
arg@5.0.2:
|
arg@5.0.2:
|
||||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||||
|
|
||||||
|
aria-hidden@1.2.4:
|
||||||
|
resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
autoprefixer@10.4.19:
|
autoprefixer@10.4.19:
|
||||||
resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==}
|
resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
@@ -1005,6 +1100,9 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
detect-node-es@1.1.0:
|
||||||
|
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||||
|
|
||||||
didyoumean@1.2.2:
|
didyoumean@1.2.2:
|
||||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||||
|
|
||||||
@@ -1066,6 +1164,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
get-nonce@1.0.1:
|
||||||
|
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -1330,6 +1432,36 @@ packages:
|
|||||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
react-remove-scroll-bar@2.3.6:
|
||||||
|
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
react-remove-scroll@2.5.7:
|
||||||
|
resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
react-style-singleton@2.2.1:
|
||||||
|
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
react@19.0.0-rc-d025ddd3-20240722:
|
react@19.0.0-rc-d025ddd3-20240722:
|
||||||
resolution: {integrity: sha512-pZZ3zHponPdVhwgOoQoI7qoHh+Hn3GAVc+/g4LICRv/XZ0IEJPsPOehV/Iu8Ssl/DoruEO4EdeK6kTSmcaAo8A==}
|
resolution: {integrity: sha512-pZZ3zHponPdVhwgOoQoI7qoHh+Hn3GAVc+/g4LICRv/XZ0IEJPsPOehV/Iu8Ssl/DoruEO4EdeK6kTSmcaAo8A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1463,6 +1595,9 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
tslib@2.6.3:
|
||||||
|
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
|
||||||
|
|
||||||
types-react-dom@19.0.0-rc.1:
|
types-react-dom@19.0.0-rc.1:
|
||||||
resolution: {integrity: sha512-VSLZJl8VXCD0fAWp7DUTFUDCcZ8DVXOQmjhJMD03odgeFmu14ZQJHCXeETm3BEAhJqfgJaFkLnGkQv88sRx0fQ==}
|
resolution: {integrity: sha512-VSLZJl8VXCD0fAWp7DUTFUDCcZ8DVXOQmjhJMD03odgeFmu14ZQJHCXeETm3BEAhJqfgJaFkLnGkQv88sRx0fQ==}
|
||||||
|
|
||||||
@@ -1488,6 +1623,26 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
browserslist: '>= 4.21.0'
|
browserslist: '>= 4.21.0'
|
||||||
|
|
||||||
|
use-callback-ref@1.3.2:
|
||||||
|
resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
use-sidecar@1.1.2:
|
||||||
|
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
use-sync-external-store@1.2.2:
|
use-sync-external-store@1.2.2:
|
||||||
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
|
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1938,12 +2093,81 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': types-react@19.0.0-rc.1
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
|
'@radix-ui/react-dialog@1.1.1(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.0
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-context': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-focus-guards': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-focus-scope': 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-id': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-portal': 1.1.1(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-presence': 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-primitive': 2.0.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-slot': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
aria-hidden: 1.2.4
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
react-dom: 19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722)
|
||||||
|
react-remove-scroll: 2.5.7(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
'@types/react-dom': types-react-dom@19.0.0-rc.1
|
||||||
|
|
||||||
'@radix-ui/react-direction@1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)':
|
'@radix-ui/react-direction@1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.0.0-rc-d025ddd3-20240722
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': types-react@19.0.0-rc.1
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
|
'@radix-ui/react-dismissable-layer@1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.0
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-primitive': 2.0.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-use-escape-keydown': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
react-dom: 19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
'@types/react-dom': types-react-dom@19.0.0-rc.1
|
||||||
|
|
||||||
|
'@radix-ui/react-focus-guards@1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)':
|
||||||
|
dependencies:
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
|
'@radix-ui/react-focus-scope@1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-primitive': 2.0.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
react-dom: 19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
'@types/react-dom': types-react-dom@19.0.0-rc.1
|
||||||
|
|
||||||
|
'@radix-ui/react-id@1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
|
'@radix-ui/react-portal@1.1.1(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-primitive': 2.0.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
react-dom: 19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
'@types/react-dom': types-react-dom@19.0.0-rc.1
|
||||||
|
|
||||||
'@radix-ui/react-presence@1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)':
|
'@radix-ui/react-presence@1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
'@radix-ui/react-compose-refs': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
@@ -1993,6 +2217,20 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': types-react@19.0.0-rc.1
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
|
'@radix-ui/react-use-controllable-state@1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
|
'@radix-ui/react-use-escape-keydown@1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
'@radix-ui/react-use-layout-effect@1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)':
|
'@radix-ui/react-use-layout-effect@1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.0.0-rc-d025ddd3-20240722
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
@@ -2277,6 +2515,10 @@ snapshots:
|
|||||||
|
|
||||||
arg@5.0.2: {}
|
arg@5.0.2: {}
|
||||||
|
|
||||||
|
aria-hidden@1.2.4:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.6.3
|
||||||
|
|
||||||
autoprefixer@10.4.19(postcss@8.4.40):
|
autoprefixer@10.4.19(postcss@8.4.40):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.23.2
|
browserslist: 4.23.2
|
||||||
@@ -2386,6 +2628,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
|
|
||||||
|
detect-node-es@1.1.0: {}
|
||||||
|
|
||||||
didyoumean@1.2.2: {}
|
didyoumean@1.2.2: {}
|
||||||
|
|
||||||
dlv@1.1.3: {}
|
dlv@1.1.3: {}
|
||||||
@@ -2458,6 +2702,8 @@ snapshots:
|
|||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
|
get-nonce@1.0.1: {}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -2671,6 +2917,34 @@ snapshots:
|
|||||||
|
|
||||||
react-refresh@0.14.2: {}
|
react-refresh@0.14.2: {}
|
||||||
|
|
||||||
|
react-remove-scroll-bar@2.3.6(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1):
|
||||||
|
dependencies:
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
react-style-singleton: 2.2.1(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
tslib: 2.6.3
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
|
react-remove-scroll@2.5.7(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1):
|
||||||
|
dependencies:
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
react-remove-scroll-bar: 2.3.6(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
react-style-singleton: 2.2.1(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
tslib: 2.6.3
|
||||||
|
use-callback-ref: 1.3.2(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
use-sidecar: 1.1.2(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
|
react-style-singleton@2.2.1(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1):
|
||||||
|
dependencies:
|
||||||
|
get-nonce: 1.0.1
|
||||||
|
invariant: 2.2.4
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
tslib: 2.6.3
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
react@19.0.0-rc-d025ddd3-20240722: {}
|
react@19.0.0-rc-d025ddd3-20240722: {}
|
||||||
|
|
||||||
read-cache@1.0.0:
|
read-cache@1.0.0:
|
||||||
@@ -2826,6 +3100,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.5.4
|
typescript: 5.5.4
|
||||||
|
|
||||||
|
tslib@2.6.3: {}
|
||||||
|
|
||||||
types-react-dom@19.0.0-rc.1:
|
types-react-dom@19.0.0-rc.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 18.3.3
|
'@types/react': 18.3.3
|
||||||
@@ -2851,6 +3127,21 @@ snapshots:
|
|||||||
escalade: 3.1.2
|
escalade: 3.1.2
|
||||||
picocolors: 1.0.1
|
picocolors: 1.0.1
|
||||||
|
|
||||||
|
use-callback-ref@1.3.2(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1):
|
||||||
|
dependencies:
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
tslib: 2.6.3
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
|
use-sidecar@1.1.2(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1):
|
||||||
|
dependencies:
|
||||||
|
detect-node-es: 1.1.0
|
||||||
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
tslib: 2.6.3
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
|
|
||||||
use-sync-external-store@1.2.2(react@19.0.0-rc-d025ddd3-20240722):
|
use-sync-external-store@1.2.2(react@19.0.0-rc-d025ddd3-20240722):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.0.0-rc-d025ddd3-20240722
|
react: 19.0.0-rc-d025ddd3-20240722
|
||||||
|
|||||||
@@ -141,6 +141,15 @@ pub async fn connect_account(uri: &str, state: State<'_, Nostr>) -> Result<Strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, ()> {
|
||||||
|
let contact_list = state.contact_list.lock().await;
|
||||||
|
let list = contact_list.clone().into_iter().map(|c| c.public_key.to_hex()).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(list)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
@@ -185,6 +194,11 @@ pub async fn login(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(10))).await {
|
||||||
|
let mut contact_list = state.contact_list.lock().await;
|
||||||
|
*contact_list = contacts;
|
||||||
|
};
|
||||||
|
|
||||||
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
|
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
|
||||||
|
|
||||||
if let Ok(events) = client.get_events_of(vec![inbox], None).await {
|
if let Ok(events) = client.get_events_of(vec![inbox], None).await {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ pub async fn get_chat_messages(id: String, state: State<'_, Nostr>) -> Result<Ve
|
|||||||
|
|
||||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkeys(vec![receiver_pk, sender_pk]);
|
let filter = Filter::new().kind(Kind::GiftWrap).pubkeys(vec![receiver_pk, sender_pk]);
|
||||||
|
|
||||||
let rumors = match client.get_events_of(vec![filter], Some(Duration::from_secs(10))).await {
|
let rumors = match client.database().query(vec![filter], Order::Desc).await {
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
stream::iter(events)
|
stream::iter(events)
|
||||||
.filter_map(|ev| async move {
|
.filter_map(|ev| async move {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ fn main() {
|
|||||||
connect_account,
|
connect_account,
|
||||||
get_accounts,
|
get_accounts,
|
||||||
get_metadata,
|
get_metadata,
|
||||||
|
get_contact_list,
|
||||||
get_chats,
|
get_chats,
|
||||||
get_chat_messages,
|
get_chat_messages,
|
||||||
connect_inbox,
|
connect_inbox,
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ try {
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async getContactList() : Promise<Result<string[], null>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_contact_list") };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
async getChats() : Promise<Result<string[], string>> {
|
async getChats() : Promise<Result<string[], string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_chats") };
|
return { status: "ok", data: await TAURI_INVOKE("get_chats") };
|
||||||
|
|||||||
@@ -87,3 +87,10 @@ export function groupEventByDate(events: NostrEvent[]) {
|
|||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isEmojiOnly(str: string) {
|
||||||
|
const stringToTest = str.replace(/ /g, "");
|
||||||
|
const emojiRegex =
|
||||||
|
/^(?:(?:\p{RI}\p{RI}|\p{Emoji}(?:\p{Emoji_Modifier}|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})?(?:\u{200D}\p{Emoji}(?:\p{Emoji_Modifier}|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})?)*)|[\u{1f900}-\u{1f9ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}])+$/u;
|
||||||
|
return emojiRegex.test(stringToTest) && Number.isNaN(Number(stringToTest));
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,18 +24,20 @@ export function UserAvatar({ className }: { className?: string }) {
|
|||||||
>
|
>
|
||||||
{!user.isLoading ? (
|
{!user.isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Avatar.Image
|
{user.profile?.picture ? (
|
||||||
src={`//wsrv.nl/?url=${user.profile?.picture}&w=200&h=200`}
|
<Avatar.Image
|
||||||
alt={user.pubkey}
|
src={`//wsrv.nl/?url=${user.profile?.picture}&w=200&h=200`}
|
||||||
loading="lazy"
|
alt={user.pubkey}
|
||||||
decoding="async"
|
loading="lazy"
|
||||||
className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
decoding="async"
|
||||||
/>
|
className="w-full aspect-square object-cover outline-[.5px] outline-black/15"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<Avatar.Fallback>
|
<Avatar.Fallback>
|
||||||
<img
|
<img
|
||||||
src={fallback}
|
src={fallback}
|
||||||
alt={user.pubkey}
|
alt={user.pubkey}
|
||||||
className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5"
|
||||||
/>
|
/>
|
||||||
</Avatar.Fallback>
|
</Avatar.Fallback>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { createFileRoute } from '@tanstack/react-router'
|
|||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
|
import { Route as AccountContactsImport } from './routes/$account.contacts'
|
||||||
import { Route as AccountChatsIdImport } from './routes/$account.chats.$id'
|
import { Route as AccountChatsIdImport } from './routes/$account.chats.$id'
|
||||||
|
|
||||||
// Create Virtual Routes
|
// Create Virtual Routes
|
||||||
@@ -22,7 +23,6 @@ const NostrConnectLazyImport = createFileRoute('/nostr-connect')()
|
|||||||
const NewLazyImport = createFileRoute('/new')()
|
const NewLazyImport = createFileRoute('/new')()
|
||||||
const ImportKeyLazyImport = createFileRoute('/import-key')()
|
const ImportKeyLazyImport = createFileRoute('/import-key')()
|
||||||
const CreateAccountLazyImport = createFileRoute('/create-account')()
|
const CreateAccountLazyImport = createFileRoute('/create-account')()
|
||||||
const ContactsLazyImport = createFileRoute('/contacts')()
|
|
||||||
const AccountChatsLazyImport = createFileRoute('/$account/chats')()
|
const AccountChatsLazyImport = createFileRoute('/$account/chats')()
|
||||||
const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')()
|
const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')()
|
||||||
|
|
||||||
@@ -50,11 +50,6 @@ const CreateAccountLazyRoute = CreateAccountLazyImport.update({
|
|||||||
import('./routes/create-account.lazy').then((d) => d.Route),
|
import('./routes/create-account.lazy').then((d) => d.Route),
|
||||||
)
|
)
|
||||||
|
|
||||||
const ContactsLazyRoute = ContactsLazyImport.update({
|
|
||||||
path: '/contacts',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any).lazy(() => import('./routes/contacts.lazy').then((d) => d.Route))
|
|
||||||
|
|
||||||
const IndexRoute = IndexImport.update({
|
const IndexRoute = IndexImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@@ -67,6 +62,13 @@ const AccountChatsLazyRoute = AccountChatsLazyImport.update({
|
|||||||
import('./routes/$account.chats.lazy').then((d) => d.Route),
|
import('./routes/$account.chats.lazy').then((d) => d.Route),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const AccountContactsRoute = AccountContactsImport.update({
|
||||||
|
path: '/$account/contacts',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() =>
|
||||||
|
import('./routes/$account.contacts.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const AccountChatsNewLazyRoute = AccountChatsNewLazyImport.update({
|
const AccountChatsNewLazyRoute = AccountChatsNewLazyImport.update({
|
||||||
path: '/new',
|
path: '/new',
|
||||||
getParentRoute: () => AccountChatsLazyRoute,
|
getParentRoute: () => AccountChatsLazyRoute,
|
||||||
@@ -77,7 +79,9 @@ const AccountChatsNewLazyRoute = AccountChatsNewLazyImport.update({
|
|||||||
const AccountChatsIdRoute = AccountChatsIdImport.update({
|
const AccountChatsIdRoute = AccountChatsIdImport.update({
|
||||||
path: '/$id',
|
path: '/$id',
|
||||||
getParentRoute: () => AccountChatsLazyRoute,
|
getParentRoute: () => AccountChatsLazyRoute,
|
||||||
} as any)
|
} as any).lazy(() =>
|
||||||
|
import('./routes/$account.chats.$id.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
// Populate the FileRoutesByPath interface
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
@@ -90,13 +94,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexImport
|
preLoaderRoute: typeof IndexImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/contacts': {
|
|
||||||
id: '/contacts'
|
|
||||||
path: '/contacts'
|
|
||||||
fullPath: '/contacts'
|
|
||||||
preLoaderRoute: typeof ContactsLazyImport
|
|
||||||
parentRoute: typeof rootRoute
|
|
||||||
}
|
|
||||||
'/create-account': {
|
'/create-account': {
|
||||||
id: '/create-account'
|
id: '/create-account'
|
||||||
path: '/create-account'
|
path: '/create-account'
|
||||||
@@ -125,6 +122,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof NostrConnectLazyImport
|
preLoaderRoute: typeof NostrConnectLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/$account/contacts': {
|
||||||
|
id: '/$account/contacts'
|
||||||
|
path: '/$account/contacts'
|
||||||
|
fullPath: '/$account/contacts'
|
||||||
|
preLoaderRoute: typeof AccountContactsImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/$account/chats': {
|
'/$account/chats': {
|
||||||
id: '/$account/chats'
|
id: '/$account/chats'
|
||||||
path: '/$account/chats'
|
path: '/$account/chats'
|
||||||
@@ -153,11 +157,11 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
export const routeTree = rootRoute.addChildren({
|
export const routeTree = rootRoute.addChildren({
|
||||||
IndexRoute,
|
IndexRoute,
|
||||||
ContactsLazyRoute,
|
|
||||||
CreateAccountLazyRoute,
|
CreateAccountLazyRoute,
|
||||||
ImportKeyLazyRoute,
|
ImportKeyLazyRoute,
|
||||||
NewLazyRoute,
|
NewLazyRoute,
|
||||||
NostrConnectLazyRoute,
|
NostrConnectLazyRoute,
|
||||||
|
AccountContactsRoute,
|
||||||
AccountChatsLazyRoute: AccountChatsLazyRoute.addChildren({
|
AccountChatsLazyRoute: AccountChatsLazyRoute.addChildren({
|
||||||
AccountChatsIdRoute,
|
AccountChatsIdRoute,
|
||||||
AccountChatsNewLazyRoute,
|
AccountChatsNewLazyRoute,
|
||||||
@@ -173,20 +177,17 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
"/contacts",
|
|
||||||
"/create-account",
|
"/create-account",
|
||||||
"/import-key",
|
"/import-key",
|
||||||
"/new",
|
"/new",
|
||||||
"/nostr-connect",
|
"/nostr-connect",
|
||||||
|
"/$account/contacts",
|
||||||
"/$account/chats"
|
"/$account/chats"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.tsx"
|
"filePath": "index.tsx"
|
||||||
},
|
},
|
||||||
"/contacts": {
|
|
||||||
"filePath": "contacts.lazy.tsx"
|
|
||||||
},
|
|
||||||
"/create-account": {
|
"/create-account": {
|
||||||
"filePath": "create-account.lazy.tsx"
|
"filePath": "create-account.lazy.tsx"
|
||||||
},
|
},
|
||||||
@@ -199,6 +200,9 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"/nostr-connect": {
|
"/nostr-connect": {
|
||||||
"filePath": "nostr-connect.lazy.tsx"
|
"filePath": "nostr-connect.lazy.tsx"
|
||||||
},
|
},
|
||||||
|
"/$account/contacts": {
|
||||||
|
"filePath": "$account.contacts.tsx"
|
||||||
|
},
|
||||||
"/$account/chats": {
|
"/$account/chats": {
|
||||||
"filePath": "$account.chats.lazy.tsx",
|
"filePath": "$account.chats.lazy.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
|
|||||||
288
src/routes/$account.chats.$id.lazy.tsx
Normal file
288
src/routes/$account.chats.$id.lazy.tsx
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import { commands } from "@/commands";
|
||||||
|
import { cn, getReceivers, groupEventByDate, time } from "@/commons";
|
||||||
|
import { Spinner } from "@/components/spinner";
|
||||||
|
import { User } from "@/components/user";
|
||||||
|
import { ArrowUp, Paperclip } from "@phosphor-icons/react";
|
||||||
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
|
import type { NostrEvent } from "nostr-tools";
|
||||||
|
import { useCallback, useRef, useState, useTransition } from "react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
|
type Payload = {
|
||||||
|
event: string;
|
||||||
|
sender: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/$account/chats/$id")({
|
||||||
|
component: Screen,
|
||||||
|
pendingComponent: Pending,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Pending() {
|
||||||
|
return (
|
||||||
|
<div className="size-full flex items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
return (
|
||||||
|
<div className="size-full flex flex-col">
|
||||||
|
<Header />
|
||||||
|
<List />
|
||||||
|
<Form />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
const { account, id } = Route.useParams();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="h-12 shrink-0 flex items-center justify-between px-3.5 border-b border-neutral-100 dark:border-neutral-800"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex -space-x-1 overflow-hidden">
|
||||||
|
<User.Provider pubkey={account}>
|
||||||
|
<User.Root className="size-8 rounded-full inline-block ring-2 ring-white dark:ring-neutral-900">
|
||||||
|
<User.Avatar className="size-8 rounded-full" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
<User.Provider pubkey={id}>
|
||||||
|
<User.Root className="size-8 rounded-full inline-block ring-2 ring-white dark:ring-neutral-900">
|
||||||
|
<User.Avatar className="size-8 rounded-full" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-7 inline-flex items-center justify-center gap-1.5 px-2 rounded-full bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<span className="relative flex size-2">
|
||||||
|
<span className="animate-ping absolute inline-flex size-full rounded-full bg-teal-400 opacity-75" />
|
||||||
|
<span className="relative inline-flex rounded-full size-2 bg-teal-500" />
|
||||||
|
</span>
|
||||||
|
<div className="text-xs leading-tight">Connected</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function List() {
|
||||||
|
const { account, id } = Route.useParams();
|
||||||
|
const { isLoading, isError, data } = useQuery({
|
||||||
|
queryKey: ["chats", id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await commands.getChatMessages(id);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
const raw = res.data;
|
||||||
|
const events: NostrEvent[] = raw.map((item) => JSON.parse(item));
|
||||||
|
|
||||||
|
return events;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select: (data) => {
|
||||||
|
const groups = groupEventByDate(data);
|
||||||
|
return Object.entries(groups).reverse();
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const renderItem = useCallback(
|
||||||
|
(item: NostrEvent, idx: number) => {
|
||||||
|
const self = account === item.pubkey;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx + item.id}
|
||||||
|
className="flex items-center justify-between gap-3 my-1.5 px-3 border-l-2 border-transparent hover:border-blue-400"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-1 min-w-0 inline-flex",
|
||||||
|
self ? "justify-end" : "justify-start",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"py-2 px-3 w-fit max-w-[400px] text-pretty break-message",
|
||||||
|
!self
|
||||||
|
? "bg-neutral-100 dark:bg-neutral-800 rounded-tl-3xl rounded-tr-3xl rounded-br-3xl rounded-bl-md"
|
||||||
|
: "bg-blue-500 text-white rounded-tl-3xl rounded-tr-3xl rounded-br-md rounded-bl-3xl",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0 w-16 flex items-center justify-end">
|
||||||
|
<span className="text-xs text-right text-neutral-600 dark:text-neutral-400">
|
||||||
|
{time(item.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisten = listen<Payload>("event", async (data) => {
|
||||||
|
const event: NostrEvent = JSON.parse(data.payload.event);
|
||||||
|
const sender = data.payload.sender;
|
||||||
|
const receivers = getReceivers(event.tags);
|
||||||
|
const group = [account, id];
|
||||||
|
|
||||||
|
if (!group.includes(sender)) return;
|
||||||
|
if (!group.some((item) => receivers.includes(item))) return;
|
||||||
|
|
||||||
|
await queryClient.setQueryData(
|
||||||
|
["chats", id],
|
||||||
|
(prevEvents: NostrEvent[]) => {
|
||||||
|
if (!prevEvents) return prevEvents;
|
||||||
|
return [...prevEvents, event];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((f) => f());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="overflow-hidden flex-1 w-full"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className="relative h-full py-2 [&>div]:!flex [&>div]:flex-col [&>div]:justify-end [&>div]:min-h-full"
|
||||||
|
>
|
||||||
|
<Virtualizer scrollRef={ref} shift>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between gap-3 my-1.5 px-3">
|
||||||
|
<div className="flex-1 min-w-0 inline-flex">
|
||||||
|
<div className="w-44 h-[35px] py-2 max-w-[400px] bg-neutral-100 dark:bg-neutral-800 animate-pulse rounded-tl-3xl rounded-tr-3xl rounded-br-3xl rounded-bl-md" />
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0 w-16 flex items-center justify-end" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-3 my-1.5 px-3">
|
||||||
|
<div className="flex-1 min-w-0 inline-flex justify-end">
|
||||||
|
<div className="w-44 h-[35px] py-2 max-w-[400px] bg-blue-500 text-white animate-pulse rounded-tl-3xl rounded-tr-3xl rounded-br-md rounded-bl-3xl" />
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0 w-16 flex items-center justify-end" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : isError ? (
|
||||||
|
<div className="w-full h-56 flex items-center justify-center">
|
||||||
|
<div className="text-sm flex items-center gap-1.5">
|
||||||
|
Cannot load message. Please try again later.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item[0]}
|
||||||
|
className="w-full flex flex-col items-center mt-3 gap-3"
|
||||||
|
>
|
||||||
|
<div className="text-xs text-center text-neutral-600 dark:text-neutral-400">
|
||||||
|
{item[0]}
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
{item[1]
|
||||||
|
.sort((a, b) => a.created_at - b.created_at)
|
||||||
|
.map((item, idx) => renderItem(item, idx))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Virtualizer>
|
||||||
|
</ScrollArea.Viewport>
|
||||||
|
<ScrollArea.Scrollbar
|
||||||
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<ScrollArea.Thumb className="flex-1 bg-black/40 dark:bg-white/40 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||||
|
</ScrollArea.Scrollbar>
|
||||||
|
<ScrollArea.Corner className="bg-transparent" />
|
||||||
|
</ScrollArea.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Form() {
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const { inbox } = Route.useRouteContext();
|
||||||
|
|
||||||
|
const [newMessage, setNewMessage] = useState("");
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
if (!newMessage.length) return;
|
||||||
|
|
||||||
|
const res = await commands.sendMessage(id, newMessage);
|
||||||
|
|
||||||
|
if (res.status === "error") {
|
||||||
|
await message(res.error, { title: "Coop", kind: "error" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewMessage("");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-12 shrink-0 flex items-center justify-center px-3.5">
|
||||||
|
{!inbox.length ? (
|
||||||
|
<div className="text-xs">
|
||||||
|
This user doesn't have inbox relays. You cannot send messages to them.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex items-center gap-2">
|
||||||
|
<div className="inline-flex gap-1">
|
||||||
|
<div
|
||||||
|
title="Attach media"
|
||||||
|
className="size-9 inline-flex items-center justify-center hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-full"
|
||||||
|
>
|
||||||
|
<Paperclip className="size-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
placeholder="Message..."
|
||||||
|
value={newMessage}
|
||||||
|
onChange={(e) => setNewMessage(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") submit();
|
||||||
|
}}
|
||||||
|
className="flex-1 h-9 rounded-full px-3.5 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:outline-none focus:border-blue-500 placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Send message"
|
||||||
|
disabled={isPending}
|
||||||
|
onClick={() => submit()}
|
||||||
|
className="rounded-full size-9 inline-flex items-center justify-center bg-blue-300 hover:bg-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800 text-white"
|
||||||
|
>
|
||||||
|
{isPending ? <Spinner /> : <ArrowUp className="size-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,293 +1,9 @@
|
|||||||
import { commands } from "@/commands";
|
|
||||||
import { cn, getReceivers, groupEventByDate, time } from "@/commons";
|
|
||||||
import { Spinner } from "@/components/spinner";
|
|
||||||
import { User } from "@/components/user";
|
|
||||||
import { ArrowUp, Paperclip } from "@phosphor-icons/react";
|
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
|
||||||
import type { NostrEvent } from "nostr-tools";
|
|
||||||
import { useCallback, useRef, useState, useTransition } from "react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { Virtualizer } from "virtua";
|
|
||||||
|
|
||||||
type Payload = {
|
|
||||||
event: string;
|
|
||||||
sender: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account/chats/$id")({
|
export const Route = createFileRoute("/$account/chats/$id")({
|
||||||
beforeLoad: async ({ params }) => {
|
beforeLoad: async ({ params }) => {
|
||||||
const inbox: string[] = await invoke("connect_inbox", { id: params.id });
|
const inbox: string[] = await invoke("connect_inbox", { id: params.id });
|
||||||
return { inbox };
|
return { inbox };
|
||||||
},
|
},
|
||||||
component: Screen,
|
|
||||||
pendingComponent: Pending,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function Pending() {
|
|
||||||
return (
|
|
||||||
<div className="size-full flex items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
return (
|
|
||||||
<div className="size-full flex flex-col">
|
|
||||||
<Header />
|
|
||||||
<List />
|
|
||||||
<Form />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Header() {
|
|
||||||
const { account, id } = Route.useParams();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="h-12 shrink-0 flex items-center justify-between px-3.5 border-b border-neutral-100 dark:border-neutral-800"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div className="flex -space-x-1 overflow-hidden">
|
|
||||||
<User.Provider pubkey={account}>
|
|
||||||
<User.Root className="size-7 rounded-full inline-block ring-2 ring-white dark:ring-neutral-900">
|
|
||||||
<User.Avatar className="size-7 rounded-full" />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<User.Provider pubkey={id}>
|
|
||||||
<User.Root className="size-7 rounded-full inline-block ring-2 ring-white dark:ring-neutral-900">
|
|
||||||
<User.Avatar className="size-7 rounded-full" />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-7 inline-flex items-center justify-center gap-1.5 px-2 rounded-full bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<span className="relative flex size-2">
|
|
||||||
<span className="animate-ping absolute inline-flex size-full rounded-full bg-teal-400 opacity-75" />
|
|
||||||
<span className="relative inline-flex rounded-full size-2 bg-teal-500" />
|
|
||||||
</span>
|
|
||||||
<div className="text-xs leading-tight">Connected</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function List() {
|
|
||||||
const { account, id } = Route.useParams();
|
|
||||||
const { isLoading, isError, data } = useQuery({
|
|
||||||
queryKey: ["chats", id],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await commands.getChatMessages(id);
|
|
||||||
|
|
||||||
if (res.status === "ok") {
|
|
||||||
const raw = res.data;
|
|
||||||
const events: NostrEvent[] = raw.map((item) => JSON.parse(item));
|
|
||||||
|
|
||||||
return events;
|
|
||||||
} else {
|
|
||||||
throw new Error(res.error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
select: (data) => {
|
|
||||||
const groups = groupEventByDate(data);
|
|
||||||
return Object.entries(groups).reverse();
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
|
||||||
(item: NostrEvent, idx: number) => {
|
|
||||||
const self = account === item.pubkey;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={idx + item.id}
|
|
||||||
className="flex items-center justify-between gap-3 my-1.5 px-3 border-l-2 border-transparent hover:border-blue-400"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex-1 min-w-0 inline-flex",
|
|
||||||
self ? "justify-end" : "justify-start",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"py-2 px-3 w-fit max-w-[400px] text-pretty break-message",
|
|
||||||
!self
|
|
||||||
? "bg-neutral-100 dark:bg-neutral-800 rounded-tl-3xl rounded-tr-3xl rounded-br-3xl rounded-bl-md"
|
|
||||||
: "bg-blue-500 text-white rounded-tl-3xl rounded-tr-3xl rounded-br-md rounded-bl-3xl",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0 w-16 flex items-center justify-end">
|
|
||||||
<span className="text-xs text-right text-neutral-600 dark:text-neutral-400">
|
|
||||||
{time(item.created_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unlisten = listen<Payload>("event", async (data) => {
|
|
||||||
const event: NostrEvent = JSON.parse(data.payload.event);
|
|
||||||
const sender = data.payload.sender;
|
|
||||||
const receivers = getReceivers(event.tags);
|
|
||||||
const group = [account, id];
|
|
||||||
|
|
||||||
if (!group.includes(sender)) return;
|
|
||||||
if (!group.some((item) => receivers.includes(item))) return;
|
|
||||||
|
|
||||||
await queryClient.setQueryData(
|
|
||||||
["chats", id],
|
|
||||||
(prevEvents: NostrEvent[]) => {
|
|
||||||
if (!prevEvents) return prevEvents;
|
|
||||||
return [...prevEvents, event];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unlisten.then((f) => f());
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea.Root
|
|
||||||
type={"scroll"}
|
|
||||||
scrollHideDelay={300}
|
|
||||||
className="overflow-hidden flex-1 w-full"
|
|
||||||
>
|
|
||||||
<ScrollArea.Viewport
|
|
||||||
ref={ref}
|
|
||||||
className="relative h-full py-2 [&>div]:!flex [&>div]:flex-col [&>div]:justify-end [&>div]:min-h-full"
|
|
||||||
>
|
|
||||||
<Virtualizer scrollRef={ref} shift>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between gap-3 my-1.5 px-3">
|
|
||||||
<div className="flex-1 min-w-0 inline-flex">
|
|
||||||
<div className="w-44 h-[35px] py-2 max-w-[400px] bg-neutral-100 dark:bg-neutral-800 animate-pulse rounded-tl-3xl rounded-tr-3xl rounded-br-3xl rounded-bl-md" />
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0 w-16 flex items-center justify-end" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between gap-3 my-1.5 px-3">
|
|
||||||
<div className="flex-1 min-w-0 inline-flex justify-end">
|
|
||||||
<div className="w-44 h-[35px] py-2 max-w-[400px] bg-blue-500 text-white animate-pulse rounded-tl-3xl rounded-tr-3xl rounded-br-md rounded-bl-3xl" />
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0 w-16 flex items-center justify-end" />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : isError ? (
|
|
||||||
<div className="w-full h-56 flex items-center justify-center">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
Cannot load message. Please try again later.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
data.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item[0]}
|
|
||||||
className="w-full flex flex-col items-center mt-3 gap-3"
|
|
||||||
>
|
|
||||||
<div className="text-xs text-center text-neutral-600 dark:text-neutral-400">
|
|
||||||
{item[0]}
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
{item[1]
|
|
||||||
.sort((a, b) => a.created_at - b.created_at)
|
|
||||||
.map((item, idx) => renderItem(item, idx))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Virtualizer>
|
|
||||||
</ScrollArea.Viewport>
|
|
||||||
<ScrollArea.Scrollbar
|
|
||||||
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
|
||||||
orientation="vertical"
|
|
||||||
>
|
|
||||||
<ScrollArea.Thumb className="flex-1 bg-black/40 dark:bg-white/40 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
|
||||||
</ScrollArea.Scrollbar>
|
|
||||||
<ScrollArea.Corner className="bg-transparent" />
|
|
||||||
</ScrollArea.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Form() {
|
|
||||||
const { id } = Route.useParams();
|
|
||||||
const { inbox } = Route.useRouteContext();
|
|
||||||
|
|
||||||
const [newMessage, setNewMessage] = useState("");
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
startTransition(async () => {
|
|
||||||
if (!newMessage.length) return;
|
|
||||||
|
|
||||||
const res = await commands.sendMessage(id, newMessage);
|
|
||||||
|
|
||||||
if (res.status === "error") {
|
|
||||||
await message(res.error, { title: "Coop", kind: "error" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setNewMessage("");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-12 shrink-0 flex items-center justify-center px-3.5">
|
|
||||||
{!inbox.length ? (
|
|
||||||
<div className="text-xs">
|
|
||||||
This user doesn't have inbox relays. You cannot send messages to them.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex-1 flex items-center gap-2">
|
|
||||||
<div className="inline-flex gap-1">
|
|
||||||
<div
|
|
||||||
title="Attach media"
|
|
||||||
className="size-9 inline-flex items-center justify-center hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-full"
|
|
||||||
>
|
|
||||||
<Paperclip className="size-5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
placeholder="Message..."
|
|
||||||
value={newMessage}
|
|
||||||
onChange={(e) => setNewMessage(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") submit();
|
|
||||||
}}
|
|
||||||
className="flex-1 h-9 rounded-full px-3.5 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:outline-none focus:border-blue-500"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title="Send message"
|
|
||||||
disabled={isPending}
|
|
||||||
onClick={() => submit()}
|
|
||||||
className="rounded-full size-9 inline-flex items-center justify-center bg-blue-300 hover:bg-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800 text-white"
|
|
||||||
>
|
|
||||||
{isPending ? <Spinner /> : <ArrowUp className="size-5" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { commands } from "@/commands";
|
import { commands } from "@/commands";
|
||||||
import { ago, cn } from "@/commons";
|
import { ago, cn } from "@/commons";
|
||||||
|
import { Spinner } from "@/components/spinner";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import { DotsThree, Plus, UsersThree } from "@phosphor-icons/react";
|
import { ArrowRight, CirclesFour, Plus, X } from "@phosphor-icons/react";
|
||||||
|
import * as Dialog from "@radix-ui/react-dialog";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router";
|
import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import type { NostrEvent } from "nostr-tools";
|
import type { NostrEvent } from "nostr-tools";
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useState, useTransition } from "react";
|
||||||
|
|
||||||
type Payload = {
|
type Payload = {
|
||||||
event: string;
|
event: string;
|
||||||
@@ -28,7 +31,6 @@ function Screen() {
|
|||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<ChatList />
|
<ChatList />
|
||||||
<CurrentUser />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 min-h-0 bg-white dark:bg-neutral-900 overflow-auto">
|
<div className="flex-1 min-w-0 min-h-0 bg-white dark:bg-neutral-900 overflow-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
@@ -38,24 +40,27 @@ function Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
|
const { platform } = Route.useRouteContext();
|
||||||
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="shrink-0 h-12 px-3.5 flex items-center justify-end"
|
className={cn(
|
||||||
|
"shrink-0 h-12 flex items-center justify-between",
|
||||||
|
platform === "macos" ? "pl-24 pr-3.5" : "px-3.5",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<CurrentUser />
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Link
|
<Link
|
||||||
to="/contacts"
|
to="/$account/contacts"
|
||||||
className="size-7 rounded-lg inline-flex items-center justify-center text-neutral-600 dark:text-neutral-400 hover:bg-black/5 dark:hover:bg-white/5"
|
params={{ account }}
|
||||||
|
className="size-8 rounded-full inline-flex items-center justify-center bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<UsersThree className="size-4" />
|
<CirclesFour className="size-4" />
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/new"
|
|
||||||
className="h-7 w-12 rounded-t-lg rounded-l-lg rounded-r inline-flex items-center justify-center bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<Plus className="size-4" />
|
|
||||||
</Link>
|
</Link>
|
||||||
|
<Compose />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -83,7 +88,7 @@ function ChatList() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = listen("synchronized", async () => {
|
const unlisten = listen("synchronized", async () => {
|
||||||
await queryClient.refetchQueries({ queryKey: ["chats"] })
|
await queryClient.refetchQueries({ queryKey: ["chats"] });
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -193,6 +198,146 @@ function ChatList() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Compose() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [target, setTarget] = useState("");
|
||||||
|
const [newMessage, setNewMessage] = useState("");
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const { account } = Route.useParams();
|
||||||
|
const { isLoading, data: contacts } = useQuery({
|
||||||
|
queryKey: ["contacts", account],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await commands.getContactList();
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
return res.data;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
enabled: isOpen,
|
||||||
|
});
|
||||||
|
|
||||||
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
|
const sendMessage = async () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
if (!newMessage.length) return;
|
||||||
|
if (!target.length) return;
|
||||||
|
|
||||||
|
const res = await commands.sendMessage(target, newMessage);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
navigate({
|
||||||
|
to: "/$account/chats/$id",
|
||||||
|
params: { account, id: target },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await message(res.error, { title: "Coop", kind: "error" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<Dialog.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="size-8 rounded-full inline-flex items-center justify-center bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||||
|
>
|
||||||
|
<Plus className="size-4" weight="bold" />
|
||||||
|
</button>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className="bg-black/20 dark:bg-white/20 data-[state=open]:animate-overlay fixed inset-0" />
|
||||||
|
<Dialog.Content className="flex flex-col data-[state=open]:animate-content fixed top-[50%] left-[50%] w-full h-full max-h-[500px] max-w-[400px] translate-x-[-50%] translate-y-[-50%] rounded-xl bg-white dark:bg-neutral-900 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none">
|
||||||
|
<div className="h-28 shrink-0 flex flex-col justify-end">
|
||||||
|
<div className="h-10 inline-flex items-center justify-between px-3.5 text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||||
|
Send to
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<button type="button">
|
||||||
|
<X className="size-4" />
|
||||||
|
</button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 px-3.5 border-b border-neutral-100 dark:border-neutral-800">
|
||||||
|
<span className="shrink-0 font-medium">To:</span>
|
||||||
|
<input
|
||||||
|
placeholder="npub1..."
|
||||||
|
value={target}
|
||||||
|
onChange={(e) => setTarget(e.target.value)}
|
||||||
|
disabled={isPending || isLoading}
|
||||||
|
className="flex-1 h-9 bg-transparent focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 px-3.5 border-b border-neutral-100 dark:border-neutral-800">
|
||||||
|
<span className="shrink-0 font-medium">Message:</span>
|
||||||
|
<input
|
||||||
|
placeholder="hello..."
|
||||||
|
value={newMessage}
|
||||||
|
onChange={(e) => setNewMessage(e.target.value)}
|
||||||
|
disabled={isPending || isLoading}
|
||||||
|
className="flex-1 h-9 bg-transparent focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isPending || isLoading || !newMessage.length}
|
||||||
|
onClick={() => sendMessage()}
|
||||||
|
className="rounded-full size-7 inline-flex items-center justify-center bg-blue-300 hover:bg-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800 text-white"
|
||||||
|
>
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="overflow-hidden flex-1 size-full"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport className="relative h-full p-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="h-[400px] flex items-center justify-center">
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
</div>
|
||||||
|
) : !contacts?.length ? (
|
||||||
|
<div className="h-[400px] flex items-center justify-center">
|
||||||
|
<p className="text-sm">Contact is empty.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
contacts?.map((contact) => (
|
||||||
|
<button
|
||||||
|
key={contact}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTarget(contact)}
|
||||||
|
className="block w-full p-2 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
<User.Provider pubkey={contact}>
|
||||||
|
<User.Root className="flex items-center gap-2">
|
||||||
|
<User.Avatar className="size-10 rounded-full" />
|
||||||
|
<User.Name className="font-medium" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ScrollArea.Viewport>
|
||||||
|
<ScrollArea.Scrollbar
|
||||||
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<ScrollArea.Thumb className="flex-1 bg-black/40 dark:bg-white/40 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||||
|
</ScrollArea.Scrollbar>
|
||||||
|
<ScrollArea.Corner className="bg-transparent" />
|
||||||
|
</ScrollArea.Root>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CurrentUser() {
|
function CurrentUser() {
|
||||||
const params = Route.useParams();
|
const params = Route.useParams();
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
@@ -201,6 +346,14 @@ function CurrentUser() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const menuItems = await Promise.all([
|
const menuItems = await Promise.all([
|
||||||
|
MenuItem.new({
|
||||||
|
text: "Contacts",
|
||||||
|
action: () =>
|
||||||
|
navigate({
|
||||||
|
to: "/$account/contacts",
|
||||||
|
params: { account: params.account },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Settings",
|
text: "Settings",
|
||||||
action: () => navigate({ to: "/" }),
|
action: () => navigate({ to: "/" }),
|
||||||
@@ -224,20 +377,16 @@ function CurrentUser() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 h-12 flex items-center justify-between px-3.5 border-t border-black/5 dark:border-white/5">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => showContextMenu(e)}
|
||||||
|
className="shrink-0 size-8 flex items-center justify-center rounded-full ring-1 ring-teal-500"
|
||||||
|
>
|
||||||
<User.Provider pubkey={params.account}>
|
<User.Provider pubkey={params.account}>
|
||||||
<User.Root className="inline-flex items-center gap-2">
|
<User.Root className="shrink-0">
|
||||||
<User.Avatar className="size-8 rounded-full" />
|
<User.Avatar className="size-7 rounded-full" />
|
||||||
<User.Name className="text-sm font-medium leading-tight" />
|
|
||||||
</User.Root>
|
</User.Root>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
<button
|
</button>
|
||||||
type="button"
|
|
||||||
onClick={(e) => showContextMenu(e)}
|
|
||||||
className="size-7 inline-flex items-center justify-center rounded-md text-neutral-700 dark:text-neutral-300 hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
>
|
|
||||||
<DotsThree className="size-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
63
src/routes/$account.contacts.lazy.tsx
Normal file
63
src/routes/$account.contacts.lazy.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { User } from "@/components/user";
|
||||||
|
import { X } from "@phosphor-icons/react";
|
||||||
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
|
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/$account/contacts")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const params = Route.useParams();
|
||||||
|
const contacts = Route.useLoaderData();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="overflow-hidden size-full flex flex-col"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="h-12 shrink-0 flex items-center justify-between px-3.5"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
<div className="text-sm font-semibold uppercase">Contact List</div>
|
||||||
|
<div className="inline-flex items-center justify-end">
|
||||||
|
<Link
|
||||||
|
to="/$account/chats/new"
|
||||||
|
params={{ account: params.account }}
|
||||||
|
className="size-7 inline-flex items-center justify-center rounded-md hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
>
|
||||||
|
<X className="size-5" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ScrollArea.Viewport className="relative h-full flex-1 px-3.5 pb-3.5">
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
{contacts.map((contact) => (
|
||||||
|
<Link
|
||||||
|
key={contact}
|
||||||
|
to="/$account/chats/$id"
|
||||||
|
params={{ account: params.account, id: contact }}
|
||||||
|
>
|
||||||
|
<User.Provider key={contact} pubkey={contact}>
|
||||||
|
<User.Root className="h-44 flex flex-col items-center justify-center gap-3 p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/5">
|
||||||
|
<User.Avatar className="size-16 rounded-full" />
|
||||||
|
<User.Name className="text-sm font-medium" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea.Viewport>
|
||||||
|
<ScrollArea.Scrollbar
|
||||||
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<ScrollArea.Thumb className="flex-1 bg-black/40 dark:bg-white/40 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||||
|
</ScrollArea.Scrollbar>
|
||||||
|
<ScrollArea.Corner className="bg-transparent" />
|
||||||
|
</ScrollArea.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/routes/$account.contacts.tsx
Normal file
14
src/routes/$account.contacts.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { commands } from "@/commands";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/$account/contacts")({
|
||||||
|
loader: async () => {
|
||||||
|
const res = await commands.getContactList();
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
return res.data;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute('/contacts')({
|
|
||||||
component: () => <div>Hello /contacts!</div>
|
|
||||||
})
|
|
||||||
@@ -2,7 +2,22 @@
|
|||||||
export default {
|
export default {
|
||||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
keyframes: {
|
||||||
|
overlay: {
|
||||||
|
from: { opacity: '0' },
|
||||||
|
to: { opacity: '1' },
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
from: { opacity: '0', transform: 'translate(-50%, -48%) scale(0.96)' },
|
||||||
|
to: { opacity: '1', transform: 'translate(-50%, -50%) scale(1)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
overlay: 'overlay 150ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
content: 'content 150ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user