Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0820a9187 | ||
|
|
d6504a8170 | ||
|
|
4dffc4de20 | ||
|
|
0be73e8e82 | ||
|
|
15cefbd84f | ||
|
|
dd2f940e33 | ||
|
|
af740f462c | ||
|
|
703fe988bd | ||
|
|
187e53b3a2 | ||
|
|
b9093f49a0 | ||
|
|
aac0b7510a | ||
|
|
98eda38377 | ||
|
|
fab34de1ee | ||
|
|
9730837e00 | ||
|
|
ec34255df1 | ||
|
|
a262217ab2 | ||
|
|
c5d06a2492 | ||
|
|
c93edde7d2 | ||
|
|
5103126001 | ||
|
|
b0c49c5141 | ||
|
|
2ed2cb3afd | ||
|
|
2bcda1f2ef | ||
|
|
ca20bbd298 | ||
|
|
c6da06cd4d | ||
|
|
50bf6c04c1 | ||
|
|
490417771c | ||
|
|
0cb491eaf9 | ||
|
|
ece6bcc125 | ||
|
|
4b79e559d2 | ||
|
|
322e510db2 | ||
|
|
4e279f127d | ||
|
|
5655a8136d | ||
|
|
d80534c51f | ||
|
|
0b97248fb8 | ||
|
|
f54f448ecb | ||
|
|
bd1f2b899d | ||
|
|
efd3c83193 | ||
|
|
85fa1e2359 | ||
|
|
cd6ba5884f | ||
|
|
bfed56ba13 | ||
|
|
d1018ba8d1 | ||
|
|
a42542c16e | ||
|
|
26a6ec954c | ||
|
|
f346282c3c | ||
|
|
f3e875eeea | ||
|
|
6ad8ffddf0 | ||
|
|
d37e2a3c80 | ||
|
|
18e1ac0e6c | ||
|
|
aa4f21a869 | ||
|
|
bbc052eebc | ||
|
|
c201b5816c | ||
|
|
043cabfd4e | ||
|
|
9b02ab5842 | ||
|
|
618a45d349 | ||
|
|
11dcef4e87 | ||
|
|
d87371aec4 | ||
|
|
cfb017f70b | ||
|
|
9ba95301db | ||
|
|
0518389f50 | ||
|
|
eb6e3e52df | ||
|
|
1e95a2fd95 | ||
|
|
83d24351cd | ||
|
|
470dc1c759 | ||
|
|
42b780ce6a | ||
|
|
5ab2b1ae31 | ||
|
|
055d73c829 | ||
|
|
469296790e | ||
|
|
c032dbea1a | ||
|
|
172566028b | ||
|
|
cc7de41bfd | ||
|
|
ba9c81a10a | ||
|
|
e158f2e4d7 | ||
|
|
62bd689031 | ||
|
|
cb6006f596 | ||
|
|
adad048873 | ||
|
|
790fee6c05 | ||
|
|
9f5b14956a | ||
|
|
e04497d841 | ||
|
|
106c627ec4 | ||
|
|
c40762cc04 | ||
|
|
d2b5ae0507 | ||
|
|
8c6aea8050 | ||
|
|
090a815f99 | ||
|
|
d841163ba7 | ||
|
|
8398ae80d3 | ||
|
|
fe60f75e96 | ||
|
|
e098743d43 | ||
|
|
0c19ada1ab | ||
|
|
09db39fce1 | ||
|
|
f0fc89724d | ||
|
|
afa9327bb7 | ||
|
|
5c3644f977 | ||
|
|
0a8eed9a46 | ||
|
|
bacfaed48a | ||
|
|
3d5085785b | ||
|
|
9152c3e122 | ||
|
|
a5574bef6c | ||
|
|
2c7f3685b6 | ||
|
|
be0abc4075 | ||
|
|
dafe35cd1f | ||
|
|
b23903240b | ||
|
|
872a6cee36 | ||
|
|
ac7ce726c5 | ||
|
|
e5e290c0c3 | ||
|
|
2eab6f04c7 | ||
|
|
e06b0334a5 | ||
|
|
74d8bf2ead | ||
|
|
d128af1db8 | ||
|
|
f6eb5eea44 | ||
|
|
bca2e0b7b7 | ||
|
|
61ad96ca63 | ||
|
|
26ae473521 | ||
|
|
bcc5e18082 | ||
|
|
307fff7a53 | ||
|
|
ce7828310b | ||
|
|
beac1a189e | ||
|
|
4cb49d44c7 | ||
|
|
be16d5c21d | ||
|
|
da8162069b | ||
|
|
e2103ae23a | ||
|
|
4c6d1c768a | ||
|
|
9b75a04f91 | ||
|
|
a5255fa503 | ||
|
|
954a17b541 | ||
|
|
a55b31b0e6 | ||
|
|
bdf3ffd7bf | ||
|
|
07ce253f5b | ||
|
|
f3db010c74 | ||
|
|
dcf2791fe5 | ||
|
|
8fcf3551d8 | ||
|
|
2d987849d8 | ||
|
|
3b99926f3b | ||
|
|
113d69a4df | ||
|
|
5d12ba7216 | ||
|
|
72b59020b4 | ||
|
|
4c323b9daa | ||
|
|
72da83d648 | ||
|
|
783a4538a4 | ||
|
|
15e62cad11 | ||
|
|
c52b20ca80 | ||
|
|
04706a6d7c | ||
|
|
0755cbeb6c | ||
|
|
8eb01c8bbf | ||
|
|
ed4f89ff66 | ||
|
|
d9fe647f8e | ||
|
|
843c2d52e7 | ||
|
|
017a3676a4 | ||
|
|
fcb70c0e9a | ||
|
|
0fec21b9ce | ||
|
|
968b1ada94 | ||
|
|
5c9b599b1e | ||
|
|
717c3e17df | ||
|
|
a4540a0802 | ||
|
|
31bacc2646 | ||
|
|
6e5d0f0e76 | ||
|
|
f0712e5961 | ||
|
|
3fbd66dece | ||
|
|
1283432632 | ||
|
|
59eaaec903 | ||
|
|
4f0f210076 | ||
|
|
e4a317f038 | ||
|
|
9779d020c7 | ||
|
|
f8280ec8ee | ||
|
|
6c26f8967b | ||
|
|
e1424b851c | ||
|
|
d14e609579 | ||
|
|
8c0627ff27 | ||
|
|
18c133d096 | ||
|
|
0061ecea78 | ||
|
|
d01cf8319d | ||
|
|
843895d876 | ||
|
|
7c99ed39e4 | ||
|
|
71be59b2e9 | ||
|
|
1c20512ecc | ||
|
|
90342c552f | ||
|
|
b396c8a695 | ||
|
|
6996e30889 | ||
|
|
7ba793fad8 | ||
|
|
f11f836518 | ||
|
|
04fe0fcec8 | ||
|
|
799835a629 | ||
|
|
4e7da4108b | ||
|
|
7c7b082b3a | ||
|
|
38d6c51921 | ||
|
|
1738cbdd97 | ||
|
|
2e885b76a1 | ||
|
|
f94680e487 | ||
|
|
c682a58842 | ||
|
|
921cf871ee | ||
|
|
d5b1593aca | ||
|
|
6676b4e2a4 | ||
|
|
5f30ddcfca | ||
|
|
41d0de539d | ||
|
|
e254ee3203 | ||
|
|
6d42360549 | ||
|
|
70c5143445 | ||
|
|
41b66b18f5 | ||
|
|
dda0720ed4 | ||
|
|
4b60b39119 | ||
|
|
d2e5122d5a | ||
|
|
32f3315344 | ||
|
|
5ca9444358 | ||
|
|
4dc13385a5 | ||
|
|
b90ad1421f | ||
|
|
bba324ea53 | ||
|
|
7449000f5f | ||
|
|
dc7762ca11 | ||
|
|
3a3f960dde | ||
|
|
12e066ff2e | ||
|
|
fe4f965ed5 | ||
|
|
5d3f2264e9 | ||
|
|
407fe40b67 | ||
|
|
1f38eba2cc | ||
|
|
9b5867f80c | ||
|
|
cac774a0c1 | ||
|
|
82689bf3c3 | ||
|
|
f60e438a64 | ||
|
|
ca06f2b6ed | ||
|
|
99d9c70826 | ||
|
|
60afbf090b | ||
|
|
10ca4e6ff4 | ||
|
|
b0f387d029 | ||
|
|
1a8f750640 | ||
|
|
25523229a2 | ||
|
|
47835ed857 | ||
|
|
d84647bc6b | ||
|
|
7724eccd72 | ||
|
|
8ea2335225 | ||
|
|
b60d4db0df | ||
|
|
f1e17ff3c4 | ||
|
|
32954f17b6 | ||
|
|
cf70b0f882 | ||
|
|
135d0918b3 | ||
|
|
e1fbcf0460 | ||
|
|
99aaf3da82 | ||
|
|
3ef13e43f1 | ||
|
|
8939196ae4 | ||
|
|
571d4b4004 | ||
|
|
73f80f27fb | ||
|
|
b46a5cf68f | ||
|
|
8c0d03aed0 | ||
|
|
777eb15b4f | ||
|
|
c8e1b8b8bd | ||
|
|
437cd71f7e | ||
|
|
afb7c87fa3 | ||
|
|
c843626bca | ||
|
|
28337e5915 | ||
|
|
a4aef25adb | ||
|
|
61d1f095d4 | ||
|
|
f027eae52d | ||
|
|
174a3cc74e | ||
|
|
c00a7749b4 | ||
|
|
c755b8d137 | ||
|
|
17766d29d6 | ||
|
|
3b13dfeed8 | ||
|
|
17ba79e01b | ||
|
|
bafad544e9 | ||
|
|
89c36423ae | ||
|
|
cd31b99559 | ||
|
|
f3c52237fa | ||
|
|
413d8d82df | ||
|
|
2eb2010d43 | ||
|
|
94d400cab2 | ||
|
|
09b143cb08 | ||
|
|
e3ede34108 | ||
|
|
ed6aca41ea | ||
|
|
89f577fbef | ||
|
|
a14aeaeb55 | ||
|
|
35cf0abda4 | ||
|
|
8a7b246315 | ||
|
|
d98c6d0709 | ||
|
|
bda20e8fe8 | ||
|
|
c342daecc8 | ||
|
|
5e6692cd6d | ||
|
|
420be77b5c | ||
|
|
999073f84c | ||
|
|
174b28f1a7 | ||
|
|
763cb10e85 | ||
|
|
89bb8d88f6 | ||
|
|
09aa2ecafc | ||
|
|
7271e9ea87 | ||
|
|
a02e496b29 | ||
|
|
cbbf5eaf50 | ||
|
|
d3fa59d2b1 | ||
|
|
aa23e39334 | ||
|
|
c8e2018fd0 | ||
|
|
6e489f1c49 | ||
|
|
a49b88ab35 | ||
|
|
31839531ea | ||
|
|
ec0f3fabc0 | ||
|
|
dd7155a3a6 | ||
|
|
cb565ff35b | ||
|
|
5d59040224 | ||
|
|
7fabf949c6 | ||
|
|
ea5120e2f0 | ||
|
|
14f07dfea8 | ||
|
|
1de48cc640 | ||
|
|
f72eb456e8 | ||
|
|
05b52564e0 | ||
|
|
c8e014f33e | ||
|
|
46cc01e0ee | ||
|
|
16e6d234e5 | ||
|
|
3005d27403 | ||
|
|
1be84f3139 | ||
|
|
04bde7dd43 | ||
|
|
f1504d99ac | ||
|
|
e928f2ee37 | ||
|
|
ccab78ca11 | ||
|
|
5b13c3e45c | ||
|
|
e6b97ab9ae | ||
|
|
bb6badfed6 | ||
|
|
0c4b309a11 | ||
|
|
f4400d711f | ||
|
|
a3e46aa96b | ||
|
|
a4fdcfdf0b | ||
|
|
25d07303a3 | ||
|
|
0e1e7524c9 | ||
|
|
32d770eb91 | ||
|
|
e93cbf78d5 | ||
|
|
71a2290b8f | ||
|
|
95294a80cb | ||
|
|
8eaf47f6d2 | ||
|
|
86183d799a | ||
|
|
2dbfff3c17 | ||
|
|
d1f5c372ae | ||
|
|
13281455ed | ||
|
|
9cfb32be4d | ||
|
|
cb0672c22a | ||
|
|
ca0e041731 | ||
|
|
cfcb9bc6ed | ||
|
|
09df8672d0 | ||
|
|
2403231ac4 | ||
|
|
98ef1927f2 | ||
|
|
63db8b1423 | ||
|
|
2c8dd71792 | ||
|
|
fe73e1428b | ||
|
|
88a6c3c81f | ||
|
|
84584a4d1f | ||
|
|
64286aa354 | ||
|
|
e9ce932646 | ||
|
|
4c28b4879c | ||
|
|
9127d5c5ea | ||
|
|
090e54ec5a | ||
|
|
d0c9f93ebb | ||
|
|
e2cdc5b576 | ||
|
|
bfe35ad885 | ||
|
|
0f06522c28 | ||
|
|
f47eba5af7 | ||
|
|
f28a7ae82f | ||
|
|
296b11b7b8 | ||
|
|
a0d9a729dd | ||
|
|
1de8c7240d | ||
|
|
70126ef1b3 | ||
|
|
cdf29f8a54 | ||
|
|
6171b9bed1 | ||
|
|
60fd09000b | ||
|
|
4292def206 | ||
|
|
90f149b09c | ||
|
|
ed52105c02 | ||
|
|
1950cb59a2 | ||
|
|
9753e6d6b4 | ||
|
|
3488f05960 | ||
|
|
c809ab6b4e | ||
|
|
c6674c4a2d | ||
|
|
35c5b5fb78 | ||
|
|
739ba63e6c | ||
|
|
ec78cf8bf7 | ||
|
|
17052aeeaa | ||
|
|
d7bbda6e7b | ||
|
|
6a08c1de10 | ||
|
|
3c4bd39384 | ||
|
|
a4069dae99 | ||
|
|
55cd556cd6 | ||
|
|
a21da11a91 | ||
|
|
08fa7de01d | ||
|
|
2a58326cd1 | ||
|
|
8a17c36a5b | ||
|
|
7bd6f6a8db | ||
|
|
3ba870be4b | ||
|
|
bd2b6a3759 | ||
|
|
a3a8f57bfc | ||
|
|
fd393a4d30 | ||
|
|
e8bd48e51b | ||
|
|
0191180f31 | ||
|
|
60ed56b1b9 | ||
|
|
da722afed3 | ||
|
|
d8eb51e49c | ||
|
|
c700a45ab6 | ||
|
|
b806a34edb | ||
|
|
21989e6fa5 | ||
|
|
0539c5649d | ||
|
|
ad488ff72d | ||
|
|
02e0309a41 | ||
|
|
b7f4af7883 | ||
|
|
cc48a4f36b | ||
|
|
46ed3330fc | ||
|
|
1fa1872ca6 | ||
|
|
c389a23365 | ||
|
|
eaf9bda077 | ||
|
|
84a248a5a9 | ||
|
|
711c1d561a | ||
|
|
21210b4336 | ||
|
|
3bd480b75e | ||
|
|
2b19650e46 | ||
|
|
23482531c5 | ||
|
|
cfda9ba899 | ||
|
|
698bd78684 | ||
|
|
b97676dd3e | ||
|
|
25ae4f2201 | ||
|
|
59435ccd13 | ||
|
|
e81912c5e9 | ||
|
|
af1b4e60d3 | ||
|
|
648cbf6f80 | ||
|
|
7b06a82ee7 | ||
|
|
d18de93c60 | ||
|
|
df15eb7a03 | ||
|
|
06674df6cc | ||
|
|
8295625a44 | ||
|
|
b11e2a4291 | ||
|
|
353c18bb76 | ||
|
|
02b0c9e48a | ||
|
|
ff73c8ac88 | ||
|
|
bc48391a1a | ||
|
|
b0a443c002 | ||
|
|
bef1f136ad | ||
|
|
9ba584bf14 | ||
|
|
43509fc943 | ||
|
|
4a99eb94e2 | ||
|
|
74426e13c8 | ||
|
|
bd45c36072 | ||
|
|
c13aefcd15 | ||
|
|
167caee8bc | ||
|
|
d527078d5c | ||
|
|
763ace5ddf | ||
|
|
057c57b70f | ||
|
|
cb71786ac1 | ||
|
|
67afeac198 | ||
|
|
f4ee25de8e | ||
|
|
445a218a9e | ||
|
|
f09139ffbe | ||
|
|
446721729b | ||
|
|
e0250d7f5c | ||
|
|
9fcdac4edb | ||
|
|
b726ae3c7c | ||
|
|
a3460418f6 | ||
|
|
f65175f11e | ||
|
|
16efd495a0 | ||
|
|
ed6423e4aa | ||
|
|
0e9418949b | ||
|
|
240fe8bc7c | ||
|
|
c3482cddd8 | ||
|
|
d13e7b3ef6 | ||
|
|
47800bd2ff | ||
|
|
c0305db5fc | ||
|
|
0b745cb40e | ||
|
|
a20f5ca15d | ||
|
|
c29b4e173e | ||
|
|
33dd8b1d8a | ||
|
|
1503d90bd5 | ||
|
|
6581ffb92b | ||
|
|
939dfd9cc1 | ||
|
|
a98ffd4887 | ||
|
|
2e23b3ae06 |
@@ -1,70 +0,0 @@
|
|||||||
name: 'Publish'
|
|
||||||
on: workflow_dispatch
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
RUST_BACKTRACE: short
|
|
||||||
RUSTFLAGS: '-W unreachable-pub -W rust-2021-compatibility'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish-tauri:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
settings:
|
|
||||||
- platform: 'macos-latest'
|
|
||||||
args: '--target universal-apple-darwin'
|
|
||||||
- platform: 'ubuntu-22.04'
|
|
||||||
args: ''
|
|
||||||
- platform: 'windows-latest'
|
|
||||||
args: '--target x86_64-pc-windows-msvc'
|
|
||||||
runs-on: ${{ matrix.settings.platform }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: setup node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: aarch64-apple-darwin
|
|
||||||
- name: install dependencies (ubuntu only)
|
|
||||||
if: matrix.settings.platform == 'ubuntu-22.04'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y build-essential libssl-dev javascriptcoregtk-4.1 libayatana-appindicator3-dev libsoup-3.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev webkit2gtk-4.1 librsvg2-dev patchelf
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8.x.x
|
|
||||||
run_install: false
|
|
||||||
- name: Setup node and cache for package data
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 'lts/*'
|
|
||||||
cache: 'pnpm'
|
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
cache-on-failure: true
|
|
||||||
- run: pnpm install
|
|
||||||
- uses: tauri-apps/tauri-action@dev
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
|
||||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
||||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
||||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
with:
|
|
||||||
tagName: v__VERSION__
|
|
||||||
releaseName: 'v__VERSION__'
|
|
||||||
releaseBody: 'See the assets to download this version and install.'
|
|
||||||
releaseDraft: true
|
|
||||||
prerelease: false
|
|
||||||
args: ${{ matrix.settings.args }}
|
|
||||||
includeDebug: true
|
|
||||||
@@ -1,38 +1,23 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
dist/
|
||||||
|
|
||||||
# Dependencies
|
# These are backup files generated by rustfmt
|
||||||
node_modules
|
**/*.rs.bk
|
||||||
.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# Local env files
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
.env
|
*.pdb
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# Testing
|
# RustRover
|
||||||
coverage
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
.idea/
|
||||||
|
|
||||||
# Turbo
|
# Useless stuffs
|
||||||
.turbo
|
|
||||||
|
|
||||||
# Vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# Build Outputs
|
|
||||||
.next/
|
|
||||||
out/
|
|
||||||
build
|
|
||||||
dist
|
|
||||||
|
|
||||||
|
|
||||||
# Debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
# Added by goreleaser init:
|
||||||
|
.intentionally-empty-file.o
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
[submodule "modules/depot"]
|
|
||||||
path = modules/depot
|
|
||||||
url = https://github.com/luminous-devs/depot.git
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = ["crates/*"]
|
||||||
|
default-members = ["crates/lume"]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
# GPUI
|
||||||
|
gpui = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
gpui-component = { git = "https://github.com/longbridge/gpui-component" }
|
||||||
|
gpui_tokio = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
|
||||||
|
# Nostr
|
||||||
|
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
|
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
|
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
|
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "all-nips", "pow-multi-thread" ] }
|
||||||
|
|
||||||
|
# Others
|
||||||
|
anyhow = "1.0.44"
|
||||||
|
chrono = "0.4.38"
|
||||||
|
dirs = "5.0"
|
||||||
|
futures = "0.3"
|
||||||
|
itertools = "0.13.0"
|
||||||
|
log = "0.4"
|
||||||
|
reqwest = { version = "0.12", features = ["multipart", "stream", "json"] }
|
||||||
|
flume = { version = "0.12", default-features = false, features = ["async", "select"] }
|
||||||
|
rust-embed = "8.5.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
smallvec = "1.14.0"
|
||||||
|
smol = "2"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
@@ -1,190 +1,190 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
GNU General Public License for most of our software; it applies also to
|
GNU General Public License for most of our software; it applies also to
|
||||||
any other work released this way by its authors. You can apply it to
|
any other work released this way by its authors. You can apply it to
|
||||||
your programs, too.
|
your programs, too.
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
have the freedom to distribute copies of free software (and charge for
|
have the freedom to distribute copies of free software (and charge for
|
||||||
them if you wish), that you receive source code or can get it if you
|
them if you wish), that you receive source code or can get it if you
|
||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
To protect your rights, we need to prevent others from denying you
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
For example, if you distribute copies of such a program, whether
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
or can get the source code. And you must show them these terms so they
|
or can get the source code. And you must show them these terms so they
|
||||||
know their rights.
|
know their rights.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
that there is no warranty for this free software. For both users' and
|
that there is no warranty for this free software. For both users' and
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
changed, so that their problems will not be attributed erroneously to
|
changed, so that their problems will not be attributed erroneously to
|
||||||
authors of previous versions.
|
authors of previous versions.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
Some devices are designed to deny users access to install or run
|
||||||
modified versions of the software inside them, although the manufacturer
|
modified versions of the software inside them, although the manufacturer
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
protecting users' freedom to change the software. The systematic
|
protecting users' freedom to change the software. The systematic
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
products. If such problems arise substantially in other domains, we
|
products. If such problems arise substantially in other domains, we
|
||||||
stand ready to extend this provision to those domains in future versions
|
stand ready to extend this provision to those domains in future versions
|
||||||
of the GPL, as needed to protect the freedom of users.
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
Finally, every program is threatened constantly by software patents.
|
||||||
States should not allow patents to restrict development and use of
|
States should not allow patents to restrict development and use of
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
avoid the special danger that patents applied to a free program could
|
avoid the special danger that patents applied to a free program could
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
patents cannot be used to render the program non-free.
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
"recipients" may be individuals or organizations.
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
earlier work or a work "based on" the earlier work.
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
A "covered work" means either the unmodified Program or a work based
|
||||||
on the Program.
|
on the Program.
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
To "propagate" a work means to do anything with it that, without
|
||||||
permission, would make you directly or secondarily liable for
|
permission, would make you directly or secondarily liable for
|
||||||
infringement under applicable copyright law, except executing it on a
|
infringement under applicable copyright law, except executing it on a
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
distribution (with or without modification), making available to the
|
distribution (with or without modification), making available to the
|
||||||
public, and in some countries other activities as well.
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
To "convey" a work means any kind of propagation that enables other
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
to the extent that it includes a convenient and prominently visible
|
to the extent that it includes a convenient and prominently visible
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
tells the user that there is no warranty for the work (except to the
|
tells the user that there is no warranty for the work (except to the
|
||||||
extent that warranties are provided), that licensees may convey the
|
extent that warranties are provided), that licensees may convey the
|
||||||
work under this License, and how to view a copy of this License. If
|
work under this License, and how to view a copy of this License. If
|
||||||
the interface presents a list of user commands or options, such as a
|
the interface presents a list of user commands or options, such as a
|
||||||
menu, a prominent item in the list meets this criterion.
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
1. Source Code.
|
1. Source Code.
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
The "source code" for a work means the preferred form of the work
|
||||||
for making modifications to it. "Object code" means any non-source
|
for making modifications to it. "Object code" means any non-source
|
||||||
form of a work.
|
form of a work.
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
A "Standard Interface" means an interface that either is an official
|
||||||
standard defined by a recognized standards body, or, in the case of
|
standard defined by a recognized standards body, or, in the case of
|
||||||
interfaces specified for a particular programming language, one that
|
interfaces specified for a particular programming language, one that
|
||||||
is widely used among developers working in that language.
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
The "System Libraries" of an executable work include anything, other
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
packaging a Major Component, but which is not part of that Major
|
packaging a Major Component, but which is not part of that Major
|
||||||
Component, and (b) serves only to enable use of the work with that
|
Component, and (b) serves only to enable use of the work with that
|
||||||
Major Component, or to implement a Standard Interface for which an
|
Major Component, or to implement a Standard Interface for which an
|
||||||
implementation is available to the public in source code form. A
|
implementation is available to the public in source code form. A
|
||||||
"Major Component", in this context, means a major essential component
|
"Major Component", in this context, means a major essential component
|
||||||
(kernel, window system, and so on) of the specific operating system
|
(kernel, window system, and so on) of the specific operating system
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
produce the work, or an object code interpreter used to run it.
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
The "Corresponding Source" for a work in object code form means all
|
||||||
the source code needed to generate, install, and (for an executable
|
the source code needed to generate, install, and (for an executable
|
||||||
work) run the object code and to modify the work, including scripts to
|
work) run the object code and to modify the work, including scripts to
|
||||||
control those activities. However, it does not include the work's
|
control those activities. However, it does not include the work's
|
||||||
System Libraries, or general-purpose tools or generally available free
|
System Libraries, or general-purpose tools or generally available free
|
||||||
programs which are used unmodified in performing those activities but
|
programs which are used unmodified in performing those activities but
|
||||||
which are not part of the work. For example, Corresponding Source
|
which are not part of the work. For example, Corresponding Source
|
||||||
includes interface definition files associated with source files for
|
includes interface definition files associated with source files for
|
||||||
the work, and the source code for shared libraries and dynamically
|
the work, and the source code for shared libraries and dynamically
|
||||||
linked subprograms that the work is specifically designed to require,
|
linked subprograms that the work is specifically designed to require,
|
||||||
such as by intimate data communication or control flow between those
|
such as by intimate data communication or control flow between those
|
||||||
subprograms and other parts of the work.
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
The Corresponding Source need not include anything that users
|
||||||
can regenerate automatically from other parts of the Corresponding
|
can regenerate automatically from other parts of the Corresponding
|
||||||
Source.
|
Source.
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
The Corresponding Source for a work in source code form is that
|
||||||
same work.
|
same work.
|
||||||
|
|
||||||
2. Basic Permissions.
|
2. Basic Permissions.
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
All rights granted under this License are granted for the term of
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
permission to run the unmodified Program. The output from running a
|
permission to run the unmodified Program. The output from running a
|
||||||
covered work is covered by this License only if the output, given its
|
covered work is covered by this License only if the output, given its
|
||||||
content, constitutes a covered work. This License acknowledges your
|
content, constitutes a covered work. This License acknowledges your
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
You may make, run and propagate covered works that you do not
|
||||||
convey, without conditions so long as your license otherwise remains
|
convey, without conditions so long as your license otherwise remains
|
||||||
in force. You may convey covered works to others for the sole purpose
|
in force. You may convey covered works to others for the sole purpose
|
||||||
of having them make modifications exclusively for you, or provide you
|
of having them make modifications exclusively for you, or provide you
|
||||||
with facilities for running those works, provided that you comply with
|
with facilities for running those works, provided that you comply with
|
||||||
the terms of this License in conveying all material for which you do
|
the terms of this License in conveying all material for which you do
|
||||||
not control copyright. Those thus making or running the covered works
|
not control copyright. Those thus making or running the covered works
|
||||||
for you must do so exclusively on your behalf, under your direction
|
for you must do so exclusively on your behalf, under your direction
|
||||||
and control, on terms that prohibit them from making any copies of
|
and control, on terms that prohibit them from making any copies of
|
||||||
your copyrighted material outside their relationship with you.
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
Conveying under any other circumstances is permitted solely under
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
makes it unnecessary.
|
makes it unnecessary.
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
No covered work shall be deemed part of an effective technological
|
||||||
measure under any applicable law fulfilling obligations under article
|
measure under any applicable law fulfilling obligations under article
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
similar laws prohibiting or restricting circumvention of such
|
similar laws prohibiting or restricting circumvention of such
|
||||||
measures.
|
measures.
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
circumvention of technological measures to the extent such circumvention
|
circumvention of technological measures to the extent such circumvention
|
||||||
is effected by exercising rights under this License with respect to
|
is effected by exercising rights under this License with respect to
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
@@ -192,9 +192,9 @@ modification of the work as a means of enforcing, against the work's
|
|||||||
users, your or third parties' legal rights to forbid circumvention of
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
technological measures.
|
technological measures.
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
You may convey verbatim copies of the Program's source code as you
|
||||||
receive it, in any medium, provided that you conspicuously and
|
receive it, in any medium, provided that you conspicuously and
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
keep intact all notices stating that this License and any
|
keep intact all notices stating that this License and any
|
||||||
@@ -202,12 +202,12 @@ non-permissive terms added in accord with section 7 apply to the code;
|
|||||||
keep intact all notices of the absence of any warranty; and give all
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
recipients a copy of this License along with the Program.
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
You may charge any price or no price for each copy that you convey,
|
||||||
and you may offer support or warranty protection for a fee.
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
You may convey a work based on the Program, or the modifications to
|
||||||
produce it from the Program, in the form of source code under the
|
produce it from the Program, in the form of source code under the
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
@@ -232,19 +232,19 @@ terms of section 4, provided that you also meet all of these conditions:
|
|||||||
interfaces that do not display Appropriate Legal Notices, your
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
work need not make them do so.
|
work need not make them do so.
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
A compilation of a covered work with other separate and independent
|
||||||
works, which are not by their nature extensions of the covered work,
|
works, which are not by their nature extensions of the covered work,
|
||||||
and which are not combined with it such as to form a larger program,
|
and which are not combined with it such as to form a larger program,
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
used to limit the access or legal rights of the compilation's users
|
used to limit the access or legal rights of the compilation's users
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
in an aggregate does not cause this License to apply to the other
|
in an aggregate does not cause this License to apply to the other
|
||||||
parts of the aggregate.
|
parts of the aggregate.
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
You may convey a covered work in object code form under the terms
|
||||||
of sections 4 and 5, provided that you also convey the
|
of sections 4 and 5, provided that you also convey the
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
in one of these ways:
|
in one of these ways:
|
||||||
@@ -290,75 +290,75 @@ in one of these ways:
|
|||||||
Source of the work are being offered to the general public at no
|
Source of the work are being offered to the general public at no
|
||||||
charge under subsection 6d.
|
charge under subsection 6d.
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
A separable portion of the object code, whose source code is excluded
|
||||||
from the Corresponding Source as a System Library, need not be
|
from the Corresponding Source as a System Library, need not be
|
||||||
included in conveying the object code work.
|
included in conveying the object code work.
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
tangible personal property which is normally used for personal, family,
|
tangible personal property which is normally used for personal, family,
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
product received by a particular user, "normally used" refers to a
|
product received by a particular user, "normally used" refers to a
|
||||||
typical or common use of that class of product, regardless of the status
|
typical or common use of that class of product, regardless of the status
|
||||||
of the particular user or of the way in which the particular user
|
of the particular user or of the way in which the particular user
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
is a consumer product regardless of whether the product has substantial
|
is a consumer product regardless of whether the product has substantial
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
the only significant mode of use of the product.
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
"Installation Information" for a User Product means any methods,
|
||||||
procedures, authorization keys, or other information required to install
|
procedures, authorization keys, or other information required to install
|
||||||
and execute modified versions of a covered work in that User Product from
|
and execute modified versions of a covered work in that User Product from
|
||||||
a modified version of its Corresponding Source. The information must
|
a modified version of its Corresponding Source. The information must
|
||||||
suffice to ensure that the continued functioning of the modified object
|
suffice to ensure that the continued functioning of the modified object
|
||||||
code is in no case prevented or interfered with solely because
|
code is in no case prevented or interfered with solely because
|
||||||
modification has been made.
|
modification has been made.
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
If you convey an object code work under this section in, or with, or
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
part of a transaction in which the right of possession and use of the
|
part of a transaction in which the right of possession and use of the
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
by the Installation Information. But this requirement does not apply
|
by the Installation Information. But this requirement does not apply
|
||||||
if neither you nor any third party retains the ability to install
|
if neither you nor any third party retains the ability to install
|
||||||
modified object code on the User Product (for example, the work has
|
modified object code on the User Product (for example, the work has
|
||||||
been installed in ROM).
|
been installed in ROM).
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
The requirement to provide Installation Information does not include a
|
||||||
requirement to continue to provide support service, warranty, or updates
|
requirement to continue to provide support service, warranty, or updates
|
||||||
for a work that has been modified or installed by the recipient, or for
|
for a work that has been modified or installed by the recipient, or for
|
||||||
the User Product in which it has been modified or installed. Access to a
|
the User Product in which it has been modified or installed. Access to a
|
||||||
network may be denied when the modification itself materially and
|
network may be denied when the modification itself materially and
|
||||||
adversely affects the operation of the network or violates the rules and
|
adversely affects the operation of the network or violates the rules and
|
||||||
protocols for communication across the network.
|
protocols for communication across the network.
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
in accord with this section must be in a format that is publicly
|
in accord with this section must be in a format that is publicly
|
||||||
documented (and with an implementation available to the public in
|
documented (and with an implementation available to the public in
|
||||||
source code form), and must require no special password or key for
|
source code form), and must require no special password or key for
|
||||||
unpacking, reading or copying.
|
unpacking, reading or copying.
|
||||||
|
|
||||||
7. Additional Terms.
|
7. Additional Terms.
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
License by making exceptions from one or more of its conditions.
|
License by making exceptions from one or more of its conditions.
|
||||||
Additional permissions that are applicable to the entire Program shall
|
Additional permissions that are applicable to the entire Program shall
|
||||||
be treated as though they were included in this License, to the extent
|
be treated as though they were included in this License, to the extent
|
||||||
that they are valid under applicable law. If additional permissions
|
that they are valid under applicable law. If additional permissions
|
||||||
apply only to part of the Program, that part may be used separately
|
apply only to part of the Program, that part may be used separately
|
||||||
under those permissions, but the entire Program remains governed by
|
under those permissions, but the entire Program remains governed by
|
||||||
this License without regard to the additional permissions.
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
When you convey a copy of a covered work, you may at your option
|
||||||
remove any additional permissions from that copy, or from any part of
|
remove any additional permissions from that copy, or from any part of
|
||||||
it. (Additional permissions may be written to require their own
|
it. (Additional permissions may be written to require their own
|
||||||
removal in certain cases when you modify the work.) You may place
|
removal in certain cases when you modify the work.) You may place
|
||||||
additional permissions on material, added by you to a covered work,
|
additional permissions on material, added by you to a covered work,
|
||||||
for which you have or can give appropriate copyright permission.
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
Notwithstanding any other provision of this License, for material you
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
that material) supplement the terms of this License with terms:
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
@@ -385,74 +385,74 @@ that material) supplement the terms of this License with terms:
|
|||||||
any liability that these contractual assumptions directly impose on
|
any liability that these contractual assumptions directly impose on
|
||||||
those licensors and authors.
|
those licensors and authors.
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
All other non-permissive additional terms are considered "further
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
received it, or any part of it, contains a notice stating that it is
|
received it, or any part of it, contains a notice stating that it is
|
||||||
governed by this License along with a term that is a further
|
governed by this License along with a term that is a further
|
||||||
restriction, you may remove that term. If a license document contains
|
restriction, you may remove that term. If a license document contains
|
||||||
a further restriction but permits relicensing or conveying under this
|
a further restriction but permits relicensing or conveying under this
|
||||||
License, you may add to a covered work material governed by the terms
|
License, you may add to a covered work material governed by the terms
|
||||||
of that license document, provided that the further restriction does
|
of that license document, provided that the further restriction does
|
||||||
not survive such relicensing or conveying.
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
If you add terms to a covered work in accord with this section, you
|
||||||
must place, in the relevant source files, a statement of the
|
must place, in the relevant source files, a statement of the
|
||||||
additional terms that apply to those files, or a notice indicating
|
additional terms that apply to those files, or a notice indicating
|
||||||
where to find the applicable terms.
|
where to find the applicable terms.
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
form of a separately written license, or stated as exceptions;
|
form of a separately written license, or stated as exceptions;
|
||||||
the above requirements apply either way.
|
the above requirements apply either way.
|
||||||
|
|
||||||
8. Termination.
|
8. Termination.
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
You may not propagate or modify a covered work except as expressly
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
modify it is void, and will automatically terminate your rights under
|
modify it is void, and will automatically terminate your rights under
|
||||||
this License (including any patent licenses granted under the third
|
this License (including any patent licenses granted under the third
|
||||||
paragraph of section 11).
|
paragraph of section 11).
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
However, if you cease all violation of this License, then your
|
||||||
license from a particular copyright holder is reinstated (a)
|
license from a particular copyright holder is reinstated (a)
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
holder fails to notify you of the violation by some reasonable means
|
holder fails to notify you of the violation by some reasonable means
|
||||||
prior to 60 days after the cessation.
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
Moreover, your license from a particular copyright holder is
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
violation by some reasonable means, this is the first time you have
|
violation by some reasonable means, this is the first time you have
|
||||||
received notice of violation of this License (for any work) from that
|
received notice of violation of this License (for any work) from that
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
your receipt of the notice.
|
your receipt of the notice.
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
Termination of your rights under this section does not terminate the
|
||||||
licenses of parties who have received copies or rights from you under
|
licenses of parties who have received copies or rights from you under
|
||||||
this License. If your rights have been terminated and not permanently
|
this License. If your rights have been terminated and not permanently
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
material under section 10.
|
material under section 10.
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
You are not required to accept this License in order to receive or
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
to receive a copy likewise does not require acceptance. However,
|
to receive a copy likewise does not require acceptance. However,
|
||||||
nothing other than this License grants you permission to propagate or
|
nothing other than this License grants you permission to propagate or
|
||||||
modify any covered work. These actions infringe copyright if you do
|
modify any covered work. These actions infringe copyright if you do
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
Each time you convey a covered work, the recipient automatically
|
||||||
receives a license from the original licensors, to run, modify and
|
receives a license from the original licensors, to run, modify and
|
||||||
propagate that work, subject to this License. You are not responsible
|
propagate that work, subject to this License. You are not responsible
|
||||||
for enforcing compliance by third parties with this License.
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
An "entity transaction" is a transaction transferring control of an
|
||||||
organization, or substantially all assets of one, or subdividing an
|
organization, or substantially all assets of one, or subdividing an
|
||||||
organization, or merging organizations. If propagation of a covered
|
organization, or merging organizations. If propagation of a covered
|
||||||
work results from an entity transaction, each party to that
|
work results from an entity transaction, each party to that
|
||||||
transaction who receives a copy of the work also receives whatever
|
transaction who receives a copy of the work also receives whatever
|
||||||
licenses to the work the party's predecessor in interest had or could
|
licenses to the work the party's predecessor in interest had or could
|
||||||
@@ -460,43 +460,43 @@ give under the previous paragraph, plus a right to possession of the
|
|||||||
Corresponding Source of the work from the predecessor in interest, if
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
You may not impose any further restrictions on the exercise of the
|
||||||
rights granted or affirmed under this License. For example, you may
|
rights granted or affirmed under this License. For example, you may
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
rights granted under this License, and you may not initiate litigation
|
rights granted under this License, and you may not initiate litigation
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
sale, or importing the Program or any portion of it.
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
11. Patents.
|
11. Patents.
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
License of the Program or a work on which the Program is based. The
|
License of the Program or a work on which the Program is based. The
|
||||||
work thus licensed is called the contributor's "contributor version".
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
A contributor's "essential patent claims" are all patent claims
|
||||||
owned or controlled by the contributor, whether already acquired or
|
owned or controlled by the contributor, whether already acquired or
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
by this License, of making, using, or selling its contributor version,
|
by this License, of making, using, or selling its contributor version,
|
||||||
but do not include claims that would be infringed only as a
|
but do not include claims that would be infringed only as a
|
||||||
consequence of further modification of the contributor version. For
|
consequence of further modification of the contributor version. For
|
||||||
purposes of this definition, "control" includes the right to grant
|
purposes of this definition, "control" includes the right to grant
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
this License.
|
this License.
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
patent license under the contributor's essential patent claims, to
|
patent license under the contributor's essential patent claims, to
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
propagate the contents of its contributor version.
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
In the following three paragraphs, a "patent license" is any express
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
(such as an express permission to practice a patent or covenant not to
|
(such as an express permission to practice a patent or covenant not to
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
party means to make such an agreement or commitment not to enforce a
|
party means to make such an agreement or commitment not to enforce a
|
||||||
patent against the party.
|
patent against the party.
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
and the Corresponding Source of the work is not available for anyone
|
and the Corresponding Source of the work is not available for anyone
|
||||||
to copy, free of charge and under the terms of this License, through a
|
to copy, free of charge and under the terms of this License, through a
|
||||||
publicly available network server or other readily accessible means,
|
publicly available network server or other readily accessible means,
|
||||||
@@ -504,13 +504,13 @@ then you must either (1) cause the Corresponding Source to be so
|
|||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
consistent with the requirements of this License, to extend the patent
|
consistent with the requirements of this License, to extend the patent
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
covered work in a country, or your recipient's use of the covered work
|
covered work in a country, or your recipient's use of the covered work
|
||||||
in a country, would infringe one or more identifiable patents in that
|
in a country, would infringe one or more identifiable patents in that
|
||||||
country that you have reason to believe are valid.
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
If, pursuant to or in connection with a single transaction or
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
covered work, and grant a patent license to some of the parties
|
covered work, and grant a patent license to some of the parties
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
@@ -518,10 +518,10 @@ or convey a specific copy of the covered work, then the patent license
|
|||||||
you grant is automatically extended to all recipients of the covered
|
you grant is automatically extended to all recipients of the covered
|
||||||
work and works based on it.
|
work and works based on it.
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
A patent license is "discriminatory" if it does not include within
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
specifically granted under this License. You may not convey a covered
|
specifically granted under this License. You may not convey a covered
|
||||||
work if you are a party to an arrangement with a third party that is
|
work if you are a party to an arrangement with a third party that is
|
||||||
in the business of distributing software, under which you make payment
|
in the business of distributing software, under which you make payment
|
||||||
to the third party based on the extent of your activity of conveying
|
to the third party based on the extent of your activity of conveying
|
||||||
@@ -533,73 +533,73 @@ for and in connection with specific products or compilations that
|
|||||||
contain the covered work, unless you entered into that arrangement,
|
contain the covered work, unless you entered into that arrangement,
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
any implied license or other defenses to infringement that may
|
any implied license or other defenses to infringement that may
|
||||||
otherwise be available to you under applicable patent law.
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the special requirements of the GNU Affero General Public License,
|
||||||
section 13, concerning interaction through a network will apply to the
|
section 13, concerning interaction through a network will apply to the
|
||||||
combination as such.
|
combination as such.
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU General Public License from time to time. Such new versions will
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
Later license versions may give you additional or different
|
||||||
permissions. However, no additional obligations are imposed on any
|
permissions. However, no additional obligations are imposed on any
|
||||||
author or copyright holder as a result of your choosing to follow a
|
author or copyright holder as a result of your choosing to follow a
|
||||||
later version.
|
later version.
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
16. Limitation of Liability.
|
16. Limitation of Liability.
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
@@ -609,9 +609,9 @@ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
SUCH DAMAGES.
|
SUCH DAMAGES.
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
above cannot be given local legal effect according to their terms,
|
above cannot be given local legal effect according to their terms,
|
||||||
reviewing courts shall apply local law that most closely approximates
|
reviewing courts shall apply local law that most closely approximates
|
||||||
an absolute waiver of all civil liability in connection with the
|
an absolute waiver of all civil liability in connection with the
|
||||||
@@ -622,11 +622,11 @@ copy of the Program in return for a fee.
|
|||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
If you develop a new program, and you want it to be of the greatest
|
||||||
possible use to the public, the best way to achieve this is to make it
|
possible use to the public, the best way to achieve this is to make it
|
||||||
free software which everyone can redistribute and change under these terms.
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
To do so, attach the following notices to the program. It is safest
|
||||||
to attach them to the start of each source file to most effectively
|
to attach them to the start of each source file to most effectively
|
||||||
state the exclusion of warranty; and each file should have at least
|
state the exclusion of warranty; and each file should have at least
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
@@ -649,7 +649,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If the program does terminal interaction, make it output a short
|
||||||
notice like this when it starts in an interactive mode:
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
<program> Copyright (C) <year> <name of author>
|
||||||
@@ -658,17 +658,17 @@ notice like this when it starts in an interactive mode:
|
|||||||
under certain conditions; type `show c' for details.
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
parts of the General Public License. Of course, your program's commands
|
parts of the General Public License. Of course, your program's commands
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
The GNU General Public License does not permit incorporating your program
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
may consider it more useful to permit linking proprietary applications with
|
may consider it more useful to permit linking proprietary applications with
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License. But first, please read
|
Public License instead of this License. But first, please read
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
|||||||
@@ -1,50 +1 @@
|
|||||||
### Introduction
|
### Rebooting...
|
||||||
|
|
||||||
Lume is a nostr client
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
Download Lume for your platform here: [https://github.com/luminous-devs/lume/releases](https://github.com/luminous-devs/lume/releases)
|
|
||||||
|
|
||||||
Supported platform: macOS, Windows and Linux
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- PNPM or Bun (experiment)
|
|
||||||
|
|
||||||
- Tauri: https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-macos
|
|
||||||
|
|
||||||
### Develop
|
|
||||||
|
|
||||||
Clone project
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/luminous-devs/lume.git && cd lume
|
|
||||||
```
|
|
||||||
|
|
||||||
Install packages
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Run dev build
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm tauri dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Generate production build
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm tauri build
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Nix
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
1. [Install Nix](https://zero-to-flakes.com/install)
|
|
||||||
1. [Setup `direnv`](https://zero-to-flakes.com/direnv)
|
|
||||||
|
|
||||||
`cd` into the root folder of the project to enter `nix develop` shell. Run `direnv allow` (only once). Then run `pnpm` or `bun` (experimental) commands as described above.
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Lume</title>
|
|
||||||
</head>
|
|
||||||
<body
|
|
||||||
class="relative w-screen h-screen overflow-hidden font-sans antialiased cursor-default select-none text-neutral-950 dark:text-neutral-50"
|
|
||||||
>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="./src/main.jsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "lume",
|
|
||||||
"private": true,
|
|
||||||
"version": "3.0.0",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@columns/antenas": "workspace:^",
|
|
||||||
"@columns/default": "workspace:^",
|
|
||||||
"@columns/group": "workspace:^",
|
|
||||||
"@columns/hashtag": "workspace:^",
|
|
||||||
"@columns/thread": "workspace:^",
|
|
||||||
"@columns/timeline": "workspace:^",
|
|
||||||
"@columns/user": "workspace:^",
|
|
||||||
"@getalby/sdk": "^3.2.3",
|
|
||||||
"@lume/ark": "workspace:^",
|
|
||||||
"@lume/icons": "workspace:^",
|
|
||||||
"@lume/storage": "workspace:^",
|
|
||||||
"@lume/ui": "workspace:^",
|
|
||||||
"@lume/utils": "workspace:^",
|
|
||||||
"@nostr-dev-kit/ndk": "^2.3.2",
|
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
|
||||||
"@radix-ui/react-hover-card": "^1.0.7",
|
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
|
||||||
"@tanstack/react-query": "^5.17.9",
|
|
||||||
"@tauri-apps/api": "2.0.0-alpha.13",
|
|
||||||
"@tauri-apps/plugin-autostart": "2.0.0-alpha.5",
|
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-alpha.5",
|
|
||||||
"@tauri-apps/plugin-dialog": "2.0.0-alpha.5",
|
|
||||||
"@tauri-apps/plugin-fs": "2.0.0-alpha.6",
|
|
||||||
"@tauri-apps/plugin-http": "2.0.0-alpha.6",
|
|
||||||
"@tauri-apps/plugin-notification": "2.0.0-alpha.5",
|
|
||||||
"@tauri-apps/plugin-os": "2.0.0-alpha.6",
|
|
||||||
"@tauri-apps/plugin-process": "2.0.0-alpha.5",
|
|
||||||
"@tauri-apps/plugin-shell": "2.0.0-alpha.5",
|
|
||||||
"@tauri-apps/plugin-sql": "2.0.0-alpha.5",
|
|
||||||
"@tauri-apps/plugin-updater": "2.0.0-alpha.5",
|
|
||||||
"@tauri-apps/plugin-upload": "2.0.0-alpha.5",
|
|
||||||
"@vidstack/react": "^1.9.8",
|
|
||||||
"framer-motion": "^10.18.0",
|
|
||||||
"jotai": "^2.6.1",
|
|
||||||
"minidenticons": "^4.2.0",
|
|
||||||
"nanoid": "^5.0.4",
|
|
||||||
"nostr-fetch": "^0.15.0",
|
|
||||||
"nostr-tools": "^1.17.0",
|
|
||||||
"qrcode.react": "^3.1.0",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-currency-input-field": "^3.6.14",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-hook-form": "^7.49.3",
|
|
||||||
"react-router-dom": "^6.21.2",
|
|
||||||
"smol-toml": "^1.1.3",
|
|
||||||
"sonner": "^1.3.1",
|
|
||||||
"unique-names-generator": "^4.7.1",
|
|
||||||
"virtua": "^0.20.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@lume/tailwindcss": "workspace:^",
|
|
||||||
"@lume/tsconfig": "workspace:^",
|
|
||||||
"@lume/types": "workspace:^",
|
|
||||||
"@types/node": "^20.11.0",
|
|
||||||
"@types/react": "^18.2.47",
|
|
||||||
"@types/react-dom": "^18.2.18",
|
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
||||||
"autoprefixer": "^10.4.16",
|
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"encoding": "^0.1.13",
|
|
||||||
"postcss": "^8.4.33",
|
|
||||||
"tailwind-merge": "^2.2.0",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"typescript": "^5.3.3",
|
|
||||||
"vite": "^5.0.11",
|
|
||||||
"vite-plugin-top-level-await": "^1.4.1",
|
|
||||||
"vite-tsconfig-paths": "^4.2.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 416 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 381 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 986 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 473 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 341 KiB |
@@ -1,41 +0,0 @@
|
|||||||
/* Vidstack */
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
.break-p {
|
|
||||||
word-break: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose :where(iframe):not(:where([class~='not-prose'] *)) {
|
|
||||||
@apply w-full h-auto mx-auto aspect-video;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
@apply cursor-default no-underline !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
@apply cursor-default focus:outline-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input::-ms-reveal,
|
|
||||||
input::-ms-clear {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-input-placeholder {
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border {
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { ColumnProvider, LumeProvider } from "@lume/ark";
|
|
||||||
import { StorageProvider } from "@lume/storage";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
import { Toaster } from "sonner";
|
|
||||||
import Router from "./router";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // 10 seconds
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<Toaster position="top-center" theme="system" closeButton />
|
|
||||||
<StorageProvider>
|
|
||||||
<LumeProvider>
|
|
||||||
<ColumnProvider>
|
|
||||||
<Router />
|
|
||||||
</ColumnProvider>
|
|
||||||
</LumeProvider>
|
|
||||||
</StorageProvider>
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { createRoot } from "react-dom/client";
|
|
||||||
import App from "./app";
|
|
||||||
import "./app.css";
|
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
|
||||||
const root = createRoot(container);
|
|
||||||
|
|
||||||
root.render(<App />);
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { AppLayout, AuthLayout, HomeLayout, SettingsLayout } from "@lume/ui";
|
|
||||||
import { fetch } from "@tauri-apps/plugin-http";
|
|
||||||
import {
|
|
||||||
RouterProvider,
|
|
||||||
createBrowserRouter,
|
|
||||||
defer,
|
|
||||||
redirect,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import { ErrorScreen } from "./routes/error";
|
|
||||||
|
|
||||||
export default function Router() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
|
||||||
{
|
|
||||||
element: <AppLayout platform={storage.platform} />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
element: <HomeLayout />,
|
|
||||||
errorElement: <ErrorScreen />,
|
|
||||||
loader: async () => {
|
|
||||||
if (!ark.account) return redirect("auth");
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
async lazy() {
|
|
||||||
const { HomeScreen } = await import("./routes/home");
|
|
||||||
return { Component: HomeScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "settings",
|
|
||||||
element: <SettingsLayout />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
async lazy() {
|
|
||||||
const { UserSettingScreen } = await import(
|
|
||||||
"./routes/settings"
|
|
||||||
);
|
|
||||||
return { Component: UserSettingScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "edit-profile",
|
|
||||||
async lazy() {
|
|
||||||
const { EditProfileScreen } = await import(
|
|
||||||
"./routes/settings/editProfile"
|
|
||||||
);
|
|
||||||
return { Component: EditProfileScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "edit-contact",
|
|
||||||
async lazy() {
|
|
||||||
const { EditContactScreen } = await import(
|
|
||||||
"./routes/settings/editContact"
|
|
||||||
);
|
|
||||||
return { Component: EditContactScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "general",
|
|
||||||
async lazy() {
|
|
||||||
const { GeneralSettingScreen } = await import(
|
|
||||||
"./routes/settings/general"
|
|
||||||
);
|
|
||||||
return { Component: GeneralSettingScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "backup",
|
|
||||||
async lazy() {
|
|
||||||
const { BackupSettingScreen } = await import(
|
|
||||||
"./routes/settings/backup"
|
|
||||||
);
|
|
||||||
return { Component: BackupSettingScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "advanced",
|
|
||||||
async lazy() {
|
|
||||||
const { AdvancedSettingScreen } = await import(
|
|
||||||
"./routes/settings/advanced"
|
|
||||||
);
|
|
||||||
return { Component: AdvancedSettingScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "nwc",
|
|
||||||
async lazy() {
|
|
||||||
const { NWCScreen } = await import("./routes/settings/nwc");
|
|
||||||
return { Component: NWCScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "about",
|
|
||||||
async lazy() {
|
|
||||||
const { AboutScreen } = await import(
|
|
||||||
"./routes/settings/about"
|
|
||||||
);
|
|
||||||
return { Component: AboutScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "activity",
|
|
||||||
async lazy() {
|
|
||||||
const { ActivityScreen } = await import("./routes/activty");
|
|
||||||
return { Component: ActivityScreen };
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ":id",
|
|
||||||
async lazy() {
|
|
||||||
const { ActivityIdScreen } = await import(
|
|
||||||
"./routes/activty/id"
|
|
||||||
);
|
|
||||||
return { Component: ActivityIdScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "relays",
|
|
||||||
async lazy() {
|
|
||||||
const { RelaysScreen } = await import("./routes/relays");
|
|
||||||
return { Component: RelaysScreen };
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
async lazy() {
|
|
||||||
const { RelayGlobalScreen } = await import(
|
|
||||||
"./routes/relays/global"
|
|
||||||
);
|
|
||||||
return { Component: RelayGlobalScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "follows",
|
|
||||||
async lazy() {
|
|
||||||
const { RelayFollowsScreen } = await import(
|
|
||||||
"./routes/relays/follows"
|
|
||||||
);
|
|
||||||
return { Component: RelayFollowsScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ":url",
|
|
||||||
loader: async ({ request, params }) => {
|
|
||||||
return defer({
|
|
||||||
relay: fetch(`https://${params.url}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/nostr+json",
|
|
||||||
},
|
|
||||||
signal: request.signal,
|
|
||||||
}).then((res) => res.json()),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async lazy() {
|
|
||||||
const { RelayUrlScreen } = await import("./routes/relays/url");
|
|
||||||
return { Component: RelayUrlScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "depot",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
loader: () => {
|
|
||||||
const depot = storage.checkDepot();
|
|
||||||
if (!depot) return redirect("/depot/onboarding/");
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
async lazy() {
|
|
||||||
const { DepotScreen } = await import("./routes/depot");
|
|
||||||
return { Component: DepotScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "onboarding",
|
|
||||||
async lazy() {
|
|
||||||
const { DepotOnboardingScreen } = await import(
|
|
||||||
"./routes/depot/onboarding"
|
|
||||||
);
|
|
||||||
return { Component: DepotOnboardingScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "auth",
|
|
||||||
element: <AuthLayout platform={storage.platform} />,
|
|
||||||
errorElement: <ErrorScreen />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
async lazy() {
|
|
||||||
const { WelcomeScreen } = await import("./routes/auth/welcome");
|
|
||||||
return { Component: WelcomeScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "create",
|
|
||||||
loader: async () => {
|
|
||||||
return await ark.getOAuthServices();
|
|
||||||
},
|
|
||||||
async lazy() {
|
|
||||||
const { CreateAccountScreen } = await import(
|
|
||||||
"./routes/auth/create"
|
|
||||||
);
|
|
||||||
return { Component: CreateAccountScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "login",
|
|
||||||
async lazy() {
|
|
||||||
const { LoginScreen } = await import("./routes/auth/login");
|
|
||||||
return { Component: LoginScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "login-key",
|
|
||||||
async lazy() {
|
|
||||||
const { LoginWithKey } = await import("./routes/auth/login-key");
|
|
||||||
return { Component: LoginWithKey };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "login-nsecbunker",
|
|
||||||
async lazy() {
|
|
||||||
const { LoginWithNsecbunker } = await import(
|
|
||||||
"./routes/auth/login-nsecbunker"
|
|
||||||
);
|
|
||||||
return { Component: LoginWithNsecbunker };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "login-oauth",
|
|
||||||
async lazy() {
|
|
||||||
const { LoginWithOAuth } = await import(
|
|
||||||
"./routes/auth/login-oauth"
|
|
||||||
);
|
|
||||||
return { Component: LoginWithOAuth };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "onboarding",
|
|
||||||
async lazy() {
|
|
||||||
const { OnboardingScreen } = await import(
|
|
||||||
"./routes/auth/onboarding"
|
|
||||||
);
|
|
||||||
return { Component: OnboardingScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "tutorials/note",
|
|
||||||
async lazy() {
|
|
||||||
const { TutorialNoteScreen } = await import(
|
|
||||||
"./routes/auth/tutorials/note"
|
|
||||||
);
|
|
||||||
return { Component: TutorialNoteScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "tutorials/widget",
|
|
||||||
async lazy() {
|
|
||||||
const { TutorialWidgetScreen } = await import(
|
|
||||||
"./routes/auth/tutorials/widget"
|
|
||||||
);
|
|
||||||
return { Component: TutorialWidgetScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "tutorials/posting",
|
|
||||||
async lazy() {
|
|
||||||
const { TutorialPostingScreen } = await import(
|
|
||||||
"./routes/auth/tutorials/posting"
|
|
||||||
);
|
|
||||||
return { Component: TutorialPostingScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "tutorials/finish",
|
|
||||||
async lazy() {
|
|
||||||
const { TutorialFinishScreen } = await import(
|
|
||||||
"./routes/auth/tutorials/finish"
|
|
||||||
);
|
|
||||||
return { Component: TutorialFinishScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RouterProvider
|
|
||||||
router={router}
|
|
||||||
fallbackElement={
|
|
||||||
<div className="flex items-center justify-center w-full h-full">
|
|
||||||
<LoaderIcon className="w-6 h-6 animate-spin" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
future={{ v7_startTransition: true }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { User } from "@lume/ark";
|
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function ActivityRepost({ event }: { event: NDKEvent }) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
to={`/activity/${event.id}`}
|
|
||||||
className="block px-5 py-4 border-b border-black/5 dark:border-white/5 hover:bg-black/10 dark:hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={event.pubkey}>
|
|
||||||
<User.Root className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<User.Avatar className="size-8 rounded-lg shrink-0" />
|
|
||||||
<div className="inline-flex items-center gap-1.5">
|
|
||||||
<User.Name className="max-w-[8rem] font-semibold text-neutral-950 dark:text-neutral-50" />
|
|
||||||
<p className="shrink-0">reposted</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<User.Time
|
|
||||||
time={event.created_at}
|
|
||||||
className="text-neutral-500 dark:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { User } from "@lume/ark";
|
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function ActivityText({ event }: { event: NDKEvent }) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
to={`/activity/${event.id}`}
|
|
||||||
className="block px-5 py-4 border-b border-black/5 dark:border-white/5 hover:bg-black/10 dark:hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={event.pubkey}>
|
|
||||||
<User.Root className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<User.Avatar className="size-8 rounded-lg shrink-0" />
|
|
||||||
<div className="inline-flex items-center gap-1.5">
|
|
||||||
<User.Name className="max-w-[8rem] font-semibold text-neutral-950 dark:text-neutral-50" />
|
|
||||||
<p className="shrink-0">mention you</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<User.Time
|
|
||||||
time={event.created_at}
|
|
||||||
className="text-neutral-500 dark:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { User } from "@lume/ark";
|
|
||||||
import { compactNumber } from "@lume/utils";
|
|
||||||
import { NDKEvent, zapInvoiceFromEvent } from "@nostr-dev-kit/ndk";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function ActivityZap({ event }: { event: NDKEvent }) {
|
|
||||||
const invoice = zapInvoiceFromEvent(event);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
to={`/activity/${event.id}`}
|
|
||||||
className="block px-5 py-4 border-b border-black/5 dark:border-white/5 hover:bg-black/10 dark:hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={event.pubkey}>
|
|
||||||
<User.Root className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<User.Avatar className="size-8 rounded-lg shrink-0" />
|
|
||||||
<div className="inline-flex items-center gap-1.5">
|
|
||||||
<User.Name className="max-w-[8rem] font-semibold text-neutral-950 dark:text-neutral-50" />
|
|
||||||
<p className="shrink-0">
|
|
||||||
zapped {compactNumber.format(invoice.amount)} sats
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<User.Time
|
|
||||||
time={event.created_at}
|
|
||||||
className="text-neutral-500 dark:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { FETCH_LIMIT } from "@lume/utils";
|
|
||||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
|
||||||
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { useCallback, useMemo } from "react";
|
|
||||||
import { ActivityRepost } from "./activityRepost";
|
|
||||||
import { ActivityText } from "./activityText";
|
|
||||||
import { ActivityZap } from "./activityZap";
|
|
||||||
|
|
||||||
export function ActivityList() {
|
|
||||||
const ark = useArk();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
|
||||||
useInfiniteQuery({
|
|
||||||
queryKey: ["activity"],
|
|
||||||
initialPageParam: 0,
|
|
||||||
queryFn: async ({
|
|
||||||
signal,
|
|
||||||
pageParam,
|
|
||||||
}: {
|
|
||||||
signal: AbortSignal;
|
|
||||||
pageParam: number;
|
|
||||||
}) => {
|
|
||||||
const events = await ark.getInfiniteEvents({
|
|
||||||
filter: {
|
|
||||||
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Zap],
|
|
||||||
"#p": [ark.account.pubkey],
|
|
||||||
},
|
|
||||||
limit: FETCH_LIMIT,
|
|
||||||
pageParam,
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
return events;
|
|
||||||
},
|
|
||||||
getNextPageParam: (lastPage) => {
|
|
||||||
const lastEvent = lastPage.at(-1);
|
|
||||||
if (!lastEvent) return;
|
|
||||||
return lastEvent.created_at - 1;
|
|
||||||
},
|
|
||||||
initialData: () => {
|
|
||||||
const queryCacheData = queryClient.getQueryState(["activity"])
|
|
||||||
?.data as NDKEvent[];
|
|
||||||
if (queryCacheData) {
|
|
||||||
return {
|
|
||||||
pageParams: [undefined, 1],
|
|
||||||
pages: [queryCacheData],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
staleTime: 360 * 1000,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const allEvents = useMemo(
|
|
||||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderEvenKind = useCallback(
|
|
||||||
(event: NDKEvent) => {
|
|
||||||
if (event.pubkey === ark.account.pubkey) return null;
|
|
||||||
switch (event.kind) {
|
|
||||||
case NDKKind.Text:
|
|
||||||
return <ActivityText key={event.id} event={event} />;
|
|
||||||
case NDKKind.Repost:
|
|
||||||
return <ActivityRepost key={event.id} event={event} />;
|
|
||||||
case NDKKind.Zap:
|
|
||||||
return <ActivityZap key={event.id} event={event} />;
|
|
||||||
default:
|
|
||||||
return <ActivityText key={event.id} event={event} />;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : !allEvents.length ? (
|
|
||||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
|
||||||
<p className="mb-2 text-2xl">🎉</p>
|
|
||||||
<p className="text-center font-medium">Yo! Nothing new yet.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
allEvents.map((event) => renderEvenKind(event))
|
|
||||||
)}
|
|
||||||
<div className="flex items-center justify-center h-16 px-5">
|
|
||||||
{hasNextPage ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => fetchNextPage()}
|
|
||||||
disabled={!hasNextPage || isFetchingNextPage}
|
|
||||||
className="inline-flex items-center justify-center w-full h-12 gap-2 font-medium bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20 rounded-xl focus:outline-none"
|
|
||||||
>
|
|
||||||
{isFetchingNextPage ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArrowRightCircleIcon className="size-5" />
|
|
||||||
Load more
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { Note, useEvent } from "@lume/ark";
|
|
||||||
|
|
||||||
export function ActivityRootNote({ eventId }: { eventId: string }) {
|
|
||||||
const { isLoading, isError, data } = useEvent(eventId);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="relative flex gap-3">
|
|
||||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
|
||||||
<div className="h-4 w-full animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
return (
|
|
||||||
<div className="relative flex gap-3">
|
|
||||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
|
||||||
Failed to fetch event
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Note.Provider event={data}>
|
|
||||||
<Note.Root>
|
|
||||||
<div className="flex items-center justify-between px-3 h-14">
|
|
||||||
<Note.User className="flex-1 pr-1" />
|
|
||||||
</div>
|
|
||||||
<Note.Content className="min-w-0 px-3" />
|
|
||||||
<div className="flex items-center justify-between px-3 h-14">
|
|
||||||
<Note.Pin />
|
|
||||||
<div className="inline-flex items-center gap-10" />
|
|
||||||
</div>
|
|
||||||
</Note.Root>
|
|
||||||
</Note.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { User } from "@lume/ark";
|
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
|
||||||
import { ActivityRootNote } from "./rootNote";
|
|
||||||
|
|
||||||
export function ActivitySingleRepost({ event }: { event: NDKEvent }) {
|
|
||||||
const repostId = event.tags.find((el) => el[0] === "e")[1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="pb-3 flex flex-col">
|
|
||||||
<div className="h-14 shrink-0 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
|
|
||||||
<h3 className="text-center font-semibold leading-tight">Boost</h3>
|
|
||||||
<p className="text-sm text-blue-500 font-medium leading-tight">
|
|
||||||
@ Someone has reposted to your note
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-h-0">
|
|
||||||
<div className="max-w-xl mx-auto py-6 flex flex-col items-center gap-6">
|
|
||||||
<User.Provider pubkey={event.pubkey}>
|
|
||||||
<User.Root>
|
|
||||||
<User.Avatar className="size-10 shrink-0 rounded-lg object-cover" />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<div className="flex flex-col items-center gap-3">
|
|
||||||
<div className="h-4 w-px bg-blue-500" />
|
|
||||||
<h3 className="font-semibold">Reposted</h3>
|
|
||||||
<div className="h-4 w-px bg-blue-500" />
|
|
||||||
</div>
|
|
||||||
<ActivityRootNote eventId={repostId} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { Note, useArk } from "@lume/ark";
|
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
|
||||||
import { ActivityRootNote } from "./rootNote";
|
|
||||||
|
|
||||||
export function ActivitySingleText({ event }: { event: NDKEvent }) {
|
|
||||||
const ark = useArk();
|
|
||||||
const thread = ark.getEventThread({
|
|
||||||
content: event.content,
|
|
||||||
tags: event.tags,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full flex flex-col justify-between">
|
|
||||||
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
|
|
||||||
<h3 className="text-center font-semibold leading-tight">
|
|
||||||
Conversation
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-blue-500 font-medium leading-tight">
|
|
||||||
@ Someone has replied to your note
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-y-auto">
|
|
||||||
<div className="max-w-xl mx-auto py-6">
|
|
||||||
{thread ? (
|
|
||||||
<div className="flex flex-col gap-3 mb-1">
|
|
||||||
<ActivityRootNote eventId={thread.rootEventId} />
|
|
||||||
<ActivityRootNote eventId={thread.replyEventId} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="mt-3 flex flex-col gap-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<p className="text-teal-500 font-medium">New reply</p>
|
|
||||||
<div className="flex-1 h-px bg-teal-300" />
|
|
||||||
<div className="w-4 shrink-0 h-px bg-teal-300" />
|
|
||||||
</div>
|
|
||||||
<Note.Provider event={event}>
|
|
||||||
<Note.Root>
|
|
||||||
<div className="flex items-center justify-between px-3 h-14">
|
|
||||||
<Note.User className="flex-1 pr-1" />
|
|
||||||
</div>
|
|
||||||
<Note.Content className="min-w-0 px-3" />
|
|
||||||
<div className="flex items-center justify-between px-3 h-14">
|
|
||||||
<Note.Pin />
|
|
||||||
<div className="inline-flex items-center gap-10" />
|
|
||||||
</div>
|
|
||||||
</Note.Root>
|
|
||||||
</Note.Provider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { User } from "@lume/ark";
|
|
||||||
import { compactNumber } from "@lume/utils";
|
|
||||||
import { NDKEvent, zapInvoiceFromEvent } from "@nostr-dev-kit/ndk";
|
|
||||||
import { ActivityRootNote } from "./rootNote";
|
|
||||||
|
|
||||||
export function ActivitySingleZap({ event }: { event: NDKEvent }) {
|
|
||||||
const zapEventId = event.tags.find((el) => el[0] === "e")[1];
|
|
||||||
const invoice = zapInvoiceFromEvent(event);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full flex flex-col justify-between">
|
|
||||||
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
|
|
||||||
<h3 className="text-center font-semibold leading-tight">
|
|
||||||
Conversation
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-blue-500 font-medium leading-tight">
|
|
||||||
@ Someone has replied to your note
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-h-0">
|
|
||||||
<div className="max-w-xl mx-auto py-6 flex flex-col items-center gap-6">
|
|
||||||
<User.Provider pubkey={event.pubkey}>
|
|
||||||
<User.Root>
|
|
||||||
<User.Avatar className="size-10 shrink-0 rounded-lg object-cover" />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<div className="flex flex-col items-center gap-3">
|
|
||||||
<div className="h-4 w-px bg-blue-500" />
|
|
||||||
<h3 className="font-semibold">
|
|
||||||
Zap you {compactNumber.format(invoice.amount)} sats for
|
|
||||||
</h3>
|
|
||||||
<div className="h-4 w-px bg-blue-500" />
|
|
||||||
</div>
|
|
||||||
<ActivityRootNote eventId={zapEventId} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { useEvent } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { NDKKind } from "@nostr-dev-kit/ndk";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { ActivitySingleRepost } from "./components/singleRepost";
|
|
||||||
import { ActivitySingleText } from "./components/singleText";
|
|
||||||
import { ActivitySingleZap } from "./components/singleZap";
|
|
||||||
|
|
||||||
export function ActivityIdScreen() {
|
|
||||||
const { id } = useParams();
|
|
||||||
const { isLoading, data } = useEvent(id);
|
|
||||||
|
|
||||||
if (isLoading || !data) {
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.kind === NDKKind.Text) return <ActivitySingleText event={data} />;
|
|
||||||
if (data.kind === NDKKind.Zap) return <ActivitySingleZap event={data} />;
|
|
||||||
if (data.kind === NDKKind.Repost)
|
|
||||||
return <ActivitySingleRepost event={data} />;
|
|
||||||
|
|
||||||
return <ActivitySingleText event={data} />;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Outlet } from "react-router-dom";
|
|
||||||
import { ActivityList } from "./components/list";
|
|
||||||
|
|
||||||
export function ActivityScreen() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full rounded-xl shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:shadow-none dark:ring-1 dark:ring-white/10">
|
|
||||||
<div className="h-full flex flex-col w-96 shrink-0 rounded-l-xl bg-white/50 backdrop-blur-xl dark:bg-black/50">
|
|
||||||
<div className="h-14 shrink-0 flex items-center px-5 text-lg font-semibold border-b border-black/10 dark:border-white/10">
|
|
||||||
Activity
|
|
||||||
</div>
|
|
||||||
<ActivityList />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 rounded-r-xl bg-white pb-20 dark:bg-black">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { onboardingAtom } from "@lume/utils";
|
|
||||||
import NDK, {
|
|
||||||
NDKEvent,
|
|
||||||
NDKKind,
|
|
||||||
NDKNip46Signer,
|
|
||||||
NDKPrivateKeySigner,
|
|
||||||
} from "@nostr-dev-kit/ndk";
|
|
||||||
import * as Select from "@radix-ui/react-select";
|
|
||||||
import { downloadDir } from "@tauri-apps/api/path";
|
|
||||||
import { Window } from "@tauri-apps/api/window";
|
|
||||||
import { save } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
|
||||||
import { useSetAtom } from "jotai";
|
|
||||||
import { getPublicKey, nip19 } from "nostr-tools";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { useLoaderData, useNavigate } from "react-router-dom";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
const Item = ({ event }: { event: NDKEvent }) => {
|
|
||||||
const domain = JSON.parse(event.content).nip05.replace("_@", "");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select.Item
|
|
||||||
value={event.id}
|
|
||||||
className="relative flex items-center pr-10 leading-none rounded-md select-none text-neutral-100 rounded-mg h-9 pl-7"
|
|
||||||
>
|
|
||||||
<Select.ItemText>@{domain}</Select.ItemText>
|
|
||||||
<Select.ItemIndicator className="absolute left-0 inline-flex items-center justify-center transform h-7">
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</Select.ItemIndicator>
|
|
||||||
</Select.Item>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CreateAccountScreen() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const services = useLoaderData() as NDKEvent[];
|
|
||||||
const setOnboarding = useSetAtom(onboardingAtom);
|
|
||||||
|
|
||||||
const [serviceId, setServiceId] = useState(services?.[0]?.id);
|
|
||||||
const [loading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { isValid },
|
|
||||||
} = useForm();
|
|
||||||
|
|
||||||
const getDomainName = (id: string) => {
|
|
||||||
const event = services.find((ev) => ev.id === id);
|
|
||||||
return JSON.parse(event.content).nip05.replace("_@", "") as string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateNostrKeys = async () => {
|
|
||||||
const signer = NDKPrivateKeySigner.generate();
|
|
||||||
const pubkey = getPublicKey(signer.privateKey);
|
|
||||||
|
|
||||||
const npub = nip19.npubEncode(pubkey);
|
|
||||||
const nsec = nip19.nsecEncode(signer.privateKey);
|
|
||||||
|
|
||||||
ark.updateNostrSigner({ signer });
|
|
||||||
|
|
||||||
const downloadPath = await downloadDir();
|
|
||||||
const fileName = `nostr_keys_${new Date().getTime().toString(36)}.txt`;
|
|
||||||
const filePath = await save({
|
|
||||||
defaultPath: `${downloadPath}/${fileName}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (filePath) {
|
|
||||||
await writeTextFile(
|
|
||||||
filePath,
|
|
||||||
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${npub}\nPrivate key: ${nsec}`,
|
|
||||||
);
|
|
||||||
} // else { user cancel action }
|
|
||||||
|
|
||||||
await storage.createAccount({
|
|
||||||
pubkey: pubkey,
|
|
||||||
privkey: signer.privateKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
setOnboarding(true);
|
|
||||||
|
|
||||||
return navigate("/auth/onboarding", { replace: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (data: { username: string; email: string }) => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const domain = getDomainName(serviceId);
|
|
||||||
const service = services.find((ev) => ev.id === serviceId);
|
|
||||||
|
|
||||||
// generate ndk for nsecbunker
|
|
||||||
const localSigner = NDKPrivateKeySigner.generate();
|
|
||||||
const bunker = new NDK({
|
|
||||||
explicitRelayUrls: [
|
|
||||||
"wss://relay.nsecbunker.com/",
|
|
||||||
"wss://nostr.vulpem.com/",
|
|
||||||
],
|
|
||||||
});
|
|
||||||
await bunker.connect(2000);
|
|
||||||
|
|
||||||
// generate tmp remote singer for create account
|
|
||||||
const remoteSigner = new NDKNip46Signer(
|
|
||||||
bunker,
|
|
||||||
service.pubkey,
|
|
||||||
localSigner,
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle auth url request
|
|
||||||
let authWindow: Window;
|
|
||||||
remoteSigner.addListener("authUrl", (authUrl: string) => {
|
|
||||||
authWindow = new Window(`auth-${serviceId}`, {
|
|
||||||
url: authUrl,
|
|
||||||
title: domain,
|
|
||||||
titleBarStyle: "overlay",
|
|
||||||
width: 415,
|
|
||||||
height: 600,
|
|
||||||
center: true,
|
|
||||||
closable: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// create new account
|
|
||||||
const account = await remoteSigner.createAccount(
|
|
||||||
data.username,
|
|
||||||
domain,
|
|
||||||
data.email,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
authWindow.close();
|
|
||||||
setIsLoading(false);
|
|
||||||
|
|
||||||
return toast.error("Failed to create new account, try again later");
|
|
||||||
}
|
|
||||||
|
|
||||||
authWindow.close();
|
|
||||||
|
|
||||||
// add account to storage
|
|
||||||
await storage.createSetting("nsecbunker", "1");
|
|
||||||
const dbAccount = await storage.createAccount({
|
|
||||||
pubkey: account,
|
|
||||||
privkey: localSigner.privateKey,
|
|
||||||
});
|
|
||||||
ark.account = dbAccount;
|
|
||||||
|
|
||||||
// get final signer with newly created account
|
|
||||||
const finalSigner = new NDKNip46Signer(bunker, account, localSigner);
|
|
||||||
await finalSigner.blockUntilReady();
|
|
||||||
|
|
||||||
// update main ndk instance signer
|
|
||||||
ark.updateNostrSigner({ signer: finalSigner });
|
|
||||||
|
|
||||||
// remove default nsecbunker profile and contact list
|
|
||||||
await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] });
|
|
||||||
await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] });
|
|
||||||
|
|
||||||
setOnboarding(true);
|
|
||||||
setIsLoading(false);
|
|
||||||
|
|
||||||
return navigate("/auth/onboarding", { replace: true });
|
|
||||||
} catch (e) {
|
|
||||||
setIsLoading(false);
|
|
||||||
toast.error(String(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center justify-center w-full h-full">
|
|
||||||
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
|
|
||||||
<div className="flex flex-col gap-1 text-center items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">
|
|
||||||
Let's get you set up on Nostr.
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
|
|
||||||
With an account on Nostr, you'll be able to travel across all nostr
|
|
||||||
clients, all your data are synced.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{!services ? (
|
|
||||||
<div className="flex items-center justify-center w-full">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="flex flex-col gap-3 mb-0"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-6 p-5 bg-neutral-950 rounded-2xl">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label
|
|
||||||
htmlFor="username"
|
|
||||||
className="text-sm font-semibold uppercase text-neutral-600"
|
|
||||||
>
|
|
||||||
Username *
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center justify-between w-full gap-2 bg-neutral-900 rounded-xl">
|
|
||||||
<input
|
|
||||||
type={"text"}
|
|
||||||
{...register("username", {
|
|
||||||
required: true,
|
|
||||||
minLength: 1,
|
|
||||||
})}
|
|
||||||
spellCheck={false}
|
|
||||||
placeholder="satoshi"
|
|
||||||
className="flex-1 min-w-0 text-xl bg-transparent border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-14 ring-0 placeholder:text-neutral-600"
|
|
||||||
/>
|
|
||||||
<Select.Root value={serviceId} onValueChange={setServiceId}>
|
|
||||||
<Select.Trigger className="inline-flex items-center justify-end gap-2 pr-3 text-xl font-semibold text-blue-500 w-max shrink-0">
|
|
||||||
<Select.Value>@{getDomainName(serviceId)}</Select.Value>
|
|
||||||
<Select.Icon>
|
|
||||||
<ChevronDownIcon className="size-5" />
|
|
||||||
</Select.Icon>
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Portal>
|
|
||||||
<Select.Content className="border rounded-lg bg-neutral-950 border-neutral-900">
|
|
||||||
<Select.Viewport className="p-3">
|
|
||||||
<Select.Group>
|
|
||||||
<Select.Label className="mb-2 text-sm font-medium uppercase px-7 text-neutral-600">
|
|
||||||
Public handles
|
|
||||||
</Select.Label>
|
|
||||||
{services.map((service) => (
|
|
||||||
<Item key={service.id} event={service} />
|
|
||||||
))}
|
|
||||||
</Select.Group>
|
|
||||||
</Select.Viewport>
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Portal>
|
|
||||||
</Select.Root>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label
|
|
||||||
htmlFor="email"
|
|
||||||
className="text-sm font-semibold uppercase text-neutral-600"
|
|
||||||
>
|
|
||||||
Backup Email (Optional)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={"email"}
|
|
||||||
{...register("email", { required: false })}
|
|
||||||
spellCheck={false}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect="none"
|
|
||||||
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isValid}
|
|
||||||
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Create Account"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-neutral-900" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center">
|
|
||||||
<span className="px-2 font-medium bg-black text-neutral-600">
|
|
||||||
Or
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={generateNostrKeys}
|
|
||||||
className="mb-2 inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
|
|
||||||
>
|
|
||||||
Generate Nostr Keys
|
|
||||||
</button>
|
|
||||||
<p className="text-sm text-center text-neutral-500">
|
|
||||||
If you are using this option, please make sure keep your keys
|
|
||||||
in safe place. You{" "}
|
|
||||||
<span className="text-red-600">cannot recover</span> if it
|
|
||||||
lost, all your data will be{" "}
|
|
||||||
<span className="text-red-600">lost forever.</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { getPublicKey, nip19 } from "nostr-tools";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function LoginWithKey() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [showKey, setShowKey] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
setError,
|
|
||||||
formState: { errors, isValid },
|
|
||||||
} = useForm();
|
|
||||||
|
|
||||||
const onSubmit = async (data: { nsec: string }) => {
|
|
||||||
try {
|
|
||||||
if (!data.nsec.startsWith("nsec1"))
|
|
||||||
return toast.error("You need to enter a private key start with nsec1");
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const privkey = nip19.decode(data.nsec).data as string;
|
|
||||||
const pubkey = getPublicKey(privkey);
|
|
||||||
|
|
||||||
const account = await storage.createAccount({
|
|
||||||
pubkey: pubkey,
|
|
||||||
privkey: privkey,
|
|
||||||
});
|
|
||||||
ark.account = account;
|
|
||||||
|
|
||||||
return navigate("/auth/onboarding", { replace: true });
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
setError("nsec", {
|
|
||||||
type: "manual",
|
|
||||||
message: String(e),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center justify-center w-full h-full">
|
|
||||||
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
|
|
||||||
<div className="flex flex-col gap-1 text-center items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">Enter your Private Key</h1>
|
|
||||||
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
|
|
||||||
Lume will put your private key to{" "}
|
|
||||||
<span className="text-teal-500">
|
|
||||||
{storage.platform === "macos"
|
|
||||||
? "Apple Keychain"
|
|
||||||
: storage.platform === "windows"
|
|
||||||
? "Credential Manager"
|
|
||||||
: "Secret Service"}
|
|
||||||
</span>
|
|
||||||
.
|
|
||||||
<br />
|
|
||||||
It will be secured by your OS.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="flex flex-col gap-4 mb-0"
|
|
||||||
>
|
|
||||||
<div className="relative flex flex-col gap-1">
|
|
||||||
<input
|
|
||||||
type={showKey ? "text" : "password"}
|
|
||||||
{...register("nsec", { required: false })}
|
|
||||||
spellCheck={false}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect="none"
|
|
||||||
placeholder="nsec1..."
|
|
||||||
className="pl-3 pr-11 text-xl border-transparent rounded-xl h-14 bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
{errors.nsec && (
|
|
||||||
<p className="text-sm text-center text-red-600">
|
|
||||||
{errors.nsec.message as string}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowKey((state) => !state)}
|
|
||||||
className="absolute right-2 top-2 size-10 inline-flex items-center justify-center rounded-lg text-white bg-neutral-900 hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{showKey ? (
|
|
||||||
<EyeOnIcon className="size-5" />
|
|
||||||
) : (
|
|
||||||
<EyeOffIcon className="size-5" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isValid || loading}
|
|
||||||
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Continue"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|
||||||
import { nip19 } from "nostr-tools";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function LoginWithNsecbunker() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
setError,
|
|
||||||
formState: { errors, isValid },
|
|
||||||
} = useForm();
|
|
||||||
|
|
||||||
const onSubmit = async (data: { npub: string }) => {
|
|
||||||
try {
|
|
||||||
if (!data.npub.startsWith("npub1"))
|
|
||||||
return toast.info("You need to enter a token start with npub1");
|
|
||||||
|
|
||||||
if (!data.npub.includes("#"))
|
|
||||||
return toast.info("Token must include #secret");
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const bunker = new NDK({
|
|
||||||
explicitRelayUrls: [
|
|
||||||
"wss://relay.nsecbunker.com",
|
|
||||||
"wss://nostr.vulpem.com",
|
|
||||||
],
|
|
||||||
});
|
|
||||||
await bunker.connect(2000);
|
|
||||||
|
|
||||||
const pubkey = nip19.decode(data.npub.split("#")[0]).data as string;
|
|
||||||
const localSigner = NDKPrivateKeySigner.generate();
|
|
||||||
const remoteSigner = new NDKNip46Signer(bunker, data.npub, localSigner);
|
|
||||||
await remoteSigner.blockUntilReady();
|
|
||||||
|
|
||||||
ark.updateNostrSigner({ signer: remoteSigner });
|
|
||||||
|
|
||||||
await storage.createSetting("nsecbunker", "1");
|
|
||||||
const account = await storage.createAccount({
|
|
||||||
pubkey: pubkey,
|
|
||||||
privkey: localSigner.privateKey,
|
|
||||||
});
|
|
||||||
ark.account = account;
|
|
||||||
|
|
||||||
return navigate("/auth/onboarding", { replace: true });
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
setError("npub", {
|
|
||||||
type: "manual",
|
|
||||||
message: String(e),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center justify-center w-full h-full">
|
|
||||||
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
|
|
||||||
<div className="flex flex-col gap-1 text-center items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">
|
|
||||||
Enter your nsecbunker token
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="flex flex-col gap-4 mb-0"
|
|
||||||
>
|
|
||||||
<div className="relative flex flex-col gap-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{...register("npub", { required: false })}
|
|
||||||
spellCheck={false}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect="none"
|
|
||||||
placeholder="npub1...#..."
|
|
||||||
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
{errors.npub && (
|
|
||||||
<p className="text-sm text-center text-red-600">
|
|
||||||
{errors.npub.message as string}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isValid || loading}
|
|
||||||
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Continue"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { NIP05 } from "@lume/types";
|
|
||||||
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|
||||||
import { Window } from "@tauri-apps/api/window";
|
|
||||||
import { fetch } from "@tauri-apps/plugin-http";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
|
|
||||||
|
|
||||||
export function LoginWithOAuth() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
setError,
|
|
||||||
formState: { errors, isValid },
|
|
||||||
} = useForm();
|
|
||||||
|
|
||||||
const onSubmit = async (data: { nip05: string }) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (!emailRegex.test(data.nip05)) {
|
|
||||||
setLoading(false);
|
|
||||||
return toast.error(
|
|
||||||
"Cannot verify your NIP-05 address, please try again later.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const localPath = data.nip05.split("@")[0];
|
|
||||||
const service = data.nip05.split("@")[1];
|
|
||||||
|
|
||||||
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
|
|
||||||
|
|
||||||
const req = await fetch(verifyURL, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!req.ok) {
|
|
||||||
setLoading(false);
|
|
||||||
return toast.error(
|
|
||||||
"Cannot verify your NIP-05 address, please try again later.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res: NIP05 = await req.json();
|
|
||||||
|
|
||||||
if (!res.names[localPath.toLowerCase()] || !res.names[localPath]) {
|
|
||||||
setLoading(false);
|
|
||||||
return toast.error(
|
|
||||||
"Cannot verify your NIP-05 address, please try again later.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pubkey =
|
|
||||||
(res.names[localPath] as string) ||
|
|
||||||
(res.names[localPath.toLowerCase()] as string);
|
|
||||||
|
|
||||||
if (!res.nip46[pubkey]) {
|
|
||||||
setLoading(false);
|
|
||||||
return toast.error("Cannot found NIP-46 with this address");
|
|
||||||
}
|
|
||||||
|
|
||||||
const nip46Relays = res.nip46[pubkey] as unknown as string[];
|
|
||||||
|
|
||||||
const bunker = new NDK({
|
|
||||||
explicitRelayUrls: nip46Relays || [
|
|
||||||
"wss://relay.nsecbunker.com",
|
|
||||||
"wss://nostr.vulpem.com",
|
|
||||||
],
|
|
||||||
});
|
|
||||||
await bunker.connect(2000);
|
|
||||||
|
|
||||||
const localSigner = NDKPrivateKeySigner.generate();
|
|
||||||
const remoteSigner = new NDKNip46Signer(bunker, pubkey, localSigner);
|
|
||||||
|
|
||||||
// handle auth url request
|
|
||||||
let authWindow: Window;
|
|
||||||
remoteSigner.addListener("authUrl", (authUrl: string) => {
|
|
||||||
authWindow = new Window(`auth-${pubkey}`, {
|
|
||||||
url: authUrl,
|
|
||||||
title: "Login",
|
|
||||||
titleBarStyle: "overlay",
|
|
||||||
width: 415,
|
|
||||||
height: 600,
|
|
||||||
center: true,
|
|
||||||
closable: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const remoteUser = await remoteSigner.blockUntilReady();
|
|
||||||
|
|
||||||
if (remoteUser) {
|
|
||||||
authWindow.close();
|
|
||||||
|
|
||||||
ark.updateNostrSigner({ signer: remoteSigner });
|
|
||||||
|
|
||||||
await storage.createSetting("nsecbunker", "1");
|
|
||||||
const account = await storage.createAccount({
|
|
||||||
pubkey,
|
|
||||||
privkey: localSigner.privateKey,
|
|
||||||
});
|
|
||||||
ark.account = account;
|
|
||||||
|
|
||||||
return navigate("/auth/onboarding", { replace: true });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
setError("nip05", {
|
|
||||||
type: "manual",
|
|
||||||
message: String(e),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center justify-center w-full h-full">
|
|
||||||
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
|
|
||||||
<div className="flex flex-col gap-1 text-center items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">Enter your NIP-05 address</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="flex flex-col gap-4 mb-0"
|
|
||||||
>
|
|
||||||
<div className="relative flex flex-col gap-1">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
{...register("nip05", { required: false })}
|
|
||||||
spellCheck={false}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect="none"
|
|
||||||
placeholder="satoshi@nostr.me"
|
|
||||||
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
{errors.nip05 && (
|
|
||||||
<p className="text-sm text-center text-red-600">
|
|
||||||
{errors.nip05.message as string}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isValid || loading}
|
|
||||||
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Continue"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function LoginScreen() {
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center justify-center w-full h-full">
|
|
||||||
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
|
|
||||||
<div className="flex flex-col gap-1 text-center items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">
|
|
||||||
Continue your experience on Nostr
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Link
|
|
||||||
to="/auth/login-oauth"
|
|
||||||
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Login with address
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/auth/login-nsecbunker"
|
|
||||||
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
|
|
||||||
>
|
|
||||||
Login with nsecbunker
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-neutral-900" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center">
|
|
||||||
<span className="px-2 font-medium bg-black text-neutral-600">
|
|
||||||
Or (Not recommend)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Link
|
|
||||||
to="/auth/login-key"
|
|
||||||
className="mb-2 inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
|
|
||||||
>
|
|
||||||
Login with Private Key
|
|
||||||
</Link>
|
|
||||||
<p className="text-sm text-center text-neutral-500">
|
|
||||||
Lume will store your Private Key in{" "}
|
|
||||||
<span className="text-teal-600">OS Secure Storage</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { InfoIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { TranslateRegisterModal } from "@lume/ui";
|
|
||||||
import { FETCH_LIMIT } from "@lume/utils";
|
|
||||||
import { NDKKind } from "@nostr-dev-kit/ndk";
|
|
||||||
import * as Switch from "@radix-ui/react-switch";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import {
|
|
||||||
isPermissionGranted,
|
|
||||||
requestPermission,
|
|
||||||
} from "@tauri-apps/plugin-notification";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function OnboardingScreen() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [apiKey, setAPIKey] = useState("");
|
|
||||||
const [settings, setSettings] = useState({
|
|
||||||
notification: false,
|
|
||||||
lowPower: false,
|
|
||||||
translation: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleLowPower = async () => {
|
|
||||||
await storage.createSetting("lowPower", String(+!settings.lowPower));
|
|
||||||
setSettings((state) => ({ ...state, lowPower: !settings.lowPower }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleTranslation = async () => {
|
|
||||||
await storage.createSetting("translation", String(+!settings.translation));
|
|
||||||
setSettings((state) => ({ ...state, translation: !settings.translation }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleNofitication = async () => {
|
|
||||||
await requestPermission();
|
|
||||||
setSettings((state) => ({
|
|
||||||
...state,
|
|
||||||
notification: !settings.notification,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const completeAuth = async () => {
|
|
||||||
if (settings.translation) {
|
|
||||||
if (!apiKey.length)
|
|
||||||
return toast.warning(
|
|
||||||
"You need to provide Translate API if enable translation",
|
|
||||||
);
|
|
||||||
|
|
||||||
await storage.createSetting("translateApiKey", apiKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
// get account contacts
|
|
||||||
await ark.getUserContacts();
|
|
||||||
|
|
||||||
// refetch newsfeed
|
|
||||||
await queryClient.prefetchInfiniteQuery({
|
|
||||||
queryKey: ["timeline-9999"],
|
|
||||||
initialPageParam: 0,
|
|
||||||
queryFn: async ({
|
|
||||||
signal,
|
|
||||||
pageParam,
|
|
||||||
}: {
|
|
||||||
signal: AbortSignal;
|
|
||||||
pageParam: number;
|
|
||||||
}) => {
|
|
||||||
const events = await ark.getInfiniteEvents({
|
|
||||||
filter: {
|
|
||||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
|
||||||
authors: ark.account.contacts,
|
|
||||||
},
|
|
||||||
limit: FETCH_LIMIT,
|
|
||||||
pageParam,
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
return events;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate("/");
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function loadSettings() {
|
|
||||||
// get notification permission
|
|
||||||
const permissionGranted = await isPermissionGranted();
|
|
||||||
setSettings((prev) => ({ ...prev, notification: permissionGranted }));
|
|
||||||
|
|
||||||
// get other settings
|
|
||||||
const data = await storage.getAllSettings();
|
|
||||||
for (const item of data) {
|
|
||||||
if (item.key === "lowPower")
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
lowPower: !!parseInt(item.value),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (item.key === "translation")
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
translation: !!parseInt(item.value),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSettings();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
|
||||||
<div className="flex flex-col gap-1 text-center items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">
|
|
||||||
You're almost ready to use Lume.
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
|
|
||||||
Let's start personalizing your experience.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="flex w-full items-start justify-between gap-4 rounded-xl px-5 py-4 bg-neutral-950">
|
|
||||||
<Switch.Root
|
|
||||||
checked={settings.notification}
|
|
||||||
onClick={() => toggleNofitication()}
|
|
||||||
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-neutral-50 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-lg">Push notification</h3>
|
|
||||||
<p className="text-neutral-500">
|
|
||||||
Enabling push notifications will allow you to receive
|
|
||||||
notifications from Lume.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full items-start justify-between gap-4 rounded-xl px-5 py-4 bg-neutral-950">
|
|
||||||
<Switch.Root
|
|
||||||
checked={settings.lowPower}
|
|
||||||
onClick={() => toggleLowPower()}
|
|
||||||
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-neutral-50 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-lg">Low Power Mode</h3>
|
|
||||||
<p className="text-neutral-500">
|
|
||||||
Limited relay connection and hide all media, sustainable for low
|
|
||||||
network environment.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full items-start justify-between gap-4 rounded-xl px-5 py-4 bg-neutral-950">
|
|
||||||
<Switch.Root
|
|
||||||
checked={settings.translation}
|
|
||||||
onClick={() => toggleTranslation()}
|
|
||||||
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-neutral-50 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-lg">
|
|
||||||
Translation (nostr.wine)
|
|
||||||
</h3>
|
|
||||||
<p className="text-neutral-500">
|
|
||||||
Translate text to your preferred language, powered by Nostr
|
|
||||||
Wine.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{settings.translation ? (
|
|
||||||
<div className="flex flex-col w-full items-start justify-between gap-2 rounded-xl px-5 py-4 bg-neutral-950">
|
|
||||||
<h3 className="font-semibold">Translate API Key</h3>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
spellCheck={false}
|
|
||||||
value={apiKey}
|
|
||||||
onChange={(e) => setAPIKey(e.target.value)}
|
|
||||||
className="w-full text-xl border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-11 rounded-lg ring-0 placeholder:text-neutral-600 bg-neutral-900"
|
|
||||||
/>
|
|
||||||
<div className="w-full mt-1">
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-neutral-900" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center">
|
|
||||||
<span className="px-2 text-sm font-medium bg-neutral-950 text-neutral-600">
|
|
||||||
Not have API ?
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TranslateRegisterModal setAPIKey={setAPIKey} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="flex items-center gap-2 rounded-xl px-5 py-3 text-sm bg-blue-950 text-blue-300">
|
|
||||||
<InfoIcon className="size-8" />
|
|
||||||
<p>
|
|
||||||
There are many more settings you can configure from the
|
|
||||||
"Settings" screen. Be sure to visit it later.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={completeAuth}
|
|
||||||
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Continue"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function TutorialFinishScreen() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
|
||||||
<div className="text-center">
|
|
||||||
<img
|
|
||||||
src="/icon.png"
|
|
||||||
alt="Lume's logo"
|
|
||||||
className="mx-auto mb-1 h-auto w-16"
|
|
||||||
/>
|
|
||||||
<h1 className="text-2xl font-light">
|
|
||||||
Yo, you've understood basic features 🎉
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Start using Lume
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="https://nostr.how/"
|
|
||||||
target="_blank"
|
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Learn more about Nostr
|
|
||||||
</Link>
|
|
||||||
<p className="text-center text-sm font-medium text-neutral-500 dark:text-neutral-600">
|
|
||||||
If you've trouble when user Lume, you can report the issue{" "}
|
|
||||||
<a
|
|
||||||
href="github.com/luminous-devs/lume"
|
|
||||||
target="_blank"
|
|
||||||
className="text-blue-500 !underline"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
here
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { TextNote } from "@lume/ark";
|
|
||||||
import {
|
|
||||||
EditIcon,
|
|
||||||
ReactionIcon,
|
|
||||||
ReplyIcon,
|
|
||||||
RepostIcon,
|
|
||||||
ZapIcon,
|
|
||||||
} from "@lume/icons";
|
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function TutorialNoteScreen() {
|
|
||||||
const exampleEvent = new NDKEvent(undefined, {
|
|
||||||
id: "a3527670dd9b178bf7c2a9ea673b63bc8bfe774942b196691145343623c45821",
|
|
||||||
pubkey: "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9",
|
|
||||||
created_at: 1701355223,
|
|
||||||
kind: 1,
|
|
||||||
tags: [],
|
|
||||||
content: "good morning nostr, stay humble and stack sats 🫡",
|
|
||||||
sig: "9e0bd67ec25598744f20bff0fe360fdf190c4240edb9eea260e50f77e07f94ea767ececcc6270819b7f64e5e7ca1fe20b4971f46dc120e6db43114557f3a6dae",
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full select-text items-center justify-center">
|
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
|
||||||
<div className="flex flex-col items-center gap-3">
|
|
||||||
<div className="inline-flex h-11 w-11 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<EditIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-light">
|
|
||||||
What is a <span className="font-bold">Note?</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<p className="px-3">
|
|
||||||
Posts on Nostr based Social Network client are usually called
|
|
||||||
'Notes.' Notes are arranged chronologically on the
|
|
||||||
timeline and are updated in real-time.
|
|
||||||
</p>
|
|
||||||
<p className="px-3 font-semibold">Here is one example:</p>
|
|
||||||
<TextNote event={exampleEvent} />
|
|
||||||
<p className="px-3 font-semibold">
|
|
||||||
Here are how you can interact with a note:
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-col gap-2 px-3">
|
|
||||||
<div className="inline-flex gap-3">
|
|
||||||
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<ReplyIcon className="h-5 w-5 text-blue-500" />
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Reply - Click on this button to reply to a note. It's also
|
|
||||||
possible to reply to replies, continuing the conversation like a
|
|
||||||
thread.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex gap-3">
|
|
||||||
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<ReactionIcon className="h-5 w-5 text-red-500" />
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Reaction - You can add reactions to the Note to express your
|
|
||||||
concern.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex gap-3">
|
|
||||||
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<RepostIcon className="h-5 w-5 text-teal-500" />
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Repost - You can share that note to your own timeline. You can
|
|
||||||
also quote them with your comments.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex gap-3">
|
|
||||||
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<ZapIcon className="h-5 w-5 text-orange-500" />
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Zap - You can send tip in Bitcoin to that note owner with
|
|
||||||
zero-fees
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 flex gap-2 px-3">
|
|
||||||
<Link
|
|
||||||
to="/auth/finish"
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/auth/tutorials/widget"
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export function TutorialPostingScreen() {
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { BellIcon, HomeIcon, PlusIcon } from "@lume/icons";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function TutorialWidgetScreen() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full select-text items-center justify-center">
|
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
|
||||||
<div className="flex flex-col items-center gap-3">
|
|
||||||
<div className="inline-flex h-11 w-11 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<HomeIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-light">
|
|
||||||
The concept of <span className="font-bold">Widgets</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2 px-3">
|
|
||||||
<p>
|
|
||||||
Lume provides multiple widgets based on usage. You always can
|
|
||||||
control what you need to show on your Home.
|
|
||||||
</p>
|
|
||||||
<p className="font-semibold">Default widgets:</p>
|
|
||||||
<div className="inline-flex gap-3">
|
|
||||||
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<HomeIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<p>Newsfeed - You can view notes from accounts you follow.</p>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex gap-3">
|
|
||||||
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<BellIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Notification - You can view all notifications related to your
|
|
||||||
account.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
If you want to add more widget, you can click to this button on Home
|
|
||||||
Screen.
|
|
||||||
</p>
|
|
||||||
<div className="flex h-24 w-full items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 flex gap-2">
|
|
||||||
<Link
|
|
||||||
to="/auth/tutorials/note"
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/auth/tutorials/finish"
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
export function WelcomeScreen() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const gotoCreateAccount = () => {
|
|
||||||
setLoading(true);
|
|
||||||
navigate("/auth/create");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-between w-full h-full">
|
|
||||||
<div />
|
|
||||||
<div className="flex flex-col items-center w-full max-w-4xl gap-10 mx-auto">
|
|
||||||
<div className="flex flex-col items-center text-center">
|
|
||||||
<img
|
|
||||||
src="/heading.png"
|
|
||||||
srcSet="/heading@2x.png 2x"
|
|
||||||
alt="lume"
|
|
||||||
className="w-2/3"
|
|
||||||
/>
|
|
||||||
<p className="mt-5 text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
|
|
||||||
Lume is your safe Nostr client to meet, explore and
|
|
||||||
<br />
|
|
||||||
freely sharing your though to everyone in nostrverse
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-full max-w-xs gap-2 mx-auto">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={gotoCreateAccount}
|
|
||||||
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Create New Account"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<Link
|
|
||||||
to="/auth/login"
|
|
||||||
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center h-11">
|
|
||||||
<p className="text-neutral-800">
|
|
||||||
Before joining Nostr, you can take time to learn more about Nostr{" "}
|
|
||||||
<Link
|
|
||||||
to="https://nostr.com"
|
|
||||||
target="_blank"
|
|
||||||
className="text-blue-500"
|
|
||||||
>
|
|
||||||
here
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon, RunIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import { NDKKind } from "@nostr-dev-kit/ndk";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function DepotContactCard() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const [status, setStatus] = useState(false);
|
|
||||||
|
|
||||||
const backupContact = async () => {
|
|
||||||
try {
|
|
||||||
setStatus(true);
|
|
||||||
|
|
||||||
const event = await ark.getEventByFilter({
|
|
||||||
filter: {
|
|
||||||
authors: [ark.account.pubkey],
|
|
||||||
kinds: [NDKKind.Contacts],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// broadcast to depot
|
|
||||||
const publish = await event.publish();
|
|
||||||
|
|
||||||
if (publish) {
|
|
||||||
setStatus(false);
|
|
||||||
toast.success("Backup contact list successfully.");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setStatus(false);
|
|
||||||
toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-56 w-full flex-col gap-2 overflow-hidden rounded-xl bg-neutral-100 p-2 dark:bg-neutral-900">
|
|
||||||
<div className="flex flex-1 items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
|
|
||||||
<div className="isolate flex -space-x-2">
|
|
||||||
{ark.account.contacts?.slice(0, 8).map((item) => (
|
|
||||||
<User key={item} pubkey={item} variant="ministacked" />
|
|
||||||
))}
|
|
||||||
{ark.account.contacts?.length > 8 ? (
|
|
||||||
<div className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-neutral-300 text-neutral-900 ring-1 ring-white dark:bg-neutral-700 dark:text-neutral-100 dark:ring-black">
|
|
||||||
<span className="text-[8px] font-medium">
|
|
||||||
+{ark.account.contacts?.length - 8}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex shrink-0 items-center justify-between">
|
|
||||||
<div className="text-sm font-medium">Contacts</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={backupContact}
|
|
||||||
className="inline-flex h-8 w-max items-center justify-center gap-2 rounded-md bg-blue-500 pl-2 pr-3 font-medium text-white shadow shadow-blue-500/50 hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
{status ? (
|
|
||||||
<LoaderIcon className="size-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<RunIcon className="size-4" />
|
|
||||||
)}
|
|
||||||
Backup
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import { CancelIcon, PlusIcon, UserAddIcon, UserRemoveIcon } from "@lume/icons";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
|
||||||
import { resolveResource } from "@tauri-apps/api/path";
|
|
||||||
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
|
|
||||||
import { nip19 } from "nostr-tools";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { parse, stringify } from "smol-toml";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function DepotMembers() {
|
|
||||||
const [members, setMembers] = useState<Set<string>>(null);
|
|
||||||
const [tmpMembers, setTmpMembers] = useState<Array<string>>([]);
|
|
||||||
const [newMember, setNewMember] = useState("");
|
|
||||||
|
|
||||||
const addMember = async () => {
|
|
||||||
if (!newMember.startsWith("npub1"))
|
|
||||||
return toast.error("You need to enter a valid npub");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pubkey = nip19.decode(newMember).data as string;
|
|
||||||
setTmpMembers((prev) => [...prev, pubkey]);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeMember = (member: string) => {
|
|
||||||
setTmpMembers((prev) => prev.filter((item) => item !== member));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMembers = async () => {
|
|
||||||
setMembers(new Set(tmpMembers));
|
|
||||||
|
|
||||||
const defaultConfig = await resolveResource("resources/config.toml");
|
|
||||||
const config = await readTextFile(defaultConfig);
|
|
||||||
const configContent = parse(config);
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
|
|
||||||
configContent.authorization["pubkey_whitelist"] = [...members];
|
|
||||||
|
|
||||||
const newConfig = stringify(configContent);
|
|
||||||
|
|
||||||
return await writeTextFile(defaultConfig, newConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function loadConfig() {
|
|
||||||
const defaultConfig = await resolveResource("resources/config.toml");
|
|
||||||
const config = await readTextFile(defaultConfig);
|
|
||||||
const configContent = parse(config);
|
|
||||||
setTmpMembers(
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
|
|
||||||
Array.from(configContent.authorization["pubkey_whitelist"]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadConfig();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog.Root>
|
|
||||||
<div className="flex items-center justify-between rounded-lg bg-neutral-50 p-5 dark:bg-neutral-950">
|
|
||||||
<div className="flex flex-col items-start">
|
|
||||||
<h3 className="text-lg font-semibold">Members</h3>
|
|
||||||
<p className="text-neutral-700 dark:text-neutral-300">
|
|
||||||
Only allowed users can publish event to your Depot
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<div className="isolate flex -space-x-2">
|
|
||||||
{tmpMembers.slice(0, 5).map((item) => (
|
|
||||||
<User key={item} pubkey={item} variant="stacked" />
|
|
||||||
))}
|
|
||||||
{tmpMembers.length > 5 ? (
|
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-700">
|
|
||||||
<span className="text-xs font-medium">
|
|
||||||
+{tmpMembers.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<Dialog.Trigger className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-lg bg-blue-500 px-3 text-white hover:bg-blue-600">
|
|
||||||
<UserAddIcon className="size-4" />
|
|
||||||
Manage
|
|
||||||
</Dialog.Trigger>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Dialog.Portal>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/10 backdrop-blur-sm dark:bg-black/10" />
|
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
|
||||||
<div className="relative h-min w-full max-w-xl overflow-hidden rounded-xl bg-white dark:bg-black">
|
|
||||||
<div className="inline-flex h-14 w-full shrink-0 items-center justify-between border-b border-neutral-100 px-5 dark:border-neutral-900">
|
|
||||||
<Dialog.Title className="text-center font-semibold">
|
|
||||||
Manage member
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={updateMembers}
|
|
||||||
className="inline-flex h-8 w-max items-center justify-center rounded-lg bg-blue-500 px-2.5 text-sm font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</button>
|
|
||||||
<Dialog.Close className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800">
|
|
||||||
<CancelIcon className="size-4" />
|
|
||||||
</Dialog.Close>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pb-3">
|
|
||||||
<div className="relative mb-2 mt-4 w-full px-5">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
spellCheck={false}
|
|
||||||
value={newMember}
|
|
||||||
onChange={(e) => setNewMember(e.target.value)}
|
|
||||||
placeholder="npub1..."
|
|
||||||
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 pl-3 pr-20 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={addMember}
|
|
||||||
className="absolute right-7 top-1/2 inline-flex h-7 w-max -translate-y-1/2 transform items-center justify-center gap-1 rounded-md bg-neutral-200 px-2.5 text-sm font-medium text-blue-500 hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<PlusIcon className="size-4" />
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{tmpMembers.map((member) => (
|
|
||||||
<div
|
|
||||||
key={member}
|
|
||||||
className="group flex items-center justify-between px-5 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-900"
|
|
||||||
>
|
|
||||||
<User pubkey={member} variant="simple" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeMember(member)}
|
|
||||||
className="hidden size-6 items-center justify-center rounded-md bg-neutral-200 group-hover:inline-flex hover:bg-red-200 dark:bg-neutral-800 dark:hover:bg-red-800 dark:hover:text-red-200"
|
|
||||||
>
|
|
||||||
<UserRemoveIcon className="size-4 text-red-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon, RunIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import { NDKKind } from "@nostr-dev-kit/ndk";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function DepotProfileCard() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const [status, setStatus] = useState(false);
|
|
||||||
|
|
||||||
const backupProfile = async () => {
|
|
||||||
try {
|
|
||||||
setStatus(true);
|
|
||||||
|
|
||||||
const event = await ark.getEventByFilter({
|
|
||||||
filter: {
|
|
||||||
authors: [ark.account.pubkey],
|
|
||||||
kinds: [NDKKind.Metadata],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// broadcast to depot
|
|
||||||
const publish = await event.publish();
|
|
||||||
|
|
||||||
if (publish) {
|
|
||||||
setStatus(false);
|
|
||||||
toast.success("Backup profile successfully.");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setStatus(false);
|
|
||||||
toast.error(JSON.stringify(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-56 w-full flex-col gap-2 overflow-hidden rounded-xl bg-neutral-100 p-2 dark:bg-neutral-900">
|
|
||||||
<div className="flex flex-1 items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
|
|
||||||
<User pubkey={ark.account.pubkey} variant="simple" />
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex shrink-0 items-center justify-between">
|
|
||||||
<div className="text-sm font-medium">Profile</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={backupProfile}
|
|
||||||
className="inline-flex h-8 w-max items-center justify-center gap-2 rounded-md bg-blue-500 pl-2 pr-3 font-medium text-white shadow shadow-blue-500/50 hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
{status ? (
|
|
||||||
<LoaderIcon className="size-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<RunIcon className="size-4" />
|
|
||||||
)}
|
|
||||||
Backup
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon, RunIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { NDKKind } from "@nostr-dev-kit/ndk";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function DepotRelaysCard() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const [status, setStatus] = useState(false);
|
|
||||||
const [relaySize, setRelaySize] = useState(0);
|
|
||||||
|
|
||||||
const backupRelays = async () => {
|
|
||||||
try {
|
|
||||||
setStatus(true);
|
|
||||||
|
|
||||||
const event = await ark.getEventByFilter({
|
|
||||||
filter: {
|
|
||||||
authors: [ark.account.pubkey],
|
|
||||||
kinds: [NDKKind.RelayList],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// broadcast to depot
|
|
||||||
const publish = await event.publish();
|
|
||||||
|
|
||||||
if (publish) {
|
|
||||||
setStatus(false);
|
|
||||||
toast.success("Backup profile successfully.");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setStatus(false);
|
|
||||||
toast.error(JSON.stringify(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function loadRelays() {
|
|
||||||
const event = await ark.getEventByFilter({
|
|
||||||
filter: {
|
|
||||||
authors: [ark.account.pubkey],
|
|
||||||
kinds: [NDKKind.RelayList],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (event) setRelaySize(event.tags.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadRelays();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-56 w-full flex-col gap-2 overflow-hidden rounded-xl bg-neutral-100 p-2 dark:bg-neutral-900">
|
|
||||||
<div className="flex flex-1 items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
|
|
||||||
<p className="text-lg font-semibold">{relaySize} relays</p>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex shrink-0 items-center justify-between">
|
|
||||||
<div className="text-sm font-medium">Relay List</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={backupRelays}
|
|
||||||
className="inline-flex h-8 w-max items-center justify-center gap-2 rounded-md bg-blue-500 pl-2 pr-3 font-medium text-white shadow shadow-blue-500/50 hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
{status ? (
|
|
||||||
<LoaderIcon className="size-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<RunIcon className="size-4" />
|
|
||||||
)}
|
|
||||||
Backup
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { ChevronDownIcon, DepotIcon, GossipIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { NDKKind } from "@nostr-dev-kit/ndk";
|
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { appConfigDir } from "@tauri-apps/api/path";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { DepotContactCard } from "./components/contact";
|
|
||||||
import { DepotMembers } from "./components/members";
|
|
||||||
import { DepotProfileCard } from "./components/profile";
|
|
||||||
import { DepotRelaysCard } from "./components/relays";
|
|
||||||
|
|
||||||
export function DepotScreen() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const [dataPath, setDataPath] = useState("");
|
|
||||||
const [tunnelUrl, setTunnelUrl] = useState("");
|
|
||||||
|
|
||||||
const openFolder = async () => {
|
|
||||||
await invoke("show_in_folder", {
|
|
||||||
path: `${dataPath}/nostr.db`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateRelayList = async () => {
|
|
||||||
try {
|
|
||||||
if (tunnelUrl.length < 1)
|
|
||||||
return toast.info("Please enter a valid relay url");
|
|
||||||
if (!tunnelUrl.startsWith("ws"))
|
|
||||||
return toast.info("Please enter a valid relay url");
|
|
||||||
|
|
||||||
const relayUrl = new URL(tunnelUrl.replace(/\s/g, ""));
|
|
||||||
if (!/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/.test(relayUrl.host)) return;
|
|
||||||
|
|
||||||
const relayEvent = await ark.getEventByFilter({
|
|
||||||
filter: {
|
|
||||||
authors: [ark.account.pubkey],
|
|
||||||
kinds: [NDKKind.RelayList],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let publish: { id: string; seens: string[] };
|
|
||||||
|
|
||||||
if (!relayEvent) {
|
|
||||||
publish = await ark.createEvent({
|
|
||||||
kind: NDKKind.RelayList,
|
|
||||||
tags: [["r", tunnelUrl, ""]],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newTags = relayEvent.tags ?? [];
|
|
||||||
newTags.push(["r", tunnelUrl, ""]);
|
|
||||||
|
|
||||||
publish = await ark.createEvent({
|
|
||||||
kind: NDKKind.RelayList,
|
|
||||||
tags: newTags,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (publish) {
|
|
||||||
await storage.createSetting("tunnel_url", tunnelUrl);
|
|
||||||
toast.success("Update relay list successfully.");
|
|
||||||
|
|
||||||
setTunnelUrl("");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
toast.error("Error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function loadConfig() {
|
|
||||||
const appDir = await appConfigDir();
|
|
||||||
setDataPath(appDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadConfig();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full rounded-xl shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
|
|
||||||
<div className="h-full w-72 shrink-0 rounded-l-xl bg-white/50 px-8 pt-8 backdrop-blur-xl dark:bg-black/50">
|
|
||||||
<div className="flex flex-col justify-center gap-4">
|
|
||||||
<div className="size-16 rounded-xl bg-gradient-to-bl from-teal-300 to-teal-600 p-1">
|
|
||||||
<div className="relative inline-flex h-full w-full items-center justify-center overflow-hidden rounded-lg bg-gradient-to-bl from-teal-400 to-teal-700 shadow-sm shadow-white/20">
|
|
||||||
<DepotIcon className="size-8 text-white" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-xl font-semibold">Depot is running</h1>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<div className="text-sm font-medium">Relay URL</div>
|
|
||||||
<div className="inline-flex h-10 w-full select-text items-center rounded-lg bg-black/10 px-3 text-sm backdrop-blur-xl dark:bg-white/10">
|
|
||||||
ws://localhost:6090
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<div className="text-sm font-medium">Database</div>
|
|
||||||
<div className="inline-flex h-10 w-full items-center gap-2 truncate rounded-lg bg-black/10 p-1 backdrop-blur-xl dark:bg-white/10">
|
|
||||||
<p className="shrink-0 pl-2 text-sm">nostr.db (SQLite)</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={openFolder}
|
|
||||||
className="inline-flex h-full w-full items-center justify-center rounded-md bg-white text-sm font-medium shadow hover:bg-blue-500 hover:text-white dark:bg-black"
|
|
||||||
>
|
|
||||||
Open
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 overflow-y-auto rounded-r-xl bg-white pb-20 dark:bg-black">
|
|
||||||
<div className="mb-5 flex h-12 items-center border-b border-neutral-100 px-5 dark:border-neutral-900">
|
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Actions
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-5 px-5">
|
|
||||||
<Collapsible.Root
|
|
||||||
defaultOpen
|
|
||||||
className="flex flex-col overflow-hidden rounded-xl border border-transparent bg-neutral-50 data-[state=open]:border-blue-500 dark:bg-neutral-950"
|
|
||||||
>
|
|
||||||
<Collapsible.Trigger className="flex h-20 items-center justify-between px-5 hover:bg-neutral-100 dark:hover:bg-neutral-900">
|
|
||||||
<div className="flex flex-col items-start">
|
|
||||||
<h3 className="text-lg font-semibold">Expose</h3>
|
|
||||||
<p className="text-neutral-700 dark:text-neutral-300">
|
|
||||||
Make your Depot visible in the Internet, everyone can connect
|
|
||||||
into it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ChevronDownIcon className="size-5 shrink-0" />
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Collapsible.Content>
|
|
||||||
<div className="flex w-full flex-col gap-4 p-5">
|
|
||||||
<div>
|
|
||||||
<p className="mb-1 font-medium">ngrok</p>
|
|
||||||
<input
|
|
||||||
readOnly
|
|
||||||
value="ngrok http --domain=<your_domain> 6090"
|
|
||||||
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="mb-1 font-medium">Cloudflare Tunnel</p>
|
|
||||||
<input
|
|
||||||
readOnly
|
|
||||||
value="cloudflared tunnel --url localhost:6090"
|
|
||||||
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="mb-1 font-medium">Local Tunnel</p>
|
|
||||||
<input
|
|
||||||
readOnly
|
|
||||||
value="lt --port 6090"
|
|
||||||
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 border-t border-neutral-100 pt-4 dark:border-neutral-900">
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<GossipIcon className="size-5 text-blue-500" />
|
|
||||||
<h3 className="mb-1 font-semibold">
|
|
||||||
Support Gossip Model (Recommended)
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full max-w-xl">
|
|
||||||
<p className=" text-balance">
|
|
||||||
By adding to Relay List, other Nostr Client which support
|
|
||||||
Gossip Model will automatically connect to your Depot and
|
|
||||||
improve the discoverability.
|
|
||||||
</p>
|
|
||||||
<div className="mt-2 inline-flex w-full items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={tunnelUrl}
|
|
||||||
onChange={(e) => setTunnelUrl(e.target.value)}
|
|
||||||
spellCheck={false}
|
|
||||||
placeholder="wss://"
|
|
||||||
className="h-10 flex-1 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={updateRelayList}
|
|
||||||
className="inline-flex h-10 w-max shrink-0 items-center justify-center rounded-lg bg-blue-500 px-4 font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible.Root>
|
|
||||||
<Collapsible.Root className="flex flex-col overflow-hidden rounded-xl border border-transparent bg-neutral-50 data-[state=open]:border-blue-500 dark:bg-neutral-950">
|
|
||||||
<Collapsible.Trigger className="flex h-20 items-center justify-between px-5 hover:bg-neutral-100 dark:hover:bg-neutral-900">
|
|
||||||
<div className="flex flex-col items-start">
|
|
||||||
<h3 className="text-lg font-semibold">Backup (Recommended)</h3>
|
|
||||||
<p className="text-neutral-700 dark:text-neutral-300">
|
|
||||||
Backup all your data to Depot, it always live on your machine.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ChevronDownIcon className="size-5 shrink-0" />
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Collapsible.Content>
|
|
||||||
<div className="grid grid-cols-3 gap-4 px-5 py-5">
|
|
||||||
<DepotProfileCard />
|
|
||||||
<DepotContactCard />
|
|
||||||
<DepotRelaysCard />
|
|
||||||
</div>
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible.Root>
|
|
||||||
<DepotMembers />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { delay } from "@lume/utils";
|
|
||||||
import { resolveResource } from "@tauri-apps/api/path";
|
|
||||||
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { parse, stringify } from "smol-toml";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function DepotOnboardingScreen() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const launchDepot = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
// get default config
|
|
||||||
const defaultConfig = await resolveResource("resources/config.toml");
|
|
||||||
const config = await readTextFile(defaultConfig);
|
|
||||||
const parsedConfig = parse(config);
|
|
||||||
|
|
||||||
// add current user to whitelist
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
|
|
||||||
parsedConfig.authorization["pubkey_whitelist"].push(ark.account.pubkey);
|
|
||||||
|
|
||||||
// update new config
|
|
||||||
const newConfig = stringify(parsedConfig);
|
|
||||||
await writeTextFile(defaultConfig, newConfig);
|
|
||||||
|
|
||||||
// launch depot
|
|
||||||
await storage.launchDepot();
|
|
||||||
await storage.createSetting("depot", "1");
|
|
||||||
await delay(2000); // delay 2s to make sure depot is running
|
|
||||||
|
|
||||||
// default depot url: ws://localhost:6090
|
|
||||||
// #TODO: user can custom depot url
|
|
||||||
const connect = await ark.connectDepot();
|
|
||||||
|
|
||||||
if (connect) {
|
|
||||||
toast.success("Your Depot is successfully launch.");
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
navigate("/depot/");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-10 rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
|
|
||||||
<div className="flex flex-col items-center gap-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="mb-1 text-3xl font-semibold text-neutral-400 dark:text-neutral-600">
|
|
||||||
Run your Personal Nostr Relay inside Lume
|
|
||||||
</h1>
|
|
||||||
<h2 className="text-4xl font-semibold">Your Relay, Your Control.</h2>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl bg-blue-100 p-1.5 dark:bg-blue-900">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={launchDepot}
|
|
||||||
className="inline-flex h-11 w-36 transform items-center justify-center gap-2 rounded-lg bg-blue-500 font-medium text-white active:translate-y-1"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin" />
|
|
||||||
Launching...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
className="h-5 w-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M12 2.25a.75.75 0 0 1 .75.75v9a.75.75 0 0 1-1.5 0V3a.75.75 0 0 1 .75-.75ZM6.166 5.106a.75.75 0 0 1 0 1.06 8.25 8.25 0 1 0 11.668 0 .75.75 0 1 1 1.06-1.06c3.808 3.807 3.808 9.98 0 13.788-3.807 3.808-9.98 3.808-13.788 0-3.808-3.807-3.808-9.98 0-13.788a.75.75 0 0 1 1.06 0Z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Launch
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { downloadDir } from "@tauri-apps/api/path";
|
|
||||||
import { message, save } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
|
||||||
import { relaunch } from "@tauri-apps/plugin-process";
|
|
||||||
import { useRouteError } from "react-router-dom";
|
|
||||||
|
|
||||||
interface RouteError {
|
|
||||||
statusText: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ErrorScreen() {
|
|
||||||
const storage = useStorage();
|
|
||||||
const error = useRouteError() as RouteError;
|
|
||||||
|
|
||||||
const restart = async () => {
|
|
||||||
await relaunch();
|
|
||||||
};
|
|
||||||
|
|
||||||
const download = async () => {
|
|
||||||
try {
|
|
||||||
const downloadPath = await downloadDir();
|
|
||||||
const fileName = `nostr_keys_${new Date().toISOString()}.txt`;
|
|
||||||
const filePath = await save({
|
|
||||||
defaultPath: `${downloadPath}/${fileName}`,
|
|
||||||
});
|
|
||||||
const nsec = await storage.loadPrivkey(ark.account.pubkey);
|
|
||||||
|
|
||||||
if (filePath) {
|
|
||||||
if (nsec) {
|
|
||||||
await writeTextFile(
|
|
||||||
filePath,
|
|
||||||
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}\nPrivate key: ${nsec}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await writeTextFile(
|
|
||||||
filePath,
|
|
||||||
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} // else { user cancel action }
|
|
||||||
} catch (e) {
|
|
||||||
await message(e, {
|
|
||||||
title: "Cannot download account keys",
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="relative flex h-screen w-screen items-center justify-center bg-blue-600 overflow-hidden rounded-t-xl"
|
|
||||||
>
|
|
||||||
<div className="flex w-full max-w-2xl flex-col items-start gap-8">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<h1 className="mb-3 text-4xl font-semibold text-blue-400">
|
|
||||||
Sorry, an unexpected error has occurred.
|
|
||||||
</h1>
|
|
||||||
<h3 className="text-3xl font-semibold leading-snug text-white">
|
|
||||||
Don't panic, your account is safe.
|
|
||||||
<br />
|
|
||||||
Here are what things you can do:
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-col gap-3">
|
|
||||||
<div className="flex items-center justify-between rounded-xl bg-blue-700 px-3 py-4">
|
|
||||||
<div className="text-xl font-semibold text-white">
|
|
||||||
1. Try to close and re-open the app
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => restart()}
|
|
||||||
className="h-9 w-28 rounded-lg bg-blue-800 px-3 font-medium text-white hover:bg-blue-900"
|
|
||||||
>
|
|
||||||
Restart
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between rounded-xl bg-blue-700 px-3 py-4">
|
|
||||||
<div className="text-xl font-semibold text-white">
|
|
||||||
2. Backup Nostr account
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => download()}
|
|
||||||
className="h-9 w-28 rounded-lg bg-blue-800 px-3 font-medium text-white hover:bg-blue-900"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl bg-blue-700 px-3 py-4">
|
|
||||||
<div className="flex w-full flex-col gap-2">
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="text-xl font-semibold text-white">
|
|
||||||
3. Report this issue to Lume's Devs
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="https://github.com/luminous-devs/lume/issues/new"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-blue-800 px-3 font-medium text-white hover:bg-blue-900"
|
|
||||||
>
|
|
||||||
Report
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex h-16 items-center justify-center overflow-y-auto rounded-lg border border-dashed border-red-300 bg-blue-800 px-5">
|
|
||||||
<p className="select-text break-all text-red-400">
|
|
||||||
{error.statusText || error.message}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl bg-blue-700 px-3 py-4">
|
|
||||||
<div className="flex w-full flex-col gap-1.5">
|
|
||||||
<div className="text-xl font-semibold text-white">
|
|
||||||
4. Use another Nostr client
|
|
||||||
</div>
|
|
||||||
<div className="select-text text-lg font-medium text-blue-300">
|
|
||||||
<p>
|
|
||||||
While waiting for Lume's Devs to release the bug fixes,
|
|
||||||
you always can use other Nostr clients with your account:
|
|
||||||
</p>
|
|
||||||
<div className="mt-2 flex flex-col gap-1 text-white">
|
|
||||||
<a
|
|
||||||
className="hover:!underline"
|
|
||||||
href="https://snort.social"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
snort.social
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="hover:!underline"
|
|
||||||
href="https://primal.net"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
primal.net
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="hover:!underline"
|
|
||||||
href="https://nostrudel.ninja"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
nostrudel.ninja
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import { Antenas } from "@columns/antenas";
|
|
||||||
import { Default } from "@columns/default";
|
|
||||||
import { Group } from "@columns/group";
|
|
||||||
import { Hashtag } from "@columns/hashtag";
|
|
||||||
import { Thread } from "@columns/thread";
|
|
||||||
import { Timeline } from "@columns/timeline";
|
|
||||||
import { User } from "@columns/user";
|
|
||||||
import { useColumnContext } from "@lume/ark";
|
|
||||||
import { ArrowLeftIcon, ArrowRightIcon, PlusSquareIcon } from "@lume/icons";
|
|
||||||
import { IColumn } from "@lume/types";
|
|
||||||
import { TutorialModal } from "@lume/ui/src/tutorial/modal";
|
|
||||||
import { COL_TYPES } from "@lume/utils";
|
|
||||||
import { useRef, useState } from "react";
|
|
||||||
import { VList, VListHandle } from "virtua";
|
|
||||||
|
|
||||||
export function HomeScreen() {
|
|
||||||
const ref = useRef<VListHandle>(null);
|
|
||||||
const { addColumn, columns } = useColumnContext();
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
|
||||||
|
|
||||||
const renderItem = (column: IColumn) => {
|
|
||||||
switch (column.kind) {
|
|
||||||
case COL_TYPES.default:
|
|
||||||
return <Default key={column.id} column={column} />;
|
|
||||||
case COL_TYPES.newsfeed:
|
|
||||||
return <Timeline key={column.id} column={column} />;
|
|
||||||
case COL_TYPES.thread:
|
|
||||||
return <Thread key={column.id} column={column} />;
|
|
||||||
case COL_TYPES.user:
|
|
||||||
return <User key={column.id} column={column} />;
|
|
||||||
case COL_TYPES.hashtag:
|
|
||||||
return <Hashtag key={column.id} column={column} />;
|
|
||||||
case COL_TYPES.group:
|
|
||||||
return <Group key={column.id} column={column} />;
|
|
||||||
case COL_TYPES.antenas:
|
|
||||||
return <Antenas key={column.id} column={column} />;
|
|
||||||
default:
|
|
||||||
return <Default key={column.id} column={column} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative w-full h-full">
|
|
||||||
<VList
|
|
||||||
id="timeline"
|
|
||||||
ref={ref}
|
|
||||||
className="h-full w-full flex-nowrap overflow-x-auto !overflow-y-hidden scrollbar-none focus:outline-none"
|
|
||||||
itemSize={420}
|
|
||||||
tabIndex={0}
|
|
||||||
horizontal
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (!ref.current) return;
|
|
||||||
switch (e.code) {
|
|
||||||
case "ArrowUp":
|
|
||||||
case "ArrowLeft": {
|
|
||||||
e.preventDefault();
|
|
||||||
const prevIndex = Math.max(selectedIndex - 1, 0);
|
|
||||||
setSelectedIndex(prevIndex);
|
|
||||||
ref.current.scrollToIndex(prevIndex, {
|
|
||||||
align: "center",
|
|
||||||
smooth: true,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "ArrowDown":
|
|
||||||
case "ArrowRight": {
|
|
||||||
e.preventDefault();
|
|
||||||
const nextIndex = Math.min(selectedIndex + 1, columns.length - 1);
|
|
||||||
setSelectedIndex(nextIndex);
|
|
||||||
ref.current.scrollToIndex(nextIndex, {
|
|
||||||
align: "center",
|
|
||||||
smooth: true,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{columns.map((column) => renderItem(column))}
|
|
||||||
<div className="w-[420px]" />
|
|
||||||
</VList>
|
|
||||||
<div className="absolute bottom-3 right-3">
|
|
||||||
<div className="flex items-center gap-1 p-1 bg-black/30 dark:bg-white/30 backdrop-blur-xl rounded-xl">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
const prevIndex = Math.max(selectedIndex - 1, 0);
|
|
||||||
setSelectedIndex(prevIndex);
|
|
||||||
ref.current.scrollToIndex(prevIndex, {
|
|
||||||
align: "center",
|
|
||||||
smooth: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="inline-flex items-center justify-center rounded-lg text-white/70 hover:text-white hover:bg-black/30 size-10"
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon className="size-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
const nextIndex = Math.min(selectedIndex + 1, columns.length - 1);
|
|
||||||
setSelectedIndex(nextIndex);
|
|
||||||
ref.current.scrollToIndex(nextIndex, {
|
|
||||||
align: "center",
|
|
||||||
smooth: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="inline-flex items-center justify-center rounded-lg text-white/70 hover:text-white hover:bg-black/30 size-10"
|
|
||||||
>
|
|
||||||
<ArrowRightIcon className="size-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={async () =>
|
|
||||||
await addColumn({
|
|
||||||
kind: COL_TYPES.default,
|
|
||||||
title: "",
|
|
||||||
content: "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="inline-flex items-center justify-center rounded-lg text-white/70 hover:text-white hover:bg-black/30 size-10"
|
|
||||||
>
|
|
||||||
<PlusSquareIcon className="size-5" />
|
|
||||||
</button>
|
|
||||||
<div className="w-px h-6 bg-white/10" />
|
|
||||||
<TutorialModal />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { NoteSkeleton, RepostNote, TextNote, useArk } from "@lume/ark";
|
|
||||||
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { FETCH_LIMIT } from "@lume/utils";
|
|
||||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
||||||
import { useCallback, useMemo } from "react";
|
|
||||||
import { VList } from "virtua";
|
|
||||||
|
|
||||||
export function RelayEventList({ relayUrl }: { relayUrl: string }) {
|
|
||||||
const ark = useArk();
|
|
||||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
|
||||||
useInfiniteQuery({
|
|
||||||
queryKey: ["relay-events", relayUrl],
|
|
||||||
initialPageParam: 0,
|
|
||||||
queryFn: async ({
|
|
||||||
signal,
|
|
||||||
pageParam,
|
|
||||||
}: {
|
|
||||||
signal: AbortSignal;
|
|
||||||
pageParam: number;
|
|
||||||
}) => {
|
|
||||||
const url = `wss://${relayUrl}`;
|
|
||||||
const events = await ark.getRelayEvents({
|
|
||||||
relayUrl: url,
|
|
||||||
filter: {
|
|
||||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
|
||||||
},
|
|
||||||
limit: FETCH_LIMIT,
|
|
||||||
pageParam,
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
return events;
|
|
||||||
},
|
|
||||||
getNextPageParam: (lastPage) => {
|
|
||||||
const lastEvent = lastPage.at(-1);
|
|
||||||
if (!lastEvent) return;
|
|
||||||
return lastEvent.created_at - 1;
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const allEvents = useMemo(
|
|
||||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
|
||||||
(event: NDKEvent) => {
|
|
||||||
switch (event.kind) {
|
|
||||||
case NDKKind.Text:
|
|
||||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
|
||||||
case NDKKind.Repost:
|
|
||||||
return <RepostNote key={event.id} event={event} className="mt-3" />;
|
|
||||||
default:
|
|
||||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VList className="mx-auto h-full w-full max-w-[500px] px-3 scrollbar-none">
|
|
||||||
{status === "pending" ? (
|
|
||||||
<NoteSkeleton />
|
|
||||||
) : (
|
|
||||||
allEvents.map((item) => renderItem(item))
|
|
||||||
)}
|
|
||||||
<div className="flex h-16 items-center justify-center px-3 pb-3">
|
|
||||||
{hasNextPage ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => fetchNextPage()}
|
|
||||||
disabled={!hasNextPage || isFetchingNextPage}
|
|
||||||
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
|
|
||||||
>
|
|
||||||
{isFetchingNextPage ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
|
||||||
Load more
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</VList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { useRelaylist } from "@lume/ark";
|
|
||||||
import { PlusIcon } from "@lume/icons";
|
|
||||||
import { NDKRelayUrl } from "@nostr-dev-kit/ndk";
|
|
||||||
import { normalizeRelayUrl } from "nostr-fetch";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/;
|
|
||||||
|
|
||||||
export function RelayForm() {
|
|
||||||
const { connectRelay } = useRelaylist();
|
|
||||||
|
|
||||||
const [relay, setRelay] = useState<{
|
|
||||||
url: NDKRelayUrl;
|
|
||||||
purpose: "read" | "write" | undefined;
|
|
||||||
}>({ url: "", purpose: undefined });
|
|
||||||
|
|
||||||
const create = () => {
|
|
||||||
if (relay.url.length < 1) return toast.info("Please enter relay url");
|
|
||||||
try {
|
|
||||||
const relayUrl = new URL(relay.url.replace(/\s/g, ""));
|
|
||||||
if (
|
|
||||||
domainRegex.test(relayUrl.host) &&
|
|
||||||
(relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:")
|
|
||||||
) {
|
|
||||||
connectRelay.mutate(normalizeRelayUrl(relay.url));
|
|
||||||
setRelay({ url: "", purpose: undefined });
|
|
||||||
} else {
|
|
||||||
return toast.error(
|
|
||||||
"URL is invalid, a relay must use websocket protocol (start with wss:// or ws://). Please check again",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return toast.error("Relay URL is not valid. Please check again");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
className="h-11 w-full rounded-lg border-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 bg-white/50 dark:bg-black/50 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
placeholder="wss://"
|
|
||||||
spellCheck={false}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
value={relay.url}
|
|
||||||
onChange={(e) => setRelay((prev) => ({ ...prev, url: e.target.value }))}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => create()}
|
|
||||||
className="inline-flex size-11 shrink-0 items-center justify-center rounded-lg bg-blue-500 text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
<PlusIcon className="size-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { useRelaylist } from "@lume/ark";
|
|
||||||
import { PlusIcon, ShareIcon } from "@lume/icons";
|
|
||||||
import { normalizeRelayUrl } from "nostr-fetch";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function RelayItem({ url }: { url: string }) {
|
|
||||||
const domain = new URL(url).hostname;
|
|
||||||
const { connectRelay } = useRelaylist();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-14 w-full items-center justify-between border-b border-neutral-100 px-5 dark:border-neutral-950">
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<span className="text-sm font-semibold text-neutral-500 dark:text-neutral-400">
|
|
||||||
Relay:{" "}
|
|
||||||
</span>
|
|
||||||
<span className="max-w-[200px] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
|
||||||
{url}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<Link
|
|
||||||
to={`/relays/${domain}/`}
|
|
||||||
className="inline-flex h-6 items-center justify-center gap-1 rounded bg-neutral-100 px-1.5 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<ShareIcon className="h-3 w-3" />
|
|
||||||
Inspect
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => connectRelay.mutate(normalizeRelayUrl(url))}
|
|
||||||
className="inline-flex h-6 w-6 items-center justify-center rounded bg-blue-100 text-blue-500 hover:bg-blue-200 dark:bg-blue-900 hover:dark:bg-blue-800"
|
|
||||||
>
|
|
||||||
<PlusIcon className="size-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { useArk, useRelay } from "@lume/ark";
|
|
||||||
import { LoaderIcon, PlusIcon, ShareIcon } from "@lume/icons";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { VList } from "virtua";
|
|
||||||
|
|
||||||
export function RelayList() {
|
|
||||||
const ark = useArk();
|
|
||||||
const { connectRelay } = useRelay();
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ["relays"],
|
|
||||||
queryFn: async () => {
|
|
||||||
return await ark.getAllRelaysFromContacts();
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
staleTime: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const inspectRelay = (relayUrl: string) => {
|
|
||||||
const url = new URL(relayUrl);
|
|
||||||
navigate(`/relays/${url.hostname}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="col-span-2 bg-white">
|
|
||||||
{status === "pending" ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center pb-10">
|
|
||||||
<div className="inline-flex flex-col items-center justify-center gap-2">
|
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-neutral-900 dark:text-neutral-100" />
|
|
||||||
<p>Loading relay...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<VList className="h-full">
|
|
||||||
<div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
|
|
||||||
<h3 className="font-semibold">Relay discovery</h3>
|
|
||||||
</div>
|
|
||||||
{[...data].map(([key, value]) => (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
className="flex h-14 w-full items-center justify-between border-b border-neutral-100 px-3 dark:border-neutral-900"
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center gap-2 divide-x divide-neutral-100 dark:divide-neutral-900">
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => inspectRelay(key)}
|
|
||||||
className="inline-flex h-6 items-center justify-center gap-1 rounded bg-neutral-200 px-1.5 text-sm font-medium text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
<ShareIcon className="h-3 w-3" />
|
|
||||||
Inspect
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => connectRelay.mutate(key)}
|
|
||||||
className="inline-flex h-6 w-6 items-center justify-center rounded text-neutral-900 hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex items-center gap-2 pl-3">
|
|
||||||
<span className="text-sm font-semibold text-neutral-500 dark:text-neutral-400">
|
|
||||||
Relay:{" "}
|
|
||||||
</span>
|
|
||||||
<span className="max-w-[200px] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
|
||||||
{key}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="isolate flex -space-x-2">
|
|
||||||
{value.slice(0, 4).map((item) => (
|
|
||||||
<User key={item} pubkey={item} variant="stacked" />
|
|
||||||
))}
|
|
||||||
{value.length > 4 ? (
|
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-700">
|
|
||||||
<span className="text-xs font-medium">+{value.length}</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</VList>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { CancelIcon, RefreshIcon } from "@lume/icons";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
import { NDKKind } from "@nostr-dev-kit/ndk";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { RelayForm } from "./relayForm";
|
|
||||||
|
|
||||||
export function RelaySidebar({ className }: { className?: string }) {
|
|
||||||
const ark = useArk();
|
|
||||||
|
|
||||||
const { status, data, refetch } = useQuery({
|
|
||||||
queryKey: ["relay-personal"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const event = await ark.getEventByFilter({
|
|
||||||
filter: {
|
|
||||||
kinds: [NDKKind.RelayList],
|
|
||||||
authors: [ark.account.pubkey],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!event) return [];
|
|
||||||
return event.tags.filter((tag) => tag[0] === "r");
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentRelays = new Set(
|
|
||||||
ark.ndk.pool.connectedRelays().map((item) => item.url),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"rounded-l-xl bg-white/50 backdrop-blur-xl dark:bg-black/50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center justify-between w-full h-14 px-3 border-b border-black/10 dark:border-white/10">
|
|
||||||
<h3 className="font-semibold">Connected relays</h3>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => refetch()}
|
|
||||||
className="inline-flex items-center justify-center w-6 h-6 rounded-md shrink-0 hover:bg-neutral-100 dark:hover:bg-neutral-900"
|
|
||||||
>
|
|
||||||
<RefreshIcon className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2 px-3 mt-3">
|
|
||||||
{status === "pending" ? (
|
|
||||||
<p>Loading...</p>
|
|
||||||
) : !data.length ? (
|
|
||||||
<div className="flex items-center justify-center w-full h-20 rounded-xl bg-neutral-50 dark:bg-neutral-950">
|
|
||||||
<p className="text-sm font-medium">
|
|
||||||
You not have personal relay list yet
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
data.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item[1]}
|
|
||||||
className="flex items-center justify-between px-3 rounded-lg group h-11 bg-white/50 dark:bg-black/50"
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-baseline gap-2">
|
|
||||||
{currentRelays.has(item[1]) ? (
|
|
||||||
<span className="relative flex w-2 h-2">
|
|
||||||
<span className="absolute inline-flex w-full h-full bg-green-400 rounded-full opacity-75 animate-ping" />
|
|
||||||
<span className="relative inline-flex w-2 h-2 bg-teal-500 rounded-full" />
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="relative flex w-2 h-2">
|
|
||||||
<span className="absolute inline-flex w-full h-full bg-red-400 rounded-full opacity-75 animate-ping" />
|
|
||||||
<span className="relative inline-flex w-2 h-2 bg-red-500 rounded-full" />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<p className="max-w-[20rem] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
|
||||||
{item[1].replace("wss://", "").replace("ws://", "")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
{item[2]?.length ? (
|
|
||||||
<div className="inline-flex items-center justify-center h-6 px-2 text-xs font-medium capitalize rounded w-max bg-neutral-200 dark:bg-neutral-800">
|
|
||||||
{item[2]}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="items-center justify-center hidden w-6 h-6 rounded group-hover:inline-flex hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
<CancelIcon className="w-4 h-4 text-neutral-900 dark:text-neutral-100" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
<RelayForm />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon, PlusIcon, ShareIcon } from "@lume/icons";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { VList } from "virtua";
|
|
||||||
import { RelayItem } from "./components/relayItem";
|
|
||||||
|
|
||||||
export function RelayFollowsScreen() {
|
|
||||||
const ark = useArk();
|
|
||||||
const { isLoading, data: relays } = useQuery({
|
|
||||||
queryKey: ["relay-follows"],
|
|
||||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
|
||||||
return await ark.getAllRelaysFromContacts();
|
|
||||||
},
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VList itemSize={49}>
|
|
||||||
{relays.map((item: string) => (
|
|
||||||
<RelayItem key={item} url={item} />
|
|
||||||
))}
|
|
||||||
</VList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { fetch } from "@tauri-apps/plugin-http";
|
|
||||||
import { VList } from "virtua";
|
|
||||||
import { RelayItem } from "./components/relayItem";
|
|
||||||
|
|
||||||
export function RelayGlobalScreen() {
|
|
||||||
const { isLoading, data: relays } = useQuery({
|
|
||||||
queryKey: ["relay-global"],
|
|
||||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
|
||||||
const res = await fetch("https://api.nostr.watch/v1/online", { signal });
|
|
||||||
if (!res.ok) throw new Error("Failed to get online relays");
|
|
||||||
return (await res.json()) as string[];
|
|
||||||
},
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VList itemSize={49}>
|
|
||||||
{relays.map((item: string) => (
|
|
||||||
<RelayItem key={item} url={item} />
|
|
||||||
))}
|
|
||||||
</VList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { cn } from "@lume/utils";
|
|
||||||
import { NavLink, Outlet } from "react-router-dom";
|
|
||||||
import { RelaySidebar } from "./components/sidebar";
|
|
||||||
|
|
||||||
export function RelaysScreen() {
|
|
||||||
return (
|
|
||||||
<div className="grid h-full w-full lg:grid-cols-4 xl:grid-cols-5 rounded-xl shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:shadow-none dark:ring-1 dark:ring-white/10">
|
|
||||||
<RelaySidebar className="col-span-1" />
|
|
||||||
<div className="col-span-3 xl:col-span-4 flex flex-col rounded-r-xl bg-white dark:bg-black">
|
|
||||||
<div className="h-14 shrink-0 flex px-5 items-center gap-6 border-b border-neutral-100 dark:border-neutral-950">
|
|
||||||
<NavLink
|
|
||||||
end
|
|
||||||
to={"/relays/"}
|
|
||||||
className={({ isActive }) =>
|
|
||||||
cn(
|
|
||||||
"h-9 w-24 rounded-lg inline-flex items-center justify-center font-medium",
|
|
||||||
isActive
|
|
||||||
? "bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
|
||||||
: "",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Global
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
to={"/relays/follows/"}
|
|
||||||
className={({ isActive }) =>
|
|
||||||
cn(
|
|
||||||
"h-9 w-24 rounded-lg inline-flex items-center justify-center font-medium",
|
|
||||||
isActive
|
|
||||||
? "bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
|
||||||
: "",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Follows
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import { ArrowLeftIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { NIP11 } from "@lume/types";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import { Await, useLoaderData, useNavigate, useParams } from "react-router-dom";
|
|
||||||
import { RelayEventList } from "./components/relayEventList";
|
|
||||||
|
|
||||||
export function RelayUrlScreen() {
|
|
||||||
const { url } = useParams();
|
|
||||||
|
|
||||||
const data: { relay?: { [key: string]: string } } = useLoaderData();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const getSoftwareName = (url: string) => {
|
|
||||||
const filename = url.substring(url.lastIndexOf("/") + 1);
|
|
||||||
return filename.replace(".git", "");
|
|
||||||
};
|
|
||||||
|
|
||||||
const titleCase = (s: string) => {
|
|
||||||
return s
|
|
||||||
.replace(/^[-_]*(.)/, (_, c) => c.toUpperCase())
|
|
||||||
.replace(/[-_]+(.)/g, (_, c) => ` ${c.toUpperCase()}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid h-full w-full grid-cols-3">
|
|
||||||
<div className="col-span-2 border-r border-neutral-100 dark:border-neutral-900">
|
|
||||||
<RelayEventList relayUrl={url} />
|
|
||||||
</div>
|
|
||||||
<div className="col-span-1 px-3 py-3">
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<div className="flex items-center gap-2 text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Await
|
|
||||||
resolve={data.relay}
|
|
||||||
errorElement={
|
|
||||||
<div className="text-sm font-medium">
|
|
||||||
<p>Could not load relay information 😬</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(resolvedRelay: NIP11) => (
|
|
||||||
<div className="flex flex-col gap-5">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold">{resolvedRelay.name}</h3>
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-500">
|
|
||||||
{resolvedRelay.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{resolvedRelay.pubkey ? (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
|
||||||
Owner:
|
|
||||||
</h5>
|
|
||||||
<div className="w-full rounded-lg bg-neutral-100 px-2 py-2 dark:bg-neutral-900">
|
|
||||||
<User pubkey={resolvedRelay.pubkey} variant="simple" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{resolvedRelay.contact ? (
|
|
||||||
<div>
|
|
||||||
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
|
||||||
Contact:
|
|
||||||
</h5>
|
|
||||||
<a
|
|
||||||
href={`mailto:${resolvedRelay.contact}`}
|
|
||||||
target="_blank"
|
|
||||||
className="truncate underline after:content-['_↗'] hover:text-blue-500"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{resolvedRelay.contact}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div>
|
|
||||||
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
|
||||||
Software:
|
|
||||||
</h5>
|
|
||||||
<a
|
|
||||||
href={resolvedRelay.software}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="underline after:content-['_↗'] hover:text-blue-500"
|
|
||||||
>
|
|
||||||
{`${getSoftwareName(resolvedRelay.software)} - ${
|
|
||||||
resolvedRelay.version
|
|
||||||
}`}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
|
||||||
Supported NIPs:
|
|
||||||
</h5>
|
|
||||||
<div className="mt-2 grid grid-cols-7 gap-2">
|
|
||||||
{resolvedRelay.supported_nips.map((item) => (
|
|
||||||
<a
|
|
||||||
key={item}
|
|
||||||
href={`https://nips.be/${item}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="inline-flex aspect-square h-auto w-full items-center justify-center rounded bg-neutral-100 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{resolvedRelay.limitation ? (
|
|
||||||
<div>
|
|
||||||
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
|
||||||
Limitation
|
|
||||||
</h5>
|
|
||||||
<div className="flex flex-col gap-2 divide-y divide-white/5">
|
|
||||||
{Object.keys(resolvedRelay.limitation).map(
|
|
||||||
(key, index) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={key + index}
|
|
||||||
className="flex items-baseline justify-between pt-2"
|
|
||||||
>
|
|
||||||
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
|
||||||
{titleCase(key)}:
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
|
||||||
{resolvedRelay.limitation[key].toString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{resolvedRelay.payments_url ? (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<a
|
|
||||||
href={resolvedRelay.payments_url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Open payment website
|
|
||||||
</a>
|
|
||||||
<span className="text-center text-xs text-neutral-600 dark:text-neutral-400">
|
|
||||||
You need to make a payment to connect this relay
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Await>
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { getVersion } from '@tauri-apps/api/app';
|
|
||||||
import { relaunch } from '@tauri-apps/plugin-process';
|
|
||||||
import { Update, check } from '@tauri-apps/plugin-updater';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export function AboutScreen() {
|
|
||||||
const [version, setVersion] = useState('');
|
|
||||||
const [newUpdate, setNewUpdate] = useState<Update>(null);
|
|
||||||
|
|
||||||
const checkUpdate = async () => {
|
|
||||||
const update = await check();
|
|
||||||
if (!update) toast.info('There is no update available');
|
|
||||||
setNewUpdate(update);
|
|
||||||
};
|
|
||||||
|
|
||||||
const installUpdate = async () => {
|
|
||||||
await newUpdate.downloadAndInstall();
|
|
||||||
await relaunch();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function loadVersion() {
|
|
||||||
const appVersion = await getVersion();
|
|
||||||
setVersion(appVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadVersion();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto w-full max-w-lg">
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<img src="/icon.png" alt="Lume's logo" className="w-16 shrink-0" />
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-semibold">Lume</h1>
|
|
||||||
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
||||||
Version {version}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto mt-4 flex w-full max-w-xs flex-col gap-2">
|
|
||||||
{!newUpdate ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => checkUpdate()}
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Check for update
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => installUpdate()}
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Install {newUpdate.version}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<Link
|
|
||||||
to="https://lume.nu"
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Website
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="https://github.com/luminous-devs/lume/issues"
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Report a issue
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { useStorage } from "@lume/storage";
|
|
||||||
|
|
||||||
export function AdvancedSettingScreen() {
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const clearCache = async () => {
|
|
||||||
await storage.clearCache();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto w-full max-w-lg">
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<div className="w-24 shrink-0 text-end text-sm font-semibold">
|
|
||||||
Cache
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">Use for boost up nostr connection</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => clearCache()}
|
|
||||||
className="h-8 w-max rounded-lg px-3 text-sm font-semibold text-blue-500 bg-blue-100 hover:bg-blue-200"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { EyeOffIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { nip19 } from "nostr-tools";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export function BackupSettingScreen() {
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const [privkey, setPrivkey] = useState(null);
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
|
|
||||||
const removePrivkey = async () => {
|
|
||||||
await storage.removePrivkey(ark.account.pubkey);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function loadPrivkey() {
|
|
||||||
const key = await storage.loadPrivkey(ark.account.pubkey);
|
|
||||||
if (key) setPrivkey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPrivkey();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto w-full max-w-lg">
|
|
||||||
<div className="mb-2 text-sm font-semibold">Private key</div>
|
|
||||||
<div>
|
|
||||||
{!privkey ? (
|
|
||||||
<div className="inline-flex h-24 w-full items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
You've stored private key on Lume
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
readOnly
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
value={nip19.nsecEncode(privkey)}
|
|
||||||
className="relative h-11 w-full resize-none rounded-lg border-none bg-neutral-200 py-1 pl-3 pr-11 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
className="absolute right-1.5 top-1/2 inline-flex h-8 w-8 -translate-y-1/2 transform items-center justify-center rounded-lg bg-neutral-50 dark:bg-neutral-950"
|
|
||||||
>
|
|
||||||
<EyeOffIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removePrivkey()}
|
|
||||||
className="mt-2 inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-red-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:hover:text-white"
|
|
||||||
>
|
|
||||||
Remove private key
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { EditIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { compactNumber } from "@lume/utils";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function ContactCard() {
|
|
||||||
const ark = useArk();
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ["contacts"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const contacts = await ark.getUserContacts();
|
|
||||||
return contacts;
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
|
||||||
{status === "pending" ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
|
||||||
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
|
|
||||||
{compactNumber.format(data.length)}
|
|
||||||
</h3>
|
|
||||||
<div className="mt-auto flex h-6 w-full items-center justify-between">
|
|
||||||
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
|
|
||||||
Contacts
|
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
to="/settings/edit-contact"
|
|
||||||
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
<EditIcon className="h-3 w-3" />
|
|
||||||
Edit
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { compactNumber } from "@lume/utils";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { fetch } from "@tauri-apps/plugin-http";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function PostCard() {
|
|
||||||
const ark = useArk();
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ["user-stats", ark.account.pubkey],
|
|
||||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
|
||||||
const res = await fetch(
|
|
||||||
`https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`,
|
|
||||||
{
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Error");
|
|
||||||
}
|
|
||||||
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
staleTime: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
|
||||||
{status === "pending" ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
|
||||||
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
|
|
||||||
{compactNumber.format(
|
|
||||||
data.stats[ark.account.pubkey].pub_note_count,
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
<div className="mt-auto flex h-6 w-full items-center justify-between">
|
|
||||||
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
|
|
||||||
Posts
|
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
to={`/users/${ark.account.pubkey}`}
|
|
||||||
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { useArk, useProfile } from "@lume/ark";
|
|
||||||
import { EditIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { displayNpub } from "@lume/utils";
|
|
||||||
import * as Avatar from "@radix-ui/react-avatar";
|
|
||||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
|
||||||
import { minidenticon } from "minidenticons";
|
|
||||||
import { nip19 } from "nostr-tools";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function ProfileCard() {
|
|
||||||
const ark = useArk();
|
|
||||||
const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent(
|
|
||||||
minidenticon(ark.account.pubkey, 90, 50),
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
const { isLoading, user } = useProfile(ark.account.pubkey);
|
|
||||||
|
|
||||||
const copyNpub = async () => {
|
|
||||||
return await writeText(nip19.npubEncode(ark.account.pubkey));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-4 h-56 w-full rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
|
||||||
<div className="flex h-10 w-full justify-end gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={copyNpub}
|
|
||||||
className="inline-flex h-8 w-28 transform items-center justify-center gap-1.5 rounded-full bg-neutral-200 text-sm font-medium hover:bg-neutral-400 active:translate-y-1 dark:bg-neutral-800 dark:hover:bg-neutral-600"
|
|
||||||
>
|
|
||||||
Copy NPUB
|
|
||||||
</button>
|
|
||||||
<Link
|
|
||||||
to="/settings/edit-profile"
|
|
||||||
className="inline-flex h-8 w-20 items-center justify-center gap-1.5 rounded-full bg-neutral-200 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-600"
|
|
||||||
>
|
|
||||||
<EditIcon className="h-4 w-4" />
|
|
||||||
Edit
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2.5">
|
|
||||||
<Avatar.Root className="shrink-0">
|
|
||||||
<Avatar.Image
|
|
||||||
src={user?.picture || user?.image}
|
|
||||||
alt={ark.account.pubkey}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
style={{ contentVisibility: "auto" }}
|
|
||||||
className="h-16 w-16 rounded-xl border border-neutral-200/50 shadow-[rgba(17,_17,_26,_0.1)_0px_0px_16px] dark:border-neutral-800/50"
|
|
||||||
/>
|
|
||||||
<Avatar.Fallback delayMs={300}>
|
|
||||||
<img
|
|
||||||
src={svgURI}
|
|
||||||
alt={ark.account.pubkey}
|
|
||||||
className="h-16 w-16 rounded-xl bg-black dark:bg-white"
|
|
||||||
/>
|
|
||||||
</Avatar.Fallback>
|
|
||||||
</Avatar.Root>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-3xl font-semibold leading-8 text-neutral-900 dark:text-neutral-100">
|
|
||||||
{user?.display_name || user?.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-lg text-neutral-700 dark:text-neutral-300">
|
|
||||||
{user?.nip05 || displayNpub(ark.account.pubkey, 16)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { EditIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { compactNumber } from "@lume/utils";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export function RelayCard() {
|
|
||||||
const ark = useArk();
|
|
||||||
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ["relays", ark.account.pubkey],
|
|
||||||
queryFn: async () => {
|
|
||||||
const relays = await ark.getUserRelays({});
|
|
||||||
return relays;
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
|
||||||
{status === "pending" ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
|
||||||
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
|
|
||||||
{compactNumber.format(data?.relays?.length || 0)}
|
|
||||||
</h3>
|
|
||||||
<div className="mt-auto flex h-6 w-full items-center justify-between">
|
|
||||||
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
|
|
||||||
Relays
|
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
to="/relays"
|
|
||||||
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
<EditIcon className="h-3 w-3" />
|
|
||||||
Edit
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function NWCForm({ setWalletConnectURL }) {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const [uri, setUri] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (!uri.startsWith("nostr+walletconnect:")) {
|
|
||||||
toast.error(
|
|
||||||
"Connect URI is required and must start with format nostr+walletconnect:, please check again",
|
|
||||||
);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uriObj = new URL(uri);
|
|
||||||
const params = new URLSearchParams(uriObj.search);
|
|
||||||
|
|
||||||
if (params.has("relay") && params.has("secret")) {
|
|
||||||
await storage.createPrivkey(`${ark.account.pubkey}-nwc`, uri);
|
|
||||||
setWalletConnectURL(uri);
|
|
||||||
setLoading(false);
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
toast.error("Connect URI is not valid, please check again");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
toast.error(String(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
|
||||||
<textarea
|
|
||||||
name="walletConnectURL"
|
|
||||||
value={uri}
|
|
||||||
onChange={(e) => setUri(e.target.value)}
|
|
||||||
placeholder="nostr+walletconnect://"
|
|
||||||
className="h-40 w-full resize-none rounded-lg border-transparent bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:focus:ring-blue-800 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={submit}
|
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : "Connect"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { compactNumber } from "@lume/utils";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { fetch } from "@tauri-apps/plugin-http";
|
|
||||||
|
|
||||||
export function ZapCard() {
|
|
||||||
const ark = useArk();
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ["user-stats", ark.account.pubkey],
|
|
||||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
|
||||||
const res = await fetch(
|
|
||||||
`https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`,
|
|
||||||
{
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Error");
|
|
||||||
}
|
|
||||||
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
staleTime: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
|
||||||
{status === "pending" ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
|
||||||
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
|
|
||||||
{compactNumber.format(
|
|
||||||
data?.stats[ark.account.pubkey]?.zaps_received?.msats / 1000 || 0,
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
<div className="mt-auto flex h-6 items-center text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
|
|
||||||
Sats received
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
export function EditContactScreen() {
|
|
||||||
const ark = useArk();
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ["contacts"],
|
|
||||||
queryFn: async () => {
|
|
||||||
return await ark.getUserContacts();
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto flex w-full max-w-xl flex-col gap-3">
|
|
||||||
{status === "pending" ? (
|
|
||||||
<div className="flex h-10 w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
data.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item}
|
|
||||||
className="flex h-16 w-full items-center justify-between rounded-xl bg-neutral-100 px-2.5 dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
<User pubkey={item} variant="simple" />
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import {
|
|
||||||
CheckCircleIcon,
|
|
||||||
LoaderIcon,
|
|
||||||
PlusIcon,
|
|
||||||
UnverifiedIcon,
|
|
||||||
} from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { NDKKind, NDKUserProfile } from "@nostr-dev-kit/ndk";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
export function EditProfileScreen() {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [picture, setPicture] = useState("");
|
|
||||||
const [banner, setBanner] = useState("");
|
|
||||||
const [nip05, setNIP05] = useState({ verified: true, text: "" });
|
|
||||||
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
setError,
|
|
||||||
formState: { isValid, errors },
|
|
||||||
} = useForm({
|
|
||||||
defaultValues: async () => {
|
|
||||||
const res: NDKUserProfile = queryClient.getQueryData([
|
|
||||||
"user",
|
|
||||||
ark.account.pubkey,
|
|
||||||
]);
|
|
||||||
if (res.image) {
|
|
||||||
setPicture(res.image);
|
|
||||||
}
|
|
||||||
if (res.banner) {
|
|
||||||
setBanner(res.banner);
|
|
||||||
}
|
|
||||||
if (res.nip05) {
|
|
||||||
setNIP05((prev) => ({ ...prev, text: res.nip05 }));
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const uploadAvatar = async () => {
|
|
||||||
try {
|
|
||||||
if (!ark.ndk.signer) return navigate("/new/privkey");
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const image = await ark.upload({});
|
|
||||||
if (image) {
|
|
||||||
setPicture(image);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
await message(`Upload failed, error: ${e}`, {
|
|
||||||
title: "Lume",
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadBanner = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const image = await ark.upload({});
|
|
||||||
|
|
||||||
if (image) {
|
|
||||||
setBanner(image);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
await message(`Upload failed, error: ${e}`, {
|
|
||||||
title: "Lume",
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (data: NDKUserProfile) => {
|
|
||||||
// start loading
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
let content = {
|
|
||||||
...data,
|
|
||||||
username: data.name,
|
|
||||||
display_name: data.name,
|
|
||||||
bio: data.about,
|
|
||||||
image: data.picture,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data.nip05) {
|
|
||||||
const verify = ark.validateNIP05({
|
|
||||||
pubkey: ark.account.pubkey,
|
|
||||||
nip05: data.nip05,
|
|
||||||
});
|
|
||||||
if (verify) {
|
|
||||||
content = { ...content, nip05: data.nip05 };
|
|
||||||
} else {
|
|
||||||
setNIP05((prev) => ({ ...prev, verified: false }));
|
|
||||||
setError("nip05", {
|
|
||||||
type: "manual",
|
|
||||||
message: "Can't verify your Lume ID / NIP-05, please check again",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const publish = await ark.createEvent({
|
|
||||||
kind: NDKKind.Metadata,
|
|
||||||
tags: [],
|
|
||||||
content: JSON.stringify(content),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (publish) {
|
|
||||||
// invalid cache
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["user", ark.account.pubkey],
|
|
||||||
});
|
|
||||||
// reset form
|
|
||||||
reset();
|
|
||||||
// reset state
|
|
||||||
setLoading(false);
|
|
||||||
setPicture(null);
|
|
||||||
setBanner(null);
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto w-full max-w-md">
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0">
|
|
||||||
<input type={"hidden"} {...register("picture")} value={picture} />
|
|
||||||
<input type={"hidden"} {...register("banner")} value={banner} />
|
|
||||||
<div className="flex flex-col items-center justify-center">
|
|
||||||
<div className="relative h-36 w-full">
|
|
||||||
{banner ? (
|
|
||||||
<img
|
|
||||||
src={banner}
|
|
||||||
alt="user's banner"
|
|
||||||
className="h-full w-full rounded-xl object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="h-full w-full rounded-xl bg-neutral-200 dark:bg-neutral-900" />
|
|
||||||
)}
|
|
||||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform overflow-hidden rounded-xl">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => uploadBanner()}
|
|
||||||
className="inline-flex h-full w-full items-center justify-center bg-black/20 text-white"
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-5 px-4">
|
|
||||||
<div className="relative z-10 -mt-7 h-14 w-14 overflow-hidden rounded-xl ring-2 ring-white dark:ring-black">
|
|
||||||
<img
|
|
||||||
src={picture}
|
|
||||||
alt="user's avatar"
|
|
||||||
className="h-14 w-14 rounded-xl object-cover"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => uploadAvatar()}
|
|
||||||
className="inline-flex h-full w-full items-center justify-center rounded-xl bg-black/50 text-white"
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="display_name"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Display Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={"text"}
|
|
||||||
{...register("display_name")}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={"text"}
|
|
||||||
{...register("name")}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="nip05"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
NIP-05
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
{...register("nip05")}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
|
||||||
/>
|
|
||||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform">
|
|
||||||
{nip05.verified ? (
|
|
||||||
<span className="inline-flex h-6 items-center gap-1 rounded-full bg-teal-500 px-1 pr-1.5 text-xs font-medium text-white">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
Verified
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex h-6 items-center gap-1 rounded bg-red-500 pl-1 pr-1.5 text-xs font-medium text-white">
|
|
||||||
<UnverifiedIcon className="h-4 w-4" />
|
|
||||||
Unverified
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{errors.nip05 && (
|
|
||||||
<p className="mt-1 text-sm text-red-400">
|
|
||||||
{errors.nip05.message.toString()}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="website"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Website
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={"text"}
|
|
||||||
{...register("website", { required: false })}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="website"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Lightning address
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={"text"}
|
|
||||||
{...register("lud16", { required: false })}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="about"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Bio
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
{...register("about")}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-20 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isValid}
|
|
||||||
className="mx-auto inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
|
||||||
) : (
|
|
||||||
"Update"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
import { CheckIcon, DarkIcon, LightIcon, SystemModeIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import * as Switch from "@radix-ui/react-switch";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { getCurrent } from "@tauri-apps/api/window";
|
|
||||||
import { disable, enable, isEnabled } from "@tauri-apps/plugin-autostart";
|
|
||||||
import {
|
|
||||||
isPermissionGranted,
|
|
||||||
requestPermission,
|
|
||||||
} from "@tauri-apps/plugin-notification";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
export function GeneralSettingScreen() {
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const [apiKey, setAPIKey] = useState("");
|
|
||||||
const [settings, setSettings] = useState({
|
|
||||||
lowPower: false,
|
|
||||||
autoupdate: false,
|
|
||||||
autolaunch: false,
|
|
||||||
outbox: false,
|
|
||||||
media: true,
|
|
||||||
hashtag: true,
|
|
||||||
notification: true,
|
|
||||||
translation: false,
|
|
||||||
appearance: "system",
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeTheme = async (theme: "light" | "dark" | "auto") => {
|
|
||||||
await invoke("plugin:theme|set_theme", { theme });
|
|
||||||
// update state
|
|
||||||
setSettings((prev) => ({ ...prev, appearance: theme }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleLowPower = async () => {
|
|
||||||
await storage.createSetting("lowPower", String(+!settings.lowPower));
|
|
||||||
setSettings((state) => ({ ...state, lowPower: !settings.lowPower }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleAutolaunch = async () => {
|
|
||||||
if (!settings.autolaunch) {
|
|
||||||
await enable();
|
|
||||||
// update state
|
|
||||||
setSettings((prev) => ({ ...prev, autolaunch: true }));
|
|
||||||
} else {
|
|
||||||
await disable();
|
|
||||||
// update state
|
|
||||||
setSettings((prev) => ({ ...prev, autolaunch: false }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleMedia = async () => {
|
|
||||||
await storage.createSetting("media", String(+!settings.media));
|
|
||||||
storage.settings.media = !settings.media;
|
|
||||||
// update state
|
|
||||||
setSettings((prev) => ({ ...prev, media: !settings.media }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleHashtag = async () => {
|
|
||||||
await storage.createSetting("hashtag", String(+!settings.hashtag));
|
|
||||||
storage.settings.hashtag = !settings.hashtag;
|
|
||||||
// update state
|
|
||||||
setSettings((prev) => ({ ...prev, hashtag: !settings.hashtag }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleAutoupdate = async () => {
|
|
||||||
await storage.createSetting("autoupdate", String(+!settings.autoupdate));
|
|
||||||
storage.settings.autoupdate = !settings.autoupdate;
|
|
||||||
// update state
|
|
||||||
setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleNofitication = async () => {
|
|
||||||
if (settings.notification) return;
|
|
||||||
|
|
||||||
await requestPermission();
|
|
||||||
// update state
|
|
||||||
setSettings((prev) => ({ ...prev, notification: !settings.notification }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleTranslation = async () => {
|
|
||||||
await storage.createSetting("translation", String(+!settings.translation));
|
|
||||||
storage.settings.translation = !settings.translation;
|
|
||||||
// update state
|
|
||||||
setSettings((prev) => ({ ...prev, translation: !settings.translation }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveApi = async () => {
|
|
||||||
await storage.createSetting("translateApiKey", apiKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function loadSettings() {
|
|
||||||
const theme = await getCurrent().theme();
|
|
||||||
setSettings((prev) => ({ ...prev, appearance: theme }));
|
|
||||||
|
|
||||||
const autostart = await isEnabled();
|
|
||||||
setSettings((prev) => ({ ...prev, autolaunch: autostart }));
|
|
||||||
|
|
||||||
const permissionGranted = await isPermissionGranted();
|
|
||||||
setSettings((prev) => ({ ...prev, notification: permissionGranted }));
|
|
||||||
|
|
||||||
const data = await storage.getAllSettings();
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
for (const item of data) {
|
|
||||||
if (item.key === "autoupdate")
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
autoupdate: !!parseInt(item.value),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (item.key === "lowPower")
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
lowPower: !!parseInt(item.value),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (item.key === "outbox")
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
outbox: !!parseInt(item.value),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (item.key === "media")
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
media: !!parseInt(item.value),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (item.key === "hashtag")
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
hashtag: !!parseInt(item.value),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (item.key === "translation")
|
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
translation: !!parseInt(item.value),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSettings();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto w-full max-w-lg">
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<div className="w-24 shrink-0 text-end text-sm font-semibold">
|
|
||||||
Update
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">Automatically download new update</div>
|
|
||||||
</div>
|
|
||||||
<Switch.Root
|
|
||||||
checked={settings.autoupdate}
|
|
||||||
onClick={() => toggleAutoupdate()}
|
|
||||||
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<div className="w-24 shrink-0 text-end text-sm font-semibold">
|
|
||||||
Low Power
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">
|
|
||||||
Sustainable for low network environment.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Switch.Root
|
|
||||||
checked={settings.lowPower}
|
|
||||||
onClick={() => toggleLowPower()}
|
|
||||||
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<div className="w-24 shrink-0 text-end text-sm font-semibold">
|
|
||||||
Startup
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">Launch Lume at Login</div>
|
|
||||||
</div>
|
|
||||||
<Switch.Root
|
|
||||||
checked={settings.autolaunch}
|
|
||||||
onClick={() => toggleAutolaunch()}
|
|
||||||
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<div className="w-24 shrink-0 text-end text-sm font-semibold">
|
|
||||||
Media
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">Automatically load media</div>
|
|
||||||
</div>
|
|
||||||
<Switch.Root
|
|
||||||
checked={settings.media}
|
|
||||||
onClick={() => toggleMedia()}
|
|
||||||
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<div className="w-24 shrink-0 text-end text-sm font-semibold">
|
|
||||||
Hashtag
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">Show all hashtags in content</div>
|
|
||||||
</div>
|
|
||||||
<Switch.Root
|
|
||||||
checked={settings.hashtag}
|
|
||||||
onClick={() => toggleHashtag()}
|
|
||||||
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<div className="w-24 shrink-0 text-end text-sm font-semibold">
|
|
||||||
Notification
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">Automatically send notification</div>
|
|
||||||
</div>
|
|
||||||
<Switch.Root
|
|
||||||
checked={settings.notification}
|
|
||||||
disabled={settings.notification}
|
|
||||||
onClick={() => toggleNofitication()}
|
|
||||||
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<div className="w-24 shrink-0 text-end text-sm font-semibold">
|
|
||||||
Translation
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">Translate text to your language</div>
|
|
||||||
</div>
|
|
||||||
<Switch.Root
|
|
||||||
checked={settings.translation}
|
|
||||||
onClick={() => toggleTranslation()}
|
|
||||||
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
</div>
|
|
||||||
{settings.translation ? (
|
|
||||||
<div className="flex w-full items-center gap-8">
|
|
||||||
<div className="w-24 shrink-0 text-end text-sm font-semibold">
|
|
||||||
API Key
|
|
||||||
</div>
|
|
||||||
<div className="relative w-full">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
spellCheck={false}
|
|
||||||
value={apiKey}
|
|
||||||
onChange={(e) => setAPIKey(e.target.value)}
|
|
||||||
className="w-full border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-9 rounded-lg ring-0 placeholder:text-neutral-600 bg-neutral-100 dark:bg-neutral-900"
|
|
||||||
/>
|
|
||||||
<div className="h-9 absolute right-0 top-0 inline-flex items-center justify-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={saveApi}
|
|
||||||
className="mr-1 h-7 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="flex w-full items-start gap-8">
|
|
||||||
<div className="w-24 shrink-0 text-end text-sm font-semibold">
|
|
||||||
Appearance
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 gap-6">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => changeTheme("light")}
|
|
||||||
className="flex flex-col items-center justify-center gap-0.5"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"inline-flex h-11 w-11 items-center justify-center rounded-lg",
|
|
||||||
settings.appearance === "light"
|
|
||||||
? "bg-blue-500 text-white"
|
|
||||||
: "bg-neutral-100 dark:bg-neutral-900",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<LightIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
||||||
Light
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => changeTheme("dark")}
|
|
||||||
className="flex flex-col items-center justify-center gap-0.5"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"inline-flex h-11 w-11 items-center justify-center rounded-lg",
|
|
||||||
settings.appearance === "dark"
|
|
||||||
? "bg-blue-500 text-white"
|
|
||||||
: "bg-neutral-100 dark:bg-neutral-900",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DarkIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
||||||
Dark
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => changeTheme("auto")}
|
|
||||||
className="flex flex-col items-center justify-center gap-0.5"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"inline-flex h-11 w-11 items-center justify-center rounded-lg",
|
|
||||||
settings.appearance === "auto"
|
|
||||||
? "bg-blue-500 text-white"
|
|
||||||
: "bg-neutral-100 dark:bg-neutral-900",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SystemModeIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
||||||
System
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { ContactCard } from "./components/contactCard";
|
|
||||||
import { PostCard } from "./components/postCard";
|
|
||||||
import { ProfileCard } from "./components/profileCard";
|
|
||||||
import { RelayCard } from "./components/relayCard";
|
|
||||||
import { ZapCard } from "./components/zapCard";
|
|
||||||
|
|
||||||
export function UserSettingScreen() {
|
|
||||||
return (
|
|
||||||
<div className="mx-auto w-full max-w-xl">
|
|
||||||
<ProfileCard />
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<ContactCard />
|
|
||||||
<RelayCard />
|
|
||||||
<PostCard />
|
|
||||||
<ZapCard />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import { webln } from "@getalby/sdk";
|
|
||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { CheckCircleIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { NWCForm } from "./components/walletConnectForm";
|
|
||||||
|
|
||||||
export function NWCScreen() {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null);
|
|
||||||
const [balance, setBalance] = useState(0);
|
|
||||||
|
|
||||||
const remove = async () => {
|
|
||||||
await storage.removePrivkey(`${ark.account.pubkey}-nwc`);
|
|
||||||
setWalletConnectURL(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadBalance = async () => {
|
|
||||||
const nwc = new webln.NostrWebLNProvider({
|
|
||||||
nostrWalletConnectUrl: walletConnectURL,
|
|
||||||
});
|
|
||||||
await nwc.enable();
|
|
||||||
|
|
||||||
const balanceResponse = await nwc.getBalance();
|
|
||||||
setBalance(balanceResponse.balance);
|
|
||||||
|
|
||||||
nwc.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (walletConnectURL) loadBalance();
|
|
||||||
}, [walletConnectURL]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getNWC() {
|
|
||||||
const nwc = await storage.loadPrivkey(`${ark.account.pubkey}-nwc`);
|
|
||||||
if (nwc) setWalletConnectURL(nwc);
|
|
||||||
}
|
|
||||||
getNWC();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex w-full flex-col gap-5">
|
|
||||||
<div className="text-center">
|
|
||||||
<h3 className="text-2xl font-semibold leading-tight">
|
|
||||||
Nostr Wallet Connect
|
|
||||||
</h3>
|
|
||||||
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
|
|
||||||
Sending zap easily via Bitcoin Lightning.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto w-full max-w-lg">
|
|
||||||
{!walletConnectURL ? (
|
|
||||||
<NWCForm setWalletConnectURL={setWalletConnectURL} />
|
|
||||||
) : (
|
|
||||||
<div className="flex w-full flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
|
||||||
<div className="flex items-center justify-center gap-1.5 text-sm text-teal-500">
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
<div>You're using nostr wallet connect</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<textarea
|
|
||||||
readOnly
|
|
||||||
value={`${walletConnectURL.substring(0, 120)}****`}
|
|
||||||
className="h-40 w-full resize-none rounded-lg border-transparent bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:focus:ring-blue-800 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => remove()}
|
|
||||||
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-neutral-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:bg-neutral-800 dark:text-neutral-100"
|
|
||||||
>
|
|
||||||
Remove connection
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{walletConnectURL ? (
|
|
||||||
<div className="mt-5 flex flex-col">
|
|
||||||
<h3 className="font-medium text-neutral-600 dark:text-neutral-400">
|
|
||||||
Current balance
|
|
||||||
</h3>
|
|
||||||
<p className="text-2xl font-semibold">
|
|
||||||
{new Intl.NumberFormat().format(balance)} sats
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="mt-5 flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Introduction
|
|
||||||
</h5>
|
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
|
||||||
Nostr Wallet Connect (NWC) is a way for applications like
|
|
||||||
Nostr clients to access a remote Lightning wallet through a
|
|
||||||
standardized protocol.
|
|
||||||
</p>
|
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
|
||||||
To learn more about the details have a look at{" "}
|
|
||||||
<a
|
|
||||||
href="https://github.com/nostr-protocol/nips/blob/master/47.md"
|
|
||||||
target="_blank"
|
|
||||||
className="text-blue-500"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
the specs (NIP47)
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
About zapping
|
|
||||||
</h5>
|
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
|
||||||
Lume doesn't take any commission or platform fees when
|
|
||||||
you zap someone. Lume doesn't hold your Bitcoin
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Recommend wallet that support NWC
|
|
||||||
</h5>
|
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
|
||||||
- Mutiny Wallet:{" "}
|
|
||||||
<a
|
|
||||||
href="https://www.mutinywallet.com/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-blue-500"
|
|
||||||
>
|
|
||||||
website
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
|
||||||
- Self hosted NWC on Umbrel :{" "}
|
|
||||||
<a
|
|
||||||
href="https://apps.umbrel.com/app/alby-nostr-wallet-connect"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-blue-500"
|
|
||||||
>
|
|
||||||
website
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { useArk, useProfile } from "@lume/ark";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { NIP05 } from "@lume/ui";
|
|
||||||
import { displayNpub } from "@lume/utils";
|
|
||||||
import * as Avatar from "@radix-ui/react-avatar";
|
|
||||||
import { minidenticon } from "minidenticons";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { UserStats } from "./stats";
|
|
||||||
|
|
||||||
export function UserProfile({ pubkey }: { pubkey: string }) {
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const { user } = useProfile(pubkey);
|
|
||||||
|
|
||||||
const [followed, setFollowed] = useState(false);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent(
|
|
||||||
minidenticon(pubkey, 90, 50),
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
const follow = async () => {
|
|
||||||
try {
|
|
||||||
if (!ark.ndk.signer) return navigate("/new/privkey");
|
|
||||||
setFollowed(true);
|
|
||||||
|
|
||||||
const add = await ark.createContact({ pubkey });
|
|
||||||
|
|
||||||
if (!add) {
|
|
||||||
toast.success("You already follow this user");
|
|
||||||
setFollowed(false);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(e);
|
|
||||||
setFollowed(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const unfollow = async () => {
|
|
||||||
try {
|
|
||||||
if (!ark.ndk.signer) return navigate("/new/privkey");
|
|
||||||
setFollowed(false);
|
|
||||||
|
|
||||||
await ark.deleteContact({ pubkey });
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ark.account.contacts.includes(pubkey)) {
|
|
||||||
setFollowed(true);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!user) return <p>Loading...</p>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="h-56 w-full overflow-hidden rounded-tl-lg">
|
|
||||||
{user?.banner ? (
|
|
||||||
<img
|
|
||||||
src={user?.banner}
|
|
||||||
alt="user banner"
|
|
||||||
className="h-full w-full rounded-tl-lg object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="h-full w-full rounded-tl-lg bg-neutral-100 dark:bg-neutral-900" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="-mt-7 flex w-full flex-col items-center px-5">
|
|
||||||
<Avatar.Root className="shrink-0">
|
|
||||||
<Avatar.Image
|
|
||||||
src={user?.picture || user?.image}
|
|
||||||
alt={pubkey}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
style={{ contentVisibility: "auto" }}
|
|
||||||
className="h-14 w-14 rounded-lg bg-white object-cover ring-2 ring-neutral-100 dark:ring-neutral-900"
|
|
||||||
/>
|
|
||||||
<Avatar.Fallback delayMs={300}>
|
|
||||||
<img
|
|
||||||
src={svgURI}
|
|
||||||
alt={pubkey}
|
|
||||||
className="h-14 w-14 rounded-lg bg-black dark:bg-white"
|
|
||||||
/>
|
|
||||||
</Avatar.Fallback>
|
|
||||||
</Avatar.Root>
|
|
||||||
<div className="mt-2 flex flex-1 flex-col gap-6">
|
|
||||||
<div className="flex flex-col items-center gap-1">
|
|
||||||
<div className="inline-flex flex-col items-center">
|
|
||||||
<h5 className="text-center text-xl font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
{user?.name ||
|
|
||||||
user?.display_name ||
|
|
||||||
user?.displayName ||
|
|
||||||
"No name"}
|
|
||||||
</h5>
|
|
||||||
{user?.nip05 ? (
|
|
||||||
<NIP05
|
|
||||||
pubkey={pubkey}
|
|
||||||
nip05={user.nip05}
|
|
||||||
className="text-neutral-600 dark:text-neutral-400"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-400">
|
|
||||||
{displayNpub(pubkey, 16)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
{user?.about || user?.bio ? (
|
|
||||||
<p className="mt-2 max-w-[500px] select-text break-words text-center text-neutral-900 dark:text-neutral-100">
|
|
||||||
{user.about || user.bio}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<div />
|
|
||||||
)}
|
|
||||||
<UserStats pubkey={pubkey} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex items-center justify-center gap-2">
|
|
||||||
{followed ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={unfollow}
|
|
||||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-500 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
|
|
||||||
>
|
|
||||||
Unfollow
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={follow}
|
|
||||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-500 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
|
|
||||||
>
|
|
||||||
Follow
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<Link
|
|
||||||
to={`/chats/${pubkey}`}
|
|
||||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-neutral-200 text-sm font-medium text-neutral-900 backdrop-blur-xl hover:bg-blue-500 hover:text-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-blue-600 dark:hover:text-neutral-100"
|
|
||||||
>
|
|
||||||
Message
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { compactNumber } from "@lume/utils";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { fetch } from "@tauri-apps/plugin-http";
|
|
||||||
|
|
||||||
export function UserStats({ pubkey }: { pubkey: string }) {
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ["user-stats", pubkey],
|
|
||||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
|
||||||
const res = await fetch(
|
|
||||||
`https://api.nostr.band/v0/stats/profile/${pubkey}`,
|
|
||||||
{
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Error");
|
|
||||||
}
|
|
||||||
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
staleTime: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (status === "pending") {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-neutral-900 dark:text-neutral-100" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === "error") {
|
|
||||||
return <div className="flex w-full items-center justify-center" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center gap-10">
|
|
||||||
<div className="inline-flex flex-col items-center gap-1">
|
|
||||||
<span className="font-semibold leading-none text-neutral-900 dark:text-neutral-100">
|
|
||||||
{compactNumber.format(data?.stats[pubkey]?.followers_pubkey_count) ??
|
|
||||||
0}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm leading-none text-neutral-500 dark:text-neutral-400">
|
|
||||||
Followers
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex flex-col items-center gap-1">
|
|
||||||
<span className="font-semibold leading-none text-neutral-900 dark:text-neutral-100">
|
|
||||||
{compactNumber.format(
|
|
||||||
data?.stats[pubkey]?.pub_following_pubkey_count,
|
|
||||||
) ?? 0}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm leading-none text-neutral-500 dark:text-neutral-400">
|
|
||||||
Following
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex flex-col items-center gap-1">
|
|
||||||
<span className="font-semibold leading-none text-neutral-900 dark:text-neutral-100">
|
|
||||||
{compactNumber.format(
|
|
||||||
data?.stats[pubkey]?.zaps_received?.msats / 1000 ?? 0,
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm leading-none text-neutral-500 dark:text-neutral-400">
|
|
||||||
Zaps received
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex flex-col items-center gap-1">
|
|
||||||
<span className="font-semibold leading-none text-neutral-900 dark:text-neutral-100">
|
|
||||||
{compactNumber.format(
|
|
||||||
data?.stats[pubkey]?.zaps_sent?.msats / 1000 ?? 0,
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm leading-none text-neutral-500 dark:text-neutral-400">
|
|
||||||
Zaps sent
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { NoteSkeleton, RepostNote, TextNote, useArk } from "@lume/ark";
|
|
||||||
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { FETCH_LIMIT } from "@lume/utils";
|
|
||||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { UserProfile } from "./components/profile";
|
|
||||||
|
|
||||||
export function UserScreen() {
|
|
||||||
const { pubkey } = useParams();
|
|
||||||
const ark = useArk();
|
|
||||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
|
||||||
useInfiniteQuery({
|
|
||||||
queryKey: ["user-posts", pubkey],
|
|
||||||
initialPageParam: 0,
|
|
||||||
queryFn: async ({
|
|
||||||
signal,
|
|
||||||
pageParam,
|
|
||||||
}: {
|
|
||||||
signal: AbortSignal;
|
|
||||||
pageParam: number;
|
|
||||||
}) => {
|
|
||||||
const events = await ark.getInfiniteEvents({
|
|
||||||
filter: {
|
|
||||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
|
||||||
authors: [pubkey],
|
|
||||||
},
|
|
||||||
limit: FETCH_LIMIT,
|
|
||||||
pageParam,
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
return events;
|
|
||||||
},
|
|
||||||
getNextPageParam: (lastPage) => {
|
|
||||||
const lastEvent = lastPage.at(-1);
|
|
||||||
if (!lastEvent) return;
|
|
||||||
return lastEvent.created_at - 1;
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const allEvents = useMemo(
|
|
||||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
// render event match event kind
|
|
||||||
const renderItem = (event: NDKEvent) => {
|
|
||||||
switch (event.kind) {
|
|
||||||
case NDKKind.Text:
|
|
||||||
return <TextNote key={event.id} event={event} />;
|
|
||||||
case NDKKind.Repost:
|
|
||||||
return <RepostNote key={event.id} event={event} />;
|
|
||||||
default:
|
|
||||||
return <TextNote key={event.id} event={event} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative h-full w-full overflow-y-auto">
|
|
||||||
<UserProfile pubkey={pubkey} />
|
|
||||||
<div className="mt-6 h-full w-full border-t border-neutral-100 px-1.5 dark:border-neutral-900">
|
|
||||||
<h3 className="mb-2 pt-4 text-center text-lg font-semibold leading-none text-neutral-900 dark:text-neutral-100">
|
|
||||||
Latest posts
|
|
||||||
</h3>
|
|
||||||
<div className="mx-auto flex h-full max-w-[500px] flex-col justify-between gap-1.5 pb-4 pt-1.5">
|
|
||||||
{status === "pending" ? (
|
|
||||||
<NoteSkeleton />
|
|
||||||
) : (
|
|
||||||
allEvents.map((item) => renderItem(item))
|
|
||||||
)}
|
|
||||||
<div className="flex h-16 items-center justify-center px-3 pb-3">
|
|
||||||
{hasNextPage ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => fetchNextPage()}
|
|
||||||
disabled={!hasNextPage || isFetchingNextPage}
|
|
||||||
className="inline-flex items-center justify-center w-full h-12 gap-2 font-medium bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800 rounded-xl focus:outline-none"
|
|
||||||
>
|
|
||||||
{isFetchingNextPage ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArrowRightCircleIcon className="size-5" />
|
|
||||||
Load more
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||