feat: update ui and add compose dialog
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||
"@tanstack/query-sync-storage-persister": "^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':
|
||||
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)
|
||||
'@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':
|
||||
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)
|
||||
@@ -521,6 +524,19 @@ packages:
|
||||
'@types/react':
|
||||
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':
|
||||
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
|
||||
peerDependencies:
|
||||
@@ -530,6 +546,63 @@ packages:
|
||||
'@types/react':
|
||||
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':
|
||||
resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
|
||||
peerDependencies:
|
||||
@@ -587,6 +660,24 @@ packages:
|
||||
'@types/react':
|
||||
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':
|
||||
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
|
||||
peerDependencies:
|
||||
@@ -906,6 +997,10 @@ packages:
|
||||
arg@5.0.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -1005,6 +1100,9 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
detect-node-es@1.1.0:
|
||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
@@ -1066,6 +1164,10 @@ packages:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
get-nonce@1.0.1:
|
||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1330,6 +1432,36 @@ packages:
|
||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-pZZ3zHponPdVhwgOoQoI7qoHh+Hn3GAVc+/g4LICRv/XZ0IEJPsPOehV/Iu8Ssl/DoruEO4EdeK6kTSmcaAo8A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1463,6 +1595,9 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
tslib@2.6.3:
|
||||
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
|
||||
|
||||
types-react-dom@19.0.0-rc.1:
|
||||
resolution: {integrity: sha512-VSLZJl8VXCD0fAWp7DUTFUDCcZ8DVXOQmjhJMD03odgeFmu14ZQJHCXeETm3BEAhJqfgJaFkLnGkQv88sRx0fQ==}
|
||||
|
||||
@@ -1488,6 +1623,26 @@ packages:
|
||||
peerDependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
|
||||
peerDependencies:
|
||||
@@ -1938,12 +2093,81 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@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)':
|
||||
dependencies:
|
||||
react: 19.0.0-rc-d025ddd3-20240722
|
||||
optionalDependencies:
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@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:
|
||||
'@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)':
|
||||
dependencies:
|
||||
react: 19.0.0-rc-d025ddd3-20240722
|
||||
@@ -2277,6 +2515,10 @@ snapshots:
|
||||
|
||||
arg@5.0.2: {}
|
||||
|
||||
aria-hidden@1.2.4:
|
||||
dependencies:
|
||||
tslib: 2.6.3
|
||||
|
||||
autoprefixer@10.4.19(postcss@8.4.40):
|
||||
dependencies:
|
||||
browserslist: 4.23.2
|
||||
@@ -2386,6 +2628,8 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
detect-node-es@1.1.0: {}
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
dlv@1.1.3: {}
|
||||
@@ -2458,6 +2702,8 @@ snapshots:
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -2671,6 +2917,34 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
read-cache@1.0.0:
|
||||
@@ -2826,6 +3100,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.5.4
|
||||
|
||||
tslib@2.6.3: {}
|
||||
|
||||
types-react-dom@19.0.0-rc.1:
|
||||
dependencies:
|
||||
'@types/react': 18.3.3
|
||||
@@ -2851,6 +3127,21 @@ snapshots:
|
||||
escalade: 3.1.2
|
||||
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):
|
||||
dependencies:
|
||||
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]
|
||||
#[specta::specta]
|
||||
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);
|
||||
|
||||
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 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) => {
|
||||
stream::iter(events)
|
||||
.filter_map(|ev| async move {
|
||||
|
||||
@@ -27,6 +27,7 @@ fn main() {
|
||||
connect_account,
|
||||
get_accounts,
|
||||
get_metadata,
|
||||
get_contact_list,
|
||||
get_chats,
|
||||
get_chat_messages,
|
||||
connect_inbox,
|
||||
|
||||
@@ -47,6 +47,14 @@ try {
|
||||
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>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_chats") };
|
||||
|
||||
@@ -87,3 +87,10 @@ export function groupEventByDate(events: NostrEvent[]) {
|
||||
|
||||
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.profile?.picture ? (
|
||||
<Avatar.Image
|
||||
src={`//wsrv.nl/?url=${user.profile?.picture}&w=200&h=200`}
|
||||
alt={user.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
||||
className="w-full aspect-square object-cover outline-[.5px] outline-black/15"
|
||||
/>
|
||||
) : null}
|
||||
<Avatar.Fallback>
|
||||
<img
|
||||
src={fallback}
|
||||
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>
|
||||
</>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as AccountContactsImport } from './routes/$account.contacts'
|
||||
import { Route as AccountChatsIdImport } from './routes/$account.chats.$id'
|
||||
|
||||
// Create Virtual Routes
|
||||
@@ -22,7 +23,6 @@ const NostrConnectLazyImport = createFileRoute('/nostr-connect')()
|
||||
const NewLazyImport = createFileRoute('/new')()
|
||||
const ImportKeyLazyImport = createFileRoute('/import-key')()
|
||||
const CreateAccountLazyImport = createFileRoute('/create-account')()
|
||||
const ContactsLazyImport = createFileRoute('/contacts')()
|
||||
const AccountChatsLazyImport = createFileRoute('/$account/chats')()
|
||||
const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')()
|
||||
|
||||
@@ -50,11 +50,6 @@ const CreateAccountLazyRoute = CreateAccountLazyImport.update({
|
||||
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({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
@@ -67,6 +62,13 @@ const AccountChatsLazyRoute = AccountChatsLazyImport.update({
|
||||
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({
|
||||
path: '/new',
|
||||
getParentRoute: () => AccountChatsLazyRoute,
|
||||
@@ -77,7 +79,9 @@ const AccountChatsNewLazyRoute = AccountChatsNewLazyImport.update({
|
||||
const AccountChatsIdRoute = AccountChatsIdImport.update({
|
||||
path: '/$id',
|
||||
getParentRoute: () => AccountChatsLazyRoute,
|
||||
} as any)
|
||||
} as any).lazy(() =>
|
||||
import('./routes/$account.chats.$id.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
@@ -90,13 +94,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/contacts': {
|
||||
id: '/contacts'
|
||||
path: '/contacts'
|
||||
fullPath: '/contacts'
|
||||
preLoaderRoute: typeof ContactsLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/create-account': {
|
||||
id: '/create-account'
|
||||
path: '/create-account'
|
||||
@@ -125,6 +122,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof NostrConnectLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/$account/contacts': {
|
||||
id: '/$account/contacts'
|
||||
path: '/$account/contacts'
|
||||
fullPath: '/$account/contacts'
|
||||
preLoaderRoute: typeof AccountContactsImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/$account/chats': {
|
||||
id: '/$account/chats'
|
||||
path: '/$account/chats'
|
||||
@@ -153,11 +157,11 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
export const routeTree = rootRoute.addChildren({
|
||||
IndexRoute,
|
||||
ContactsLazyRoute,
|
||||
CreateAccountLazyRoute,
|
||||
ImportKeyLazyRoute,
|
||||
NewLazyRoute,
|
||||
NostrConnectLazyRoute,
|
||||
AccountContactsRoute,
|
||||
AccountChatsLazyRoute: AccountChatsLazyRoute.addChildren({
|
||||
AccountChatsIdRoute,
|
||||
AccountChatsNewLazyRoute,
|
||||
@@ -173,20 +177,17 @@ export const routeTree = rootRoute.addChildren({
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/contacts",
|
||||
"/create-account",
|
||||
"/import-key",
|
||||
"/new",
|
||||
"/nostr-connect",
|
||||
"/$account/contacts",
|
||||
"/$account/chats"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
"filePath": "index.tsx"
|
||||
},
|
||||
"/contacts": {
|
||||
"filePath": "contacts.lazy.tsx"
|
||||
},
|
||||
"/create-account": {
|
||||
"filePath": "create-account.lazy.tsx"
|
||||
},
|
||||
@@ -199,6 +200,9 @@ export const routeTree = rootRoute.addChildren({
|
||||
"/nostr-connect": {
|
||||
"filePath": "nostr-connect.lazy.tsx"
|
||||
},
|
||||
"/$account/contacts": {
|
||||
"filePath": "$account.contacts.tsx"
|
||||
},
|
||||
"/$account/chats": {
|
||||
"filePath": "$account.chats.lazy.tsx",
|
||||
"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 { 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")({
|
||||
beforeLoad: async ({ params }) => {
|
||||
const inbox: string[] = await invoke("connect_inbox", { id: params.id });
|
||||
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 { ago, cn } from "@/commons";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
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 { useQuery } from "@tanstack/react-query";
|
||||
import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import type { NostrEvent } from "nostr-tools";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback, useEffect, useState, useTransition } from "react";
|
||||
|
||||
type Payload = {
|
||||
event: string;
|
||||
@@ -28,7 +31,6 @@ function Screen() {
|
||||
>
|
||||
<Header />
|
||||
<ChatList />
|
||||
<CurrentUser />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 min-h-0 bg-white dark:bg-neutral-900 overflow-auto">
|
||||
<Outlet />
|
||||
@@ -38,24 +40,27 @@ function Screen() {
|
||||
}
|
||||
|
||||
function Header() {
|
||||
const { platform } = Route.useRouteContext();
|
||||
const { account } = Route.useParams();
|
||||
|
||||
return (
|
||||
<div
|
||||
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
|
||||
to="/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"
|
||||
to="/$account/contacts"
|
||||
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" />
|
||||
</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" />
|
||||
<CirclesFour className="size-4" />
|
||||
</Link>
|
||||
<Compose />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -83,7 +88,7 @@ function ChatList() {
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen("synchronized", async () => {
|
||||
await queryClient.refetchQueries({ queryKey: ["chats"] })
|
||||
await queryClient.refetchQueries({ queryKey: ["chats"] });
|
||||
});
|
||||
|
||||
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() {
|
||||
const params = Route.useParams();
|
||||
const navigate = Route.useNavigate();
|
||||
@@ -201,6 +346,14 @@ function CurrentUser() {
|
||||
e.preventDefault();
|
||||
|
||||
const menuItems = await Promise.all([
|
||||
MenuItem.new({
|
||||
text: "Contacts",
|
||||
action: () =>
|
||||
navigate({
|
||||
to: "/$account/contacts",
|
||||
params: { account: params.account },
|
||||
}),
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: "Settings",
|
||||
action: () => navigate({ to: "/" }),
|
||||
@@ -224,20 +377,16 @@ function CurrentUser() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="shrink-0 h-12 flex items-center justify-between px-3.5 border-t border-black/5 dark:border-white/5">
|
||||
<User.Provider pubkey={params.account}>
|
||||
<User.Root className="inline-flex items-center gap-2">
|
||||
<User.Avatar className="size-8 rounded-full" />
|
||||
<User.Name className="text-sm font-medium leading-tight" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<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"
|
||||
className="shrink-0 size-8 flex items-center justify-center rounded-full ring-1 ring-teal-500"
|
||||
>
|
||||
<DotsThree className="size-5" />
|
||||
<User.Provider pubkey={params.account}>
|
||||
<User.Root className="shrink-0">
|
||||
<User.Avatar className="size-7 rounded-full" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</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 {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
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: [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user