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 |
@@ -1,44 +0,0 @@
|
|||||||
# Dependencies
|
|
||||||
**/node_modules
|
|
||||||
.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# Local env files
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
coverage
|
|
||||||
|
|
||||||
# Turbo
|
|
||||||
.turbo
|
|
||||||
|
|
||||||
# Vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# Build Outputs
|
|
||||||
**/.next/
|
|
||||||
**/out/
|
|
||||||
**/build
|
|
||||||
**/dist
|
|
||||||
**/target
|
|
||||||
|
|
||||||
|
|
||||||
# Debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# Unnecessary files
|
|
||||||
**/.git/
|
|
||||||
.github/
|
|
||||||
flatpak/*.xml
|
|
||||||
flatpak/*.desktop
|
|
||||||
flatpak/*.yml
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
name: Flatpak
|
|
||||||
on: workflow_dispatch
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
prepare-repo:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: 'recursive'
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: cache of container
|
|
||||||
id: cache-container
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: prepare-dist
|
|
||||||
key: ${{ runner.os }}-container-${{ hashFiles('prepare-dist') }}
|
|
||||||
- name: Run latest-tag
|
|
||||||
id: latest-tag
|
|
||||||
uses: oprypin/find-latest-tag@v1
|
|
||||||
with:
|
|
||||||
repository:
|
|
||||||
lumehq/lume
|
|
||||||
#FIXME: lumehq after merged fix, now it just won't find tags
|
|
||||||
# repository: ${{ github.repository }}
|
|
||||||
|
|
||||||
- name: Build container
|
|
||||||
# if: steps.cache-container.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
docker buildx build -t flatpak-prepare-lume --build-arg=${{steps.latest-tag.outputs.tag}} --rm --output=. --target=final -f flatpak/Containerfile .
|
|
||||||
- name: Copy flatpak files content
|
|
||||||
run: |
|
|
||||||
cp -r flatpak/*.xml flatpak/*.desktop flatpak/*.yml prepare-dist
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: repo-dist
|
|
||||||
path: prepare-dist
|
|
||||||
flatpak:
|
|
||||||
name: flatpak-bundle
|
|
||||||
needs: prepare-repo
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: bilelmoussaoui/flatpak-github-actions:gnome-45
|
|
||||||
options: --privileged
|
|
||||||
steps:
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: repo-dist
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: flathub/shared-modules
|
|
||||||
path: shared-modules
|
|
||||||
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6
|
|
||||||
with:
|
|
||||||
bundle: lume.flatpak
|
|
||||||
manifest-path: nu.lume.Lume.yml
|
|
||||||
restore-cache: false
|
|
||||||
# cache-key: flatpak-builder-${{ github.sha }}
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
append_body: true
|
|
||||||
files: lume.flatpak
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- uses: geekyeggo/delete-artifact@v4
|
|
||||||
with:
|
|
||||||
name: repo-dist
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -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: 20
|
|
||||||
- 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: false
|
|
||||||
@@ -1,37 +1,23 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
# Dependencies
|
debug/
|
||||||
node_modules
|
target/
|
||||||
.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# Local env files
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
coverage/
|
|
||||||
|
|
||||||
# Turbo
|
|
||||||
.turbo/
|
|
||||||
|
|
||||||
# Vercel
|
|
||||||
.vercel/
|
|
||||||
|
|
||||||
# Build Outputs
|
|
||||||
.next/
|
|
||||||
out/
|
|
||||||
build/
|
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
# Debug
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
*.log.*
|
*.pdb
|
||||||
|
|
||||||
# Misc
|
# RustRover
|
||||||
|
# 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/
|
||||||
|
|
||||||
|
# Useless stuffs
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
# Added by goreleaser init:
|
||||||
.vscode/
|
.intentionally-empty-file.o
|
||||||
|
|||||||
@@ -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,70 +1 @@
|
|||||||
_Note_: Lume is under rewrite to using Rust Nostr as back-end and more lightweight front-end. If you need stable version, you can download v3 and below.
|
### Rebooting...
|
||||||
|
|
||||||
Source code for v3 is stored here: https://github.com/lumehq/lume/tree/old
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
Lume is a Nostr client for desktop include Linux, Windows and macOS. It is free and open source, you can look at source code on Github. Lume is actively improving the app and adding new features, you can expect new update every month.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Download Lume v3 (v3.0.1-stable) for your platform here: [https://github.com/lumehq/lume/releases](https://github.com/lumehq/lume/releases)
|
|
||||||
|
|
||||||
Supported platform: macOS, Windows and Linux
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Node.js >= 18: https://nodejs.org/en
|
|
||||||
|
|
||||||
- Rust: https://rustup.rs/
|
|
||||||
|
|
||||||
- PNPM: https://pnpm.io
|
|
||||||
|
|
||||||
- Tauri v2: https://beta.tauri.app/guides/prerequisites/
|
|
||||||
|
|
||||||
## Develop
|
|
||||||
|
|
||||||
Clone project
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/lumehq/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.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Copyright (C) 2023-2024 Ren Amamiya & other Lume contributors (see AUTHORS.md)
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
src/router.gen.ts
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Lume Desktop</title>
|
|
||||||
</head>
|
|
||||||
<body
|
|
||||||
class="relative h-screen w-screen cursor-default select-none overflow-hidden bg-white font-sans text-black antialiased dark:bg-black dark:text-white"
|
|
||||||
>
|
|
||||||
<div id="root" class="h-full w-full"></div>
|
|
||||||
<script type="module" src="/src/app.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@lume/desktop2",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@lume/ark": "workspace:^",
|
|
||||||
"@lume/icons": "workspace:^",
|
|
||||||
"@lume/ui": "workspace:^",
|
|
||||||
"@lume/utils": "workspace:^",
|
|
||||||
"@radix-ui/react-checkbox": "^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-popover": "^1.0.7",
|
|
||||||
"@tanstack/query-sync-storage-persister": "^5.24.1",
|
|
||||||
"@tanstack/react-query": "^5.24.1",
|
|
||||||
"@tanstack/react-query-persist-client": "^5.24.1",
|
|
||||||
"@tanstack/react-router": "^1.18.1",
|
|
||||||
"i18next": "^23.10.0",
|
|
||||||
"i18next-resources-to-backend": "^1.2.0",
|
|
||||||
"nostr-tools": "^2.3.1",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-currency-input-field": "^3.8.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-i18next": "^14.0.5",
|
|
||||||
"slate": "^0.101.5",
|
|
||||||
"slate-react": "^0.101.6",
|
|
||||||
"sonner": "^1.4.3",
|
|
||||||
"virtua": "^0.27.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@lume/tailwindcss": "workspace:^",
|
|
||||||
"@lume/tsconfig": "workspace:^",
|
|
||||||
"@lume/types": "workspace:^",
|
|
||||||
"@tanstack/router-devtools": "^1.18.1",
|
|
||||||
"@tanstack/router-vite-plugin": "^1.18.1",
|
|
||||||
"@types/react": "^18.2.61",
|
|
||||||
"@types/react-dom": "^18.2.19",
|
|
||||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
|
||||||
"autoprefixer": "^10.4.18",
|
|
||||||
"postcss": "^8.4.35",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"typescript": "^5.3.3",
|
|
||||||
"vite": "^5.1.4",
|
|
||||||
"vite-plugin-top-level-await": "^1.4.1",
|
|
||||||
"vite-tsconfig-paths": "^4.3.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 951 KiB |
@@ -1,46 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
media-controller {
|
|
||||||
@apply w-full overflow-hidden rounded-xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
.content-break {
|
|
||||||
word-break: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow-toolbar {
|
|
||||||
box-shadow:
|
|
||||||
0 0 #0000,
|
|
||||||
0 0 #0000,
|
|
||||||
0 8px 24px 0 rgba(0, 0, 0, 0.2),
|
|
||||||
0 2px 8px 0 rgba(0, 0, 0, 0.08),
|
|
||||||
inset 0 0 0 1px rgba(0, 0, 0, 0.2),
|
|
||||||
inset 0 0 0 2px hsla(0, 0%, 100%, 0.14);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { ArkProvider } from "./ark";
|
|
||||||
import { QueryClient } from "@tanstack/react-query";
|
|
||||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
|
||||||
import React, { StrictMode } from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
import { I18nextProvider } from "react-i18next";
|
|
||||||
import "./app.css";
|
|
||||||
import i18n from "./locale";
|
|
||||||
import { Toaster } from "sonner";
|
|
||||||
import { locale, platform } from "@tauri-apps/plugin-os";
|
|
||||||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
|
||||||
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
|
||||||
import { routeTree } from "./router.gen"; // auto generated file
|
|
||||||
import { CancelCircleIcon, CheckCircleIcon, InfoCircleIcon } from "@lume/icons";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
gcTime: 1000 * 60 * 60 * 24, // 24 hours
|
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const persister = createSyncStoragePersister({
|
|
||||||
storage: window.localStorage,
|
|
||||||
});
|
|
||||||
|
|
||||||
const platformName = await platform();
|
|
||||||
const osLocale = (await locale()).slice(0, 2);
|
|
||||||
|
|
||||||
// Set up a Router instance
|
|
||||||
const router = createRouter({
|
|
||||||
routeTree,
|
|
||||||
context: {
|
|
||||||
ark: undefined!,
|
|
||||||
platform: platformName,
|
|
||||||
locale: osLocale,
|
|
||||||
queryClient,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register things for typesafety
|
|
||||||
declare module "@tanstack/react-router" {
|
|
||||||
interface Register {
|
|
||||||
router: typeof router;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function InnerApp() {
|
|
||||||
const ark = useArk();
|
|
||||||
return <RouterProvider router={router} context={{ ark }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<ArkProvider>
|
|
||||||
<InnerApp />
|
|
||||||
</ArkProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: idk
|
|
||||||
const rootElement = document.getElementById("root")!;
|
|
||||||
|
|
||||||
if (!rootElement.innerHTML) {
|
|
||||||
const root = ReactDOM.createRoot(rootElement);
|
|
||||||
root.render(
|
|
||||||
<I18nextProvider i18n={i18n} defaultNS={"translation"}>
|
|
||||||
<PersistQueryClientProvider
|
|
||||||
client={queryClient}
|
|
||||||
persistOptions={{ persister }}
|
|
||||||
>
|
|
||||||
<StrictMode>
|
|
||||||
<Toaster
|
|
||||||
position="bottom-right"
|
|
||||||
icons={{
|
|
||||||
success: <CheckCircleIcon className="size-5" />,
|
|
||||||
info: <InfoCircleIcon className="size-5" />,
|
|
||||||
error: <CancelCircleIcon className="size-5" />,
|
|
||||||
}}
|
|
||||||
closeButton
|
|
||||||
theme="system"
|
|
||||||
/>
|
|
||||||
<App />
|
|
||||||
</StrictMode>
|
|
||||||
</PersistQueryClientProvider>
|
|
||||||
</I18nextProvider>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { Ark, ArkContext } from "@lume/ark";
|
|
||||||
import { PropsWithChildren, useMemo } from "react";
|
|
||||||
|
|
||||||
export const ArkProvider = ({ children }: PropsWithChildren<object>) => {
|
|
||||||
const ark = useMemo(() => new Ark(), []);
|
|
||||||
return <ArkContext.Provider value={ark}>{children}</ArkContext.Provider>;
|
|
||||||
};
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { Account } from "@lume/types";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
|
||||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
||||||
import { BackupDialog } from "./backup";
|
|
||||||
import { LoginDialog } from "./login";
|
|
||||||
|
|
||||||
export function Accounts() {
|
|
||||||
const ark = useArk();
|
|
||||||
const params = useParams({ strict: false });
|
|
||||||
|
|
||||||
const [accounts, setAccounts] = useState<Account[]>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getAllAccounts() {
|
|
||||||
const data = await ark.get_all_accounts();
|
|
||||||
if (data) setAccounts(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllAccounts();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-tauri-drag-region className="flex items-center gap-4">
|
|
||||||
{accounts
|
|
||||||
? accounts.map((account) =>
|
|
||||||
// @ts-ignore, useless
|
|
||||||
account.npub === params.account ? (
|
|
||||||
<Active key={account.npub} pubkey={account.npub} />
|
|
||||||
) : (
|
|
||||||
<Inactive key={account.npub} pubkey={account.npub} />
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Inactive({ pubkey }: { pubkey: string }) {
|
|
||||||
const ark = useArk();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const changeAccount = async (npub: string) => {
|
|
||||||
const select = await ark.load_selected_account(npub);
|
|
||||||
if (select)
|
|
||||||
navigate({ to: "/$account/home/local", params: { account: npub } });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button type="button" onClick={() => changeAccount(pubkey)}>
|
|
||||||
<User.Provider pubkey={pubkey}>
|
|
||||||
<User.Root className="rounded-full">
|
|
||||||
<User.Avatar className="aspect-square h-auto w-8 rounded-full object-cover" />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Active({ pubkey }: { pubkey: string }) {
|
|
||||||
const ark = useArk();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// @ts-ignore, magic !!!
|
|
||||||
const { guest } = useSearch({ strict: false });
|
|
||||||
// @ts-ignore, magic !!!
|
|
||||||
const { account } = useParams({ strict: false });
|
|
||||||
|
|
||||||
if (guest) {
|
|
||||||
return (
|
|
||||||
<Popover.Root open={true}>
|
|
||||||
<Popover.Trigger asChild>
|
|
||||||
<button type="button">
|
|
||||||
<User.Provider pubkey={pubkey}>
|
|
||||||
<User.Root className="rounded-full ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950">
|
|
||||||
<User.Avatar className="aspect-square h-auto w-7 rounded-full object-cover" />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</button>
|
|
||||||
</Popover.Trigger>
|
|
||||||
<Popover.Portal>
|
|
||||||
<Popover.Content
|
|
||||||
className="flex w-[280px] flex-col gap-4 rounded-xl bg-black p-5 text-neutral-100 focus:outline-none dark:bg-white dark:text-neutral-900 dark:shadow-none"
|
|
||||||
sideOffset={10}
|
|
||||||
side="bottom"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h1 className="mb-1 font-semibold">
|
|
||||||
You're using random account
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-600">
|
|
||||||
You can continue by claim and backup this account, or you can
|
|
||||||
import your own account.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<BackupDialog />
|
|
||||||
<LoginDialog />
|
|
||||||
</div>
|
|
||||||
<Popover.Arrow className="fill-black dark:fill-white" />
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Portal>
|
|
||||||
</Popover.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu.Root>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<User.Provider pubkey={pubkey}>
|
|
||||||
<User.Root className="rounded-full ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950">
|
|
||||||
<User.Avatar className="aspect-square h-auto w-7 rounded-full object-cover" />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Portal>
|
|
||||||
<DropdownMenu.Content
|
|
||||||
className="flex w-[220px] flex-col rounded-xl bg-black p-2 text-neutral-100 focus:outline-none dark:bg-white dark:text-neutral-900 dark:shadow-none"
|
|
||||||
sideOffset={10}
|
|
||||||
side="bottom"
|
|
||||||
>
|
|
||||||
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
|
|
||||||
Add account
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onClick={() => ark.open_profile(account)}
|
|
||||||
className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100"
|
|
||||||
>
|
|
||||||
Profile
|
|
||||||
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
|
||||||
⌘+Shift+P
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onClick={() => navigate({ to: "/", search: { manually: true } })}
|
|
||||||
className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100"
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
|
||||||
⌘+Shift+L
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Arrow className="fill-black dark:fill-white" />
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import { ArrowRightIcon, CancelIcon } from "@lume/icons";
|
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
|
||||||
import { Link, useParams } from "@tanstack/react-router";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function BackupDialog() {
|
|
||||||
// @ts-ignore, magic!!!
|
|
||||||
const { account } = useParams({ strict: false });
|
|
||||||
|
|
||||||
const [key, setKey] = useState(null);
|
|
||||||
const [passphase, setPassphase] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const encryptKey = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const encrypted: string = await invoke("get_encrypted_key", {
|
|
||||||
npub: account,
|
|
||||||
password: passphase,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (encrypted) {
|
|
||||||
setKey(encrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
toast.error(String(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog.Root>
|
|
||||||
<Dialog.Trigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-900 text-sm font-medium leading-tight text-neutral-100 hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Claim & Backup
|
|
||||||
</button>
|
|
||||||
</Dialog.Trigger>
|
|
||||||
<Dialog.Portal>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/30 backdrop-blur dark:bg-white/30" />
|
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
|
||||||
<Dialog.Close className="absolute right-5 top-5 flex w-12 flex-col items-center justify-center gap-1 text-white">
|
|
||||||
<CancelIcon className="size-8" />
|
|
||||||
<span className="text-sm font-medium uppercase text-neutral-400 dark:text-neutral-600">
|
|
||||||
Esc
|
|
||||||
</span>
|
|
||||||
</Dialog.Close>
|
|
||||||
<div className="relative flex h-min w-full max-w-xl flex-col gap-8 rounded-xl bg-white p-5 dark:bg-black">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<h3 className="text-lg font-semibold">
|
|
||||||
This is your account key
|
|
||||||
</h3>
|
|
||||||
<p>
|
|
||||||
It's use for login to Lume or other Nostr clients. You will lost
|
|
||||||
access to your account if you lose this key.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-5">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label htmlFor="nsec">Set a passphase to secure your key</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
name="passphase"
|
|
||||||
type="password"
|
|
||||||
value={passphase}
|
|
||||||
onChange={(e) => setPassphase(e.target.value)}
|
|
||||||
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{key ? (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label htmlFor="nsec">
|
|
||||||
Copy this key and keep it in safe place
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="nsec"
|
|
||||||
type="text"
|
|
||||||
value={key}
|
|
||||||
readOnly
|
|
||||||
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{!key ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={encryptKey}
|
|
||||||
disabled={loading}
|
|
||||||
className="inline-flex h-11 w-full items-center justify-between gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
<div className="size-5" />
|
|
||||||
<div>Submit</div>
|
|
||||||
<ArrowRightIcon className="size-5" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
to="/$account/home/local"
|
|
||||||
params={{ account }}
|
|
||||||
search={{ guest: false }}
|
|
||||||
className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
I've safely store my account key
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import { getBitcoinDisplayValues } from "@lume/utils";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export function Balance({
|
|
||||||
recipient,
|
|
||||||
account,
|
|
||||||
}: {
|
|
||||||
recipient: string;
|
|
||||||
account: string;
|
|
||||||
}) {
|
|
||||||
const [t] = useTranslation();
|
|
||||||
const [balance, setBalance] = useState(0);
|
|
||||||
|
|
||||||
const ark = useArk();
|
|
||||||
const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getBalance() {
|
|
||||||
const val = await ark.get_balance();
|
|
||||||
setBalance(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
getBalance();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="flex h-16 items-center justify-end px-3"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="text-end">
|
|
||||||
<div className="text-sm leading-tight text-neutral-700 dark:text-neutral-300">
|
|
||||||
Your balance
|
|
||||||
</div>
|
|
||||||
<div className="font-medium leading-tight">
|
|
||||||
₿ {value.bitcoinFormatted}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<User.Provider pubkey={account}>
|
|
||||||
<User.Root>
|
|
||||||
<User.Avatar className="size-9 rounded-full" />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { ArrowRightIcon, CancelIcon } from "@lume/icons";
|
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function LoginDialog() {
|
|
||||||
const ark = useArk();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [nsec, setNsec] = useState("");
|
|
||||||
const [passphase, setPassphase] = useState("");
|
|
||||||
|
|
||||||
const login = async () => {
|
|
||||||
try {
|
|
||||||
if (!nsec.length) {
|
|
||||||
return toast.info("You must enter a valid nsec or ncrypto");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nsec.startsWith("ncrypto") && !passphase.length) {
|
|
||||||
return toast.warning("You must provide a passphase for ncrypto key");
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = await ark.save_account(nsec, passphase);
|
|
||||||
|
|
||||||
if (save) {
|
|
||||||
navigate({ to: "/", search: { guest: false } });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(String(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog.Root>
|
|
||||||
<Dialog.Trigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-900 text-sm font-medium leading-tight text-neutral-100 hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Add account
|
|
||||||
</button>
|
|
||||||
</Dialog.Trigger>
|
|
||||||
<Dialog.Portal>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/30 backdrop-blur dark:bg-white/30" />
|
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
|
||||||
<Dialog.Close className="absolute right-5 top-5 flex w-12 flex-col items-center justify-center gap-1 text-white">
|
|
||||||
<CancelIcon className="size-8" />
|
|
||||||
<span className="text-sm font-medium uppercase text-neutral-400 dark:text-neutral-600">
|
|
||||||
Esc
|
|
||||||
</span>
|
|
||||||
</Dialog.Close>
|
|
||||||
<div className="relative flex h-min w-full max-w-xl flex-col gap-8 rounded-xl bg-white p-5 dark:bg-black">
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<h3 className="text-lg font-semibold">Add new account with</h3>
|
|
||||||
<div className="flex h-11 items-center overflow-hidden rounded-lg bg-neutral-100 p-1 dark:bg-neutral-900">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="h-full flex-1 rounded-md bg-white text-sm font-medium dark:bg-black"
|
|
||||||
>
|
|
||||||
nsec
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex h-full flex-1 flex-col items-center justify-center rounded-md text-sm font-medium"
|
|
||||||
>
|
|
||||||
<span className="leading-tight">nsecBunker</span>
|
|
||||||
<span className="text-xs font-normal leading-tight text-neutral-700 dark:text-neutral-300">
|
|
||||||
coming soon
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex h-full flex-1 flex-col items-center justify-center rounded-md text-sm font-medium"
|
|
||||||
>
|
|
||||||
<span className="leading-tight">Address</span>
|
|
||||||
<span className="text-xs font-normal leading-tight text-neutral-700 dark:text-neutral-300">
|
|
||||||
coming soon
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-5">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label htmlFor="nsec">
|
|
||||||
Enter sign in key start with nsec or ncrypto
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="nsec"
|
|
||||||
type="text"
|
|
||||||
placeholder="nsec or ncrypto..."
|
|
||||||
value={nsec}
|
|
||||||
onChange={(e) => setNsec(e.target.value)}
|
|
||||||
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label htmlFor="nsec">Passphase (optional)</label>
|
|
||||||
<input
|
|
||||||
name="passphase"
|
|
||||||
type="password"
|
|
||||||
value={passphase}
|
|
||||||
onChange={(e) => setPassphase(e.target.value)}
|
|
||||||
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={login}
|
|
||||||
className="inline-flex h-11 w-full items-center justify-between gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
<div className="size-5" />
|
|
||||||
<div>Add account</div>
|
|
||||||
<ArrowRightIcon className="size-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { RepostIcon } from "@lume/icons";
|
|
||||||
import { Event } from "@lume/types";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { Note, User } from "@lume/ui";
|
|
||||||
|
|
||||||
export function RepostNote({
|
|
||||||
event,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
event: Event;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
const ark = useArk();
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const {
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
data: repostEvent,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["repost", event.id],
|
|
||||||
queryFn: async () => {
|
|
||||||
try {
|
|
||||||
if (event.content.length > 50) {
|
|
||||||
const embed: Event = JSON.parse(event.content);
|
|
||||||
return embed;
|
|
||||||
}
|
|
||||||
const id = event.tags.find((el) => el[0] === "e")[1];
|
|
||||||
return await ark.get_event(id);
|
|
||||||
} catch {
|
|
||||||
throw new Error("Failed to get repost event");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <div className="w-full px-3 pb-3">Loading...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError || !repostEvent) {
|
|
||||||
return (
|
|
||||||
<Note.Root className={className}>
|
|
||||||
<User.Provider pubkey={event.pubkey}>
|
|
||||||
<User.Root className="flex h-14 gap-2 px-3">
|
|
||||||
<div className="inline-flex w-10 shrink-0 items-center justify-center">
|
|
||||||
<RepostIcon className="h-5 w-5 text-blue-500" />
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<User.Avatar className="size-6 shrink-0 rounded object-cover" />
|
|
||||||
<div className="inline-flex items-baseline gap-1">
|
|
||||||
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
|
|
||||||
<span className="text-blue-500">{t("note.reposted")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<div className="mb-3 select-text px-3">
|
|
||||||
<div className="flex flex-col items-start justify-start rounded-lg bg-red-100 px-3 py-3 dark:bg-red-900">
|
|
||||||
<p className="text-red-500">Failed to get event</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Note.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Note.Root
|
|
||||||
className={cn(
|
|
||||||
"mb-5 flex flex-col gap-2 border-b border-neutral-100 pb-5 dark:border-neutral-900",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={event.pubkey}>
|
|
||||||
<User.Root className="flex gap-3">
|
|
||||||
<div className="inline-flex w-11 shrink-0 items-center justify-center">
|
|
||||||
<RepostIcon className="h-5 w-5 text-blue-500" />
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
|
|
||||||
<div className="inline-flex items-baseline gap-1">
|
|
||||||
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
|
|
||||||
<span className="text-blue-500">{t("note.reposted")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<Note.Provider event={repostEvent}>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Note.User />
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<div className="size-11 shrink-0" />
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<Note.Content />
|
|
||||||
<div className="mt-4 flex items-center justify-between">
|
|
||||||
<div className="-ml-1 inline-flex items-center gap-4">
|
|
||||||
<Note.Reply />
|
|
||||||
<Note.Repost />
|
|
||||||
<Note.Zap />
|
|
||||||
</div>
|
|
||||||
<Note.Menu />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Note.Provider>
|
|
||||||
</Note.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
|
|
||||||
export function Suggest() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { isLoading, isError, data } = useQuery({
|
|
||||||
queryKey: ["trending-users"],
|
|
||||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
|
||||||
const res = await fetch("https://api.nostr.band/v0/trending/profiles", {
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Failed to fetch trending users from nostr.band API.");
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900">
|
|
||||||
<div className="h-10 shrink-0 text-lg font-semibold">
|
|
||||||
Suggested Follows
|
|
||||||
</div>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex h-44 w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="size-4 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : isError ? (
|
|
||||||
<div className="flex h-44 w-full items-center justify-center">
|
|
||||||
{t("suggestion.error")}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
data?.profiles.map((item: { pubkey: string }) => (
|
|
||||||
<div key={item.pubkey} className="h-max w-full overflow-hidden py-5">
|
|
||||||
<User.Provider pubkey={item.pubkey}>
|
|
||||||
<User.Root>
|
|
||||||
<div className="flex h-full w-full flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
<User.Avatar className="size-10 shrink-0 rounded-full" />
|
|
||||||
<User.Name className="leadning-tight max-w-[15rem] truncate font-semibold" />
|
|
||||||
</div>
|
|
||||||
<User.Button className="inline-flex h-8 w-20 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" />
|
|
||||||
</div>
|
|
||||||
<User.About className="mt-1 line-clamp-3 max-w-none select-text text-neutral-800 dark:text-neutral-400" />
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Event } from "@lume/types";
|
|
||||||
import { Note } from "@lume/ui";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
|
|
||||||
export function TextNote({
|
|
||||||
event,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
event: Event;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Note.Provider event={event}>
|
|
||||||
<Note.Root
|
|
||||||
className={cn(
|
|
||||||
"mb-5 flex flex-col gap-2 border-b border-neutral-100 pb-5 dark:border-neutral-900",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Note.User />
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<div className="size-11 shrink-0" />
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<Note.Content className="mb-2" />
|
|
||||||
<Note.Thread />
|
|
||||||
<div className="mt-4 flex items-center justify-between">
|
|
||||||
<div className="-ml-1 inline-flex items-center gap-4">
|
|
||||||
<Note.Reply />
|
|
||||||
<Note.Repost />
|
|
||||||
<Note.Zap />
|
|
||||||
</div>
|
|
||||||
<Note.Menu />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Note.Root>
|
|
||||||
</Note.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { resolveResource } from "@tauri-apps/api/path";
|
|
||||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
|
||||||
import i18n from "i18next";
|
|
||||||
import resourcesToBackend from "i18next-resources-to-backend";
|
|
||||||
import { initReactI18next } from "react-i18next";
|
|
||||||
|
|
||||||
i18n
|
|
||||||
.use(
|
|
||||||
resourcesToBackend(async (language: string) => {
|
|
||||||
const file_path = await resolveResource(`locales/${language}.json`);
|
|
||||||
return JSON.parse(await readTextFile(file_path));
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.use(initReactI18next)
|
|
||||||
.init({
|
|
||||||
lng: "en",
|
|
||||||
fallbackLng: "en",
|
|
||||||
interpolation: {
|
|
||||||
escapeValue: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default i18n;
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import {
|
|
||||||
BellFilledIcon,
|
|
||||||
BellIcon,
|
|
||||||
ComposeFilledIcon,
|
|
||||||
HomeFilledIcon,
|
|
||||||
HomeIcon,
|
|
||||||
HorizontalDotsIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
SpaceFilledIcon,
|
|
||||||
SpaceIcon,
|
|
||||||
} from "@lume/icons";
|
|
||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
import { Accounts } from "@/components/accounts";
|
|
||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { Box } from "@lume/ui";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account")({
|
|
||||||
component: App,
|
|
||||||
});
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const ark = useArk();
|
|
||||||
const context = Route.useRouteContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className={cn(
|
|
||||||
"flex h-11 shrink-0 items-center justify-between pr-4",
|
|
||||||
context.platform === "macos" ? "pl-24" : "pl-4",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Navigation />
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Accounts />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => ark.open_editor()}
|
|
||||||
className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
<ComposeFilledIcon className="size-4" />
|
|
||||||
New post
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => ark.open_settings()}
|
|
||||||
className="inline-flex size-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-800 hover:bg-neutral-400 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-600"
|
|
||||||
>
|
|
||||||
<HorizontalDotsIcon className="size-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Box>
|
|
||||||
<Outlet />
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Navigation() {
|
|
||||||
// @ts-ignore, useless
|
|
||||||
const { account } = Route.useParams();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="flex h-full flex-1 items-center gap-2"
|
|
||||||
>
|
|
||||||
<Link to="/$account/home/local" params={{ account }}>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3",
|
|
||||||
isActive
|
|
||||||
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
|
||||||
: "hover:bg-black/10 dark:hover:bg-white/10",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isActive ? (
|
|
||||||
<HomeFilledIcon className="size-5" />
|
|
||||||
) : (
|
|
||||||
<HomeIcon className="size-5" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium">Home</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
<Link to="/$account/space" params={{ account }}>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3 hover:bg-black/10 dark:hover:bg-white/10",
|
|
||||||
isActive
|
|
||||||
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
|
||||||
: "hover:bg-black/10 dark:hover:bg-white/10",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isActive ? (
|
|
||||||
<SpaceFilledIcon className="size-5" />
|
|
||||||
) : (
|
|
||||||
<SpaceIcon className="size-5" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium">Space</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
<Link to="/$account/activity" params={{ account }}>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3 hover:bg-black/10 dark:hover:bg-white/10",
|
|
||||||
isActive
|
|
||||||
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
|
||||||
: "hover:bg-black/10 dark:hover:bg-white/10",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isActive ? (
|
|
||||||
<BellFilledIcon className="size-5" />
|
|
||||||
) : (
|
|
||||||
<BellIcon className="size-5" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium">Activity</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/$account/activity")({
|
|
||||||
component: Activity,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Activity() {
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full overflow-hidden 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-none dark:ring-1 dark:ring-white/5">
|
|
||||||
<p>Activity</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { GlobalIcon, LoaderIcon, LocalIcon, RefreshIcon } from "@lume/icons";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account/home")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { account } = Route.useParams();
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
const queryKey = `${window.location.pathname.split("/").at(-1)}_newsfeed`;
|
|
||||||
await queryClient.refetchQueries({ queryKey: [queryKey, account] });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="mx-auto mb-4 flex h-16 w-full max-w-xl shrink-0 items-center justify-between border-b border-neutral-100 dark:border-neutral-900">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Link to="/$account/home/local">
|
|
||||||
{({ isActive }) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center justify-center gap-2 rounded-full px-4 py-2 text-sm leading-tight hover:bg-neutral-100 dark:hover:bg-neutral-900",
|
|
||||||
isActive
|
|
||||||
? "bg-neutral-100 font-semibold text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
|
||||||
: "text-neutral-600 dark:text-neutral-400",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<LocalIcon className="size-4" />
|
|
||||||
Local
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
<Link to="/$account/home/global">
|
|
||||||
{({ isActive }) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center justify-center gap-2 rounded-full px-4 py-2 text-sm leading-tight hover:bg-neutral-100 dark:hover:bg-neutral-900",
|
|
||||||
isActive
|
|
||||||
? "bg-neutral-100 font-semibold text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
|
||||||
: "text-neutral-600 dark:text-neutral-400",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<GlobalIcon className="size-4" />
|
|
||||||
Global
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={refresh}
|
|
||||||
className="text-neutral-700 hover:text-blue-500 dark:text-neutral-300"
|
|
||||||
>
|
|
||||||
<RefreshIcon className="size-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { RepostNote } from "@/components/repost";
|
|
||||||
import { Suggest } from "@/components/suggest";
|
|
||||||
import { TextNote } from "@/components/text";
|
|
||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
|
||||||
import { Event, Kind } from "@lume/types";
|
|
||||||
import { FETCH_LIMIT } from "@lume/utils";
|
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Virtualizer } from "virtua";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/$account/home/global")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const ark = useArk();
|
|
||||||
const { account } = Route.useParams();
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
hasNextPage,
|
|
||||||
isLoading,
|
|
||||||
isRefetching,
|
|
||||||
isFetchingNextPage,
|
|
||||||
fetchNextPage,
|
|
||||||
} = useInfiniteQuery({
|
|
||||||
queryKey: ["global_newsfeed", account],
|
|
||||||
initialPageParam: 0,
|
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
|
||||||
const events = await ark.get_events(
|
|
||||||
"global",
|
|
||||||
FETCH_LIMIT,
|
|
||||||
pageParam,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
return events;
|
|
||||||
},
|
|
||||||
getNextPageParam: (lastPage) => {
|
|
||||||
const lastEvent = lastPage?.at(-1);
|
|
||||||
if (!lastEvent) return;
|
|
||||||
return lastEvent.created_at - 1;
|
|
||||||
},
|
|
||||||
select: (data) => data?.pages.flatMap((page) => page),
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
|
||||||
if (!event) return;
|
|
||||||
switch (event.kind) {
|
|
||||||
case Kind.Repost:
|
|
||||||
return <RepostNote key={event.id} event={event} />;
|
|
||||||
default:
|
|
||||||
return <TextNote key={event.id} event={event} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto flex w-full max-w-xl flex-1 flex-col">
|
|
||||||
<div className="flex-1">
|
|
||||||
{isLoading || isRefetching ? (
|
|
||||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Virtualizer overscan={3}>
|
|
||||||
{data.map((item) => renderItem(item))}
|
|
||||||
</Virtualizer>
|
|
||||||
)}
|
|
||||||
<div className="flex h-20 items-center justify-center">
|
|
||||||
{hasNextPage ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => fetchNextPage()}
|
|
||||||
disabled={!hasNextPage || isFetchingNextPage}
|
|
||||||
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{isFetchingNextPage ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArrowRightCircleIcon className="size-5" />
|
|
||||||
Load more
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import { RepostNote } from "@/components/repost";
|
|
||||||
import { Suggest } from "@/components/suggest";
|
|
||||||
import { TextNote } from "@/components/text";
|
|
||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
|
||||||
import { Event, Kind } from "@lume/types";
|
|
||||||
import { FETCH_LIMIT } from "@lume/utils";
|
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Virtualizer } from "virtua";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/$account/home/local")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const ark = useArk();
|
|
||||||
const { account } = Route.useParams();
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
hasNextPage,
|
|
||||||
isLoading,
|
|
||||||
isRefetching,
|
|
||||||
isFetchingNextPage,
|
|
||||||
fetchNextPage,
|
|
||||||
} = useInfiniteQuery({
|
|
||||||
queryKey: ["local_newsfeed", account],
|
|
||||||
initialPageParam: 0,
|
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
|
||||||
const events = await ark.get_events(
|
|
||||||
"local",
|
|
||||||
FETCH_LIMIT,
|
|
||||||
pageParam,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
return events;
|
|
||||||
},
|
|
||||||
getNextPageParam: (lastPage) => {
|
|
||||||
const lastEvent = lastPage?.at(-1);
|
|
||||||
if (!lastEvent) return;
|
|
||||||
return lastEvent.created_at - 1;
|
|
||||||
},
|
|
||||||
select: (data) => data?.pages.flatMap((page) => page),
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
|
||||||
if (!event) return;
|
|
||||||
switch (event.kind) {
|
|
||||||
case Kind.Repost:
|
|
||||||
return <RepostNote key={event.id} event={event} />;
|
|
||||||
default:
|
|
||||||
return <TextNote key={event.id} event={event} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto flex w-full max-w-xl flex-1 flex-col">
|
|
||||||
<div className="flex-1">
|
|
||||||
{isLoading || isRefetching ? (
|
|
||||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : !data.length ? (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950">
|
|
||||||
<InfoIcon className="size-6" />
|
|
||||||
<p>
|
|
||||||
Empty newsfeed. Or you view the{" "}
|
|
||||||
<Link
|
|
||||||
to="/$account/home/global"
|
|
||||||
className="text-blue-500 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
Global Newsfeed
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Suggest />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Virtualizer overscan={3}>
|
|
||||||
{data.map((item) => renderItem(item))}
|
|
||||||
</Virtualizer>
|
|
||||||
)}
|
|
||||||
<div className="flex h-20 items-center justify-center">
|
|
||||||
{hasNextPage ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => fetchNextPage()}
|
|
||||||
disabled={!hasNextPage || isFetchingNextPage}
|
|
||||||
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{isFetchingNextPage ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArrowRightCircleIcon className="size-5" />
|
|
||||||
Load more
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/$account/space")({
|
|
||||||
component: Space,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Space() {
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full overflow-hidden 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-none dark:ring-1 dark:ring-white/5">
|
|
||||||
<p>Space</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import {
|
|
||||||
Outlet,
|
|
||||||
ScrollRestoration,
|
|
||||||
createRootRouteWithContext,
|
|
||||||
} from "@tanstack/react-router";
|
|
||||||
import { type Ark } from "@lume/ark";
|
|
||||||
import { type QueryClient } from "@tanstack/react-query";
|
|
||||||
import { type Platform } from "@tauri-apps/plugin-os";
|
|
||||||
|
|
||||||
interface RouterContext {
|
|
||||||
ark: Ark;
|
|
||||||
queryClient: QueryClient;
|
|
||||||
platform: Platform;
|
|
||||||
locale: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
||||||
component: () => (
|
|
||||||
<>
|
|
||||||
<ScrollRestoration />
|
|
||||||
<Outlet />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
pendingComponent: Pending,
|
|
||||||
wrapInSuspense: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Pending() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-screen flex-col items-center justify-center">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/auth/create/")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [t] = useTranslation();
|
|
||||||
const [method, setMethod] = useState<"self" | "managed">("self");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const next = () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (method === "self") {
|
|
||||||
navigate({ to: "/auth/create/self" });
|
|
||||||
} else {
|
|
||||||
navigate({ to: "/auth/create/managed" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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-8">
|
|
||||||
<div className="flex flex-col items-center gap-1 text-center">
|
|
||||||
<h1 className="text-2xl font-semibold">{t("signup.title")}</h1>
|
|
||||||
<p className="text-lg leading-snug text-neutral-600 dark:text-neutral-500">
|
|
||||||
{t("signup.subtitle")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMethod("self")}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col items-start rounded-xl bg-neutral-100 px-4 py-3.5 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800",
|
|
||||||
method === "self"
|
|
||||||
? "ring-1 ring-blue-500 ring-offset-2 ring-offset-white dark:ring-offset-black"
|
|
||||||
: "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<p className="font-semibold">{t("signup.selfManageMethod")}</p>
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-500">
|
|
||||||
{t("signup.selfManageMethodDescription")}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMethod("managed")}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col items-start rounded-xl bg-neutral-100 px-4 py-3.5 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800",
|
|
||||||
method === "managed"
|
|
||||||
? "ring-1 ring-blue-500 ring-offset-2 ring-offset-white dark:ring-offset-black"
|
|
||||||
: "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<p className="font-semibold">{t("signup.providerMethod")}</p>
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-500">
|
|
||||||
{t("signup.providerMethodDescription")}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={next}
|
|
||||||
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
t("global.continue")
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{method === "managed" ? (
|
|
||||||
<div className="flex flex-col gap-1.5 rounded-xl border border-red-200 bg-red-100 p-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-900 dark:text-red-200">
|
|
||||||
<p className="font-semibold text-red-900 dark:text-red-100">
|
|
||||||
Attention:
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
You're chosing Managed by Provider, this feature still in
|
|
||||||
"Beta".
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Some functions still missing or not work as expected, you
|
|
||||||
shouldn't create your main account with this method
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="https://github.com/kind-0/nsecbunkerd/blob/master/OAUTH-LIKE-FLOW.md"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-blue-500"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute('/auth/create/managed')({
|
|
||||||
component: () => <div>Hello /auth/create/managed!</div>
|
|
||||||
})
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { CheckIcon, EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { Keys } from "@lume/types";
|
|
||||||
import * as Checkbox from "@radix-ui/react-checkbox";
|
|
||||||
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/auth/create/self")({
|
|
||||||
component: Create,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Create() {
|
|
||||||
const ark = useArk();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [t] = useTranslation();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [showKey, setShowKey] = useState(false);
|
|
||||||
const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false });
|
|
||||||
const [keys, setKeys] = useState<Keys>(null);
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
await ark.save_account(keys);
|
|
||||||
navigate({
|
|
||||||
to: "/$account/home/local",
|
|
||||||
params: { account: keys.npub },
|
|
||||||
search: { onboarding: true },
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function genKeys() {
|
|
||||||
const res = await ark.create_keys();
|
|
||||||
setKeys(res);
|
|
||||||
}
|
|
||||||
genKeys();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
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-8">
|
|
||||||
<div className="flex flex-col items-center gap-1 text-center">
|
|
||||||
<h1 className="text-2xl font-semibold">
|
|
||||||
{t("signupWithSelfManage.title")}
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg leading-snug text-neutral-600 dark:text-neutral-500">
|
|
||||||
{t("signupWithSelfManage.subtitle")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mb-0 flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="relative">
|
|
||||||
{keys ? (
|
|
||||||
<input
|
|
||||||
readOnly
|
|
||||||
value={keys.nsec}
|
|
||||||
type={showKey ? "text" : "password"}
|
|
||||||
className="h-12 w-full resize-none rounded-xl border-transparent bg-neutral-100 pl-3 pr-14 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowKey((state) => !state)}
|
|
||||||
className="absolute right-2 top-2 inline-flex size-8 items-center justify-center rounded-lg bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
{showKey ? (
|
|
||||||
<EyeOnIcon className="size-4" />
|
|
||||||
) : (
|
|
||||||
<EyeOffIcon className="size-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox.Root
|
|
||||||
checked={confirm.c1}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setConfirm((state) => ({ ...state, c1: !state.c1 }))
|
|
||||||
}
|
|
||||||
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-100 outline-none dark:bg-neutral-900"
|
|
||||||
id="confirm1"
|
|
||||||
>
|
|
||||||
<Checkbox.Indicator className="text-blue-500">
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</Checkbox.Indicator>
|
|
||||||
</Checkbox.Root>
|
|
||||||
<label
|
|
||||||
className="text-sm text-neutral-700 dark:text-neutral-400"
|
|
||||||
htmlFor="confirm1"
|
|
||||||
>
|
|
||||||
{t("signupWithSelfManage.confirm1")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox.Root
|
|
||||||
checked={confirm.c3}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setConfirm((state) => ({ ...state, c3: !state.c3 }))
|
|
||||||
}
|
|
||||||
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-100 outline-none dark:bg-neutral-900"
|
|
||||||
id="confirm3"
|
|
||||||
>
|
|
||||||
<Checkbox.Indicator className="text-blue-500">
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</Checkbox.Indicator>
|
|
||||||
</Checkbox.Root>
|
|
||||||
<label
|
|
||||||
className="text-sm text-neutral-700 dark:text-neutral-400"
|
|
||||||
htmlFor="confirm3"
|
|
||||||
>
|
|
||||||
{t("signupWithSelfManage.confirm3")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox.Root
|
|
||||||
checked={confirm.c2}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setConfirm((state) => ({ ...state, c2: !state.c2 }))
|
|
||||||
}
|
|
||||||
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-100 outline-none dark:bg-neutral-900"
|
|
||||||
id="confirm2"
|
|
||||||
>
|
|
||||||
<Checkbox.Indicator className="text-blue-500">
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</Checkbox.Indicator>
|
|
||||||
</Checkbox.Root>
|
|
||||||
<label
|
|
||||||
className="text-sm text-neutral-700 dark:text-neutral-400"
|
|
||||||
htmlFor="confirm2"
|
|
||||||
>
|
|
||||||
{t("signupWithSelfManage.confirm2")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={submit}
|
|
||||||
disabled={!confirm.c1 || !confirm.c2 || !confirm.c3}
|
|
||||||
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
t("signupWithSelfManage.button")
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/auth/import")({
|
|
||||||
component: Import,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Import() {
|
|
||||||
const ark = useArk();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [t] = useTranslation();
|
|
||||||
const [key, setKey] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (!key.startsWith("nsec1")) return;
|
|
||||||
if (key.length < 30) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const npub: string = await invoke("get_public_key", { nsec: key });
|
|
||||||
await ark.save_account({
|
|
||||||
npub,
|
|
||||||
nsec: key,
|
|
||||||
});
|
|
||||||
navigate({
|
|
||||||
to: "/$account/home/local",
|
|
||||||
params: { account: npub },
|
|
||||||
search: { onboarding: true },
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isNip05 = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test(key);
|
|
||||||
const isNip49 = key.startsWith("ncryptsec");
|
|
||||||
|
|
||||||
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-8">
|
|
||||||
<div className="flex flex-col items-center gap-1 text-center">
|
|
||||||
<h1 className="text-2xl font-semibold">{t("login.title")}</h1>
|
|
||||||
<p className="text-lg leading-snug text-neutral-600 dark:text-neutral-500">
|
|
||||||
{t("login.subtitle")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
value={key}
|
|
||||||
type="text"
|
|
||||||
onChange={(e) => setKey(e.target.value)}
|
|
||||||
className="h-12 w-full resize-none rounded-xl border-transparent bg-neutral-100 pl-3 pr-10 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isNip05 || isNip49 ? (
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
|
||||||
>
|
|
||||||
Password *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
value={password}
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
className="h-12 w-full resize-none rounded-xl border-transparent bg-neutral-100 pl-3 pr-10 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={submit}
|
|
||||||
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Import"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { AddMediaIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { cn, insertImage, isImagePath } from "@lume/utils";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useSlateStatic } from "slate-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { getCurrent } from "@tauri-apps/api/window";
|
|
||||||
import { UnlistenFn } from "@tauri-apps/api/event";
|
|
||||||
|
|
||||||
export function MediaButton({ className }: { className?: string }) {
|
|
||||||
const ark = useArk();
|
|
||||||
const editor = useSlateStatic();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const uploadToNostrBuild = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const image = await ark.upload();
|
|
||||||
|
|
||||||
if (image) {
|
|
||||||
insertImage(editor, image);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
toast.error(`Upload failed, error: ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let unlisten: UnlistenFn = undefined;
|
|
||||||
|
|
||||||
async function listenFileDrop() {
|
|
||||||
const window = getCurrent();
|
|
||||||
if (!unlisten) {
|
|
||||||
unlisten = await window.listen("tauri://file-drop", async (event) => {
|
|
||||||
// @ts-ignore, lfg !!!
|
|
||||||
const items: string[] = event.payload.paths;
|
|
||||||
// start loading
|
|
||||||
setLoading(true);
|
|
||||||
// upload all images
|
|
||||||
for (const item of items) {
|
|
||||||
if (isImagePath(item)) {
|
|
||||||
const image = await ark.upload(item);
|
|
||||||
insertImage(editor, image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// stop loading
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listenFileDrop();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (unlisten) unlisten();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => uploadToNostrBuild()}
|
|
||||||
disabled={loading}
|
|
||||||
className={cn("inline-flex items-center justify-center", className)}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<AddMediaIcon className="size-5" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,438 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon, TrashIcon } from "@lume/icons";
|
|
||||||
import {
|
|
||||||
Portal,
|
|
||||||
cn,
|
|
||||||
insertImage,
|
|
||||||
insertMention,
|
|
||||||
insertNostrEvent,
|
|
||||||
isImageUrl,
|
|
||||||
sendNativeNotification,
|
|
||||||
} from "@lume/utils";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { MediaButton } from "./-components/media";
|
|
||||||
import { MentionNote } from "@lume/ui/src/note/mentions/note";
|
|
||||||
import {
|
|
||||||
Descendant,
|
|
||||||
Editor,
|
|
||||||
Node,
|
|
||||||
Range,
|
|
||||||
Transforms,
|
|
||||||
createEditor,
|
|
||||||
} from "slate";
|
|
||||||
import {
|
|
||||||
ReactEditor,
|
|
||||||
useSlateStatic,
|
|
||||||
useSelected,
|
|
||||||
useFocused,
|
|
||||||
withReact,
|
|
||||||
Slate,
|
|
||||||
Editable,
|
|
||||||
} from "slate-react";
|
|
||||||
import { Contact } from "@lume/types";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import { nip19 } from "nostr-tools";
|
|
||||||
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
|
|
||||||
type EditorElement = {
|
|
||||||
type: string;
|
|
||||||
children: Descendant[];
|
|
||||||
eventId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const contactQueryOptions = queryOptions({
|
|
||||||
queryKey: ["contacts"],
|
|
||||||
queryFn: () => invoke("get_contact_metadata"),
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/editor/")({
|
|
||||||
loader: ({ context }) =>
|
|
||||||
context.queryClient.ensureQueryData(contactQueryOptions),
|
|
||||||
component: Screen,
|
|
||||||
pendingComponent: Pending,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
// @ts-ignore, useless
|
|
||||||
const { reply_to, quote } = Route.useSearch();
|
|
||||||
|
|
||||||
let initialValue: EditorElement[];
|
|
||||||
|
|
||||||
if (quote) {
|
|
||||||
initialValue = [
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
children: [{ text: "" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
eventId: `nostr:${nip19.noteEncode(reply_to)}`,
|
|
||||||
children: [{ text: "" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
children: [{ text: "" }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
initialValue = [
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
children: [{ text: "" }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ark = useArk();
|
|
||||||
const ref = useRef<HTMLDivElement | null>();
|
|
||||||
const contacts = useSuspenseQuery(contactQueryOptions).data as Contact[];
|
|
||||||
|
|
||||||
const [t] = useTranslation();
|
|
||||||
const [editorValue, setEditorValue] = useState(initialValue);
|
|
||||||
const [target, setTarget] = useState<Range | undefined>();
|
|
||||||
const [index, setIndex] = useState(0);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [editor] = useState(() =>
|
|
||||||
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
|
||||||
);
|
|
||||||
|
|
||||||
const filters = contacts
|
|
||||||
?.filter((c) =>
|
|
||||||
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
|
|
||||||
)
|
|
||||||
?.slice(0, 5);
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
// @ts-expect-error, backlog
|
|
||||||
editor.children = [{ type: "paragraph", children: [{ text: "" }] }];
|
|
||||||
setEditorValue([{ type: "paragraph", children: [{ text: "" }] }]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const serialize = (nodes: Descendant[]) => {
|
|
||||||
return nodes
|
|
||||||
.map((n) => {
|
|
||||||
// @ts-expect-error, backlog
|
|
||||||
if (n.type === "image") return n.url;
|
|
||||||
// @ts-expect-error, backlog
|
|
||||||
if (n.type === "event") return n.eventId;
|
|
||||||
|
|
||||||
// @ts-expect-error, backlog
|
|
||||||
if (n.children.length) {
|
|
||||||
// @ts-expect-error, backlog
|
|
||||||
return n.children
|
|
||||||
.map((n) => {
|
|
||||||
if (n.type === "mention") return n.npub;
|
|
||||||
return Node.string(n).trim();
|
|
||||||
})
|
|
||||||
.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Node.string(n);
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
const publish = async () => {
|
|
||||||
try {
|
|
||||||
// start loading
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const content = serialize(editor.children);
|
|
||||||
const eventId = await ark.publish(content, reply_to, quote);
|
|
||||||
|
|
||||||
if (eventId) {
|
|
||||||
await sendNativeNotification("You've publish new post successfully.");
|
|
||||||
return reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop loading
|
|
||||||
setLoading(false);
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
await sendNativeNotification(String(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (target && filters.length > 0) {
|
|
||||||
const el = ref.current;
|
|
||||||
const domRange = ReactEditor.toDOMRange(editor, target);
|
|
||||||
const rect = domRange.getBoundingClientRect();
|
|
||||||
el.style.top = `${rect.top + window.scrollY + 24}px`;
|
|
||||||
el.style.left = `${rect.left + window.scrollX}px`;
|
|
||||||
}
|
|
||||||
}, [filters.length, editor, index, search, target]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
|
|
||||||
<Slate
|
|
||||||
editor={editor}
|
|
||||||
initialValue={editorValue}
|
|
||||||
onChange={() => {
|
|
||||||
const { selection } = editor;
|
|
||||||
|
|
||||||
if (selection && Range.isCollapsed(selection)) {
|
|
||||||
const [start] = Range.edges(selection);
|
|
||||||
const wordBefore = Editor.before(editor, start, { unit: "word" });
|
|
||||||
const before = wordBefore && Editor.before(editor, wordBefore);
|
|
||||||
const beforeRange = before && Editor.range(editor, before, start);
|
|
||||||
const beforeText =
|
|
||||||
beforeRange && Editor.string(editor, beforeRange);
|
|
||||||
const beforeMatch = beforeText?.match(/^@(\w+)$/);
|
|
||||||
const after = Editor.after(editor, start);
|
|
||||||
const afterRange = Editor.range(editor, start, after);
|
|
||||||
const afterText = Editor.string(editor, afterRange);
|
|
||||||
const afterMatch = afterText.match(/^(\s|$)/);
|
|
||||||
|
|
||||||
if (beforeMatch && afterMatch) {
|
|
||||||
setTarget(beforeRange);
|
|
||||||
setSearch(beforeMatch[1]);
|
|
||||||
setIndex(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTarget(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="flex h-16 w-full shrink-0 items-center justify-end gap-3 px-2"
|
|
||||||
>
|
|
||||||
<MediaButton className="size-9 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={publish}
|
|
||||||
className="inline-flex h-9 w-24 items-center justify-center rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
t("global.post")
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-full min-h-0 w-full">
|
|
||||||
<div className="flex h-full w-full flex-1 flex-col gap-2 px-2 pb-2">
|
|
||||||
{reply_to && !quote ? (
|
|
||||||
<div className="flex flex-col rounded-xl bg-white p-5 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-none dark:ring-1 dark:ring-white/5">
|
|
||||||
<h3 className="font-medium">Reply to:</h3>
|
|
||||||
<MentionNote eventId={reply_to} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="h-full w-full flex-1 overflow-hidden overflow-y-auto rounded-xl bg-white p-5 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-none dark:ring-1 dark:ring-white/5">
|
|
||||||
<Editable
|
|
||||||
key={JSON.stringify(editorValue)}
|
|
||||||
autoFocus={true}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect="none"
|
|
||||||
spellCheck={false}
|
|
||||||
renderElement={(props) => <Element {...props} />}
|
|
||||||
placeholder={t("editor.placeholder")}
|
|
||||||
className="focus:outline-none"
|
|
||||||
/>
|
|
||||||
{target && filters.length > 0 && (
|
|
||||||
<Portal>
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className="absolute left-[-9999px] top-[-9999px] z-10 w-[250px] rounded-xl border border-neutral-50 bg-white p-2 shadow-lg dark:border-neutral-900 dark:bg-neutral-950"
|
|
||||||
>
|
|
||||||
{filters.map((contact) => (
|
|
||||||
<button
|
|
||||||
key={contact.pubkey}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
Transforms.select(editor, target);
|
|
||||||
insertMention(editor, contact);
|
|
||||||
setTarget(null);
|
|
||||||
}}
|
|
||||||
className="flex w-full flex-col rounded-lg p-2 hover:bg-neutral-100 dark:hover:bg-neutral-900"
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={contact.pubkey}>
|
|
||||||
<User.Root className="flex w-full items-center gap-2">
|
|
||||||
<User.Avatar className="size-7 shrink-0 rounded-full object-cover" />
|
|
||||||
<div className="flex w-full flex-col items-start">
|
|
||||||
<User.Name className="max-w-[8rem] truncate text-sm font-medium" />
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Slate>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Pending() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center gap-2.5">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
<p>Loading cache...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const withNostrEvent = (editor: ReactEditor) => {
|
|
||||||
const { insertData, isVoid } = editor;
|
|
||||||
|
|
||||||
editor.isVoid = (element) => {
|
|
||||||
// @ts-expect-error, wtf
|
|
||||||
return element.type === "event" ? true : isVoid(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.insertData = (data) => {
|
|
||||||
const text = data.getData("text/plain");
|
|
||||||
|
|
||||||
if (text.startsWith("nevent1") || text.startsWith("note1")) {
|
|
||||||
insertNostrEvent(editor, text);
|
|
||||||
} else {
|
|
||||||
insertData(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return editor;
|
|
||||||
};
|
|
||||||
|
|
||||||
const withMentions = (editor: ReactEditor) => {
|
|
||||||
const { isInline, isVoid, markableVoid } = editor;
|
|
||||||
|
|
||||||
editor.isInline = (element) => {
|
|
||||||
// @ts-expect-error, wtf
|
|
||||||
return element.type === "mention" ? true : isInline(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.isVoid = (element) => {
|
|
||||||
// @ts-expect-error, wtf
|
|
||||||
return element.type === "mention" ? true : isVoid(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.markableVoid = (element) => {
|
|
||||||
// @ts-expect-error, wtf
|
|
||||||
return element.type === "mention" || markableVoid(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
return editor;
|
|
||||||
};
|
|
||||||
|
|
||||||
const withImages = (editor: ReactEditor) => {
|
|
||||||
const { insertData, isVoid } = editor;
|
|
||||||
|
|
||||||
editor.isVoid = (element) => {
|
|
||||||
// @ts-expect-error, wtf
|
|
||||||
return element.type === "image" ? true : isVoid(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.insertData = (data) => {
|
|
||||||
const text = data.getData("text/plain");
|
|
||||||
|
|
||||||
if (isImageUrl(text)) {
|
|
||||||
insertImage(editor, text);
|
|
||||||
} else {
|
|
||||||
insertData(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return editor;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Image = ({ attributes, children, element }) => {
|
|
||||||
const editor = useSlateStatic();
|
|
||||||
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
|
||||||
|
|
||||||
const selected = useSelected();
|
|
||||||
const focused = useFocused();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...attributes}>
|
|
||||||
{children}
|
|
||||||
<div contentEditable={false} className="relative my-2">
|
|
||||||
<img
|
|
||||||
src={element.url}
|
|
||||||
alt={element.url}
|
|
||||||
className={cn(
|
|
||||||
"h-auto w-full rounded-lg border border-neutral-100 object-cover ring-2 dark:border-neutral-900",
|
|
||||||
selected && focused ? "ring-blue-500" : "ring-transparent",
|
|
||||||
)}
|
|
||||||
contentEditable={false}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
contentEditable={false}
|
|
||||||
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
|
||||||
className="absolute right-2 top-2 inline-flex size-8 items-center justify-center rounded-lg bg-red-500 text-white hover:bg-red-600"
|
|
||||||
>
|
|
||||||
<TrashIcon className="size-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Mention = ({ attributes, element }) => {
|
|
||||||
const editor = useSlateStatic();
|
|
||||||
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
{...attributes}
|
|
||||||
type="button"
|
|
||||||
contentEditable={false}
|
|
||||||
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
|
||||||
className="inline-block align-baseline text-blue-500 hover:text-blue-600"
|
|
||||||
>{`@${element.name}`}</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Event = ({ attributes, element, children }) => {
|
|
||||||
const editor = useSlateStatic();
|
|
||||||
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...attributes}>
|
|
||||||
{children}
|
|
||||||
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
|
|
||||||
<div
|
|
||||||
contentEditable={false}
|
|
||||||
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
|
||||||
className="user-select-none relative my-2"
|
|
||||||
>
|
|
||||||
<MentionNote
|
|
||||||
eventId={element.eventId.replace("nostr:", "")}
|
|
||||||
openable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Element = (props) => {
|
|
||||||
const { attributes, children, element } = props;
|
|
||||||
|
|
||||||
switch (element.type) {
|
|
||||||
case "image":
|
|
||||||
return <Image {...props} />;
|
|
||||||
case "mention":
|
|
||||||
return <Mention {...props} />;
|
|
||||||
case "event":
|
|
||||||
return <Event {...props} />;
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<p {...attributes} className="text-lg">
|
|
||||||
{children}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { useEvent } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { Box, Container, Note, User } from "@lume/ui";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { WindowVirtualizer } from "virtua";
|
|
||||||
import { ReplyList } from "./-components/replyList";
|
|
||||||
import { Event } from "@lume/types";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/events/$eventId")({
|
|
||||||
component: Event,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Event() {
|
|
||||||
const { eventId } = Route.useParams();
|
|
||||||
const { isLoading, isError, data } = useEvent(eventId);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<p>Not found.</p>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WindowVirtualizer>
|
|
||||||
<Container withDrag>
|
|
||||||
<Box className="px-3 pt-3">
|
|
||||||
<MainNote data={data} />
|
|
||||||
{data ? <ReplyList eventId={eventId} /> : null}
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
</WindowVirtualizer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MainNote({ data }: { data: Event }) {
|
|
||||||
return (
|
|
||||||
<Note.Provider event={data}>
|
|
||||||
<Note.Root className="flex flex-col pb-3">
|
|
||||||
<User.Provider pubkey={data.pubkey}>
|
|
||||||
<User.Root className="mb-3 flex flex-1 items-center gap-3">
|
|
||||||
<User.Avatar className="size-11 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
|
|
||||||
<div className="flex flex-1 flex-col">
|
|
||||||
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
|
|
||||||
<div className="inline-flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
<User.Time time={data.created_at} />
|
|
||||||
<span>·</span>
|
|
||||||
<User.NIP05 />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<Note.Thread className="mb-2" />
|
|
||||||
<Note.Content className="min-w-0" />
|
|
||||||
<div className="mt-4 flex items-center justify-between">
|
|
||||||
<div className="-ml-1 inline-flex items-center gap-4">
|
|
||||||
<Note.Repost />
|
|
||||||
<Note.Zap />
|
|
||||||
</div>
|
|
||||||
<Note.Menu />
|
|
||||||
</div>
|
|
||||||
</Note.Root>
|
|
||||||
</Note.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { EventWithReplies } from "@lume/types";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
import { Note, User } from "@lume/ui";
|
|
||||||
import { SubReply } from "./subReply";
|
|
||||||
|
|
||||||
export function Reply({ event }: { event: EventWithReplies }) {
|
|
||||||
return (
|
|
||||||
<Note.Provider event={event}>
|
|
||||||
<Note.Root className="border-t border-neutral-100 pt-3 dark:border-neutral-900">
|
|
||||||
<User.Provider pubkey={event.pubkey}>
|
|
||||||
<User.Root className="mb-2 flex items-center justify-between">
|
|
||||||
<div className="inline-flex gap-2">
|
|
||||||
<User.Avatar className="size-6 rounded-full" />
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<User.Name className="font-semibold" />
|
|
||||||
<User.NIP05 className="text-base lowercase text-neutral-600 dark:text-neutral-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<User.Time time={event.created_at} />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<Note.Content />
|
|
||||||
<div className="mt-4 flex items-center justify-between">
|
|
||||||
<div className="-ml-1 inline-flex items-center gap-4">
|
|
||||||
<Note.Reply />
|
|
||||||
<Note.Repost />
|
|
||||||
<Note.Zap />
|
|
||||||
</div>
|
|
||||||
<Note.Menu />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
event.replies?.length > 0
|
|
||||||
? "my-3 mt-6 flex flex-col gap-3 divide-y divide-neutral-100 border-l-2 border-neutral-100 pl-6 dark:divide-neutral-900 dark:border-neutral-900"
|
|
||||||
: "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{event.replies?.length > 0
|
|
||||||
? event.replies?.map((childEvent) => (
|
|
||||||
<SubReply key={childEvent.id} event={childEvent} />
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
</Note.Root>
|
|
||||||
</Note.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { EventWithReplies } from "@lume/types";
|
|
||||||
import { Reply } from "./reply";
|
|
||||||
|
|
||||||
export function ReplyList({
|
|
||||||
eventId,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
eventId: string;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
const ark = useArk();
|
|
||||||
|
|
||||||
const [t] = useTranslation();
|
|
||||||
const [data, setData] = useState<null | EventWithReplies[]>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getReplies() {
|
|
||||||
const events = await ark.get_event_thread(eventId);
|
|
||||||
setData(events);
|
|
||||||
}
|
|
||||||
getReplies();
|
|
||||||
}, [eventId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("flex flex-col gap-3", className)}>
|
|
||||||
{!data ? (
|
|
||||||
<div className="mt-4 flex h-16 items-center justify-center p-3">
|
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : data.length === 0 ? (
|
|
||||||
<div className="mt-4 flex w-full items-center justify-center">
|
|
||||||
<div className="flex flex-col items-center justify-center gap-2 py-6">
|
|
||||||
<h3 className="text-3xl">👋</h3>
|
|
||||||
<p className="leading-none text-neutral-600 dark:text-neutral-400">
|
|
||||||
{t("note.reply.empty")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
data.map((event) => <Reply key={event.id} event={event} />)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Event } from "@lume/types";
|
|
||||||
import { Note, User } from "@lume/ui";
|
|
||||||
|
|
||||||
export function SubReply({ event }: { event: Event; rootEventId?: string }) {
|
|
||||||
return (
|
|
||||||
<Note.Provider event={event}>
|
|
||||||
<Note.Root className="pt-3">
|
|
||||||
<User.Provider pubkey={event.pubkey}>
|
|
||||||
<User.Root className="mb-2 flex items-center justify-between">
|
|
||||||
<div className="inline-flex gap-2">
|
|
||||||
<User.Avatar className="size-6 rounded-full" />
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<User.Name className="font-semibold" />
|
|
||||||
<User.NIP05 className="text-base lowercase text-neutral-600 dark:text-neutral-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<User.Time time={event.created_at} />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<Note.Content />
|
|
||||||
<div className="mt-4 flex items-center justify-between">
|
|
||||||
<div className="-ml-1 inline-flex items-center gap-4">
|
|
||||||
<Note.Reply />
|
|
||||||
<Note.Repost />
|
|
||||||
<Note.Zap />
|
|
||||||
</div>
|
|
||||||
<Note.Menu />
|
|
||||||
</div>
|
|
||||||
</Note.Root>
|
|
||||||
</Note.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon, PlusIcon } from "@lume/icons";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
|
||||||
beforeLoad: async ({ search, context }) => {
|
|
||||||
const ark = context.ark;
|
|
||||||
const accounts = await ark.get_all_accounts();
|
|
||||||
|
|
||||||
switch (accounts.length) {
|
|
||||||
// Guest account
|
|
||||||
case 0:
|
|
||||||
const guest = await ark.create_guest_account();
|
|
||||||
throw redirect({
|
|
||||||
to: "/$account/home/local",
|
|
||||||
params: { account: guest },
|
|
||||||
search: { guest: true },
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
// Only 1 account, skip account selection screen
|
|
||||||
case 1:
|
|
||||||
// @ts-ignore, totally fine !!!
|
|
||||||
if (search.manually) return;
|
|
||||||
|
|
||||||
const account = accounts[0].npub;
|
|
||||||
const loadedAccount = await ark.load_selected_account(account);
|
|
||||||
|
|
||||||
if (loadedAccount) {
|
|
||||||
throw redirect({
|
|
||||||
to: "/$account/home/local",
|
|
||||||
params: { account },
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Account selection
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const ark = useArk();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const select = async (npub: string) => {
|
|
||||||
setLoading(true);
|
|
||||||
const loadAccount = await ark.load_selected_account(npub);
|
|
||||||
if (loadAccount) {
|
|
||||||
navigate({
|
|
||||||
to: "/$account/home/local",
|
|
||||||
params: { account: npub },
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentDate = new Date().toLocaleString("default", {
|
|
||||||
weekday: "long",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
|
||||||
<div className="relative z-20 flex flex-col items-center gap-16">
|
|
||||||
<div className="text-center text-white">
|
|
||||||
<h2 className="mb-1 text-2xl">{currentDate}</h2>
|
|
||||||
<h2 className="text-2xl font-semibold">Welcome back!</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center gap-6">
|
|
||||||
{loading ? (
|
|
||||||
<div className="inline-flex size-6 items-center justify-center">
|
|
||||||
<LoaderIcon className="size-6 animate-spin text-white" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{ark.accounts.map((account) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
key={account.npub}
|
|
||||||
onClick={() => select(account.npub)}
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={account.npub}>
|
|
||||||
<User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 hover:bg-white/10 dark:hover:bg-black/10">
|
|
||||||
<User.Avatar className="size-20 rounded-full object-cover" />
|
|
||||||
<User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<Link to="/landing">
|
|
||||||
<div className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 text-white hover:bg-white/10 dark:hover:bg-black/10">
|
|
||||||
<div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20">
|
|
||||||
<PlusIcon className="size-5" />
|
|
||||||
</div>
|
|
||||||
<p className="text-lg font-medium leading-tight">Add</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="absolute z-10 h-full w-full bg-white/10 backdrop-blur-lg dark:bg-black/10" />
|
|
||||||
<div className="absolute inset-0 h-full w-full">
|
|
||||||
<img
|
|
||||||
src="/lock-screen.jpg"
|
|
||||||
srcSet="/lock-screen@2x.jpg 2x"
|
|
||||||
alt="Lock Screen Background"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
|
|
||||||
target="_blank"
|
|
||||||
className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white dark:bg-black/20"
|
|
||||||
>
|
|
||||||
Design by NoGood
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/landing/")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const context = Route.useRouteContext();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-screen bg-black">
|
|
||||||
<div className="flex h-full w-full flex-col items-center justify-between">
|
|
||||||
<div />
|
|
||||||
<div className="mx-auto flex w-full max-w-4xl flex-col items-center gap-10">
|
|
||||||
<div className="flex flex-col items-center text-center">
|
|
||||||
<img
|
|
||||||
src={`/heading-${context.locale}.png`}
|
|
||||||
srcSet={`/heading-${context.locale}@2x.png 2x`}
|
|
||||||
alt="lume"
|
|
||||||
className="w-2/3"
|
|
||||||
/>
|
|
||||||
<p className="mt-5 whitespace-pre-line text-lg font-medium leading-snug text-neutral-700">
|
|
||||||
{t("welcome.title")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto flex w-full max-w-sm flex-col gap-2">
|
|
||||||
<Link
|
|
||||||
to="/auth/create"
|
|
||||||
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
{t("welcome.signup")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/auth/import"
|
|
||||||
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-neutral-950 text-lg font-medium text-white hover:bg-neutral-900"
|
|
||||||
>
|
|
||||||
{t("welcome.login")}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-11 items-center justify-center">
|
|
||||||
<p className="text-neutral-800">
|
|
||||||
{t("welcome.footer")}{" "}
|
|
||||||
<Link
|
|
||||||
to="https://nostr.com"
|
|
||||||
target="_blank"
|
|
||||||
className="text-blue-500"
|
|
||||||
>
|
|
||||||
here
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { ArrowRightIcon, ZapIcon } from "@lume/icons";
|
|
||||||
import { Container } from "@lume/ui";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/nwc")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const ark = useArk();
|
|
||||||
|
|
||||||
const [uri, setUri] = useState("");
|
|
||||||
const [isDone, setIsDone] = useState(false);
|
|
||||||
|
|
||||||
const save = async () => {
|
|
||||||
const nwc = await ark.set_nwc(uri);
|
|
||||||
|
|
||||||
if (nwc) {
|
|
||||||
setIsDone(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container withDrag>
|
|
||||||
<div className="h-full w-full flex-1 px-5">
|
|
||||||
{!isDone ? (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="inline-flex size-14 items-center justify-center rounded-xl bg-black text-white shadow-md">
|
|
||||||
<ZapIcon className="size-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-2xl font-light">
|
|
||||||
Connect <span className="font-semibold">bitcoin wallet</span>{" "}
|
|
||||||
to start zapping to your favorite content and creator.
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-10 flex flex-col gap-2">
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<label>Paste a Nostr Wallet Connect connection string</label>
|
|
||||||
<textarea
|
|
||||||
value={uri}
|
|
||||||
onChange={(e) => setUri(e.target.value)}
|
|
||||||
placeholder="nostrconnect://"
|
|
||||||
className="h-24 w-full resize-none rounded-lg border-transparent bg-white placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-black dark:focus:ring-blue-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={save}
|
|
||||||
className="inline-flex h-11 w-full items-center justify-between gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
<div className="size-5" />
|
|
||||||
<div>Save & Connect</div>
|
|
||||||
<ArrowRightIcon className="size-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div>Done</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/settings/")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
return <div>Settings</div>;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { WindowVirtualizer } from "virtua";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import { EventList } from "./-components/eventList";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/users/$pubkey")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const { pubkey } = Route.useParams();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WindowVirtualizer>
|
|
||||||
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
|
|
||||||
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
|
|
||||||
<div className="flex h-full min-h-0 w-full">
|
|
||||||
<div className="h-full w-full flex-1 px-2 pb-2">
|
|
||||||
<div className="h-full w-full overflow-hidden overflow-y-auto 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-none dark:ring-1 dark:ring-white/5">
|
|
||||||
<User.Provider pubkey={pubkey}>
|
|
||||||
<User.Root>
|
|
||||||
<User.Cover className="h-44 w-full object-cover" />
|
|
||||||
<div className="relative -mt-8 flex flex-col gap-4 px-5">
|
|
||||||
<User.Avatar className="size-14 rounded-full" />
|
|
||||||
<div className="inline-flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<User.Name className="font-semibold leading-tight" />
|
|
||||||
<User.NIP05 className="text-sm leading-tight text-neutral-600 dark:text-neutral-400" />
|
|
||||||
</div>
|
|
||||||
<User.Button className="h-9 w-24 rounded-full bg-black text-sm font-medium text-white hover:bg-neutral-900 dark:bg-neutral-900" />
|
|
||||||
</div>
|
|
||||||
<User.About />
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<div className="mt-4 px-5">
|
|
||||||
<h3 className="mb-4 text-lg font-semibold">Notes</h3>
|
|
||||||
<EventList id={pubkey} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</WindowVirtualizer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { TextNote } from "@/components/text";
|
|
||||||
import { RepostNote } from "@/components/repost";
|
|
||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { ArrowRightCircleIcon, InfoIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { Event, Kind } from "@lume/types";
|
|
||||||
import { FETCH_LIMIT } from "@lume/utils";
|
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
export function EventList({ id }: { id: string }) {
|
|
||||||
const ark = useArk();
|
|
||||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
|
||||||
useInfiniteQuery({
|
|
||||||
queryKey: ["events", id],
|
|
||||||
initialPageParam: 0,
|
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
|
||||||
const events = await ark.get_events_from(id, FETCH_LIMIT, pageParam);
|
|
||||||
return events;
|
|
||||||
},
|
|
||||||
getNextPageParam: (lastPage) => {
|
|
||||||
const lastEvent = lastPage?.at(-1);
|
|
||||||
if (!lastEvent) return;
|
|
||||||
return lastEvent.created_at - 1;
|
|
||||||
},
|
|
||||||
select: (data) => data?.pages.flatMap((page) => page),
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
|
||||||
if (!event) return;
|
|
||||||
switch (event.kind) {
|
|
||||||
case Kind.Repost:
|
|
||||||
return <RepostNote key={event.id} event={event} />;
|
|
||||||
default:
|
|
||||||
return <TextNote key={event.id} event={event} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : !data.length ? (
|
|
||||||
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950">
|
|
||||||
<InfoIcon className="size-6" />
|
|
||||||
<p>Empty newsfeed.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
data.map((item) => renderItem(item))
|
|
||||||
)}
|
|
||||||
<div className="flex h-20 items-center justify-center">
|
|
||||||
{hasNextPage ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => fetchNextPage()}
|
|
||||||
disabled={!hasNextPage || isFetchingNextPage}
|
|
||||||
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{isFetchingNextPage ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArrowRightCircleIcon className="size-5" />
|
|
||||||
Load more
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import { Balance } from "@/components/balance";
|
|
||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { Box, Container, User } from "@lume/ui";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { getCurrent } from "@tauri-apps/api/webviewWindow";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import CurrencyInput from "react-currency-input-field";
|
|
||||||
|
|
||||||
const DEFAULT_VALUES = [69, 100, 200, 500];
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/zap/$id")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { id } = Route.useParams();
|
|
||||||
// @ts-ignore, magic !!!
|
|
||||||
const { pubkey, account } = Route.useSearch();
|
|
||||||
|
|
||||||
const [amount, setAmount] = useState(21);
|
|
||||||
const [message, setMessage] = useState("");
|
|
||||||
const [isCompleted, setIsCompleted] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const ark = useArk();
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
try {
|
|
||||||
// start loading
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const val = await ark.zap_event(id, amount, message);
|
|
||||||
|
|
||||||
if (val) {
|
|
||||||
setIsCompleted(true);
|
|
||||||
const window = getCurrent();
|
|
||||||
// close current window
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setIsLoading(false);
|
|
||||||
toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Balance recipient={pubkey} account={account} />
|
|
||||||
<Box className="flex flex-col gap-3">
|
|
||||||
<div className="flex h-full flex-col justify-between py-5">
|
|
||||||
<div className="flex h-11 shrink-0 items-center justify-center gap-2">
|
|
||||||
{t("note.zap.modalTitle")}{" "}
|
|
||||||
<User.Provider pubkey={pubkey}>
|
|
||||||
<User.Root className="inline-flex items-center gap-2 rounded-full bg-neutral-100 p-1 dark:bg-neutral-900">
|
|
||||||
<User.Avatar className="size-6 rounded-full" />
|
|
||||||
<User.Name className="pr-2 text-sm font-medium" />
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col justify-between px-5">
|
|
||||||
<div className="relative flex flex-1 flex-col pb-8">
|
|
||||||
<div className="inline-flex h-full flex-1 items-center justify-center gap-1">
|
|
||||||
<CurrencyInput
|
|
||||||
placeholder="0"
|
|
||||||
defaultValue={21}
|
|
||||||
value={amount}
|
|
||||||
decimalsLimit={2}
|
|
||||||
min={0} // 0 sats
|
|
||||||
max={10000} // 1M sats
|
|
||||||
maxLength={10000} // 1M sats
|
|
||||||
onValueChange={(value) => setAmount(Number(value))}
|
|
||||||
className="w-full flex-1 border-none bg-transparent text-right text-4xl font-semibold placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<span className="w-full flex-1 text-left text-4xl font-semibold text-neutral-500 dark:text-neutral-400">
|
|
||||||
sats
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex items-center justify-center gap-2">
|
|
||||||
{DEFAULT_VALUES.map((value) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setAmount(value)}
|
|
||||||
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{value} sats
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-col gap-2">
|
|
||||||
<input
|
|
||||||
name="message"
|
|
||||||
value={message}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
spellCheck={false}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
placeholder={t("note.zap.messagePlaceholder")}
|
|
||||||
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => submit()}
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg border-t border-neutral-900 bg-neutral-950 pb-[2px] font-semibold text-neutral-50 hover:bg-neutral-900 dark:border-neutral-800 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{isCompleted
|
|
||||||
? t("note.zap.buttonFinish")
|
|
||||||
: isLoading
|
|
||||||
? t("note.zap.buttonLoading")
|
|
||||||
: t("note.zap.zap")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
|
|
||||||
import preset from "@lume/tailwindcss";
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
content: [
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
|
||||||
"../../packages/@columns/**/*{.js,.ts,.jsx,.tsx}",
|
|
||||||
"../../packages/ark/**/*{.js,.ts,.jsx,.tsx}",
|
|
||||||
"../../packages/ui/**/*{.js,.ts,.jsx,.tsx}",
|
|
||||||
"index.html",
|
|
||||||
],
|
|
||||||
presets: [preset],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@lume/tsconfig/base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "dist",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"routesDirectory": "./src/routes",
|
|
||||||
"generatedRouteTree": "./src/router.gen.ts",
|
|
||||||
"routeFileIgnorePrefix": "-",
|
|
||||||
"quoteStyle": "single"
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { TanStackRouterVite } from "@tanstack/router-vite-plugin";
|
|
||||||
import react from "@vitejs/plugin-react-swc";
|
|
||||||
import { defineConfig } from "vite";
|
|
||||||
import topLevelAwait from "vite-plugin-top-level-await";
|
|
||||||
import viteTsconfigPaths from "vite-tsconfig-paths";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
react(),
|
|
||||||
viteTsconfigPaths(),
|
|
||||||
topLevelAwait({
|
|
||||||
promiseExportName: "__tla",
|
|
||||||
promiseImportName: (i) => `__tla_${i}`,
|
|
||||||
}),
|
|
||||||
TanStackRouterVite(),
|
|
||||||
],
|
|
||||||
build: {
|
|
||||||
outDir: "../../dist",
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
strictPort: true,
|
|
||||||
port: 3000,
|
|
||||||
},
|
|
||||||
clearScreen: false,
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# build output
|
|
||||||
dist/
|
|
||||||
# generated types
|
|
||||||
.astro/
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# logs
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
|
|
||||||
# environment variables
|
|
||||||
.env
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# macOS-specific files
|
|
||||||
.DS_Store
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Astro Starter Kit: Minimal
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm create astro@latest -- --template minimal
|
|
||||||
```
|
|
||||||
|
|
||||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
|
|
||||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
|
|
||||||
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
|
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
|
||||||
|
|
||||||
## 🚀 Project Structure
|
|
||||||
|
|
||||||
Inside of your Astro project, you'll see the following folders and files:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/
|
|
||||||
├── public/
|
|
||||||
├── src/
|
|
||||||
│ └── pages/
|
|
||||||
│ └── index.astro
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
|
||||||
|
|
||||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
|
||||||
|
|
||||||
Any static assets, like images, can be placed in the `public/` directory.
|
|
||||||
|
|
||||||
## 🧞 Commands
|
|
||||||
|
|
||||||
All commands are run from the root of the project, from a terminal:
|
|
||||||
|
|
||||||
| Command | Action |
|
|
||||||
| :------------------------ | :----------------------------------------------- |
|
|
||||||
| `npm install` | Installs dependencies |
|
|
||||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
|
||||||
| `npm run build` | Build your production site to `./dist/` |
|
|
||||||
| `npm run preview` | Preview your build locally, before deploying |
|
|
||||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
|
||||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
|
||||||
|
|
||||||
## 👀 Want to learn more?
|
|
||||||
|
|
||||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
|
||||||
|
|
||||||
import tailwind from "@astrojs/tailwind";
|
|
||||||
|
|
||||||
// https://astro.build/config
|
|
||||||
export default defineConfig({
|
|
||||||
integrations: [tailwind()]
|
|
||||||
});
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@lume/web",
|
|
||||||
"type": "module",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "astro dev",
|
|
||||||
"start": "astro dev",
|
|
||||||
"build": "astro check && astro build",
|
|
||||||
"preview": "astro preview",
|
|
||||||
"astro": "astro"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@astrojs/check": "^0.4.1",
|
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
|
||||||
"@fontsource/geist-mono": "^5.0.1",
|
|
||||||
"astro": "^4.4.9",
|
|
||||||
"astro-seo-meta": "^4.1.0",
|
|
||||||
"astro-seo-schema": "^4.0.0",
|
|
||||||
"schema-dts": "^1.1.2",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"typescript": "^5.3.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@tailwindcss/typography": "^0.5.10"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<svg width="824" height="824" viewBox="0 0 824 824" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_564_71)">
|
|
||||||
<rect width="824" height="824" rx="184" fill="white" style="fill:white;fill-opacity:1;"/>
|
|
||||||
<circle cx="267" cy="594" r="42" fill="black" style="fill:black;fill-opacity:1;"/>
|
|
||||||
<circle cx="267" cy="594" r="42" fill="url(#paint0_radial_564_71)" fill-opacity="0.5" style=""/>
|
|
||||||
<circle cx="267" cy="594" r="42" fill="url(#paint1_radial_564_71)" fill-opacity="0.3" style=""/>
|
|
||||||
<circle cx="557" cy="594" r="42" fill="black" style="fill:black;fill-opacity:1;"/>
|
|
||||||
<circle cx="557" cy="594" r="42" fill="url(#paint2_radial_564_71)" fill-opacity="0.5" style=""/>
|
|
||||||
<circle cx="557" cy="594" r="42" fill="url(#paint3_radial_564_71)" fill-opacity="0.3" style=""/>
|
|
||||||
<path d="M412 691C382.859 691 353.717 686.063 337.654 682.804C333.024 681.865 329.866 686.676 333.074 690.144C345.098 703.138 370.814 724 412 724C453.186 724 478.902 703.138 490.926 690.144C494.134 686.676 490.976 681.865 486.346 682.804C470.283 686.063 441.141 691 412 691Z" fill="url(#paint4_linear_564_71)" style=""/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<radialGradient id="paint0_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(241.807 578.038) rotate(112.103) scale(88.2816 69.6512)">
|
|
||||||
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
|
|
||||||
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="paint1_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(288.309 621.165) rotate(126.504) scale(25.5816 15.4047)">
|
|
||||||
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
|
|
||||||
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="paint2_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(531.807 578.038) rotate(112.103) scale(88.2816 69.6512)">
|
|
||||||
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
|
|
||||||
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="paint3_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(578.309 621.165) rotate(126.504) scale(25.5816 15.4047)">
|
|
||||||
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
|
|
||||||
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
|
||||||
</radialGradient>
|
|
||||||
<linearGradient id="paint4_linear_564_71" x1="293.565" y1="686.595" x2="316.497" y2="774.784" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#FF9F5A" style="stop-color:#FF9F5A;stop-color:color(display-p3 1.0000 0.6235 0.3529);stop-opacity:1;"/>
|
|
||||||
<stop offset="1" stop-color="#FF9F5A" style="stop-color:#FF9F5A;stop-color:color(display-p3 1.0000 0.6235 0.3529);stop-opacity:1;"/>
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="clip0_564_71">
|
|
||||||
<rect width="824" height="824" fill="white" style="fill:white;fill-opacity:1;"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1 +0,0 @@
|
|||||||
/// <reference types="astro/client" />
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
---
|
|
||||||
import { Seo } from "astro-seo-meta";
|
|
||||||
---
|
|
||||||
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<meta name="generator" content={Astro.generator} />
|
|
||||||
<title>Lume</title>
|
|
||||||
<Seo
|
|
||||||
title="Lume"
|
|
||||||
description="A multiple columns Nostr client for desktop."
|
|
||||||
keywords={[
|
|
||||||
"nostr",
|
|
||||||
"nostr client",
|
|
||||||
"social network",
|
|
||||||
"desktop app",
|
|
||||||
"timeline",
|
|
||||||
"application",
|
|
||||||
"columns",
|
|
||||||
]}
|
|
||||||
themeColor="#fafafa"
|
|
||||||
colorScheme="light"
|
|
||||||
facebook={{
|
|
||||||
image: "/og-image.jpg",
|
|
||||||
url: "https://lume.nu",
|
|
||||||
type: "website",
|
|
||||||
}}
|
|
||||||
twitter={{
|
|
||||||
image: "/og-image.jpg",
|
|
||||||
card: "summary",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</head>
|
|
||||||
<body
|
|
||||||
class="w-full h-full antialiased font-mono bg-neutral-50 dark:bg-neutral-950 text-neutral-950 dark:text-neutral-50"
|
|
||||||
>
|
|
||||||
<div class="max-w-2xl mx-auto w-full py-16 md:px-0 px-2">
|
|
||||||
<div class="flex flex-col gap-16">
|
|
||||||
<div class="prose dark:prose-invert prose-neutral max-w-none">
|
|
||||||
<h3>About Lume</h3>
|
|
||||||
<p>
|
|
||||||
Lume is a <b>Nostr client</b> for desktop include Linux, Windows and
|
|
||||||
macOS. It is free and open source, you can look at source code <a
|
|
||||||
href="https://github.com/lumehq/lume"
|
|
||||||
target="_blank">on Github</a
|
|
||||||
>. Lume is actively improving the app and adding new features, you
|
|
||||||
can expect new update every month.
|
|
||||||
</p>
|
|
||||||
<a href="#download">Download</a>
|
|
||||||
<h3>What is nostr & how does it work?</h3>
|
|
||||||
<p>
|
|
||||||
Nostr stands for Notes and Other Stuff Transmitted by Relays. It is
|
|
||||||
an open, permission-less protocol that aims to provide
|
|
||||||
censorship-resistance and interoperability. It can be used to create
|
|
||||||
social networks or just about any other type of app (other stuff
|
|
||||||
part of the acronym). It is not a single website or app, but the
|
|
||||||
glue that holds together many apps (clients) and <b>Lume</b> is one of
|
|
||||||
it.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
At its core, nostr consists of relays and events. A person does
|
|
||||||
something (event) and this event is sent to a relay. The relay
|
|
||||||
stores the event, then waits for another person to request it. The
|
|
||||||
most common types of events are notes and reactions - the stuff
|
|
||||||
social media is made of, but there are many other types of events.
|
|
||||||
It works very similar to how any other app would work with a
|
|
||||||
database, except in nostr there is no single database, rather a
|
|
||||||
large number of relays that store the events.
|
|
||||||
</p>
|
|
||||||
<h3>Lume is multiple columns experience</h3>
|
|
||||||
<p>
|
|
||||||
Lume is display your timeline as multiple column, each column is
|
|
||||||
each different content and you can define your experience
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
You can create a column to display newsfeed from specific people,
|
|
||||||
you can create a column to display all contents related to some
|
|
||||||
hashtags. It all up to you.
|
|
||||||
</p>
|
|
||||||
<img
|
|
||||||
src="https://image.nostr.build/fd3e3cdeb4fb9f0f3de5c5e668a11dcae55f50cc9a78fc2b57b063240191a0f9.png"
|
|
||||||
alt="columns"
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
class="w-full h-auto rounded-lg"
|
|
||||||
/>
|
|
||||||
<h3>"For You"</h3>
|
|
||||||
<p>
|
|
||||||
Unlike some social networks, they feed you by algorithm. In Lume,
|
|
||||||
you totally control what to will see
|
|
||||||
</p>
|
|
||||||
<img
|
|
||||||
src="https://image.nostr.build/5afd79de15929a4ac6f6e933791c942555baa4206fecee54fed61dde9fe167e1.png"
|
|
||||||
alt="for you"
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
class="w-full h-auto rounded-lg"
|
|
||||||
/>
|
|
||||||
<h3 id="download">Download and Explore</h3>
|
|
||||||
<p>
|
|
||||||
(Universal) macOS: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/Lume_3.0.0_universal.dmg"
|
|
||||||
>Lume_3.0.0_universal.dmg
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
(x86-64) Windows 11: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/Lume_3.0.0_x64-setup.exe"
|
|
||||||
>Lume_3.0.0_x64-setup.exe
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
(x86-64) Ubuntu: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume_3.0.0_amd64.deb"
|
|
||||||
>lume_3.0.0_amd64.deb
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
(x86-64) Fedora: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume-3.0.0-1.x86_64.rpm"
|
|
||||||
>lume-3.0.0-1.x86_64.rpm
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
(x86-64) Linux Flatpak: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume_3.0.0_amd64.flatpak"
|
|
||||||
>lume_3.0.0_amd64.flatpak
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
(x86-64) Linux AppImage: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume_3.0.0_amd64.AppImage"
|
|
||||||
>lume_3.0.0_amd64.AppImage
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Support for ARM, RISC-V and Loongarch architecture are coming soon.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-sm text-neutral-500 dark:text-neutral-600">
|
|
||||||
Supported by <a
|
|
||||||
href="https://opensats.org"
|
|
||||||
target="_blank"
|
|
||||||
class="text-orange-500">Open Sats</a
|
|
||||||
> and Community
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
|
|
||||||
const defaultTheme = require("tailwindcss/defaultTheme");
|
|
||||||
|
|
||||||
export default {
|
|
||||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
fontFamily: {
|
|
||||||
mono: ["Geist Mono", ...defaultTheme.fontFamily.mono],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [require("@tailwindcss/typography")],
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "astro/tsconfigs/strict"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M3 12L21 12M21 12L12.5 3.5M21 12L12.5 20.5" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||||
|
After Width: | Height: | Size: 321 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M5 13L9 17L19 7" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||||
|
After Width: | Height: | Size: 294 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M9 6L15 12L9 18" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||||
|
After Width: | Height: | Size: 294 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M9.17218 14.8284L12.0006 12M14.829 9.17157L12.0006 12M12.0006 12L9.17218 9.17157M12.0006 12L14.829 14.8284" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||||
|
After Width: | Height: | Size: 599 B |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M6.75827 17.2426L12.0009 12M17.2435 6.75736L12.0009 12M12.0009 12L6.75827 6.75736M12.0009 12L17.2435 17.2426" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||||
|
After Width: | Height: | Size: 387 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm8.25 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm-16.5 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"/>
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm8.25 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm-16.5 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 457 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.75 5.75a2 2 0 0 1 2-2h12.5a2 2 0 0 1 2 2v12.5a2 2 0 0 1-2 2H5.75a2 2 0 0 1-2-2V5.75Zm5-1.75v16"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 303 B |
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
|
|
||||||
"organizeImports": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"style": {
|
|
||||||
"noNonNullAssertion": "warn"
|
|
||||||
},
|
|
||||||
"correctness": {
|
|
||||||
"useExhaustiveDependencies": "off"
|
|
||||||
},
|
|
||||||
"a11y": {
|
|
||||||
"noSvgWithoutTitle": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "account"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
state = { path = "../state" }
|
||||||
|
|
||||||
|
gpui.workspace = true
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
use anyhow::Error;
|
||||||
|
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::client;
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
Account::set_global(cx.new(Account::new), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalAccount(Entity<Account>);
|
||||||
|
|
||||||
|
impl Global for GlobalAccount {}
|
||||||
|
|
||||||
|
pub struct Account {
|
||||||
|
/// Public Key of the account
|
||||||
|
public_key: Option<PublicKey>,
|
||||||
|
|
||||||
|
/// Event subscriptions
|
||||||
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
|
|
||||||
|
/// Tasks for asynchronous operations
|
||||||
|
_tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Account {
|
||||||
|
/// Retrieve the global account state
|
||||||
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
|
cx.global::<GlobalAccount>().0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the global account state exists
|
||||||
|
pub fn has_global(cx: &App) -> bool {
|
||||||
|
cx.has_global::<GlobalAccount>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the global account state
|
||||||
|
pub fn remove_global(cx: &mut App) {
|
||||||
|
cx.remove_global::<GlobalAccount>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the global account instance
|
||||||
|
fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||||
|
cx.set_global(GlobalAccount(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new account instance
|
||||||
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Listen for public key set
|
||||||
|
cx.observe_self(move |this, cx| {
|
||||||
|
if let Some(public_key) = this.public_key {
|
||||||
|
this.init(public_key, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
public_key: None,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
_tasks: smallvec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
|
||||||
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
|
let client = client();
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
// Construct a filter to get the user's metadata
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::Metadata)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Subscribe to the user metadata
|
||||||
|
client.subscribe(filter, Some(opts)).await?;
|
||||||
|
|
||||||
|
// Construct a filter to get the user's contact list
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::ContactList)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Subscribe to the user's contact list
|
||||||
|
client.subscribe(filter, Some(opts)).await?;
|
||||||
|
|
||||||
|
// Construct a filter to get the user's other metadata
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kinds(vec![
|
||||||
|
Kind::MuteList,
|
||||||
|
Kind::Bookmarks,
|
||||||
|
Kind::BookmarkSet,
|
||||||
|
Kind::SearchRelays,
|
||||||
|
Kind::BlockedRelays,
|
||||||
|
Kind::RelaySet,
|
||||||
|
Kind::Custom(10012),
|
||||||
|
])
|
||||||
|
.author(public_key)
|
||||||
|
.limit(24);
|
||||||
|
|
||||||
|
// Subscribe to the user's other metadata
|
||||||
|
client.subscribe(filter, Some(opts)).await?;
|
||||||
|
|
||||||
|
log::info!("Subscribed to user metadata");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
self._tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the public key of the account
|
||||||
|
pub fn set_public_key(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
|
||||||
|
self.public_key = Some(public_key);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the account entity has a public key
|
||||||
|
pub fn has_account(&self) -> bool {
|
||||||
|
self.public_key.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the public key of the account
|
||||||
|
pub fn public_key(&self) -> PublicKey {
|
||||||
|
// This method is only called when user is logged in, so unwrap safely
|
||||||
|
self.public_key.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "assets"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpui.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
rust-embed.workspace = true
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use gpui::{App, AssetSource, Result, SharedString};
|
||||||
|
use rust_embed::RustEmbed;
|
||||||
|
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "../../assets"]
|
||||||
|
#[include = "fonts/**/*"]
|
||||||
|
#[include = "brand/**/*"]
|
||||||
|
#[include = "icons/**/*"]
|
||||||
|
#[exclude = "*.DS_Store"]
|
||||||
|
pub struct Assets;
|
||||||
|
|
||||||
|
impl AssetSource for Assets {
|
||||||
|
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||||
|
Self::get(path)
|
||||||
|
.map(|f| Some(f.data))
|
||||||
|
.with_context(|| format!("loading asset at path {path:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
||||||
|
Ok(Self::iter()
|
||||||
|
.filter_map(|p| {
|
||||||
|
if p.starts_with(path) {
|
||||||
|
Some(p.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Assets {
|
||||||
|
/// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory.
|
||||||
|
pub fn load_fonts(&self, cx: &App) -> anyhow::Result<()> {
|
||||||
|
let font_paths = self.list("fonts")?;
|
||||||
|
let mut embedded_fonts = Vec::new();
|
||||||
|
for font_path in font_paths {
|
||||||
|
if font_path.ends_with(".ttf") {
|
||||||
|
let font_bytes = cx
|
||||||
|
.asset_source()
|
||||||
|
.load(&font_path)?
|
||||||
|
.expect("Assets should never return None");
|
||||||
|
embedded_fonts.push(font_bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.text_system().add_fonts(embedded_fonts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_test_fonts(&self, cx: &App) {
|
||||||
|
cx.text_system()
|
||||||
|
.add_fonts(vec![self
|
||||||
|
.load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()])
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "common"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpui.workspace = true
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
|
||||||
|
dirs = "5.0"
|
||||||
|
nostr = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/// Client (or application) name.
|
||||||
|
pub const CLIENT_NAME: &str = "Lume";
|
||||||
|
|
||||||
|
/// Application ID.
|
||||||
|
pub const APP_ID: &str = "su.reya.lume";
|
||||||
|
|
||||||
|
/// Bootstrap Relays.
|
||||||
|
pub const BOOTSTRAP_RELAYS: [&str; 5] = [
|
||||||
|
"wss://relay.damus.io",
|
||||||
|
"wss://relay.primal.net",
|
||||||
|
"wss://relay.nos.social",
|
||||||
|
"wss://user.kindpag.es",
|
||||||
|
"wss://purplepag.es",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Default relay for Nostr Connect
|
||||||
|
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
||||||
|
|
||||||
|
/// Default timeout for Nostr Connect (seconds)
|
||||||
|
pub const NOSTR_CONNECT_TIMEOUT: u64 = 30;
|
||||||
|
|
||||||
|
/// Default width of the sidebar.
|
||||||
|
pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub use constants::*;
|
||||||
|
pub use paths::*;
|
||||||
|
pub use utils::*;
|
||||||
|
|
||||||
|
mod constants;
|
||||||
|
mod paths;
|
||||||
|
mod utils;
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
/// Returns the path to the user's home directory.
|
||||||
|
pub fn home_dir() -> &'static PathBuf {
|
||||||
|
static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the configuration directory used by Lume.
|
||||||
|
pub fn config_dir() -> &'static PathBuf {
|
||||||
|
static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
CONFIG_DIR.get_or_init(|| {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
return dirs::config_dir()
|
||||||
|
.expect("failed to determine RoamingAppData directory")
|
||||||
|
.join("Lume");
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||||
|
return if let Ok(flatpak_xdg_config) = std::env::var("FLATPAK_XDG_CONFIG_HOME") {
|
||||||
|
flatpak_xdg_config.into()
|
||||||
|
} else {
|
||||||
|
dirs::config_dir().expect("failed to determine XDG_CONFIG_HOME directory")
|
||||||
|
}
|
||||||
|
.join("lume");
|
||||||
|
}
|
||||||
|
|
||||||
|
home_dir().join(".config").join("lume")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the support directory used by Lume.
|
||||||
|
pub fn support_dir() -> &'static PathBuf {
|
||||||
|
static SUPPORT_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
SUPPORT_DIR.get_or_init(|| {
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
return home_dir().join("Library/Application Support/Lume");
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||||
|
return if let Ok(flatpak_xdg_data) = std::env::var("FLATPAK_XDG_DATA_HOME") {
|
||||||
|
flatpak_xdg_data.into()
|
||||||
|
} else {
|
||||||
|
dirs::data_local_dir().expect("failed to determine XDG_DATA_HOME directory")
|
||||||
|
}
|
||||||
|
.join("lume");
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
return dirs::data_local_dir()
|
||||||
|
.expect("failed to determine LocalAppData directory")
|
||||||
|
.join("lume");
|
||||||
|
}
|
||||||
|
|
||||||
|
config_dir().clone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the `nostr` file.
|
||||||
|
pub fn nostr_file() -> &'static PathBuf {
|
||||||
|
static NOSTR_FILE: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
NOSTR_FILE.get_or_init(|| support_dir().join("nostr-db"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String {
|
||||||
|
let Ok(pubkey) = public_key.to_bech32();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{}:{}",
|
||||||
|
&pubkey[0..(len + 1)],
|
||||||
|
&pubkey[pubkey.len() - len..]
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[package]
|
||||||
|
name = "lume"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "lume"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
assets = { path = "../assets" }
|
||||||
|
state = { path = "../state" }
|
||||||
|
account = { path = "../account" }
|
||||||
|
person = { path = "../person" }
|
||||||
|
|
||||||
|
gpui.workspace = true
|
||||||
|
gpui-component.workspace = true
|
||||||
|
gpui_tokio.workspace = true
|
||||||
|
reqwest_client.workspace = true
|
||||||
|
|
||||||
|
nostr-connect.workspace = true
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
flume.workspace = true
|
||||||
|
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||||
|
qrcode = "0.14.1"
|
||||||
|
webbrowser = "1.0.6"
|
||||||