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 |
@@ -1,71 +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:
|
|
||||||
include:
|
|
||||||
- platform: "macos-latest" # for Arm based macs (M1 and above).
|
|
||||||
args: "--target aarch64-apple-darwin"
|
|
||||||
- platform: "macos-latest" # for Intel based macs.
|
|
||||||
args: "--target x86_64-apple-darwin"
|
|
||||||
- platform: "macos-latest" # for Intel & Arm based macs.
|
|
||||||
args: "--target universal-apple-darwin"
|
|
||||||
- platform: 'windows-latest'
|
|
||||||
args: '--target x86_64-pc-windows-msvc'
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "lts/*"
|
|
||||||
|
|
||||||
- name: Install PNPM
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8.x.x
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
|
||||||
|
|
||||||
- name: Install dependencies (ubuntu only)
|
|
||||||
if: matrix.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 frontend dependencies
|
|
||||||
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.args }}
|
|
||||||
includeDebug: false
|
|
||||||
@@ -1,27 +1,23 @@
|
|||||||
# Logs
|
# Generated by Cargo
|
||||||
logs
|
# will have compiled files and executables
|
||||||
*.log
|
debug/
|
||||||
npm-debug.log*
|
target/
|
||||||
yarn-debug.log*
|
dist/
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
# These are backup files generated by rustfmt
|
||||||
dist
|
**/*.rs.bk
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
.vscode/*
|
*.pdb
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
# 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
|
||||||
*.suo
|
# Added by goreleaser init:
|
||||||
*.ntvs*
|
.intentionally-empty-file.o
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
src/routes.gen.ts
|
|
||||||
src/commands.gen.ts
|
|
||||||
|
|||||||
@@ -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,33 +1 @@
|
|||||||
## Introduction
|
### Rebooting...
|
||||||
|
|
||||||
Lume is a Nostr client for macOS and Windows 11. 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.
|
|
||||||
|
|
||||||
## Installation and Usage
|
|
||||||
|
|
||||||
- *Microsoft Windows*: See the releases area for a file named something like Lume_VERSION_x64-setup.exe or Lume_VERSION_x64_en-US.msi
|
|
||||||
|
|
||||||
- *macOS*: See the releases area for a file named something like Lume_VERSION_PLATFORM.dmg
|
|
||||||
|
|
||||||
Lume only supported macOS and Windows 11. Linux user can consider using [Gossip client](https://github.com/mikedilger/gossip)
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
## Building from Source
|
|
||||||
|
|
||||||
See [Developing](docs/DEVELOPING.md)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Copyright (C) 2023-2024 Ren Amamiya & other Lume contributors
|
|
||||||
|
|
||||||
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/.
|
|
||||||
|
|||||||
@@ -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,28 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
|
|
||||||
"organizeImports": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"ignore": ["./src/routes.gen.ts", "./src/commands.gen.ts"]
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"style": {
|
|
||||||
"noNonNullAssertion": "warn",
|
|
||||||
"noUselessElse": "off"
|
|
||||||
},
|
|
||||||
"correctness": {
|
|
||||||
"useExhaustiveDependencies": "off"
|
|
||||||
},
|
|
||||||
"a11y": {
|
|
||||||
"noSvgWithoutTitle": "off"
|
|
||||||
},
|
|
||||||
"complexity": {
|
|
||||||
"noStaticOnlyClass": "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"
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use gpui::{actions, App};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
|
||||||
|
actions!(lume, [Quit, About, Open]);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LumeAuthUrlHandler;
|
||||||
|
|
||||||
|
impl AuthUrlHandler for LumeAuthUrlHandler {
|
||||||
|
#[allow(mismatched_lifetime_syntaxes)]
|
||||||
|
fn on_auth_url(&self, auth_url: Url) -> BoxedFuture<Result<()>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
log::info!("Received Auth URL: {auth_url}");
|
||||||
|
webbrowser::open(auth_url.as_str())?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_embedded_fonts(cx: &App) {
|
||||||
|
let asset_source = cx.asset_source();
|
||||||
|
let font_paths = asset_source.list("fonts").unwrap();
|
||||||
|
let embedded_fonts = Mutex::new(Vec::new());
|
||||||
|
let executor = cx.background_executor();
|
||||||
|
|
||||||
|
executor.block(executor.scoped(|scope| {
|
||||||
|
for font_path in &font_paths {
|
||||||
|
if !font_path.ends_with(".ttf") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.spawn(async {
|
||||||
|
let font_bytes = asset_source.load(font_path).unwrap().unwrap();
|
||||||
|
embedded_fonts.lock().unwrap().push(font_bytes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
cx.text_system()
|
||||||
|
.add_fonts(embedded_fonts.into_inner().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use assets::Assets;
|
||||||
|
use common::{APP_ID, BOOTSTRAP_RELAYS, CLIENT_NAME};
|
||||||
|
use gpui::{
|
||||||
|
point, px, size, AppContext, Application, Bounds, SharedString, TitlebarOptions,
|
||||||
|
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||||
|
};
|
||||||
|
use gpui_component::Root;
|
||||||
|
use state::client;
|
||||||
|
|
||||||
|
use crate::actions::load_embedded_fonts;
|
||||||
|
use crate::workspace::Workspace;
|
||||||
|
|
||||||
|
mod actions;
|
||||||
|
mod menus;
|
||||||
|
mod panels;
|
||||||
|
mod sidebar;
|
||||||
|
mod themes;
|
||||||
|
mod title_bar;
|
||||||
|
mod workspace;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Initialize logging
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
// Initialize the Application
|
||||||
|
let app = Application::new()
|
||||||
|
.with_assets(Assets)
|
||||||
|
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
|
||||||
|
|
||||||
|
// Initialize the nostr client
|
||||||
|
let client = client();
|
||||||
|
|
||||||
|
// Establish connection to all bootstrap relays
|
||||||
|
app.background_executor()
|
||||||
|
.spawn_with_priority(gpui::Priority::High, async move {
|
||||||
|
// Add bootstrap relays to the relay pool
|
||||||
|
for url in BOOTSTRAP_RELAYS {
|
||||||
|
client.add_relay(url).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to all added relays
|
||||||
|
client.connect().await;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
// Run application
|
||||||
|
app.run(move |cx| {
|
||||||
|
// Load embedded fonts in assets/fonts
|
||||||
|
load_embedded_fonts(cx);
|
||||||
|
|
||||||
|
// Set up the window bounds
|
||||||
|
let bounds = Bounds::centered(None, size(px(920.0), px(700.0)), cx);
|
||||||
|
|
||||||
|
// Set up the window options
|
||||||
|
let opts = WindowOptions {
|
||||||
|
window_background: WindowBackgroundAppearance::Opaque,
|
||||||
|
window_decorations: Some(WindowDecorations::Client),
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
kind: WindowKind::Normal,
|
||||||
|
app_id: Some(APP_ID.to_owned()),
|
||||||
|
titlebar: Some(TitlebarOptions {
|
||||||
|
title: Some(SharedString::new_static(CLIENT_NAME)),
|
||||||
|
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||||
|
appears_transparent: true,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open a window with default options
|
||||||
|
cx.open_window(opts, |window, cx| {
|
||||||
|
// Bring the app to the foreground
|
||||||
|
cx.activate(true);
|
||||||
|
|
||||||
|
// Initialize the tokio runtime
|
||||||
|
gpui_tokio::init(cx);
|
||||||
|
|
||||||
|
// Initialize components
|
||||||
|
gpui_component::init(cx);
|
||||||
|
|
||||||
|
// Initialize themes
|
||||||
|
themes::init(cx);
|
||||||
|
|
||||||
|
// Initialize account
|
||||||
|
account::init(cx);
|
||||||
|
|
||||||
|
// Initialize person registry
|
||||||
|
person::init(cx);
|
||||||
|
|
||||||
|
let workspace = cx.new(|cx| Workspace::new(window, cx));
|
||||||
|
cx.new(|cx| Root::new(workspace, window, cx))
|
||||||
|
})
|
||||||
|
.expect("Failed to open window. Please restart the application.");
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
use gpui::{App, Entity, Menu, MenuItem, SharedString};
|
||||||
|
use gpui_component::menu::AppMenuBar;
|
||||||
|
use gpui_component::{ActiveTheme as _, Theme, ThemeMode, ThemeRegistry};
|
||||||
|
|
||||||
|
use crate::actions::{About, Open, Quit};
|
||||||
|
use crate::themes::{SwitchTheme, SwitchThemeMode};
|
||||||
|
|
||||||
|
pub fn init(title: impl Into<SharedString>, cx: &mut App) -> Entity<AppMenuBar> {
|
||||||
|
let app_menu_bar = AppMenuBar::new(cx);
|
||||||
|
let title: SharedString = title.into();
|
||||||
|
|
||||||
|
update_app_menu(title.clone(), app_menu_bar.clone(), cx);
|
||||||
|
|
||||||
|
// Observe theme changes to update the menu to refresh the checked state
|
||||||
|
cx.observe_global::<Theme>({
|
||||||
|
let title = title.clone();
|
||||||
|
let app_menu_bar = app_menu_bar.clone();
|
||||||
|
|
||||||
|
move |cx| {
|
||||||
|
update_app_menu(title.clone(), app_menu_bar.clone(), cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
app_menu_bar
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_app_menu(title: impl Into<SharedString>, app_menu_bar: Entity<AppMenuBar>, cx: &mut App) {
|
||||||
|
let mode = cx.theme().mode;
|
||||||
|
|
||||||
|
cx.set_menus(vec![
|
||||||
|
Menu {
|
||||||
|
name: title.into(),
|
||||||
|
items: vec![
|
||||||
|
MenuItem::action("About", About),
|
||||||
|
MenuItem::Separator,
|
||||||
|
MenuItem::action("Open...", Open),
|
||||||
|
MenuItem::Separator,
|
||||||
|
MenuItem::Submenu(Menu {
|
||||||
|
name: "Appearance".into(),
|
||||||
|
items: vec![
|
||||||
|
MenuItem::action("Light", SwitchThemeMode(ThemeMode::Light))
|
||||||
|
.checked(!mode.is_dark()),
|
||||||
|
MenuItem::action("Dark", SwitchThemeMode(ThemeMode::Dark))
|
||||||
|
.checked(mode.is_dark()),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
theme_menu(cx),
|
||||||
|
MenuItem::Separator,
|
||||||
|
MenuItem::action("Quit", Quit),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Menu {
|
||||||
|
name: "Edit".into(),
|
||||||
|
items: vec![
|
||||||
|
MenuItem::action("Undo", gpui_component::input::Undo),
|
||||||
|
MenuItem::action("Redo", gpui_component::input::Redo),
|
||||||
|
MenuItem::separator(),
|
||||||
|
MenuItem::action("Cut", gpui_component::input::Cut),
|
||||||
|
MenuItem::action("Copy", gpui_component::input::Copy),
|
||||||
|
MenuItem::action("Paste", gpui_component::input::Paste),
|
||||||
|
MenuItem::separator(),
|
||||||
|
MenuItem::action("Delete", gpui_component::input::Delete),
|
||||||
|
MenuItem::action(
|
||||||
|
"Delete Previous Word",
|
||||||
|
gpui_component::input::DeleteToPreviousWordStart,
|
||||||
|
),
|
||||||
|
MenuItem::action(
|
||||||
|
"Delete Next Word",
|
||||||
|
gpui_component::input::DeleteToNextWordEnd,
|
||||||
|
),
|
||||||
|
MenuItem::separator(),
|
||||||
|
MenuItem::action("Find", gpui_component::input::Search),
|
||||||
|
MenuItem::separator(),
|
||||||
|
MenuItem::action("Select All", gpui_component::input::SelectAll),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Menu {
|
||||||
|
name: "Help".into(),
|
||||||
|
items: vec![MenuItem::action("Open Website", Open)],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
app_menu_bar.update(cx, |menu_bar, cx| {
|
||||||
|
menu_bar.reload(cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theme_menu(cx: &App) -> MenuItem {
|
||||||
|
let themes = ThemeRegistry::global(cx).sorted_themes();
|
||||||
|
let current_name = cx.theme().theme_name();
|
||||||
|
|
||||||
|
MenuItem::Submenu(Menu {
|
||||||
|
name: "Theme".into(),
|
||||||
|
items: themes
|
||||||
|
.iter()
|
||||||
|
.map(|theme| {
|
||||||
|
let checked = current_name == &theme.name;
|
||||||
|
MenuItem::action(theme.name.clone(), SwitchTheme(theme.name.clone()))
|
||||||
|
.checked(checked)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, img, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
|
||||||
|
ParentElement, Render, SharedString, Styled, Task, Window,
|
||||||
|
};
|
||||||
|
use gpui_component::dock::{Panel, PanelEvent};
|
||||||
|
use gpui_component::{h_flex, v_flex, ActiveTheme};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use person::PersonRegistry;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::client;
|
||||||
|
|
||||||
|
/// Feed
|
||||||
|
pub struct Feed {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
|
||||||
|
/// All notes that match the query
|
||||||
|
notes: Entity<Option<Events>>,
|
||||||
|
|
||||||
|
/// Public Key
|
||||||
|
public_key: Option<PublicKey>,
|
||||||
|
|
||||||
|
/// Relay Url
|
||||||
|
relay_url: Option<RelayUrl>,
|
||||||
|
|
||||||
|
/// Async operations
|
||||||
|
_tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Feed {
|
||||||
|
pub fn new(
|
||||||
|
public_key: Option<PublicKey>,
|
||||||
|
relay_url: Option<RelayUrl>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let notes = cx.new(|_| None);
|
||||||
|
let async_url = relay_url.clone();
|
||||||
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Load newsfeed in the background
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let task: Task<Result<Events, Error>> = cx.background_spawn(async move {
|
||||||
|
let client = client();
|
||||||
|
|
||||||
|
let mut filter = Filter::new()
|
||||||
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
|
.limit(20);
|
||||||
|
|
||||||
|
if let Some(author) = public_key {
|
||||||
|
filter = filter.author(author);
|
||||||
|
};
|
||||||
|
|
||||||
|
let events = match async_url {
|
||||||
|
Some(url) => {
|
||||||
|
client
|
||||||
|
.fetch_events_from(vec![url], filter, Duration::from_secs(5))
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
None => client.fetch_events(filter, Duration::from_secs(5)).await?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(events)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Ok(events) = task.await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.notes.update(cx, |this, cx| {
|
||||||
|
*this = Some(events);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
notes,
|
||||||
|
public_key,
|
||||||
|
relay_url,
|
||||||
|
_tasks: tasks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for Feed {
|
||||||
|
fn panel_name(&self) -> &'static str {
|
||||||
|
"Feed"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.when_some(self.public_key.as_ref(), |this, public_key| {
|
||||||
|
let person = PersonRegistry::global(cx);
|
||||||
|
let profile = person.read(cx).get(public_key, cx);
|
||||||
|
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.when_some(profile.metadata().picture.as_ref(), |this, url| {
|
||||||
|
this.child(img(SharedString::from(url)).size_4().rounded_full())
|
||||||
|
})
|
||||||
|
.child(SharedString::from(profile.name())),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(self.relay_url.as_ref(), |this, url| {
|
||||||
|
this.child(SharedString::from(url.to_string()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<PanelEvent> for Feed {}
|
||||||
|
|
||||||
|
impl Focusable for Feed {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Feed {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let person = PersonRegistry::global(cx);
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.p_2()
|
||||||
|
.gap_3()
|
||||||
|
.when_some(self.notes.read(cx).as_ref(), |this, notes| {
|
||||||
|
this.children({
|
||||||
|
let mut items = Vec::with_capacity(notes.len());
|
||||||
|
|
||||||
|
for note in notes.iter() {
|
||||||
|
let profile = person.read(cx).get(¬e.pubkey, cx);
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.when_some(
|
||||||
|
profile.metadata().picture.as_ref(),
|
||||||
|
|this, url| {
|
||||||
|
this.child(
|
||||||
|
img(SharedString::from(url))
|
||||||
|
.size_6()
|
||||||
|
.rounded_full(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.child(SharedString::from(profile.name())),
|
||||||
|
)
|
||||||
|
.child(SharedString::from(
|
||||||
|
note.created_at.to_human_datetime(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.child(SharedString::from(note.content.clone())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use account::Account;
|
||||||
|
use anyhow::{anyhow, Error};
|
||||||
|
use common::NOSTR_CONNECT_TIMEOUT;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, relative, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
|
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
||||||
|
};
|
||||||
|
use gpui_component::button::{Button, ButtonVariants};
|
||||||
|
use gpui_component::dock::{Panel, PanelEvent};
|
||||||
|
use gpui_component::input::{Input, InputEvent, InputState};
|
||||||
|
use gpui_component::notification::Notification;
|
||||||
|
use gpui_component::{v_flex, ActiveTheme, Disableable, StyledExt, WindowExt};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::client;
|
||||||
|
|
||||||
|
use crate::actions::LumeAuthUrlHandler;
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
||||||
|
cx.new(|cx| Login::new(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Login {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
|
||||||
|
/// Input for nsec for bunker uri
|
||||||
|
key_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Input for decryption password when available
|
||||||
|
pass_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Timeout countdown
|
||||||
|
countdown: Entity<Option<u64>>,
|
||||||
|
|
||||||
|
/// Whether password is required
|
||||||
|
require_password: bool,
|
||||||
|
|
||||||
|
/// Whether logging in is in progress
|
||||||
|
logging_in: bool,
|
||||||
|
|
||||||
|
/// Event subscriptions
|
||||||
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Login {
|
||||||
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let key_input = cx.new(|cx| InputState::new(window, cx));
|
||||||
|
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
|
||||||
|
let error = cx.new(|_| None);
|
||||||
|
let countdown = cx.new(|_| None);
|
||||||
|
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Subscribe to key input events and process login when the user presses enter
|
||||||
|
cx.subscribe_in(&key_input, window, |this, input, event, window, cx| {
|
||||||
|
match event {
|
||||||
|
InputEvent::PressEnter { .. } => {
|
||||||
|
this.login(window, cx);
|
||||||
|
}
|
||||||
|
InputEvent::Change => {
|
||||||
|
if input.read(cx).value().starts_with("ncryptsec1") {
|
||||||
|
this.require_password = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
key_input,
|
||||||
|
pass_input,
|
||||||
|
error,
|
||||||
|
countdown,
|
||||||
|
logging_in: false,
|
||||||
|
require_password: false,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.logging_in {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent duplicate login requests
|
||||||
|
self.set_logging_in(true, cx);
|
||||||
|
|
||||||
|
let value = self.key_input.read(cx).value();
|
||||||
|
let password = self.pass_input.read(cx).value();
|
||||||
|
|
||||||
|
if value.starts_with("bunker://") {
|
||||||
|
self.login_with_bunker(&value, window, cx);
|
||||||
|
} else if value.starts_with("ncryptsec1") {
|
||||||
|
self.login_with_password(&value, &password, cx);
|
||||||
|
} else if value.starts_with("nsec1") {
|
||||||
|
if let Ok(secret) = SecretKey::parse(&value) {
|
||||||
|
let keys = Keys::new(secret);
|
||||||
|
self.login_with_keys(keys, cx);
|
||||||
|
} else {
|
||||||
|
self.set_error("Invalid", cx);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.set_error("Invalid", cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login_with_bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Ok(uri) = NostrConnectUri::parse(content) else {
|
||||||
|
self.set_error("Bunker URI is not valid", cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let app_keys = Keys::generate();
|
||||||
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
|
let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
||||||
|
|
||||||
|
// Handle auth url with the default browser
|
||||||
|
signer.auth_url_handler(LumeAuthUrlHandler);
|
||||||
|
|
||||||
|
// Start countdown
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
for i in (0..=NOSTR_CONNECT_TIMEOUT).rev() {
|
||||||
|
if i == 0 {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_countdown(None, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
} else {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_countdown(Some(i), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
// Handle connection
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result: Result<PublicKey, Error> = cx
|
||||||
|
.background_executor()
|
||||||
|
.await_on_background(async move {
|
||||||
|
let client = client();
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
// Update the signer
|
||||||
|
client.set_signer(signer).await;
|
||||||
|
|
||||||
|
// Return the URI for storing the connection
|
||||||
|
Ok(public_key)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
this.update_in(cx, |_this, window, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(public_key) => {
|
||||||
|
let account = Account::global(cx);
|
||||||
|
account.update(cx, |this, cx| {
|
||||||
|
this.set_public_key(public_key, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login_with_password(&mut self, content: &str, pwd: &str, cx: &mut Context<Self>) {
|
||||||
|
if pwd.is_empty() {
|
||||||
|
self.set_error("Password is required", cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(enc) = EncryptedSecretKey::from_bech32(content) else {
|
||||||
|
self.set_error("Secret Key is invalid", cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let password = pwd.to_owned();
|
||||||
|
|
||||||
|
// Decrypt in the background to ensure it doesn't block the UI
|
||||||
|
let task = cx.background_spawn(async move {
|
||||||
|
if let Ok(content) = enc.decrypt(&password) {
|
||||||
|
Ok(Keys::new(content))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Invalid password"))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let result = task.await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(keys) => {
|
||||||
|
this.login_with_keys(keys, cx);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.set_error(e.to_string(), cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
|
||||||
|
let public_key = keys.public_key();
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor()
|
||||||
|
.await_on_background(async move {
|
||||||
|
let client = client();
|
||||||
|
client.set_signer(keys).await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
let account = Account::global(cx);
|
||||||
|
account.update(cx, |this, cx| {
|
||||||
|
this.set_public_key(public_key, cx);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
|
// Reset the log in state
|
||||||
|
self.set_logging_in(false, cx);
|
||||||
|
|
||||||
|
// Reset the countdown
|
||||||
|
self.set_countdown(None, cx);
|
||||||
|
|
||||||
|
// Update error message
|
||||||
|
self.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(message.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the error message after 3 secs
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.error.update(cx, |this, cx| {
|
||||||
|
*this = None;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
|
self.logging_in = status;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_countdown(&mut self, i: Option<u64>, cx: &mut Context<Self>) {
|
||||||
|
self.countdown.update(cx, |this, cx| {
|
||||||
|
*this = i;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for Login {
|
||||||
|
fn panel_name(&self) -> &'static str {
|
||||||
|
"Login"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
SharedString::from("Login")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<PanelEvent> for Login {}
|
||||||
|
|
||||||
|
impl Focusable for Login {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Login {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.relative()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_96()
|
||||||
|
.gap_10()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_center()
|
||||||
|
.text_lg()
|
||||||
|
.font_semibold()
|
||||||
|
.line_height(relative(1.3))
|
||||||
|
.child(SharedString::from("Continue with Private Key or Bunker")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_3()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child("nsec or bunker://")
|
||||||
|
.child(Input::new(&self.key_input)),
|
||||||
|
)
|
||||||
|
.when(self.require_password, |this| {
|
||||||
|
this.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child("Password:")
|
||||||
|
.child(Input::new(&self.pass_input)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Button::new("login")
|
||||||
|
.label("Continue")
|
||||||
|
.primary()
|
||||||
|
.loading(self.logging_in)
|
||||||
|
.disabled(self.logging_in)
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.login(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
||||||
|
let msg = format!(
|
||||||
|
"Approve connection request from your signer in {} seconds",
|
||||||
|
i
|
||||||
|
);
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(SharedString::from(msg)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().danger_foreground)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod feed;
|
||||||
|
pub mod login;
|
||||||
|
pub mod new_account;
|
||||||
|
pub mod onboarding;
|
||||||
|
pub mod startup;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use gpui::{
|
||||||
|
div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
|
||||||
|
ParentElement, Render, SharedString, Window,
|
||||||
|
};
|
||||||
|
use gpui_component::dock::{Panel, PanelEvent};
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
||||||
|
cx.new(|cx| NewAccount::new(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NewAccount {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewAccount {
|
||||||
|
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for NewAccount {
|
||||||
|
fn panel_name(&self) -> &'static str {
|
||||||
|
"New Account"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
SharedString::from("New Account")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<PanelEvent> for NewAccount {}
|
||||||
|
|
||||||
|
impl Focusable for NewAccount {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for NewAccount {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div().child("New Account")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use account::Account;
|
||||||
|
use anyhow::Error;
|
||||||
|
use common::{CLIENT_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, img, px, relative, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
|
Image, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||||
|
StatefulInteractiveElement, Styled, Task, Window,
|
||||||
|
};
|
||||||
|
use gpui_component::button::{Button, ButtonVariants};
|
||||||
|
use gpui_component::divider::Divider;
|
||||||
|
use gpui_component::dock::{Panel, PanelEvent};
|
||||||
|
use gpui_component::notification::Notification;
|
||||||
|
use gpui_component::{h_flex, v_flex, ActiveTheme, Icon, IconName, Sizable, StyledExt, WindowExt};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
use qrcode::render::svg;
|
||||||
|
use qrcode::QrCode;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::client;
|
||||||
|
|
||||||
|
use crate::workspace::OpenPanel;
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||||
|
cx.new(|cx| Onboarding::new(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum NostrConnectApp {
|
||||||
|
Nsec(String),
|
||||||
|
Amber(String),
|
||||||
|
Aegis(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NostrConnectApp {
|
||||||
|
pub fn all() -> Vec<Self> {
|
||||||
|
vec![
|
||||||
|
NostrConnectApp::Nsec("https://nsec.app".to_string()),
|
||||||
|
NostrConnectApp::Amber("https://github.com/greenart7c3/Amber".to_string()),
|
||||||
|
NostrConnectApp::Aegis("https://github.com/ZharlieW/Aegis".to_string()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn url(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Nsec(url) | Self::Amber(url) | Self::Aegis(url) => url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> String {
|
||||||
|
match self {
|
||||||
|
NostrConnectApp::Nsec(_) => "nsec.app (Desktop)".into(),
|
||||||
|
NostrConnectApp::Amber(_) => "Amber (Android)".into(),
|
||||||
|
NostrConnectApp::Aegis(_) => "Aegis (iOS)".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Onboarding
|
||||||
|
pub struct Onboarding {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// App keys for communicating with the remote signer
|
||||||
|
app_keys: Keys,
|
||||||
|
|
||||||
|
/// QR code for logging in with Nostr Connect
|
||||||
|
qr_code: Option<Arc<Image>>,
|
||||||
|
|
||||||
|
/// Background tasks
|
||||||
|
_tasks: SmallVec<[Task<()>; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Onboarding {
|
||||||
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let app_keys = Keys::generate();
|
||||||
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
|
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||||
|
let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME);
|
||||||
|
let qr_code = Self::generate_qr(uri.to_string().as_ref());
|
||||||
|
|
||||||
|
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||||
|
//
|
||||||
|
// Direct connection initiated by the client
|
||||||
|
let signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
||||||
|
|
||||||
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Wait for nostr connect
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result: Result<PublicKey, Error> = cx
|
||||||
|
.background_executor()
|
||||||
|
.await_on_background(async move {
|
||||||
|
let client = client();
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
// Update the signer
|
||||||
|
client.set_signer(signer).await;
|
||||||
|
|
||||||
|
// Return the URI for storing the connection
|
||||||
|
Ok(public_key)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
this.update_in(cx, |_this, window, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(public_key) => {
|
||||||
|
let account = Account::global(cx);
|
||||||
|
account.update(cx, |this, cx| {
|
||||||
|
this.set_public_key(public_key, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
qr_code,
|
||||||
|
app_keys,
|
||||||
|
_tasks: tasks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_qr(value: &str) -> Option<Arc<Image>> {
|
||||||
|
let code = QrCode::new(value).unwrap();
|
||||||
|
let svg = code
|
||||||
|
.render()
|
||||||
|
.min_dimensions(256, 256)
|
||||||
|
.dark_color(svg::Color("#000000"))
|
||||||
|
.light_color(svg::Color("#FFFFFF"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Some(Arc::new(Image::from_bytes(
|
||||||
|
gpui::ImageFormat::Svg,
|
||||||
|
svg.into_bytes(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_app<T>(&self, ix: usize, label: T, url: &str, cx: &Context<Self>) -> impl IntoElement
|
||||||
|
where
|
||||||
|
T: Into<SharedString>,
|
||||||
|
{
|
||||||
|
div()
|
||||||
|
.id(ix)
|
||||||
|
.flex_1()
|
||||||
|
.rounded_md()
|
||||||
|
.py_0p5()
|
||||||
|
.px_2()
|
||||||
|
.bg(cx.theme().list)
|
||||||
|
.child(label.into())
|
||||||
|
.on_click({
|
||||||
|
let url = url.to_owned();
|
||||||
|
move |_e, _window, cx| {
|
||||||
|
cx.open_url(&url);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_apps(&self, cx: &Context<Self>) -> impl IntoIterator<Item = impl IntoElement> {
|
||||||
|
let all_apps = NostrConnectApp::all();
|
||||||
|
let mut items = Vec::with_capacity(all_apps.len());
|
||||||
|
|
||||||
|
for (ix, item) in all_apps.into_iter().enumerate() {
|
||||||
|
items.push(self.render_app(ix, item.as_str(), item.url(), cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for Onboarding {
|
||||||
|
fn panel_name(&self) -> &'static str {
|
||||||
|
"Onboarding"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
SharedString::from("Onboarding")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<PanelEvent> for Onboarding {}
|
||||||
|
|
||||||
|
impl EventEmitter<OpenPanel> for Onboarding {}
|
||||||
|
|
||||||
|
impl Focusable for Onboarding {
|
||||||
|
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Onboarding {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.size_full()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.flex_1()
|
||||||
|
.h_full()
|
||||||
|
.gap_10()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.items_center()
|
||||||
|
.text_center()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xl()
|
||||||
|
.font_semibold()
|
||||||
|
.line_height(relative(1.3))
|
||||||
|
.child(SharedString::from("Welcome to Lume")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(SharedString::from("An unambitious Nostr client")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_80()
|
||||||
|
.gap_3()
|
||||||
|
.child(
|
||||||
|
Button::new("continue")
|
||||||
|
.icon(Icon::new(IconName::ArrowRight))
|
||||||
|
.label("Start Browsing")
|
||||||
|
.primary()
|
||||||
|
.on_click(cx.listener(move |_this, _ev, _window, cx| {
|
||||||
|
cx.emit(OpenPanel::Signup);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Divider::horizontal()
|
||||||
|
.label("Already have an account? Continue with")
|
||||||
|
.text_xs(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("key")
|
||||||
|
.label("Secret Key or Bunker")
|
||||||
|
.large()
|
||||||
|
.link()
|
||||||
|
.small()
|
||||||
|
.on_click(cx.listener(move |_this, _ev, _window, cx| {
|
||||||
|
cx.emit(OpenPanel::Login);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.flex_1()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.gap_5()
|
||||||
|
.bg(cx.theme().muted)
|
||||||
|
.when_some(self.qr_code.as_ref(), |this, qr| {
|
||||||
|
this.child(
|
||||||
|
img(qr.clone())
|
||||||
|
.size(px(256.))
|
||||||
|
.rounded_xl()
|
||||||
|
.shadow_lg()
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().primary_active),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.text_center()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.font_semibold()
|
||||||
|
.line_height(relative(1.3))
|
||||||
|
.child(SharedString::from("Continue with Nostr Connect")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(SharedString::from(
|
||||||
|
"Use Nostr Connect apps to scan the code",
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.mt_2()
|
||||||
|
.gap_1()
|
||||||
|
.text_xs()
|
||||||
|
.justify_center()
|
||||||
|
.children(self.render_apps(cx)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use gpui::{
|
||||||
|
div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
|
||||||
|
ParentElement, Render, SharedString, Window,
|
||||||
|
};
|
||||||
|
use gpui_component::dock::{Panel, PanelEvent};
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Startup> {
|
||||||
|
cx.new(|cx| Startup::new(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Startup {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Startup {
|
||||||
|
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for Startup {
|
||||||
|
fn panel_name(&self) -> &'static str {
|
||||||
|
"Startup"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
SharedString::from("Welcome")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<PanelEvent> for Startup {}
|
||||||
|
|
||||||
|
impl Focusable for Startup {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Startup {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div().child("Startup")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
use common::BOOTSTRAP_RELAYS;
|
||||||
|
use gpui::{
|
||||||
|
div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||||
|
StatefulInteractiveElement, Styled, Window,
|
||||||
|
};
|
||||||
|
use gpui_component::dock::{Panel, PanelEvent};
|
||||||
|
use gpui_component::scroll::ScrollableElement;
|
||||||
|
use gpui_component::{h_flex, v_flex, ActiveTheme, StyledExt};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use person::PersonRegistry;
|
||||||
|
|
||||||
|
use crate::workspace::OpenPanel;
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
||||||
|
cx.new(|cx| Sidebar::new(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Sidebar {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sidebar {
|
||||||
|
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for Sidebar {
|
||||||
|
fn panel_name(&self) -> &'static str {
|
||||||
|
"Sidebar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<PanelEvent> for Sidebar {}
|
||||||
|
impl EventEmitter<OpenPanel> for Sidebar {}
|
||||||
|
|
||||||
|
impl Focusable for Sidebar {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Sidebar {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let person = PersonRegistry::global(cx);
|
||||||
|
let contacts = Vec::new();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.id("sidebar-wrapper")
|
||||||
|
.size_full()
|
||||||
|
.relative()
|
||||||
|
.px_2()
|
||||||
|
.overflow_y_scrollbar()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.mt_4()
|
||||||
|
.mb_2()
|
||||||
|
.font_semibold()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child("Relays"),
|
||||||
|
)
|
||||||
|
.child(v_flex().gap_2().children({
|
||||||
|
let mut items = Vec::with_capacity(BOOTSTRAP_RELAYS.len());
|
||||||
|
|
||||||
|
for (ix, relay) in BOOTSTRAP_RELAYS.into_iter().enumerate() {
|
||||||
|
items.push(
|
||||||
|
h_flex()
|
||||||
|
.id(SharedString::from(format!("relay-{ix}")))
|
||||||
|
.h_7()
|
||||||
|
.px_2()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.hover(|this| this.bg(cx.theme().list_hover))
|
||||||
|
.child(div().text_sm().child(SharedString::from(relay)))
|
||||||
|
.on_click(cx.listener(move |_this, _ev, _window, cx| {
|
||||||
|
if let Ok(url) = RelayUrl::parse(relay) {
|
||||||
|
cx.emit(OpenPanel::Relay(url));
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.mt_4()
|
||||||
|
.mb_2()
|
||||||
|
.font_semibold()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child("Contacts"),
|
||||||
|
)
|
||||||
|
.child(v_flex().gap_2().children({
|
||||||
|
let mut items = Vec::with_capacity(contacts.len());
|
||||||
|
|
||||||
|
for (ix, contact) in contacts.iter().enumerate() {
|
||||||
|
let profile = person.read(cx).get(contact, cx);
|
||||||
|
let name = SharedString::from(profile.name());
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
h_flex()
|
||||||
|
.id(SharedString::from(format!("contact-{ix}")))
|
||||||
|
.h_7()
|
||||||
|
.px_2()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.hover(|this| this.bg(cx.theme().list_hover))
|
||||||
|
.child(div().text_sm().child(name.clone()))
|
||||||
|
.on_click(cx.listener(move |_this, _ev, _window, cx| {
|
||||||
|
cx.emit(OpenPanel::PublicKey(profile.public_key()));
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use common::home_dir;
|
||||||
|
use gpui::{Action, App, SharedString};
|
||||||
|
use gpui_component::scroll::ScrollbarShow;
|
||||||
|
use gpui_component::{ActiveTheme, Theme, ThemeMode, ThemeRegistry};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Action, Clone, PartialEq)]
|
||||||
|
#[action(namespace = themes, no_json)]
|
||||||
|
pub(crate) struct SwitchTheme(pub(crate) SharedString);
|
||||||
|
|
||||||
|
#[derive(Action, Clone, PartialEq)]
|
||||||
|
#[action(namespace = themes, no_json)]
|
||||||
|
pub(crate) struct SwitchThemeMode(pub(crate) ThemeMode);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct State {
|
||||||
|
theme: SharedString,
|
||||||
|
scrollbar_show: Option<ScrollbarShow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
theme: "Default Light".into(),
|
||||||
|
scrollbar_show: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
// Load last theme state
|
||||||
|
let path = home_dir().join("state.json");
|
||||||
|
let json = std::fs::read_to_string(path).unwrap_or_default();
|
||||||
|
let state = serde_json::from_str::<State>(&json).unwrap_or_default();
|
||||||
|
|
||||||
|
if let Err(err) = ThemeRegistry::watch_dir(PathBuf::from("./themes"), cx, move |cx| {
|
||||||
|
if let Some(theme) = ThemeRegistry::global(cx)
|
||||||
|
.themes()
|
||||||
|
.get(&state.theme)
|
||||||
|
.cloned()
|
||||||
|
{
|
||||||
|
Theme::global_mut(cx).apply_config(&theme);
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
log::error!("Failed to watch themes directory: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(scrollbar_show) = state.scrollbar_show {
|
||||||
|
Theme::global_mut(cx).scrollbar_show = scrollbar_show;
|
||||||
|
}
|
||||||
|
cx.refresh_windows();
|
||||||
|
|
||||||
|
cx.observe_global::<Theme>(|cx| {
|
||||||
|
let state = State {
|
||||||
|
theme: cx.theme().theme_name().clone(),
|
||||||
|
scrollbar_show: Some(cx.theme().scrollbar_show),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(json) = serde_json::to_string_pretty(&state) {
|
||||||
|
let path = home_dir().join("state.json");
|
||||||
|
// Ignore write errors - if STATE_FILE doesn't exist or can't be written, do nothing
|
||||||
|
let _ = std::fs::write(path, json);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.on_action(|switch: &SwitchTheme, cx| {
|
||||||
|
let theme_name = switch.0.clone();
|
||||||
|
if let Some(theme_config) = ThemeRegistry::global(cx).themes().get(&theme_name).cloned() {
|
||||||
|
Theme::global_mut(cx).apply_config(&theme_config);
|
||||||
|
}
|
||||||
|
cx.refresh_windows();
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.on_action(|switch: &SwitchThemeMode, cx| {
|
||||||
|
let mode = switch.0;
|
||||||
|
Theme::change(mode, None, cx);
|
||||||
|
cx.refresh_windows();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
div, AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, MouseButton,
|
||||||
|
ParentElement as _, Render, SharedString, Styled as _, Subscription, Window,
|
||||||
|
};
|
||||||
|
use gpui_component::menu::AppMenuBar;
|
||||||
|
use gpui_component::TitleBar;
|
||||||
|
|
||||||
|
use crate::menus;
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub struct AppTitleBar {
|
||||||
|
/// The app menu bar
|
||||||
|
app_menu_bar: Entity<AppMenuBar>,
|
||||||
|
|
||||||
|
/// Child elements
|
||||||
|
child: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||||
|
|
||||||
|
/// Event subscriptions
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppTitleBar {
|
||||||
|
pub fn new(
|
||||||
|
title: impl Into<SharedString>,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let app_menu_bar = menus::init(title, cx);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
app_menu_bar,
|
||||||
|
child: Rc::new(|_, _| div().into_any_element()),
|
||||||
|
_subscriptions: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn child<F, E>(mut self, f: F) -> Self
|
||||||
|
where
|
||||||
|
E: IntoElement,
|
||||||
|
F: Fn(&mut Window, &mut App) -> E + 'static,
|
||||||
|
{
|
||||||
|
self.child = Rc::new(move |window, cx| f(window, cx).into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AppTitleBar {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
TitleBar::new()
|
||||||
|
// left side
|
||||||
|
.child(div().flex().items_center().child(self.app_menu_bar.clone()))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_end()
|
||||||
|
.px_2()
|
||||||
|
.gap_2()
|
||||||
|
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||||
|
.child((self.child.clone())(window, cx)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use account::Account;
|
||||||
|
use common::{CLIENT_NAME, DEFAULT_SIDEBAR_WIDTH};
|
||||||
|
use gpui::{
|
||||||
|
div, px, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||||
|
Render, Styled, Subscription, Window,
|
||||||
|
};
|
||||||
|
use gpui_component::dock::{DockArea, DockItem, DockPlacement, PanelStyle};
|
||||||
|
use gpui_component::{v_flex, Root, Theme};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
|
use crate::panels::feed::Feed;
|
||||||
|
use crate::panels::{login, new_account, onboarding, startup};
|
||||||
|
use crate::sidebar;
|
||||||
|
use crate::title_bar::AppTitleBar;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum OpenPanel {
|
||||||
|
PublicKey(PublicKey),
|
||||||
|
Relay(RelayUrl),
|
||||||
|
Signup,
|
||||||
|
Login,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Workspace {
|
||||||
|
/// The dock area for the workspace.
|
||||||
|
dock: Entity<DockArea>,
|
||||||
|
|
||||||
|
/// App's title bar.
|
||||||
|
title_bar: Entity<AppTitleBar>,
|
||||||
|
|
||||||
|
/// Event subscriptions
|
||||||
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Workspace {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let account = Account::global(cx);
|
||||||
|
let onboarding = Arc::new(onboarding::init(window, cx));
|
||||||
|
|
||||||
|
// App's title bar
|
||||||
|
let title_bar = cx.new(|cx| AppTitleBar::new(CLIENT_NAME, window, cx));
|
||||||
|
|
||||||
|
// Dock area for the workspace.
|
||||||
|
let dock = cx.new(|cx| {
|
||||||
|
let mut this = DockArea::new("dock", None, window, cx).panel_style(PanelStyle::TabBar);
|
||||||
|
this.set_center(DockItem::panel(onboarding.clone()), window, cx);
|
||||||
|
this
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
// Observe account entity changes
|
||||||
|
subscriptions.push(
|
||||||
|
cx.observe_in(&account, window, move |this, state, window, cx| {
|
||||||
|
if state.read(cx).has_account() {
|
||||||
|
this.init_app_layout(window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Observe onboarding panel events
|
||||||
|
subscriptions.push(cx.subscribe_in(
|
||||||
|
&onboarding,
|
||||||
|
window,
|
||||||
|
|this, _sidebar, event: &OpenPanel, window, cx| {
|
||||||
|
match event {
|
||||||
|
OpenPanel::Login => {
|
||||||
|
let view = login::init(window, cx);
|
||||||
|
|
||||||
|
this.dock.update(cx, |this, cx| {
|
||||||
|
this.set_center(DockItem::panel(Arc::new(view)), window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
OpenPanel::Signup => {
|
||||||
|
let view = new_account::init(window, cx);
|
||||||
|
|
||||||
|
this.dock.update(cx, |this, cx| {
|
||||||
|
this.set_center(DockItem::panel(Arc::new(view)), window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Automatically sync theme with system appearance
|
||||||
|
subscriptions.push(window.observe_window_appearance(|window, cx| {
|
||||||
|
Theme::sync_system_appearance(Some(window), cx);
|
||||||
|
}));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
dock,
|
||||||
|
title_bar,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_app_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let weak_dock = self.dock.downgrade();
|
||||||
|
|
||||||
|
let sidebar = sidebar::init(window, cx);
|
||||||
|
let startup = Arc::new(startup::init(window, cx));
|
||||||
|
|
||||||
|
self._subscriptions.push(cx.subscribe_in(
|
||||||
|
&sidebar,
|
||||||
|
window,
|
||||||
|
|this, _sidebar, event: &OpenPanel, window, cx| {
|
||||||
|
match event {
|
||||||
|
OpenPanel::PublicKey(public_key) => {
|
||||||
|
let view = cx.new(|cx| Feed::new(Some(*public_key), None, window, cx));
|
||||||
|
|
||||||
|
this.dock.update(cx, |this, cx| {
|
||||||
|
this.add_panel(Arc::new(view), DockPlacement::Center, None, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
OpenPanel::Relay(relay) => {
|
||||||
|
let view = cx.new(|cx| Feed::new(None, Some(relay.to_owned()), window, cx));
|
||||||
|
|
||||||
|
this.dock.update(cx, |this, cx| {
|
||||||
|
this.add_panel(Arc::new(view), DockPlacement::Center, None, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Construct left dock (sidebar)
|
||||||
|
let left = DockItem::panel(Arc::new(sidebar));
|
||||||
|
|
||||||
|
// Construct center dock
|
||||||
|
let center = DockItem::split_with_sizes(
|
||||||
|
Axis::Vertical,
|
||||||
|
vec![DockItem::tabs(vec![startup], &weak_dock, window, cx)],
|
||||||
|
vec![None],
|
||||||
|
&weak_dock,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update dock layout
|
||||||
|
self.dock.update(cx, |this, cx| {
|
||||||
|
this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx);
|
||||||
|
this.set_center(center, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Workspace {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let dialog_layer = Root::render_dialog_layer(window, cx);
|
||||||
|
let sheet_layer = Root::render_sheet_layer(window, cx);
|
||||||
|
let notification_layer = Root::render_notification_layer(window, cx);
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id("root")
|
||||||
|
.relative()
|
||||||
|
.size_full()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
// Title bar
|
||||||
|
.child(self.title_bar.clone())
|
||||||
|
// Dock
|
||||||
|
.child(self.dock.clone()),
|
||||||
|
)
|
||||||
|
// Notifications
|
||||||
|
.children(notification_layer)
|
||||||
|
// Sheets
|
||||||
|
.children(sheet_layer)
|
||||||
|
// Modals
|
||||||
|
.children(dialog_layer)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "note"
|
||||||
|
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
|
||||||
|
flume.workspace = true
|
||||||
|
log.workspace = true
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::client;
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
NoteRegistry::set_global(cx.new(NoteRegistry::new), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalNoteRegistry(Entity<NoteRegistry>);
|
||||||
|
|
||||||
|
impl Global for GlobalNoteRegistry {}
|
||||||
|
|
||||||
|
/// Note Registry
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NoteRegistry {
|
||||||
|
/// Collection of all notes
|
||||||
|
pub notes: HashMap<EventId, Event>,
|
||||||
|
|
||||||
|
/// Tasks for asynchronous operations
|
||||||
|
_tasks: SmallVec<[Task<()>; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteRegistry {
|
||||||
|
/// Retrieve the global note registry state
|
||||||
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
|
cx.global::<GlobalNoteRegistry>().0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the global note registry instance
|
||||||
|
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||||
|
cx.set_global(GlobalNoteRegistry(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new note registry instance
|
||||||
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
// Channel for communication between Nostr and GPUI
|
||||||
|
let (tx, rx) = flume::bounded::<Event>(2048);
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Handle nostr notifications
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = client();
|
||||||
|
let mut notifications = client.notifications();
|
||||||
|
let mut processed_events: HashSet<EventId> = HashSet::default();
|
||||||
|
|
||||||
|
while let Ok(notification) = notifications.recv().await {
|
||||||
|
let RelayPoolNotification::Message { message, .. } = notification else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let RelayMessage::Event { event, .. } = message {
|
||||||
|
// Skip if already processed
|
||||||
|
if !processed_events.insert(event.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.kind == Kind::TextNote || event.kind == Kind::Repost {
|
||||||
|
tx.send_async(event.into_owned()).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Update GPUI state
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
while let Ok(event) = rx.recv_async().await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.notes.insert(event.id, event);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
notes: HashMap::new(),
|
||||||
|
_tasks: tasks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, id: &EventId) -> Option<&Event> {
|
||||||
|
self.notes.get(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "person"
|
||||||
|
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
|
||||||
|
flume.workspace = true
|
||||||
|
log.workspace = true
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use gpui::{App, AppContext, AsyncApp, Context, Entity, Global, Task};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::client;
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalPersonRegistry(Entity<PersonRegistry>);
|
||||||
|
|
||||||
|
impl Global for GlobalPersonRegistry {}
|
||||||
|
|
||||||
|
/// Person Registry
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PersonRegistry {
|
||||||
|
/// Collection of all profiels
|
||||||
|
pub persons: HashMap<PublicKey, Entity<Profile>>,
|
||||||
|
|
||||||
|
/// Tasks for asynchronous operations
|
||||||
|
_tasks: SmallVec<[Task<()>; 3]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersonRegistry {
|
||||||
|
/// Retrieve the global person registry state
|
||||||
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
|
cx.global::<GlobalPersonRegistry>().0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the global person registry instance
|
||||||
|
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||||
|
cx.set_global(GlobalPersonRegistry(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new person registry instance
|
||||||
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
// Channel for communication between Nostr and GPUI
|
||||||
|
let (tx, rx) = flume::bounded::<Profile>(1024);
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Load all user profiles from the database
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let task = Self::init(cx);
|
||||||
|
|
||||||
|
match task.await {
|
||||||
|
Ok(profiles) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.bulk_insert(profiles, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to load user profiles from database: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Handle nostr notifications
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = client();
|
||||||
|
let mut notifications = client.notifications();
|
||||||
|
let mut processed_events: HashSet<EventId> = HashSet::default();
|
||||||
|
|
||||||
|
while let Ok(notification) = notifications.recv().await {
|
||||||
|
let RelayPoolNotification::Message { message, .. } = notification else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let RelayMessage::Event { event, .. } = message {
|
||||||
|
// Skip if already processed
|
||||||
|
if !processed_events.insert(event.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.kind == Kind::Metadata {
|
||||||
|
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
||||||
|
let profile = Profile::new(event.pubkey, metadata);
|
||||||
|
|
||||||
|
tx.send_async(profile).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Update GPUI state
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
while let Ok(profile) = rx.recv_async().await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.insert_or_update(&profile, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
persons: HashMap::new(),
|
||||||
|
_tasks: tasks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load all user profiles from the database
|
||||||
|
fn init(cx: &AsyncApp) -> Task<Result<Vec<Profile>, Error>> {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = client();
|
||||||
|
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
||||||
|
let events = client.database().query(filter).await?;
|
||||||
|
|
||||||
|
let mut profiles = vec![];
|
||||||
|
|
||||||
|
for event in events.into_iter() {
|
||||||
|
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||||
|
let profile = Profile::new(event.pubkey, metadata);
|
||||||
|
profiles.push(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert batch of persons
|
||||||
|
fn bulk_insert(&mut self, profiles: Vec<Profile>, cx: &mut Context<Self>) {
|
||||||
|
for profile in profiles.into_iter() {
|
||||||
|
self.persons
|
||||||
|
.insert(profile.public_key(), cx.new(|_| profile));
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert or update a person
|
||||||
|
pub fn insert_or_update(&mut self, profile: &Profile, cx: &mut App) {
|
||||||
|
let public_key = profile.public_key();
|
||||||
|
|
||||||
|
if let Some(person) = self.persons.get(&public_key) {
|
||||||
|
person.update(cx, |this, cx| {
|
||||||
|
*this = profile.to_owned();
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.persons
|
||||||
|
.insert(public_key, cx.new(|_| profile.to_owned()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get person
|
||||||
|
pub fn get(&self, public_key: &PublicKey, cx: &App) -> Profile {
|
||||||
|
self.persons
|
||||||
|
.get(public_key)
|
||||||
|
.map(|e| e.read(cx).clone())
|
||||||
|
.unwrap_or(Profile::new(public_key.to_owned(), Metadata::default()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "state"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
nostr-lmdb.workspace = true
|
||||||
|
nostr-gossip-memory.workspace = true
|
||||||
|
|
||||||
|
gpui.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
rustls = "0.23.23"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum StateEvent {
|
||||||
|
ReceivedContactList,
|
||||||
|
ReceivedProfile(Box<Profile>),
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use common::config_dir;
|
||||||
|
pub use event::*;
|
||||||
|
use nostr_gossip_memory::prelude::*;
|
||||||
|
use nostr_lmdb::NostrLmdb;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
mod event;
|
||||||
|
|
||||||
|
static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new();
|
||||||
|
|
||||||
|
pub fn client() -> &'static Client {
|
||||||
|
NOSTR_CLIENT.get_or_init(|| {
|
||||||
|
// rustls uses the `aws_lc_rs` provider by default
|
||||||
|
// This only errors if the default provider has already
|
||||||
|
// been installed. We can ignore this `Result`.
|
||||||
|
rustls::crypto::aws_lc_rs::default_provider()
|
||||||
|
.install_default()
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
// Construct the nostr client options
|
||||||
|
let opts = ClientOptions::new()
|
||||||
|
.automatic_authentication(false)
|
||||||
|
.verify_subscriptions(false)
|
||||||
|
.sleep_when_idle(SleepWhenIdle::Enabled {
|
||||||
|
timeout: Duration::from_secs(600),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Construct the lmdb
|
||||||
|
let lmdb = smol::block_on(async move {
|
||||||
|
let path = config_dir().join("nostr");
|
||||||
|
NostrLmdb::open(path)
|
||||||
|
.await
|
||||||
|
.expect("Failed to initialize database")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Construct the nostr client
|
||||||
|
ClientBuilder::default()
|
||||||
|
.database(lmdb)
|
||||||
|
.gossip(NostrGossipMemory::unbounded())
|
||||||
|
.opts(opts)
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Developing
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Node.js >= 20: https://nodejs.org/
|
|
||||||
|
|
||||||
- Rust: https://rustup.rs/
|
|
||||||
|
|
||||||
- PNPM: https://pnpm.io/
|
|
||||||
|
|
||||||
- Tauri: https://tauri.app/guides/prerequisites/
|
|
||||||
|
|
||||||
## Build from source
|
|
||||||
|
|
||||||
Clone project
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/lumehq/lume.git && cd lume
|
|
||||||
```
|
|
||||||
|
|
||||||
Install required dependencies
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Run dev
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm tauri dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Build
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm tauri build
|
|
||||||
```
|
|
||||||
|
After Width: | Height: | Size: 1.3 MiB |
@@ -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 font-sans text-black antialiased dark:text-white"
|
|
||||||
>
|
|
||||||
<div id="root" class="h-full w-full"></div>
|
|
||||||
<script type="module" src="/src/app.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "lume",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"tauri": "tauri"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@getalby/bitcoin-connect-react": "^3.6.2",
|
|
||||||
"@phosphor-icons/react": "^2.1.7",
|
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
|
||||||
"@radix-ui/react-popover": "^1.1.2",
|
|
||||||
"@radix-ui/react-progress": "^1.1.0",
|
|
||||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
|
||||||
"@radix-ui/react-switch": "^1.1.1",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
|
||||||
"@tanstack/query-broadcast-client-experimental": "^5.59.16",
|
|
||||||
"@tanstack/query-persist-client-core": "^5.59.16",
|
|
||||||
"@tanstack/react-query": "^5.59.16",
|
|
||||||
"@tanstack/react-router": "^1.77.5",
|
|
||||||
"@tauri-apps/api": "^2.0.3",
|
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
|
||||||
"@tauri-apps/plugin-fs": "^2.0.1",
|
|
||||||
"@tauri-apps/plugin-http": "^2.0.1",
|
|
||||||
"@tauri-apps/plugin-os": "^2.0.0",
|
|
||||||
"@tauri-apps/plugin-process": "^2.0.0",
|
|
||||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
|
||||||
"@tauri-apps/plugin-store": "github:tauri-apps/tauri-plugin-store#a564510",
|
|
||||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
|
||||||
"@tauri-apps/plugin-upload": "^2.0.0",
|
|
||||||
"@tauri-apps/plugin-window-state": "^2.0.0",
|
|
||||||
"bitcoin-units": "^1.0.0",
|
|
||||||
"dayjs": "^1.11.13",
|
|
||||||
"embla-carousel-react": "^8.3.0",
|
|
||||||
"i18next": "^23.16.4",
|
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
|
||||||
"light-bolt11-decoder": "^3.2.0",
|
|
||||||
"minidenticons": "^4.2.1",
|
|
||||||
"nanoid": "^5.0.7",
|
|
||||||
"nostr-tools": "^2.9.4",
|
|
||||||
"react": "19.0.0-rc-cae764ce-20241025",
|
|
||||||
"react-currency-input-field": "^3.8.0",
|
|
||||||
"react-dom": "19.0.0-rc-cae764ce-20241025",
|
|
||||||
"react-hook-form": "^7.53.1",
|
|
||||||
"react-i18next": "^15.1.0",
|
|
||||||
"react-string-replace": "^1.1.1",
|
|
||||||
"rich-textarea": "^0.26.3",
|
|
||||||
"use-debounce": "^10.0.4",
|
|
||||||
"virtua": "^0.34.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@biomejs/biome": "^1.9.4",
|
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
|
||||||
"@tanstack/router-devtools": "^1.77.5",
|
|
||||||
"@tanstack/router-plugin": "^1.76.4",
|
|
||||||
"@tauri-apps/cli": "^2.0.4",
|
|
||||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
|
||||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
|
||||||
"autoprefixer": "^10.4.20",
|
|
||||||
"babel-plugin-react-compiler": "0.0.0-experimental-b4db8c3-20241001",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"postcss": "^8.4.47",
|
|
||||||
"tailwind-gradient-mask-image": "^1.2.0",
|
|
||||||
"tailwind-merge": "^2.5.4",
|
|
||||||
"tailwind-scrollbar": "^3.1.0",
|
|
||||||
"tailwindcss": "^3.4.14",
|
|
||||||
"tailwindcss-content-visibility": "^1.0.0",
|
|
||||||
"typescript": "^5.6.3",
|
|
||||||
"vite": "^5.4.10",
|
|
||||||
"vite-tsconfig-paths": "^5.0.1"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"@types/react": "npm:types-react@rc",
|
|
||||||
"@types/react-dom": "npm:types-react-dom@rc"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 191 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="32" height="36" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M28.2711 21.2958C27.1084 21.2958 26.0749 21.8372 25.4037 22.6797L21.8702 20.6405C22.2577 19.8106 22.4755 18.8846 22.4755 17.908C22.4755 16.9314 22.2577 16.0053 21.8702 15.1755L25.3404 13.1742C26.0091 14.0648 27.0704 14.6442 28.2711 14.6442C30.2949 14.6442 31.9363 13.0047 31.9363 10.9831C31.9363 8.96158 30.2949 7.32208 28.2711 7.32208C26.2472 7.32208 24.6058 8.96158 24.6058 10.9831C24.6058 11.4006 24.6793 11.8003 24.8084 12.1748L21.3028 14.1963C20.2338 12.6732 18.5241 11.6333 16.5635 11.4638V7.274C18.3189 7.00076 19.6639 5.49029 19.6639 3.66104C19.6639 1.6395 18.0225 0 15.9987 0C13.9748 0 12.3334 1.6395 12.3334 3.66104C12.3334 5.49029 13.6784 7.00329 15.4338 7.274V11.4638C13.4733 11.6333 11.7635 12.6732 10.6946 14.1963L7.1889 12.1748C7.31808 11.8003 7.39154 11.4006 7.39154 10.9831C7.39154 8.96158 5.75015 7.32208 3.72629 7.32208C1.70242 7.32208 0.0610352 8.96158 0.0610352 10.9831C0.0610352 13.0047 1.70242 14.6442 3.72629 14.6442C4.92693 14.6442 5.98825 14.0648 6.65697 13.1742L10.1272 15.1755C9.73963 16.0053 9.52179 16.9314 9.52179 17.908C9.52179 18.8846 9.73963 19.8106 10.1272 20.643L6.59364 22.6822C5.9224 21.8397 4.88893 21.2983 3.72629 21.2983C1.70242 21.2983 0.0610352 22.9378 0.0610352 24.9593C0.0610352 26.9809 1.70242 28.6204 3.72629 28.6204C5.75015 28.6204 7.39154 26.9809 7.39154 24.9593C7.39154 24.5039 7.30542 24.0687 7.1509 23.6639L10.6946 21.6196C11.3329 22.5279 12.1992 23.2667 13.2098 23.7499V32.3497C13.2098 32.9721 13.5569 33.8551 13.9799 34.3131L15.2286 35.6565C15.6516 36.1145 16.3457 36.1145 16.7712 35.6565L18.02 34.3131C18.443 33.8551 18.79 32.9721 18.79 32.3497V23.7499C19.8007 23.2667 20.667 22.5304 21.3053 21.6196L24.849 23.6639C24.697 24.0662 24.6083 24.5014 24.6083 24.9593C24.6083 26.9809 26.2497 28.6204 28.2736 28.6204C30.2975 28.6204 31.9388 26.9809 31.9388 24.9593C31.9388 22.9378 30.2975 21.2983 28.2736 21.2983L28.2711 21.2958Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.90"
|
||||||
|
profile = "minimal"
|
||||||
|
components = ["rustfmt", "clippy"]
|
||||||
|
targets = [
|
||||||
|
"x86_64-apple-darwin",
|
||||||
|
"aarch64-apple-darwin",
|
||||||
|
"x86_64-unknown-linux-gnu",
|
||||||
|
"x86_64-pc-windows-msvc",
|
||||||
|
]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
tab_spaces = 4
|
||||||
|
newline_style = "Auto"
|
||||||
|
reorder_imports = true
|
||||||
|
reorder_modules = true
|
||||||
|
reorder_impl_items = true
|
||||||
|
indent_style = "Block"
|
||||||
|
normalize_comments = false
|
||||||
|
imports_granularity = "Module"
|
||||||
|
group_imports = "StdExternalCrate"
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -xeuo pipefail
|
||||||
|
|
||||||
|
# if root or if sudo/unavailable, define an empty variable
|
||||||
|
if [ "$(id -u)" -eq 0 ]
|
||||||
|
then maysudo=''
|
||||||
|
else maysudo="$(command -v sudo || command -v doas || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
function finalize {
|
||||||
|
# after packages install (curl, etc), get the rust toolchain
|
||||||
|
which rustup > /dev/null 2>&1 || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
# verify the mold situation
|
||||||
|
if ! command -v mold >/dev/null 2>&1; then
|
||||||
|
echo "Warning: Mold binaries are unavailable on your system." >&2
|
||||||
|
echo " Builds will be slower without mold. Try: script/install-mold" >&2
|
||||||
|
fi
|
||||||
|
echo "Finished installing Linux dependencies with script/linux"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ubuntu, Debian, Mint, Kali, Pop!_OS, Raspbian, etc.
|
||||||
|
apt=$(command -v apt-get || true)
|
||||||
|
if [[ -n $apt ]]; then
|
||||||
|
deps=(
|
||||||
|
gcc
|
||||||
|
g++
|
||||||
|
libasound2-dev
|
||||||
|
libfontconfig-dev
|
||||||
|
libwayland-dev
|
||||||
|
libx11-xcb-dev
|
||||||
|
libxkbcommon-x11-dev
|
||||||
|
libssl-dev
|
||||||
|
libzstd-dev
|
||||||
|
libvulkan1
|
||||||
|
libgit2-dev
|
||||||
|
make
|
||||||
|
cmake
|
||||||
|
clang
|
||||||
|
jq
|
||||||
|
git
|
||||||
|
curl
|
||||||
|
gettext-base
|
||||||
|
elfutils
|
||||||
|
libsqlite3-dev
|
||||||
|
musl-tools
|
||||||
|
musl-dev
|
||||||
|
build-essential
|
||||||
|
)
|
||||||
|
if (grep -qP 'PRETTY_NAME="(Linux Mint 22|.+24\.(04|10))' /etc/os-release); then
|
||||||
|
deps+=( mold libstdc++-14-dev )
|
||||||
|
elif (grep -qP 'PRETTY_NAME="((Debian|Raspbian).+12|Linux Mint 21|.+22\.04)' /etc/os-release); then
|
||||||
|
deps+=( mold libstdc++-12-dev )
|
||||||
|
elif (grep -qP 'PRETTY_NAME="((Debian|Raspbian).+11|Linux Mint 20|.+20\.04)' /etc/os-release); then
|
||||||
|
deps+=( libstdc++-10-dev )
|
||||||
|
fi
|
||||||
|
|
||||||
|
$maysudo "$apt" update
|
||||||
|
$maysudo "$apt" install -y "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fedora, CentOS, RHEL, Alma, Amazon 2023, Oracle, etc.
|
||||||
|
dnf=$(command -v dnf || true)
|
||||||
|
# Old Redhat (yum only): Amazon Linux 2, Oracle Linux 7, etc.
|
||||||
|
yum=$(command -v yum || true)
|
||||||
|
|
||||||
|
if [[ -n $dnf ]] || [[ -n $yum ]]; then
|
||||||
|
pkg_cmd="${dnf:-${yum}}"
|
||||||
|
deps=(
|
||||||
|
musl-gcc
|
||||||
|
gcc
|
||||||
|
clang
|
||||||
|
cmake
|
||||||
|
alsa-lib-devel
|
||||||
|
fontconfig-devel
|
||||||
|
wayland-devel
|
||||||
|
libxcb-devel
|
||||||
|
libxkbcommon-x11-devel
|
||||||
|
openssl-devel
|
||||||
|
libzstd-devel
|
||||||
|
vulkan-loader
|
||||||
|
sqlite-devel
|
||||||
|
jq
|
||||||
|
git
|
||||||
|
tar
|
||||||
|
)
|
||||||
|
# perl used for building openssl-sys crate. See: https://docs.rs/openssl/latest/openssl/
|
||||||
|
if grep -qP '^ID="?(fedora)' /etc/os-release; then
|
||||||
|
deps+=(
|
||||||
|
perl-FindBin
|
||||||
|
perl-IPC-Cmd
|
||||||
|
perl-File-Compare
|
||||||
|
perl-File-Copy
|
||||||
|
mold
|
||||||
|
)
|
||||||
|
elif grep -qP '^ID="?(rhel|rocky|alma|centos|ol)' /etc/os-release; then
|
||||||
|
deps+=( perl-interpreter )
|
||||||
|
fi
|
||||||
|
|
||||||
|
# gcc-c++ is g++ on RHEL8 and 8.x clones
|
||||||
|
if grep -qP '^ID="?(rhel|rocky|alma|centos|ol)' /etc/os-release \
|
||||||
|
&& grep -qP '^VERSION_ID="?(8)' /etc/os-release; then
|
||||||
|
deps+=( gcc-c++ )
|
||||||
|
else
|
||||||
|
deps+=( g++ )
|
||||||
|
fi
|
||||||
|
|
||||||
|
# libxkbcommon-x11-devel is in a non-default repo on RHEL 8.x/9.x (except on AmazonLinux)
|
||||||
|
if grep -qP '^VERSION_ID="?(8|9)' /etc/os-release && grep -qP '^ID="?(rhel|rocky|centos|alma|ol)' /etc/os-release; then
|
||||||
|
$maysudo dnf install -y 'dnf-command(config-manager)'
|
||||||
|
if grep -qP '^PRETTY_NAME="(AlmaLinux 8|Rocky Linux 8)' /etc/os-release; then
|
||||||
|
$maysudo dnf config-manager --set-enabled powertools
|
||||||
|
elif grep -qP '^PRETTY_NAME="((AlmaLinux|Rocky|CentOS Stream) 9|Red Hat.+(8|9))' /etc/os-release; then
|
||||||
|
$maysudo dnf config-manager --set-enabled crb
|
||||||
|
elif grep -qP '^PRETTY_NAME="Oracle Linux Server 8' /etc/os-release; then
|
||||||
|
$maysudo dnf config-manager --set-enabled ol8_codeready_builder
|
||||||
|
elif grep -qP '^PRETTY_NAME="Oracle Linux Server 9' /etc/os-release; then
|
||||||
|
$maysudo dnf config-manager --set-enabled ol9_codeready_builder
|
||||||
|
else
|
||||||
|
echo "Unexpected distro" && grep 'PRETTY_NAME' /etc/os-release && exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
$maysudo "$pkg_cmd" install -y "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# openSUSE
|
||||||
|
# https://software.opensuse.org/
|
||||||
|
zyp=$(command -v zypper || true)
|
||||||
|
if [[ -n $zyp ]]; then
|
||||||
|
deps=(
|
||||||
|
alsa-devel
|
||||||
|
clang
|
||||||
|
cmake
|
||||||
|
fontconfig-devel
|
||||||
|
gcc
|
||||||
|
gcc-c++
|
||||||
|
git
|
||||||
|
gzip
|
||||||
|
jq
|
||||||
|
libvulkan1
|
||||||
|
libxcb-devel
|
||||||
|
libxkbcommon-devel
|
||||||
|
libxkbcommon-x11-devel
|
||||||
|
libzstd-devel
|
||||||
|
make
|
||||||
|
mold
|
||||||
|
openssl-devel
|
||||||
|
sqlite3-devel
|
||||||
|
tar
|
||||||
|
wayland-devel
|
||||||
|
xcb-util-devel
|
||||||
|
)
|
||||||
|
$maysudo "$zyp" install -y "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Arch, Manjaro, etc.
|
||||||
|
# https://archlinux.org/packages
|
||||||
|
pacman=$(command -v pacman || true)
|
||||||
|
if [[ -n $pacman ]]; then
|
||||||
|
deps=(
|
||||||
|
gcc
|
||||||
|
clang
|
||||||
|
musl
|
||||||
|
cmake
|
||||||
|
alsa-lib
|
||||||
|
fontconfig
|
||||||
|
wayland
|
||||||
|
libgit2
|
||||||
|
libxcb
|
||||||
|
libxkbcommon-x11
|
||||||
|
openssl
|
||||||
|
zstd
|
||||||
|
pkgconf
|
||||||
|
mold
|
||||||
|
sqlite
|
||||||
|
jq
|
||||||
|
git
|
||||||
|
)
|
||||||
|
$maysudo "$pacman" -Syu --needed --noconfirm "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Void
|
||||||
|
# https://voidlinux.org/packages/
|
||||||
|
xbps=$(command -v xbps-install || true)
|
||||||
|
if [[ -n $xbps ]]; then
|
||||||
|
deps=(
|
||||||
|
gettext-devel
|
||||||
|
clang
|
||||||
|
cmake
|
||||||
|
jq
|
||||||
|
elfutils-devel
|
||||||
|
gcc
|
||||||
|
alsa-lib-devel
|
||||||
|
fontconfig-devel
|
||||||
|
libxcb-devel
|
||||||
|
libxkbcommon-devel
|
||||||
|
libzstd-devel
|
||||||
|
openssl-devel
|
||||||
|
wayland-devel
|
||||||
|
vulkan-loader
|
||||||
|
mold
|
||||||
|
)
|
||||||
|
$maysudo "$xbps" -Syu "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gentoo
|
||||||
|
# https://packages.gentoo.org/
|
||||||
|
emerge=$(command -v emerge || true)
|
||||||
|
if [[ -n $emerge ]]; then
|
||||||
|
deps=(
|
||||||
|
app-arch/zstd
|
||||||
|
app-misc/jq
|
||||||
|
dev-libs/openssl
|
||||||
|
dev-libs/wayland
|
||||||
|
dev-util/cmake
|
||||||
|
media-libs/alsa-lib
|
||||||
|
media-libs/fontconfig
|
||||||
|
media-libs/vulkan-loader
|
||||||
|
x11-libs/libxcb
|
||||||
|
x11-libs/libxkbcommon
|
||||||
|
sys-devel/mold
|
||||||
|
)
|
||||||
|
$maysudo "$emerge" -u "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Unsupported Linux distribution in script/linux"
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -xeuo pipefail
|
||||||
|
|
||||||
|
# if root or if sudo/unavailable, define an empty variable
|
||||||
|
if [ "$(id -u)" -eq 0 ]
|
||||||
|
then maysudo=''
|
||||||
|
else maysudo="$(command -v sudo || command -v doas || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
function finalize {
|
||||||
|
# after packages install (curl, etc), get the rust toolchain
|
||||||
|
which rustup > /dev/null 2>&1 || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
# verify the mold situation
|
||||||
|
if ! command -v mold >/dev/null 2>&1; then
|
||||||
|
echo "Warning: Mold binaries are unavailable on your system." >&2
|
||||||
|
echo " Builds will be slower without mold. Try: script/install-mold" >&2
|
||||||
|
fi
|
||||||
|
echo "Finished installing Linux dependencies with script/linux"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ubuntu, Debian, Mint, Kali, Pop!_OS, Raspbian, etc.
|
||||||
|
apt=$(command -v apt-get || true)
|
||||||
|
if [[ -n $apt ]]; then
|
||||||
|
deps=(
|
||||||
|
gcc
|
||||||
|
g++
|
||||||
|
libasound2-dev
|
||||||
|
libfontconfig-dev
|
||||||
|
libwayland-dev
|
||||||
|
libx11-xcb-dev
|
||||||
|
libxkbcommon-x11-dev
|
||||||
|
libssl-dev
|
||||||
|
libzstd-dev
|
||||||
|
libvulkan1
|
||||||
|
libgit2-dev
|
||||||
|
libx11-dev
|
||||||
|
make
|
||||||
|
cmake
|
||||||
|
clang
|
||||||
|
jq
|
||||||
|
git
|
||||||
|
curl
|
||||||
|
gettext-base
|
||||||
|
elfutils
|
||||||
|
musl-tools
|
||||||
|
musl-dev
|
||||||
|
build-essential
|
||||||
|
)
|
||||||
|
if (grep -qP 'PRETTY_NAME="(Linux Mint 22|.+24\.(04|10))' /etc/os-release); then
|
||||||
|
deps+=( mold libstdc++-14-dev )
|
||||||
|
elif (grep -qP 'PRETTY_NAME="((Debian|Raspbian).+12|Linux Mint 21|.+22\.04)' /etc/os-release); then
|
||||||
|
deps+=( mold libstdc++-12-dev )
|
||||||
|
elif (grep -qP 'PRETTY_NAME="((Debian|Raspbian).+11|Linux Mint 20|.+20\.04)' /etc/os-release); then
|
||||||
|
deps+=( libstdc++-10-dev )
|
||||||
|
fi
|
||||||
|
|
||||||
|
$maysudo "$apt" update
|
||||||
|
$maysudo "$apt" install -y "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fedora, CentOS, RHEL, Alma, Amazon 2023, Oracle, etc.
|
||||||
|
dnf=$(command -v dnf || true)
|
||||||
|
# Old Redhat (yum only): Amazon Linux 2, Oracle Linux 7, etc.
|
||||||
|
yum=$(command -v yum || true)
|
||||||
|
|
||||||
|
if [[ -n $dnf ]] || [[ -n $yum ]]; then
|
||||||
|
pkg_cmd="${dnf:-${yum}}"
|
||||||
|
deps=(
|
||||||
|
musl-gcc
|
||||||
|
gcc
|
||||||
|
clang
|
||||||
|
cmake
|
||||||
|
alsa-lib-devel
|
||||||
|
fontconfig-devel
|
||||||
|
wayland-devel
|
||||||
|
libxcb-devel
|
||||||
|
libxkbcommon-x11-devel
|
||||||
|
openssl-devel
|
||||||
|
libzstd-devel
|
||||||
|
vulkan-loader
|
||||||
|
jq
|
||||||
|
git
|
||||||
|
tar
|
||||||
|
)
|
||||||
|
# perl used for building openssl-sys crate. See: https://docs.rs/openssl/latest/openssl/
|
||||||
|
if grep -qP '^ID="?(fedora)' /etc/os-release; then
|
||||||
|
deps+=(
|
||||||
|
perl-FindBin
|
||||||
|
perl-IPC-Cmd
|
||||||
|
perl-File-Compare
|
||||||
|
perl-File-Copy
|
||||||
|
mold
|
||||||
|
)
|
||||||
|
elif grep -qP '^ID="?(rhel|rocky|alma|centos|ol)' /etc/os-release; then
|
||||||
|
deps+=( perl-interpreter )
|
||||||
|
fi
|
||||||
|
|
||||||
|
# gcc-c++ is g++ on RHEL8 and 8.x clones
|
||||||
|
if grep -qP '^ID="?(rhel|rocky|alma|centos|ol)' /etc/os-release \
|
||||||
|
&& grep -qP '^VERSION_ID="?(8)' /etc/os-release; then
|
||||||
|
deps+=( gcc-c++ )
|
||||||
|
else
|
||||||
|
deps+=( g++ )
|
||||||
|
fi
|
||||||
|
|
||||||
|
# libxkbcommon-x11-devel is in a non-default repo on RHEL 8.x/9.x (except on AmazonLinux)
|
||||||
|
if grep -qP '^VERSION_ID="?(8|9)' /etc/os-release && grep -qP '^ID="?(rhel|rocky|centos|alma|ol)' /etc/os-release; then
|
||||||
|
$maysudo dnf install -y 'dnf-command(config-manager)'
|
||||||
|
if grep -qP '^PRETTY_NAME="(AlmaLinux 8|Rocky Linux 8)' /etc/os-release; then
|
||||||
|
$maysudo dnf config-manager --set-enabled powertools
|
||||||
|
elif grep -qP '^PRETTY_NAME="((AlmaLinux|Rocky|CentOS Stream) 9|Red Hat.+(8|9))' /etc/os-release; then
|
||||||
|
$maysudo dnf config-manager --set-enabled crb
|
||||||
|
elif grep -qP '^PRETTY_NAME="Oracle Linux Server 8' /etc/os-release; then
|
||||||
|
$maysudo dnf config-manager --set-enabled ol8_codeready_builder
|
||||||
|
elif grep -qP '^PRETTY_NAME="Oracle Linux Server 9' /etc/os-release; then
|
||||||
|
$maysudo dnf config-manager --set-enabled ol9_codeready_builder
|
||||||
|
else
|
||||||
|
echo "Unexpected distro" && grep 'PRETTY_NAME' /etc/os-release && exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
$maysudo "$pkg_cmd" install -y "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# openSUSE
|
||||||
|
# https://software.opensuse.org/
|
||||||
|
zyp=$(command -v zypper || true)
|
||||||
|
if [[ -n $zyp ]]; then
|
||||||
|
deps=(
|
||||||
|
alsa-devel
|
||||||
|
clang
|
||||||
|
cmake
|
||||||
|
fontconfig-devel
|
||||||
|
gcc
|
||||||
|
gcc-c++
|
||||||
|
git
|
||||||
|
gzip
|
||||||
|
jq
|
||||||
|
libvulkan1
|
||||||
|
libxcb-devel
|
||||||
|
libxkbcommon-devel
|
||||||
|
libxkbcommon-x11-devel
|
||||||
|
libzstd-devel
|
||||||
|
make
|
||||||
|
mold
|
||||||
|
openssl-devel
|
||||||
|
tar
|
||||||
|
wayland-devel
|
||||||
|
xcb-util-devel
|
||||||
|
)
|
||||||
|
$maysudo "$zyp" install -y "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Arch, Manjaro, etc.
|
||||||
|
# https://archlinux.org/packages
|
||||||
|
pacman=$(command -v pacman || true)
|
||||||
|
if [[ -n $pacman ]]; then
|
||||||
|
deps=(
|
||||||
|
gcc
|
||||||
|
clang
|
||||||
|
musl
|
||||||
|
cmake
|
||||||
|
alsa-lib
|
||||||
|
fontconfig
|
||||||
|
wayland
|
||||||
|
libgit2
|
||||||
|
libxcb
|
||||||
|
libxkbcommon-x11
|
||||||
|
openssl
|
||||||
|
zstd
|
||||||
|
pkgconf
|
||||||
|
mold
|
||||||
|
jq
|
||||||
|
git
|
||||||
|
)
|
||||||
|
$maysudo "$pacman" -Syu --needed --noconfirm "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Void
|
||||||
|
# https://voidlinux.org/packages/
|
||||||
|
xbps=$(command -v xbps-install || true)
|
||||||
|
if [[ -n $xbps ]]; then
|
||||||
|
deps=(
|
||||||
|
gettext-devel
|
||||||
|
clang
|
||||||
|
cmake
|
||||||
|
jq
|
||||||
|
elfutils-devel
|
||||||
|
gcc
|
||||||
|
alsa-lib-devel
|
||||||
|
fontconfig-devel
|
||||||
|
libxcb-devel
|
||||||
|
libxkbcommon-devel
|
||||||
|
libzstd-devel
|
||||||
|
openssl-devel
|
||||||
|
wayland-devel
|
||||||
|
vulkan-loader
|
||||||
|
mold
|
||||||
|
)
|
||||||
|
$maysudo "$xbps" -Syu "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gentoo
|
||||||
|
# https://packages.gentoo.org/
|
||||||
|
emerge=$(command -v emerge || true)
|
||||||
|
if [[ -n $emerge ]]; then
|
||||||
|
deps=(
|
||||||
|
app-arch/zstd
|
||||||
|
app-misc/jq
|
||||||
|
dev-libs/openssl
|
||||||
|
dev-libs/wayland
|
||||||
|
dev-util/cmake
|
||||||
|
media-libs/alsa-lib
|
||||||
|
media-libs/fontconfig
|
||||||
|
media-libs/vulkan-loader
|
||||||
|
x11-libs/libxcb
|
||||||
|
x11-libs/libxkbcommon
|
||||||
|
sys-devel/mold
|
||||||
|
)
|
||||||
|
$maysudo "$emerge" -u "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Unsupported Linux distribution in script/linux"
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -xeuo pipefail
|
||||||
|
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=1
|
||||||
|
|
||||||
|
# if root or if sudo/unavailable, define an empty variable
|
||||||
|
if [ "$(id -u)" -eq 0 ]
|
||||||
|
then maysudo=''
|
||||||
|
else maysudo="$(command -v sudo || command -v doas || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
function finalize {
|
||||||
|
# after packages install (curl, etc), get the rust toolchain
|
||||||
|
which rustup > /dev/null 2>&1 || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
# verify the mold situation
|
||||||
|
if ! command -v mold >/dev/null 2>&1; then
|
||||||
|
echo "Warning: Mold binaries are unavailable on your system." >&2
|
||||||
|
echo " Builds will be slower without mold. Try: script/install-mold" >&2
|
||||||
|
fi
|
||||||
|
echo "Finished installing MacOS dependencies with script/macos"
|
||||||
|
}
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
brew=$(command -v brew || true)
|
||||||
|
if [[ -n $brew ]]; then
|
||||||
|
deps=(
|
||||||
|
gcc
|
||||||
|
libx11
|
||||||
|
libxkbcommon
|
||||||
|
openssl
|
||||||
|
zstd
|
||||||
|
vulkan-headers
|
||||||
|
libgit2
|
||||||
|
libx11
|
||||||
|
make
|
||||||
|
cmake
|
||||||
|
jq
|
||||||
|
git
|
||||||
|
curl
|
||||||
|
gettext
|
||||||
|
)
|
||||||
|
|
||||||
|
$brew update
|
||||||
|
for dep in "${deps[@]}";do
|
||||||
|
$brew search "$dep";
|
||||||
|
done
|
||||||
|
$brew install "${deps[@]}"
|
||||||
|
finalize
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Generated by Cargo
|
|
||||||
# will have compiled files and executables
|
|
||||||
/target/
|
|
||||||
|
|
||||||
# Generated by Tauri
|
|
||||||
# will have schema files for capabilities auto-completion
|
|
||||||
/gen/schemas
|
|
||||||
|
|
||||||
# Config
|
|
||||||
.cargo
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "Lume"
|
|
||||||
version = "24.11.0"
|
|
||||||
description = "nostr client"
|
|
||||||
authors = ["npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445"]
|
|
||||||
repository = "https://github.com/lumehq/lume"
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.70"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
tauri-build = { version = "2.0.0", features = [] }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tauri = { version = "2.0.0", features = [ "protocol-asset",
|
|
||||||
"unstable",
|
|
||||||
"tray-icon",
|
|
||||||
"macos-private-api"
|
|
||||||
] }
|
|
||||||
tauri-plugin-window-state = "2.0.0"
|
|
||||||
tauri-plugin-clipboard-manager = "2.0.0"
|
|
||||||
tauri-plugin-dialog = "2.0.0"
|
|
||||||
tauri-plugin-fs = "2.0.0"
|
|
||||||
tauri-plugin-http = "2.0.0"
|
|
||||||
tauri-plugin-notification = "2.0.0"
|
|
||||||
tauri-plugin-os = "2.0.0"
|
|
||||||
tauri-plugin-process = "2.0.0"
|
|
||||||
tauri-plugin-shell = "2.0.0"
|
|
||||||
tauri-plugin-updater = "2.0.0"
|
|
||||||
tauri-plugin-upload = "2.0.0"
|
|
||||||
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", rev = "8c67d44" }
|
|
||||||
tauri-plugin-prevent-default = "0.6"
|
|
||||||
tauri-plugin-theme = "2.1.2"
|
|
||||||
tauri-plugin-decorum = { git = "https://github.com/clearlysid/tauri-plugin-decorum" }
|
|
||||||
tauri-specta = { version = "2.0.0-rc.15", features = ["derive", "typescript"] }
|
|
||||||
|
|
||||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = ["lmdb", "webln", "all-nips"] }
|
|
||||||
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
|
||||||
|
|
||||||
specta = "^2.0.0-rc.20"
|
|
||||||
specta-typescript = "0.0.7"
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
reqwest = "0.12.4"
|
|
||||||
url = "2.5.0"
|
|
||||||
futures = "0.3.30"
|
|
||||||
linkify = "0.10.0"
|
|
||||||
regex = "1.10.4"
|
|
||||||
keyring = { version = "3", features = ["apple-native", "windows-native"] }
|
|
||||||
keyring-search = { git = "https://github.com/reyamir/keyring-search" }
|
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
|
||||||
async-trait = "0.1.83"
|
|
||||||
webbrowser = "1.0.2"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
|
||||||
border = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }
|
|
||||||
share-picker = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
codegen-units = 1
|
|
||||||
lto = true
|
|
||||||
panic = "abort"
|
|
||||||
incremental = false
|
|
||||||
opt-level = "z"
|
|
||||||
strip = true
|
|
||||||
rpath = false
|
|
||||||
debug = false
|
|
||||||
debug-assertions = false
|
|
||||||
overflow-checks = false
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
tauri_build::build()
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
|
||||||
"identifier": "window",
|
|
||||||
"description": "Capability for the desktop",
|
|
||||||
"platforms": [
|
|
||||||
"macOS",
|
|
||||||
"windows"
|
|
||||||
],
|
|
||||||
"windows": [
|
|
||||||
"*"
|
|
||||||
],
|
|
||||||
"permissions": [
|
|
||||||
"core:path:default",
|
|
||||||
"core:event:default",
|
|
||||||
"core:window:default",
|
|
||||||
"core:app:default",
|
|
||||||
"core:resources:default",
|
|
||||||
"core:menu:default",
|
|
||||||
"core:tray:default",
|
|
||||||
"core:window:allow-create",
|
|
||||||
"core:window:allow-close",
|
|
||||||
"core:window:allow-destroy",
|
|
||||||
"core:window:allow-set-focus",
|
|
||||||
"core:window:allow-center",
|
|
||||||
"core:window:allow-minimize",
|
|
||||||
"core:window:allow-maximize",
|
|
||||||
"core:window:allow-set-size",
|
|
||||||
"core:window:allow-start-dragging",
|
|
||||||
"core:window:allow-toggle-maximize",
|
|
||||||
"core:webview:allow-create-webview-window",
|
|
||||||
"core:webview:allow-create-webview",
|
|
||||||
"core:webview:allow-set-webview-size",
|
|
||||||
"core:webview:allow-set-webview-position",
|
|
||||||
"core:webview:allow-webview-close",
|
|
||||||
"core:menu:allow-new",
|
|
||||||
"core:menu:allow-popup",
|
|
||||||
"notification:allow-is-permission-granted",
|
|
||||||
"notification:allow-request-permission",
|
|
||||||
"notification:default",
|
|
||||||
"os:allow-locale",
|
|
||||||
"os:allow-platform",
|
|
||||||
"os:allow-os-type",
|
|
||||||
"updater:default",
|
|
||||||
"updater:allow-check",
|
|
||||||
"updater:allow-download-and-install",
|
|
||||||
"decorum:allow-show-snap-overlay",
|
|
||||||
"clipboard-manager:allow-write-text",
|
|
||||||
"clipboard-manager:allow-read-text",
|
|
||||||
"dialog:default",
|
|
||||||
"process:allow-restart",
|
|
||||||
"process:allow-exit",
|
|
||||||
"fs:allow-read-file",
|
|
||||||
"shell:allow-open",
|
|
||||||
"store:default",
|
|
||||||
"prevent-default:default",
|
|
||||||
"theme:default",
|
|
||||||
{
|
|
||||||
"identifier": "http:default",
|
|
||||||
"allow": [
|
|
||||||
{
|
|
||||||
"url": "http://**/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://**/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identifier": "fs:allow-read-text-file",
|
|
||||||
"allow": [
|
|
||||||
{
|
|
||||||
"path": "$RESOURCE/locales/*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "$RESOURCE/resources/*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"window":{"identifier":"window","description":"Capability for the desktop","local":true,"windows":["*"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-close","core:window:allow-destroy","core:window:allow-set-focus","core:window:allow-center","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-set-size","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-set-webview-size","core:webview:allow-set-webview-position","core:webview:allow-webview-close","core:menu:allow-new","core:menu:allow-popup","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:default","process:allow-restart","process:allow-exit","fs:allow-read-file","shell:allow-open","store:default","prevent-default:default","theme:default",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["macOS","windows"]}}
|
|
||||||
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 620 B |
@@ -1,295 +0,0 @@
|
|||||||
{
|
|
||||||
"global": {
|
|
||||||
"relay": "Relay",
|
|
||||||
"back": "Back",
|
|
||||||
"continue": "Continue",
|
|
||||||
"loading": "Loading",
|
|
||||||
"error": "Error",
|
|
||||||
"moveLeft": "Move Left",
|
|
||||||
"moveRight": "Move Right",
|
|
||||||
"newColumn": "New Column",
|
|
||||||
"inspect": "Inspect",
|
|
||||||
"loadMore": "Load more",
|
|
||||||
"delete": "Delete",
|
|
||||||
"refresh": "Refresh",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"save": "Save",
|
|
||||||
"post": "Post",
|
|
||||||
"update": "Update",
|
|
||||||
"noResult": "No results found.",
|
|
||||||
"emptyFeedTitle": "This feed is empty",
|
|
||||||
"emptyFeedSubtitle": "You can follow more users to build up your timeline",
|
|
||||||
"apiKey": "API Key",
|
|
||||||
"skip": "Skip",
|
|
||||||
"close": "Close"
|
|
||||||
},
|
|
||||||
"nip89": {
|
|
||||||
"unsupported": "Lume isn't support this event",
|
|
||||||
"openWith": "Open with"
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"showThread": "Show thread",
|
|
||||||
"showMore": "Show more",
|
|
||||||
"error": "Failed to fetch event.",
|
|
||||||
"posted": "posted",
|
|
||||||
"replied": "replied",
|
|
||||||
"reposted": "reposted",
|
|
||||||
"menu": {
|
|
||||||
"viewThread": "View thread",
|
|
||||||
"copyLink": "Copy shareable link",
|
|
||||||
"copyNoteId": "Copy note ID",
|
|
||||||
"copyAuthorId": "Copy author ID",
|
|
||||||
"viewAuthor": "View author",
|
|
||||||
"pinAuthor": "Pin author",
|
|
||||||
"copyRaw": "Copy raw event",
|
|
||||||
"mute": "Mute"
|
|
||||||
},
|
|
||||||
"buttons": {
|
|
||||||
"pin": "Pin",
|
|
||||||
"pinTooltip": "Pin Note",
|
|
||||||
"repost": "Repost",
|
|
||||||
"quote": "Quote",
|
|
||||||
"viewProfile": "View profile"
|
|
||||||
},
|
|
||||||
"zap": {
|
|
||||||
"zap": "Zap",
|
|
||||||
"tooltip": "Send zap",
|
|
||||||
"modalTitle": "Send zap to",
|
|
||||||
"messagePlaceholder": "Enter message (optional)",
|
|
||||||
"buttonFinish": "Zapped",
|
|
||||||
"buttonLoading": "Processing...",
|
|
||||||
"invoiceButton": "Scan to zap",
|
|
||||||
"invoiceFooter": "You must use Bitcoin wallet which support Lightning\nsuch as: Blue Wallet, Bitkit, Phoenix,..."
|
|
||||||
},
|
|
||||||
"reply": {
|
|
||||||
"single": "reply",
|
|
||||||
"plural": "replies",
|
|
||||||
"empty": "Be the first to Reply!"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"avatar": "Avatar",
|
|
||||||
"displayName": "Display Name",
|
|
||||||
"name": "Name",
|
|
||||||
"bio": "Bio",
|
|
||||||
"lna": "Lightning address",
|
|
||||||
"website": "Website",
|
|
||||||
"verified": "Verified",
|
|
||||||
"unverified": "Unverified",
|
|
||||||
"follow": "Follow",
|
|
||||||
"unfollow": "Unfollow",
|
|
||||||
"latestPosts": "Latest posts",
|
|
||||||
"avatarButton": "Change avatar",
|
|
||||||
"coverButton": "Change cover",
|
|
||||||
"editProfile": "Edit profile",
|
|
||||||
"settings": "Settings",
|
|
||||||
"logout": "Log out",
|
|
||||||
"logoutConfirmTitle": "Are you sure!",
|
|
||||||
"logoutConfirmSubtitle": "You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account."
|
|
||||||
},
|
|
||||||
"editor": {
|
|
||||||
"title": "New Post",
|
|
||||||
"placeholder": "What are you up to?",
|
|
||||||
"successMessage": "Your note has been published successfully.",
|
|
||||||
"replyPlaceholder": "Post your reply"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"placeholder": "Type something to search...",
|
|
||||||
"empty": "Try searching for people, notes, or keywords"
|
|
||||||
},
|
|
||||||
"welcome": {
|
|
||||||
"title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.",
|
|
||||||
"signup": "Join Nostr",
|
|
||||||
"login": "Login",
|
|
||||||
"footer": "Before joining Nostr, you can take time to learn more about Nostr"
|
|
||||||
},
|
|
||||||
"login": {
|
|
||||||
"title": "Welcome back, anon!",
|
|
||||||
"footer": "Lume will put your Private Key in Secure Storage depended on your OS Platform. It will be secured by Password or Biometric ID",
|
|
||||||
"loginWithAddress": "Login with Nostr Address",
|
|
||||||
"loginWithBunker": "Login with nsecBunker",
|
|
||||||
"or": "Or continue with",
|
|
||||||
"loginWithPrivkey": "Login with Private Key"
|
|
||||||
},
|
|
||||||
"loginWithAddress": {
|
|
||||||
"title": "Enter your Nostr Address"
|
|
||||||
},
|
|
||||||
"loginWithBunker": {
|
|
||||||
"title": "Enter your nsecbunker token"
|
|
||||||
},
|
|
||||||
"loginWithPrivkey": {
|
|
||||||
"title": "Enter your Private Key",
|
|
||||||
"subtitle": "Lume will put your private key to <1>{{service}}</1>.\nIt will be secured by your OS."
|
|
||||||
},
|
|
||||||
"signup": {
|
|
||||||
"title": "Let's Get Started",
|
|
||||||
"subtitle": "Choose one of methods below to create your account",
|
|
||||||
"selfManageMethod": "Self-Managed",
|
|
||||||
"selfManageMethodDescription": "You create your keys and keep them safe.",
|
|
||||||
"providerMethod": "Managed by Provider",
|
|
||||||
"providerMethodDescription": "A 3rd party provider will handle your sign in keys for you."
|
|
||||||
},
|
|
||||||
"signupWithSelfManage": {
|
|
||||||
"title": "This is your new Account Key",
|
|
||||||
"subtitle": "Keep your key in safe place. If you lose this key, you will lose access to your account.",
|
|
||||||
"confirm1": "I understand the risk of lost private key.",
|
|
||||||
"confirm2": "I will make sure keep it safe and not sharing with anyone.",
|
|
||||||
"confirm3": "I understand I cannot recover private key.",
|
|
||||||
"button": "Save key & Continue"
|
|
||||||
},
|
|
||||||
"signupWithProvider": {
|
|
||||||
"title": "Let's set up your account on Nostr",
|
|
||||||
"username": "Username *",
|
|
||||||
"chooseProvider": "Choose a Provider",
|
|
||||||
"usernameFooter": "Use to login to Lume and other Nostr apps. You can choose provider you trust to manage your account",
|
|
||||||
"email": "Backup Email (optional)",
|
|
||||||
"emailFooter": "Use for recover your account if you lose your password"
|
|
||||||
},
|
|
||||||
"onboardingSettings": {
|
|
||||||
"title": "You're almost ready to use Lume.",
|
|
||||||
"subtitle": "Let's start personalizing your experience.",
|
|
||||||
"notification": {
|
|
||||||
"title": "Push notification",
|
|
||||||
"subtitle": "Enabling push notifications will allow you to receive notifications from Lume."
|
|
||||||
},
|
|
||||||
"lowPower": {
|
|
||||||
"title": "Low Power Mode",
|
|
||||||
"subtitle": "Limited relay connection and hide all media, sustainable for low network environment."
|
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"title": "Translation (nostr.wine)",
|
|
||||||
"subtitle": "Translate text to your preferred language, powered by Nostr Wine."
|
|
||||||
},
|
|
||||||
"footer": "There are many more settings you can configure from the 'Settings' Screen. Be sure to visit it later."
|
|
||||||
},
|
|
||||||
"relays": {
|
|
||||||
"global": "Global",
|
|
||||||
"follows": "Follows",
|
|
||||||
"sidebar": {
|
|
||||||
"title": "Connected relays",
|
|
||||||
"empty": "Empty."
|
|
||||||
},
|
|
||||||
"relayView": {
|
|
||||||
"empty": "Could not load relay information 😬",
|
|
||||||
"owner": "Owner",
|
|
||||||
"contact": "Contact",
|
|
||||||
"software": "Software",
|
|
||||||
"nips": "Supported NIPs",
|
|
||||||
"limit": "Limitation",
|
|
||||||
"payment": "Open payment website",
|
|
||||||
"paymentNote": "You need to make a payment to connect this relay"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"suggestion": {
|
|
||||||
"title": "Suggested Follows",
|
|
||||||
"error": "Error. Cannot get trending users",
|
|
||||||
"button": "Save & Go back"
|
|
||||||
},
|
|
||||||
"interests": {
|
|
||||||
"title": "Interests",
|
|
||||||
"subtitle": "Pick things you'd like to see in your home feed.",
|
|
||||||
"edit": "Edit Interest",
|
|
||||||
"followAll": "Follow All",
|
|
||||||
"unfollowAll": "Unfollow All"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"general": {
|
|
||||||
"title": "General",
|
|
||||||
"update": {
|
|
||||||
"title": "Update",
|
|
||||||
"subtitle": "Automatically download new update"
|
|
||||||
},
|
|
||||||
"lowPower": {
|
|
||||||
"title": "Low Power",
|
|
||||||
"subtitle": "Sustainable for low network environment"
|
|
||||||
},
|
|
||||||
"startup": {
|
|
||||||
"title": "Startup",
|
|
||||||
"subtitle": "Launch Lume at Login"
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"title": "Media",
|
|
||||||
"subtitle": "Automatically load media"
|
|
||||||
},
|
|
||||||
"hashtag": {
|
|
||||||
"title": "Hashtag",
|
|
||||||
"subtitle": "Show all hashtags in content"
|
|
||||||
},
|
|
||||||
"notification": {
|
|
||||||
"title": "Notification",
|
|
||||||
"subtitle": "Automatically send notification"
|
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"title": "Translation",
|
|
||||||
"subtitle": "Translate text to your language"
|
|
||||||
},
|
|
||||||
"appearance": {
|
|
||||||
"title": "Appearance",
|
|
||||||
"light": "Light",
|
|
||||||
"dark": "Dark",
|
|
||||||
"system": "System"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"title": "User"
|
|
||||||
},
|
|
||||||
"zap": {
|
|
||||||
"title": "Zap",
|
|
||||||
"nwc": "Connection String"
|
|
||||||
},
|
|
||||||
"backup": {
|
|
||||||
"title": "Backup",
|
|
||||||
"privkey": {
|
|
||||||
"title": "Private key",
|
|
||||||
"button": "Remove private key"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"advanced": {
|
|
||||||
"title": "Advanced",
|
|
||||||
"cache": {
|
|
||||||
"title": "Cache",
|
|
||||||
"subtitle": "Use for boost up nostr connection",
|
|
||||||
"button": "Clear"
|
|
||||||
},
|
|
||||||
"instant": {
|
|
||||||
"title": "Instant Zap",
|
|
||||||
"subtitle": "Zap with default amount, no confirmation"
|
|
||||||
},
|
|
||||||
"defaultAmount": "Default amount"
|
|
||||||
},
|
|
||||||
"about": {
|
|
||||||
"title": "About",
|
|
||||||
"version": "Version",
|
|
||||||
"checkUpdate": "Check for update",
|
|
||||||
"installUpdate": "Install"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"onboarding": {
|
|
||||||
"home": {
|
|
||||||
"title": "Your account was successfully created!",
|
|
||||||
"subtitle": "For starters, let's set up your profile.",
|
|
||||||
"profileSettings": "Profile Settings"
|
|
||||||
},
|
|
||||||
"profile": {
|
|
||||||
"title": "About you",
|
|
||||||
"subtitle": "Tell Lume about yourself to start building your home feed."
|
|
||||||
},
|
|
||||||
"finish": {
|
|
||||||
"title": "Profile setup complete!",
|
|
||||||
"subtitle": "You can exit the setup here and start using Lume.",
|
|
||||||
"report": "Report a issue"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"activity": {
|
|
||||||
"title": "Activity",
|
|
||||||
"empty": "Yo! Nothing new yet.",
|
|
||||||
"mention": "mention you",
|
|
||||||
"repost": "reposted",
|
|
||||||
"zap": "zapped",
|
|
||||||
"newReply": "New reply",
|
|
||||||
"boost": "Boost",
|
|
||||||
"boostSubtitle": "@ Someone has reposted to your note",
|
|
||||||
"conversation": "Conversation",
|
|
||||||
"conversationSubtitle": "@ Someone has replied to your note"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
{
|
|
||||||
"global": {
|
|
||||||
"relay": "Relay",
|
|
||||||
"back": "Back",
|
|
||||||
"continue": "Continue",
|
|
||||||
"loading": "Loading",
|
|
||||||
"error": "Error",
|
|
||||||
"moveLeft": "Move Left",
|
|
||||||
"moveRight": "Move Right",
|
|
||||||
"newColumn": "New Column",
|
|
||||||
"inspect": "Inspect",
|
|
||||||
"loadMore": "Load more",
|
|
||||||
"delete": "Delete",
|
|
||||||
"refresh": "Refresh",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"save": "Save",
|
|
||||||
"post": "Post",
|
|
||||||
"update": "Update",
|
|
||||||
"noResult": "No results found.",
|
|
||||||
"emptyFeedTitle": "This feed is empty",
|
|
||||||
"emptyFeedSubtitle": "You can follow more users to build up your timeline",
|
|
||||||
"apiKey": "API Key",
|
|
||||||
"skip": "Skip",
|
|
||||||
"close": "Close"
|
|
||||||
},
|
|
||||||
"nip89": {
|
|
||||||
"unsupported": "Lume isn't support this event",
|
|
||||||
"openWith": "Open with"
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"showThread": "Show thread",
|
|
||||||
"showMore": "Show more",
|
|
||||||
"error": "Failed to fetch event.",
|
|
||||||
"posted": "posted",
|
|
||||||
"replied": "replied",
|
|
||||||
"reposted": "reposted",
|
|
||||||
"menu": {
|
|
||||||
"viewThread": "View thread",
|
|
||||||
"copyLink": "Copy shareable link",
|
|
||||||
"copyNoteId": "Copy note ID",
|
|
||||||
"copyAuthorId": "Copy author ID",
|
|
||||||
"viewAuthor": "View author",
|
|
||||||
"pinAuthor": "Pin author",
|
|
||||||
"copyRaw": "Copy raw event",
|
|
||||||
"mute": "Mute"
|
|
||||||
},
|
|
||||||
"buttons": {
|
|
||||||
"pin": "Pin",
|
|
||||||
"pinTooltip": "Pin Note",
|
|
||||||
"repost": "Repost",
|
|
||||||
"quote": "Quote",
|
|
||||||
"viewProfile": "View profile",
|
|
||||||
"reply": "Reply this note",
|
|
||||||
"open": "Open in new window"
|
|
||||||
},
|
|
||||||
"zap": {
|
|
||||||
"zap": "Zap",
|
|
||||||
"tooltip": "Send zap",
|
|
||||||
"modalTitle": "Send zap to",
|
|
||||||
"messagePlaceholder": "Enter message (optional)",
|
|
||||||
"buttonFinish": "Zapped",
|
|
||||||
"buttonLoading": "Processing...",
|
|
||||||
"invoiceButton": "Scan to zap",
|
|
||||||
"invoiceFooter": "You must use Bitcoin wallet which support Lightning\nsuch as: Blue Wallet, Bitkit, Phoenix,..."
|
|
||||||
},
|
|
||||||
"reply": {
|
|
||||||
"single": "reply",
|
|
||||||
"plural": "replies",
|
|
||||||
"empty": "Be the first to Reply!"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"avatar": "Avatar",
|
|
||||||
"displayName": "Display Name",
|
|
||||||
"name": "Name",
|
|
||||||
"bio": "Bio",
|
|
||||||
"lna": "Lightning address",
|
|
||||||
"website": "Website",
|
|
||||||
"verified": "Verified",
|
|
||||||
"unverified": "Unverified",
|
|
||||||
"follow": "Follow",
|
|
||||||
"unfollow": "Unfollow",
|
|
||||||
"latestPosts": "Latest posts",
|
|
||||||
"avatarButton": "Change avatar",
|
|
||||||
"coverButton": "Change cover",
|
|
||||||
"editProfile": "Edit profile",
|
|
||||||
"settings": "Settings",
|
|
||||||
"logout": "Log out",
|
|
||||||
"logoutConfirmTitle": "Are you sure!",
|
|
||||||
"logoutConfirmSubtitle": "You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account."
|
|
||||||
},
|
|
||||||
"editor": {
|
|
||||||
"title": "New Post",
|
|
||||||
"placeholder": "What are you up to?",
|
|
||||||
"successMessage": "Your note has been published successfully.",
|
|
||||||
"replyPlaceholder": "Post your reply"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"placeholder": "Type something to search...",
|
|
||||||
"empty": "Try searching for people, notes, or keywords"
|
|
||||||
},
|
|
||||||
"welcome": {
|
|
||||||
"title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.",
|
|
||||||
"signup": "Join Nostr",
|
|
||||||
"login": "Login",
|
|
||||||
"footer": "Before joining Nostr, you can take time to learn more about Nostr"
|
|
||||||
},
|
|
||||||
"login": {
|
|
||||||
"title": "Welcome back, anon!",
|
|
||||||
"subtitle": "We're so excited to see you again!",
|
|
||||||
"footer": "Lume will put your Private Key in Secure Storage depended on your OS Platform. It will be secured by Password or Biometric ID",
|
|
||||||
"loginWithAddress": "Login with Nostr Address",
|
|
||||||
"loginWithBunker": "Login with nsecBunker",
|
|
||||||
"or": "Or continue with",
|
|
||||||
"loginWithPrivkey": "Login with Private Key"
|
|
||||||
},
|
|
||||||
"loginWithAddress": {
|
|
||||||
"title": "Enter your Nostr Address"
|
|
||||||
},
|
|
||||||
"loginWithBunker": {
|
|
||||||
"title": "Enter your nsecbunker token"
|
|
||||||
},
|
|
||||||
"loginWithPrivkey": {
|
|
||||||
"title": "Enter your Private Key",
|
|
||||||
"subtitle": "Lume will put your private key to <1>{{service}}</1>.\nIt will be secured by your OS."
|
|
||||||
},
|
|
||||||
"signup": {
|
|
||||||
"title": "Let's Get Started",
|
|
||||||
"subtitle": "Choose one of methods below to create your account",
|
|
||||||
"selfManageMethod": "Self-Managed",
|
|
||||||
"selfManageMethodDescription": "You create your keys and keep them safe.",
|
|
||||||
"providerMethod": "Managed by Provider",
|
|
||||||
"providerMethodDescription": "A 3rd party provider will handle your sign in keys for you."
|
|
||||||
},
|
|
||||||
"backup": {
|
|
||||||
"title": "This is your new sign in key",
|
|
||||||
"subtitle": "Keep your key in safe place. If you lose this key, you will lose access to your account.",
|
|
||||||
"confirm1": "I understand the risk of lost private key.",
|
|
||||||
"confirm2": "I will make sure keep it safe and not sharing with anyone.",
|
|
||||||
"confirm3": "I understand I cannot recover private key.",
|
|
||||||
"button": "Save & Continue"
|
|
||||||
},
|
|
||||||
"signupWithProvider": {
|
|
||||||
"title": "Let's set up your account on Nostr",
|
|
||||||
"username": "Username *",
|
|
||||||
"chooseProvider": "Choose a Provider",
|
|
||||||
"usernameFooter": "Use to login to Lume and other Nostr apps. You can choose provider you trust to manage your account",
|
|
||||||
"email": "Backup Email (optional)",
|
|
||||||
"emailFooter": "Use for recover your account if you lose your password"
|
|
||||||
},
|
|
||||||
"onboardingSettings": {
|
|
||||||
"title": "You're almost ready to use Lume.",
|
|
||||||
"subtitle": "Let's start personalizing your experience.",
|
|
||||||
"notification": {
|
|
||||||
"title": "Push notification",
|
|
||||||
"subtitle": "Enabling push notifications will allow you to receive notifications from Lume."
|
|
||||||
},
|
|
||||||
"lowPower": {
|
|
||||||
"title": "Low Power Mode",
|
|
||||||
"subtitle": "Limited relay connection and hide all media, sustainable for low network environment."
|
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"title": "Translation (nostr.wine)",
|
|
||||||
"subtitle": "Translate text to your preferred language, powered by Nostr Wine."
|
|
||||||
},
|
|
||||||
"footer": "There are many more settings you can configure from the 'Settings' Screen. Be sure to visit it later."
|
|
||||||
},
|
|
||||||
"relays": {
|
|
||||||
"global": "Global",
|
|
||||||
"follows": "Follows",
|
|
||||||
"sidebar": {
|
|
||||||
"title": "Connected relays",
|
|
||||||
"empty": "Empty."
|
|
||||||
},
|
|
||||||
"relayView": {
|
|
||||||
"empty": "Could not load relay information 😬",
|
|
||||||
"owner": "Owner",
|
|
||||||
"contact": "Contact",
|
|
||||||
"software": "Software",
|
|
||||||
"nips": "Supported NIPs",
|
|
||||||
"limit": "Limitation",
|
|
||||||
"payment": "Open payment website",
|
|
||||||
"paymentNote": "You need to make a payment to connect this relay"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"suggestion": {
|
|
||||||
"title": "Suggested Follows",
|
|
||||||
"error": "Error. Cannot get trending users",
|
|
||||||
"button": "Save & Go back"
|
|
||||||
},
|
|
||||||
"interests": {
|
|
||||||
"title": "Interests",
|
|
||||||
"subtitle": "Pick things you'd like to see in your home feed.",
|
|
||||||
"edit": "Edit Interest",
|
|
||||||
"followAll": "Follow All",
|
|
||||||
"unfollowAll": "Unfollow All"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"general": {
|
|
||||||
"title": "General",
|
|
||||||
"update": {
|
|
||||||
"title": "Update",
|
|
||||||
"subtitle": "Automatically download new update"
|
|
||||||
},
|
|
||||||
"lowPower": {
|
|
||||||
"title": "Low Power",
|
|
||||||
"subtitle": "Sustainable for low network environment"
|
|
||||||
},
|
|
||||||
"startup": {
|
|
||||||
"title": "Startup",
|
|
||||||
"subtitle": "Launch Lume at Login"
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"title": "Media",
|
|
||||||
"subtitle": "Automatically load media"
|
|
||||||
},
|
|
||||||
"hashtag": {
|
|
||||||
"title": "Hashtag",
|
|
||||||
"subtitle": "Show all hashtags in content"
|
|
||||||
},
|
|
||||||
"notification": {
|
|
||||||
"title": "Notification",
|
|
||||||
"subtitle": "Automatically send notification"
|
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"title": "Translation",
|
|
||||||
"subtitle": "Translate text to your language"
|
|
||||||
},
|
|
||||||
"appearance": {
|
|
||||||
"title": "Appearance",
|
|
||||||
"light": "Light",
|
|
||||||
"dark": "Dark",
|
|
||||||
"system": "System"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"title": "Account"
|
|
||||||
},
|
|
||||||
"zap": {
|
|
||||||
"title": "Zap",
|
|
||||||
"nwc": "Connection String"
|
|
||||||
},
|
|
||||||
"backup": {
|
|
||||||
"title": "Backup",
|
|
||||||
"privkey": {
|
|
||||||
"title": "Private key",
|
|
||||||
"button": "Remove private key"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"advanced": {
|
|
||||||
"title": "Advanced",
|
|
||||||
"cache": {
|
|
||||||
"title": "Cache",
|
|
||||||
"subtitle": "Use for boost up nostr connection",
|
|
||||||
"button": "Clear"
|
|
||||||
},
|
|
||||||
"instant": {
|
|
||||||
"title": "Instant Zap",
|
|
||||||
"subtitle": "Zap with default amount, no confirmation"
|
|
||||||
},
|
|
||||||
"defaultAmount": "Default amount"
|
|
||||||
},
|
|
||||||
"about": {
|
|
||||||
"title": "About",
|
|
||||||
"version": "Version",
|
|
||||||
"checkUpdate": "Check for update",
|
|
||||||
"installUpdate": "Install"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"onboarding": {
|
|
||||||
"home": {
|
|
||||||
"title": "Your account was successfully created!",
|
|
||||||
"subtitle": "For starters, let's set up your profile.",
|
|
||||||
"profileSettings": "Profile Settings"
|
|
||||||
},
|
|
||||||
"profile": {
|
|
||||||
"title": "About you",
|
|
||||||
"subtitle": "Tell Lume about yourself to start building your home feed."
|
|
||||||
},
|
|
||||||
"finish": {
|
|
||||||
"title": "Profile setup complete!",
|
|
||||||
"subtitle": "You can exit the setup here and start using Lume.",
|
|
||||||
"report": "Report a issue"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"activity": {
|
|
||||||
"title": "Activity",
|
|
||||||
"empty": "Yo! Nothing new yet.",
|
|
||||||
"mention": "mention you",
|
|
||||||
"repost": "reposted",
|
|
||||||
"zap": "zapped",
|
|
||||||
"newReply": "New reply",
|
|
||||||
"boost": "Boost",
|
|
||||||
"boostSubtitle": "@ Someone has reposted to your note",
|
|
||||||
"conversation": "Conversation",
|
|
||||||
"conversationSubtitle": "@ Someone has replied to your note"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
{
|
|
||||||
"global": {
|
|
||||||
"relay": "Relais",
|
|
||||||
"back": "Retour",
|
|
||||||
"continue": "Continuer",
|
|
||||||
"loading": "Chargement",
|
|
||||||
"error": "Erreur",
|
|
||||||
"moveLeft": "Déplacer à Gauche",
|
|
||||||
"moveRight": "Déplacer à Droite",
|
|
||||||
"newColumn": "Nouvelle Colonne",
|
|
||||||
"inspect": "Inspecter",
|
|
||||||
"loadMore": "Charger plus",
|
|
||||||
"delete": "Supprimer",
|
|
||||||
"refresh": "Rafraîchir",
|
|
||||||
"cancel": "Annuler",
|
|
||||||
"save": "Enregistrer",
|
|
||||||
"post": "Poster",
|
|
||||||
"update": "Mettre à jour",
|
|
||||||
"noResult": "Aucun résultat correspondant.",
|
|
||||||
"emptyFeedTitle": "Ce flux est vide",
|
|
||||||
"emptyFeedSubtitle": "Vous pouvez suivre plus d'utilisateurs pour construire votre flux de messages",
|
|
||||||
"apiKey": "Clé d'API",
|
|
||||||
"skip": "Passer",
|
|
||||||
"close": "Fermer"
|
|
||||||
},
|
|
||||||
"nip89": {
|
|
||||||
"unsupported": "Lume ne supporte pas encore cet événement",
|
|
||||||
"openWith": "Ouvrir avec"
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"showThread": "Voir le fil",
|
|
||||||
"showMore": "Voir plus",
|
|
||||||
"error": "Echec de la récupération de l'événement.",
|
|
||||||
"posted": "posté",
|
|
||||||
"replied": "répondu",
|
|
||||||
"reposted": "reposté",
|
|
||||||
"menu": {
|
|
||||||
"viewThread": "Voir le fil",
|
|
||||||
"copyLink": "Copier le lien à partager",
|
|
||||||
"copyNoteId": "Copier l'ID de la note",
|
|
||||||
"copyAuthorId": "Copier l'ID de l'auteur",
|
|
||||||
"viewAuthor": "Voir l'auteur",
|
|
||||||
"pinAuthor": "Epingler l'auteur",
|
|
||||||
"copyRaw": "Copier le contenu brut de l'événement",
|
|
||||||
"mute": "Mettre en silencieux"
|
|
||||||
},
|
|
||||||
"buttons": {
|
|
||||||
"pin": "Epingler",
|
|
||||||
"pinTooltip": "Epingler la Note",
|
|
||||||
"repost": "Reposter",
|
|
||||||
"quote": "Citer",
|
|
||||||
"viewProfile": "Voir le profil"
|
|
||||||
},
|
|
||||||
"zap": {
|
|
||||||
"zap": "Zap",
|
|
||||||
"tooltip": "Envoyer zap",
|
|
||||||
"modalTitle": "Envoyer zap à",
|
|
||||||
"messagePlaceholder": "Entrer un message (optionnel)",
|
|
||||||
"buttonFinish": "Zappé",
|
|
||||||
"buttonLoading": "En cours...",
|
|
||||||
"invoiceButton": "Scanner pour zapper",
|
|
||||||
"invoiceFooter": "Vous devez utiliser un portefeuille Bitcoin qui prend en charge Lightning\ncomme par exemple : Blue Wallet, Bitkit, Phoenix, ..."
|
|
||||||
},
|
|
||||||
"reply": {
|
|
||||||
"single": "réponse",
|
|
||||||
"plural": "réponses",
|
|
||||||
"empty": "Soyez le premier à répondre !"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"avatar": "Avatar",
|
|
||||||
"displayName": "Nom Affiché",
|
|
||||||
"name": "Nom",
|
|
||||||
"bio": "Bio",
|
|
||||||
"lna": "Adresse Lightning",
|
|
||||||
"website": "Site Internet",
|
|
||||||
"verified": "Vérifié",
|
|
||||||
"unverified": "Non vérifié",
|
|
||||||
"follow": "Suivre",
|
|
||||||
"unfollow": "Ne plus suivre",
|
|
||||||
"latestPosts": "Derniers posts",
|
|
||||||
"avatarButton": "Changer d'avatar",
|
|
||||||
"coverButton": "Changer de bannière",
|
|
||||||
"editProfile": "Modifier profil",
|
|
||||||
"settings": "Paramètres",
|
|
||||||
"logout": "Déconnexion",
|
|
||||||
"logoutConfirmTitle": "Etes-vous sûr ?",
|
|
||||||
"logoutConfirmSubtitle": "Vous pouvez toujours vous reconnecter à tout moment. Si vous souhaitez simplement changer de compte, vous pouvez le faire en ajoutant un compte existant."
|
|
||||||
},
|
|
||||||
"editor": {
|
|
||||||
"title": "Nouveau Post",
|
|
||||||
"placeholder": "Que faites-vous ?",
|
|
||||||
"successMessage": "Votre note a été publiée avec succès.",
|
|
||||||
"replyPlaceholder": "Poster votre réponse"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"placeholder": "Saisissez quelque chose à rechercher...",
|
|
||||||
"empty": "Essayez de rechercher des personnes, des notes ou des mots-clés"
|
|
||||||
},
|
|
||||||
"welcome": {
|
|
||||||
"title": "Lume est un magnifique client Nostr qui permet de se rencontrer, d'explorer\net de partager librement ses pensées avec tout le monde.",
|
|
||||||
"signup": "Rejoindre Nostr",
|
|
||||||
"login": "Se connecter",
|
|
||||||
"footer": "Avant de rejoindre Nostr, vous pouvez prendre le temps d'en savoir plus sur Nostr"
|
|
||||||
},
|
|
||||||
"login": {
|
|
||||||
"title": "Re-Bienvenue, anon !",
|
|
||||||
"footer": "Lume placera votre clé privée dans un espace de stockage sécurisé en fonction de la plateforme de votre système d'exploitation. Elle sera sécurisée par un mot de passe ou un identifiant biométrique.",
|
|
||||||
"loginWithAddress": "Se connecter avec une Adresse Nostr",
|
|
||||||
"loginWithBunker": "Se connecter avec nsecBunker",
|
|
||||||
"or": "Ou continuer avec",
|
|
||||||
"loginWithPrivkey": "Se connecter avec une Clé Privée"
|
|
||||||
},
|
|
||||||
"loginWithAddress": {
|
|
||||||
"title": "Saisissez votre Adresse Nostr"
|
|
||||||
},
|
|
||||||
"loginWithBunker": {
|
|
||||||
"title": "Saisissez votre jeton nsecbunker"
|
|
||||||
},
|
|
||||||
"loginWithPrivkey": {
|
|
||||||
"title": "Saisissez votre Clé Privée",
|
|
||||||
"subtitle": "Lume placera votre clé privée dans <1>{{service}}</1>.\nElle sera sécurisée par votre système d'exploitation."
|
|
||||||
},
|
|
||||||
"signup": {
|
|
||||||
"title": "Commençons",
|
|
||||||
"subtitle": "Choisissez l'une des méthodes ci-dessous pour créer votre compte",
|
|
||||||
"selfManageMethod": "Autogéré",
|
|
||||||
"selfManageMethodDescription": "Vous créez vos clés et vous les conservez en toute sécurité.",
|
|
||||||
"providerMethod": "Géré par le Prestataire",
|
|
||||||
"providerMethodDescription": "Un fournisseur tiers s'occupera de vos clés de connexion pour vous."
|
|
||||||
},
|
|
||||||
"signupWithSelfManage": {
|
|
||||||
"title": "Il s'agit de votre nouvelle Clé de Compte",
|
|
||||||
"subtitle": "Conservez votre clé en lieu sûr. Si vous perdez cette clé, vous perdrez l'accès à votre compte.",
|
|
||||||
"confirm1": "Je comprends le risque de perte de la clé privée.",
|
|
||||||
"confirm2": "Je veillerai à ce qu'elle soit conservée en toute sécurité et à ce qu'elle ne soit partagée avec personne.",
|
|
||||||
"confirm3": "Je comprends que je ne peux pas récupérer la clé privée.",
|
|
||||||
"button": "Enregistrer la clé et Continuer"
|
|
||||||
},
|
|
||||||
"signupWithProvider": {
|
|
||||||
"title": "Commençons la préparation de votre compte Nostr",
|
|
||||||
"username": "Nom d'utilisateur *",
|
|
||||||
"chooseProvider": "Choisissez un Fournisseur",
|
|
||||||
"usernameFooter": "A utiliser pour vous connecter à Lume et aux autres applications Nostr. Vous pouvez choisir un fournisseur de confiance pour gérer votre compte.",
|
|
||||||
"email": "Email de sauvegarde (optionnel)",
|
|
||||||
"emailFooter": "A utiliser pour récupérer votre compte si vous perdez votre mot de passe"
|
|
||||||
},
|
|
||||||
"onboardingSettings": {
|
|
||||||
"title": "Vous êtes presque prêt à utiliser Lume.",
|
|
||||||
"subtitle": "Commençons par personnaliser votre expérience.",
|
|
||||||
"notification": {
|
|
||||||
"title": "Notifications Push",
|
|
||||||
"subtitle": "L'activation des notifications push vous permettra de recevoir des notifications de Lume."
|
|
||||||
},
|
|
||||||
"lowPower": {
|
|
||||||
"title": "Mode Basse Consommation",
|
|
||||||
"subtitle": "Connexion relais limitée et dissimulation de tous les médias, compatible avec les environnements avec peu de réseau."
|
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"title": "Traductions (nostr.wine)",
|
|
||||||
"subtitle": "Traduisez votre texte dans la langue de votre choix, grâce à Nostr Wine."
|
|
||||||
},
|
|
||||||
"footer": "Vous pouvez configurer de nombreux autres paramètres à partir de l'écran 'Paramètres'. Ne manquez pas de les consulter ultérieurement."
|
|
||||||
},
|
|
||||||
"relays": {
|
|
||||||
"global": "Général",
|
|
||||||
"follows": "Suivis",
|
|
||||||
"sidebar": {
|
|
||||||
"title": "Relais connectés",
|
|
||||||
"empty": "Vide."
|
|
||||||
},
|
|
||||||
"relayView": {
|
|
||||||
"empty": "Impossible de charger les informations du relais 😬",
|
|
||||||
"owner": "Propriétaire",
|
|
||||||
"contact": "Contact",
|
|
||||||
"software": "Logiciel",
|
|
||||||
"nips": "NIPs supportés",
|
|
||||||
"limit": "Limitation",
|
|
||||||
"payment": "Ouvrir le site de paiement",
|
|
||||||
"paymentNote": "Vous devez effectuer un paiement pour vous connecter à ce relais."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"suggestion": {
|
|
||||||
"title": "Suivis Suggérés",
|
|
||||||
"error": "Erreur. Impossible d'obtenir les utilisateurs en tendance",
|
|
||||||
"button": "Enregistrer & Retour"
|
|
||||||
},
|
|
||||||
"interests": {
|
|
||||||
"title": "Intérêts",
|
|
||||||
"subtitle": "Choisissez les éléments que vous aimeriez voir apparaître dans votre flux d'accueil.",
|
|
||||||
"edit": "Modifier vos Intérêts",
|
|
||||||
"followAll": "Suivre tout le monde",
|
|
||||||
"unfollowAll": "Ne plus suivre personne"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"general": {
|
|
||||||
"title": "Général",
|
|
||||||
"update": {
|
|
||||||
"title": "Mises à jour",
|
|
||||||
"subtitle": "Téléchargement automatique des nouvelles mises à jour"
|
|
||||||
},
|
|
||||||
"lowPower": {
|
|
||||||
"title": "Faible Consommation",
|
|
||||||
"subtitle": "Recommandé pour un environnement avec peu de réseau"
|
|
||||||
},
|
|
||||||
"startup": {
|
|
||||||
"title": "Démarrage",
|
|
||||||
"subtitle": "Lancer Lume au Démarrage"
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"title": "Média",
|
|
||||||
"subtitle": "Chargement automatique des médias"
|
|
||||||
},
|
|
||||||
"hashtag": {
|
|
||||||
"title": "Hashtag",
|
|
||||||
"subtitle": "Afficher tous les hashtags dans les contenus"
|
|
||||||
},
|
|
||||||
"notification": {
|
|
||||||
"title": "Notification",
|
|
||||||
"subtitle": "Envoi automatique des notifications"
|
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"title": "Traduction",
|
|
||||||
"subtitle": "Traduire les textes dans votre langue"
|
|
||||||
},
|
|
||||||
"appearance": {
|
|
||||||
"title": "Apparence",
|
|
||||||
"light": "Clair",
|
|
||||||
"dark": "Sombre",
|
|
||||||
"system": "Système"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"title": "Utilisateur"
|
|
||||||
},
|
|
||||||
"zap": {
|
|
||||||
"title": "Zap",
|
|
||||||
"nwc": "Chaîne de connexion NWC"
|
|
||||||
},
|
|
||||||
"backup": {
|
|
||||||
"title": "Sauvegarde",
|
|
||||||
"privkey": {
|
|
||||||
"title": "Clé Privée",
|
|
||||||
"button": "Supprimer la clé privée"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"advanced": {
|
|
||||||
"title": "Avancé",
|
|
||||||
"cache": {
|
|
||||||
"title": "Cache",
|
|
||||||
"subtitle": "A utiliser pour accélérer la connexion nostr",
|
|
||||||
"button": "Vider"
|
|
||||||
},
|
|
||||||
"instant": {
|
|
||||||
"title": "Zap instantané",
|
|
||||||
"subtitle": "Zap avec montant par défaut, sans confirmation"
|
|
||||||
},
|
|
||||||
"defaultAmount": "Montant par défaut"
|
|
||||||
},
|
|
||||||
"about": {
|
|
||||||
"title": "A propos",
|
|
||||||
"version": "Version",
|
|
||||||
"checkUpdate": "Rechercher des mises à jour",
|
|
||||||
"installUpdate": "Installer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"onboarding": {
|
|
||||||
"home": {
|
|
||||||
"title": "Votre compte a été créé avec succès !",
|
|
||||||
"subtitle": "Pour commencer, configurons votre profil.",
|
|
||||||
"profileSettings": "Paramètres du Profil"
|
|
||||||
},
|
|
||||||
"profile": {
|
|
||||||
"title": "A propos de vous",
|
|
||||||
"subtitle": "Parlez de vous à Lume pour commencer à construire votre flux d'accueil."
|
|
||||||
},
|
|
||||||
"finish": {
|
|
||||||
"title": "Configuration du profil terminée !",
|
|
||||||
"subtitle": "Vous pouvez quitter la configuration ici et commencer à utiliser Lume.",
|
|
||||||
"report": "Signaler un problème"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"activity": {
|
|
||||||
"title": "Activité",
|
|
||||||
"empty": "Hey ! Rien de nouveau pour l'instant.",
|
|
||||||
"mention": "vous a mentionné",
|
|
||||||
"repost": "reposté",
|
|
||||||
"zap": "zappé",
|
|
||||||
"newReply": "Nouvelle réponse",
|
|
||||||
"boost": "Booster",
|
|
||||||
"boostSubtitle": "@ Quelqu'un a reposté votre note",
|
|
||||||
"conversation": "Conversation",
|
|
||||||
"conversationSubtitle": "@ Quelqu'un a répondu à votre note"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
{
|
|
||||||
"global": {
|
|
||||||
"relay": "リレー",
|
|
||||||
"back": "戻る",
|
|
||||||
"continue": "続行",
|
|
||||||
"loading": "ロード中",
|
|
||||||
"error": "エラー",
|
|
||||||
"moveLeft": "左に移動",
|
|
||||||
"moveRight": "右に移動",
|
|
||||||
"newColumn": "新規カラム",
|
|
||||||
"inspect": "のぞき見",
|
|
||||||
"loadMore": "詳細",
|
|
||||||
"delete": "削除",
|
|
||||||
"refresh": "更新",
|
|
||||||
"cancel": "キャンセル",
|
|
||||||
"save": "保存",
|
|
||||||
"post": "投稿",
|
|
||||||
"update": "更新",
|
|
||||||
"noResult": "結果が見つかりません",
|
|
||||||
"emptyFeedTitle": "このフィードは空です",
|
|
||||||
"emptyFeedSubtitle": "ユーザーをフォローしてタイムラインを構築しましょう",
|
|
||||||
"apiKey": "API キー",
|
|
||||||
"skip": "スキップ",
|
|
||||||
"close": "閉じる"
|
|
||||||
},
|
|
||||||
"nip89": {
|
|
||||||
"unsupported": "このイベントは非対応です",
|
|
||||||
"openWith": "で開く"
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"showThread": "スレッドの表示",
|
|
||||||
"showMore": "さらに表示",
|
|
||||||
"error": "イベントの取得に失敗しました",
|
|
||||||
"posted": "が投稿しました",
|
|
||||||
"replied": "が返信しました",
|
|
||||||
"reposted": "がリポストしました",
|
|
||||||
"menu": {
|
|
||||||
"viewThread": "スレッドを見る",
|
|
||||||
"copyLink": "シェアリンクをコピー",
|
|
||||||
"copyNoteId": "投稿 ID をコピー",
|
|
||||||
"copyAuthorId": "投稿者 ID をコピー",
|
|
||||||
"viewAuthor": "投稿者を表示",
|
|
||||||
"pinAuthor": "投稿者をピン留め",
|
|
||||||
"copyRaw": "生イベントをコピー",
|
|
||||||
"mute": "ミュート"
|
|
||||||
},
|
|
||||||
"buttons": {
|
|
||||||
"pin": "ピン留め",
|
|
||||||
"pinTooltip": "投稿をピン留めする",
|
|
||||||
"repost": "リポスト",
|
|
||||||
"quote": "引用",
|
|
||||||
"viewProfile": "プロフィールの表示"
|
|
||||||
},
|
|
||||||
"zap": {
|
|
||||||
"zap": "Zap",
|
|
||||||
"tooltip": "zap する",
|
|
||||||
"modalTitle": "に zap する",
|
|
||||||
"messagePlaceholder": "メッセージを入力 (任意)",
|
|
||||||
"buttonFinish": "Zap しました",
|
|
||||||
"buttonLoading": "処理中...",
|
|
||||||
"invoiceButton": "スキャンして zap",
|
|
||||||
"invoiceFooter": "Blue Wallet、Bitkit、Phoenix などの\nBitcoin Lightningに対応したウォレットが必要です。"
|
|
||||||
},
|
|
||||||
"reply": {
|
|
||||||
"single": "リプライ",
|
|
||||||
"plural": "複数リプライ",
|
|
||||||
"empty": "リプライしてみましょう!"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"avatar": "アバター",
|
|
||||||
"displayName": "表示名",
|
|
||||||
"name": "名前",
|
|
||||||
"bio": "経歴",
|
|
||||||
"lna": "ライトニングアドレス",
|
|
||||||
"website": "Web サイト",
|
|
||||||
"verified": "認証済",
|
|
||||||
"unverified": "未認証",
|
|
||||||
"follow": "フォロー",
|
|
||||||
"unfollow": "アンフォロー",
|
|
||||||
"latestPosts": "最新投稿",
|
|
||||||
"avatarButton": "アバターの変更",
|
|
||||||
"coverButton": "カバーの変更",
|
|
||||||
"editProfile": "プロフィールの編集",
|
|
||||||
"settings": "設定",
|
|
||||||
"logout": "ログアウト",
|
|
||||||
"logoutConfirmTitle": "本当にログアウトしてもよろしいですか?",
|
|
||||||
"logoutConfirmSubtitle": "いつでも再ログインすることができます。アカウントを切り替えたいだけの場合は、既存のアカウントを追加することで切り替えられます。"
|
|
||||||
},
|
|
||||||
"editor": {
|
|
||||||
"title": "新規投稿",
|
|
||||||
"placeholder": "今何してる?",
|
|
||||||
"successMessage": "投稿が正常に公開されました",
|
|
||||||
"replyPlaceholder": "リプライを投稿する"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"placeholder": "入力して検索...",
|
|
||||||
"empty": "人、投稿、キーワードなどを入力して検索しましょう"
|
|
||||||
},
|
|
||||||
"welcome": {
|
|
||||||
"title": "Lume は、Nostr を通じて人々と出会い、探索し、\n自由に思いを共有するために作られた全く新しいクライアントです。",
|
|
||||||
"signup": "Nostr を始める",
|
|
||||||
"login": "ログイン",
|
|
||||||
"footer": "Nostr を始める前に、Nostrについて学ぶ"
|
|
||||||
},
|
|
||||||
"login": {
|
|
||||||
"title": "おかえりなさい、名無しさん!",
|
|
||||||
"footer": "Lume は、秘密鍵をそれぞれの OS に対応したセキュア・ストレージ内に保管します。また、入力された秘密鍵は、パスワードや生体認証 ID によって保護されます。",
|
|
||||||
"loginWithAddress": "Nostr アドレスでログイン",
|
|
||||||
"loginWithBunker": "nsecBunker でログイン",
|
|
||||||
"or": "または、",
|
|
||||||
"loginWithPrivkey": "秘密鍵でログイン"
|
|
||||||
},
|
|
||||||
"loginWithAddress": {
|
|
||||||
"title": "Nostr アドレスを入力"
|
|
||||||
},
|
|
||||||
"loginWithBunker": {
|
|
||||||
"title": "nsecBunker のトークンを入力"
|
|
||||||
},
|
|
||||||
"loginWithPrivkey": {
|
|
||||||
"title": "秘密鍵を入力",
|
|
||||||
"subtitle": "秘密鍵は <1>{{service}}</1> 内に保管されます。\nまた、OSによって秘密鍵は保護されます。"
|
|
||||||
},
|
|
||||||
"signup": {
|
|
||||||
"title": "さぁ、はじめましょう",
|
|
||||||
"subtitle": "以下からアカウント作成の方法を 1 つ選択してください",
|
|
||||||
"selfManageMethod": "自己管理",
|
|
||||||
"selfManageMethodDescription": "自分で秘密鍵を作成し安全に保管する",
|
|
||||||
"providerMethod": "プロバイダー管理",
|
|
||||||
"providerMethodDescription": "サードパーティプロバイダーに鍵管理を委任する"
|
|
||||||
},
|
|
||||||
"signupWithSelfManage": {
|
|
||||||
"title": "これが新しいアカウント用の鍵です",
|
|
||||||
"subtitle": "秘密鍵は安全な場所に保管してください。紛失してしまった場合は、永遠にアカウントにアクセスできなくなります。",
|
|
||||||
"confirm1": "秘密鍵を紛失した場合のリスクを理解しました。",
|
|
||||||
"confirm2": "秘密鍵を安全な場所に保管し、誰にも共有しないことを承諾しました。",
|
|
||||||
"confirm3": "秘密鍵の復元ができないことを理解しました。",
|
|
||||||
"button": "鍵を保存して続行"
|
|
||||||
},
|
|
||||||
"signupWithProvider": {
|
|
||||||
"title": "さぁ、Nostr のアカウントを設定しましょう",
|
|
||||||
"username": "ユーザー名 *",
|
|
||||||
"chooseProvider": "プロバイダーを選択",
|
|
||||||
"usernameFooter": "Lume や、他の Nostr アプリにログインする場合に使用されます。アカウントを管理するプロバイダーは慎重に選択してください",
|
|
||||||
"email": "E メールでバックアップ (任意)",
|
|
||||||
"emailFooter": "パスワードを紛失した場合に使用されます"
|
|
||||||
},
|
|
||||||
"onboardingSettings": {
|
|
||||||
"title": "あともう少しです",
|
|
||||||
"subtitle": "Lume の体験を最適化しましょう",
|
|
||||||
"notification": {
|
|
||||||
"title": "プッシュ通知",
|
|
||||||
"subtitle": "プッシュ通知を有効化すると Lume から通知を受け取ることができるようになります。"
|
|
||||||
},
|
|
||||||
"lowPower": {
|
|
||||||
"title": "低電力モード",
|
|
||||||
"subtitle": "リレーの接続を制限し、すべてのメディアを非表示にします。ネットワーク環境が悪い場合などにも有効です。"
|
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"title": "翻訳 (nostr.wine)",
|
|
||||||
"subtitle": "Nostr Wine を使用し、希望の言語に自動翻訳することができます。"
|
|
||||||
},
|
|
||||||
"footer": "「設定」画面からさらに細かくカスタマイズすることができます。 後で確認するのを忘れずに。"
|
|
||||||
},
|
|
||||||
"relays": {
|
|
||||||
"global": "グローバル",
|
|
||||||
"follows": "フォロー",
|
|
||||||
"sidebar": {
|
|
||||||
"title": "接続済リレー",
|
|
||||||
"empty": "空"
|
|
||||||
},
|
|
||||||
"relayView": {
|
|
||||||
"empty": "リレー情報がロードできませんでした 😬",
|
|
||||||
"owner": "オーナー",
|
|
||||||
"contact": "連絡先",
|
|
||||||
"software": "ソフトウェア",
|
|
||||||
"nips": "対応 NIPs",
|
|
||||||
"limit": "制限",
|
|
||||||
"payment": "決済用 Web サイトを開く",
|
|
||||||
"paymentNote": "このリレーへの接続には支払いが必要です"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"suggestion": {
|
|
||||||
"title": "フォロー推奨 Nostrich",
|
|
||||||
"error": "エラーが発生しています。人気の Nostrich 情報が取得ができませんでした",
|
|
||||||
"button": "保存して戻る"
|
|
||||||
},
|
|
||||||
"interests": {
|
|
||||||
"title": "興味",
|
|
||||||
"subtitle": "ホーム・フィードに表示したい内容を選択",
|
|
||||||
"edit": "興味を編集",
|
|
||||||
"followAll": "全てフォローする",
|
|
||||||
"unfollowAll": "全てアンフォローする"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"general": {
|
|
||||||
"title": "一般",
|
|
||||||
"update": {
|
|
||||||
"title": "アップデート",
|
|
||||||
"subtitle": "自動で新規アップデートをダウンロードする"
|
|
||||||
},
|
|
||||||
"lowPower": {
|
|
||||||
"title": "低電力モード",
|
|
||||||
"subtitle": "ネットワーク環境が悪い場合などにオススメ"
|
|
||||||
},
|
|
||||||
"startup": {
|
|
||||||
"title": "起動時",
|
|
||||||
"subtitle": "ログイン時に Lume を起動する"
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"title": "メディア",
|
|
||||||
"subtitle": "自動的にメディアをロードする"
|
|
||||||
},
|
|
||||||
"hashtag": {
|
|
||||||
"title": "ハッシュタグ",
|
|
||||||
"subtitle": "コンテンツ内のハッシュタグを全て表示する"
|
|
||||||
},
|
|
||||||
"notification": {
|
|
||||||
"title": "通知",
|
|
||||||
"subtitle": "プッシュ通知の自動化"
|
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"title": "翻訳",
|
|
||||||
"subtitle": "好みの言語に自動翻訳"
|
|
||||||
},
|
|
||||||
"appearance": {
|
|
||||||
"title": "外観",
|
|
||||||
"light": "ライト",
|
|
||||||
"dark": "ダーク",
|
|
||||||
"system": "システム"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"title": "ユーザー"
|
|
||||||
},
|
|
||||||
"zap": {
|
|
||||||
"title": "Zap",
|
|
||||||
"nwc": "接続文字列"
|
|
||||||
},
|
|
||||||
"backup": {
|
|
||||||
"title": "バックアップ",
|
|
||||||
"privkey": {
|
|
||||||
"title": "秘密鍵",
|
|
||||||
"button": "秘密鍵の削除"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"advanced": {
|
|
||||||
"title": "高度な設定",
|
|
||||||
"cache": {
|
|
||||||
"title": "キャッシュ",
|
|
||||||
"subtitle": "接続を高速化する際に使用",
|
|
||||||
"button": "クリア"
|
|
||||||
},
|
|
||||||
"instant": {
|
|
||||||
"title": "即 Zap",
|
|
||||||
"subtitle": "確認なしで設定されたデフォルト枚数を zap する"
|
|
||||||
},
|
|
||||||
"defaultAmount": "デフォルト枚数"
|
|
||||||
},
|
|
||||||
"about": {
|
|
||||||
"title": "概要",
|
|
||||||
"version": "バージョン",
|
|
||||||
"checkUpdate": "アップデートの確認",
|
|
||||||
"installUpdate": "インストール"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"onboarding": {
|
|
||||||
"home": {
|
|
||||||
"title": "アカウントが正常に作成されました!",
|
|
||||||
"subtitle": "初めての方はプロフィールの設定をしましょう。",
|
|
||||||
"profileSettings": "プロフィール設定"
|
|
||||||
},
|
|
||||||
"profile": {
|
|
||||||
"title": "あなたについて",
|
|
||||||
"subtitle": "ホーム・フィードを作成するためにあなたについてもう少し教えてください。"
|
|
||||||
},
|
|
||||||
"finish": {
|
|
||||||
"title": "プロフィールの設定が完了しました!",
|
|
||||||
"subtitle": "お疲れ様でした!設定はここで終了です。 Lume を開始できます",
|
|
||||||
"report": "問題を報告する"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"activity": {
|
|
||||||
"title": "アクティビティ",
|
|
||||||
"empty": "オッ! まだ何もないようですネ!",
|
|
||||||
"mention": "にメンションされました",
|
|
||||||
"repost": "にリポストされました",
|
|
||||||
"zap": "に Zapされました",
|
|
||||||
"newReply": "新規リプライ",
|
|
||||||
"boost": "ブースト",
|
|
||||||
"boostSubtitle": "@ 誰かがあなたの投稿をリポストしました",
|
|
||||||
"conversation": "会話",
|
|
||||||
"conversationSubtitle": "@ 誰かがあなたの投稿にリプライしました"
|
|
||||||
}
|
|
||||||
}
|
|
||||||