1 Commits

Author SHA1 Message Date
0e87c22e55 WIP proof of concept to add localization using fluent-rs 2024-12-05 11:48:03 -05:00
396 changed files with 16338 additions and 56331 deletions

12
.envrc
View File

@@ -1,7 +1,6 @@
# set to false if you don't care to include android stuff
export use_android=true
export android_emulator=false
export ANDROID_DIR=crates/notedeck_chrome/android
use nix --arg use_android $use_android --arg android_emulator $android_emulator
@@ -14,11 +13,6 @@ source scripts/macos_build_secrets.sh || :
export PATH=$PATH:$HOME/.cargo/bin
export JB55=32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
export OLLAMA_HOST=http://ollama.jb55.com
# simple todo reminders
export TODO_FILE=TODO
export RUST_LOG="egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug,lnsocket=trace,notedeck_clndash=debug"
2>/dev/null todo.sh ls || :
export JACK=npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m
export VROD=npub1h50pnxqw9jg7dhr906fvy4mze2yzawf895jhnc3p7qmljdugm6gsrurqev
export JEFFG=npub1zuuajd7u3sx8xu92yav9jwxpr839cs0kc3q6t56vd5u9q033xmhsk6c2uc

View File

@@ -22,5 +22,8 @@ jobs:
if: ${{ inputs.additional-setup != '' }}
run: ${{ inputs.additional-setup }}
- name: Rust cache
uses: Swatinem/rust-cache@v2
- name: Run Tests (Native Only)
run: cargo test

View File

@@ -10,45 +10,31 @@ on:
- "*"
jobs:
lint:
name: Rustfmt + Clippy
runs-on: ubuntu-22.04
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt,clippy
- run: |
cargo fmt --all -- --check
cargo clippy
components: rustfmt
- run: cargo fmt --all -- --check
android:
name: Check (android)
runs-on: ubuntu-22.04
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt,clippy
- name: Setup Java JDK
uses: actions/setup-java@v4.5.0
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Add android rust target
run: rustup target add aarch64-linux-android
- name: Install Cargo NDK
run: cargo install cargo-ndk
- name: Run tests
run: make jni-check
components: clippy
- run: cargo clippy -- -D warnings
linux-test:
name: Test (Linux)
uses: ./.github/workflows/build-and-test.yml
with:
os: ubuntu-22.04
os: ubuntu-latest
additional-setup: |
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev
@@ -66,7 +52,7 @@ jobs:
packaging:
name: rpm/deb
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: linux-test
if: github.ref_name == 'master' || github.ref_name == 'ci'
@@ -90,6 +76,9 @@ jobs:
fi
cargo install cargo-generate-rpm cargo-deb
- name: Rust cache
uses: Swatinem/rust-cache@v2
- name: Build Cross (${{ matrix.arch }})
if: matrix.arch != runner.arch
run: cargo build --release --target=${{ matrix.arch }}-unknown-linux-gnu
@@ -100,19 +89,19 @@ jobs:
- name: Build RPM (Cross)
if: matrix.arch != runner.arch
run: cargo generate-rpm -p crates/notedeck_chrome --target=${{ matrix.arch }}-unknown-linux-gnu
run: cargo generate-rpm --target=${{ matrix.arch }}-unknown-linux-gnu
- name: Build RPM
if: matrix.arch == runner.arch
run: cargo generate-rpm -p crates/notedeck_chrome
run: cargo generate-rpm
- name: Build deb (Cross)
if: matrix.arch != runner.arch
run: cargo deb -p notedeck_chrome --target=${{ matrix.arch }}-unknown-linux-gnu
run: cargo deb --target=${{ matrix.arch }}-unknown-linux-gnu
- name: Build deb
if: matrix.arch == runner.arch
run: cargo deb -p notedeck_chrome
run: cargo deb
- name: Upload RPM
uses: actions/upload-artifact@v4
@@ -189,15 +178,10 @@ jobs:
path: packages/notedeck-${{ matrix.arch }}.dmg
windows-installer:
name: Windows Installer
name: Build Windows Installer (x86_64)
runs-on: windows-latest
needs: windows-test
if: github.ref_name == 'master' || github.ref_name == 'ci'
strategy:
fail-fast: false
matrix:
arch: [x86_64, aarch64]
steps:
# Checkout the repository
- name: Checkout Code
@@ -219,91 +203,19 @@ jobs:
- name: Install Inno Setup
run: choco install innosetup --no-progress --yes
# Set up Rust toolchain
- name: Install Rust toolchain
run: rustup target add ${{ matrix.arch }}-pc-windows-msvc
# Build
- name: Build
shell: pwsh
run: |
$target = "${{ matrix.arch }}-pc-windows-msvc"
Write-Output "Building for target: $target"
cargo build --release --target=$target
# Generate ISS Script
- name: Generate Inno Setup Script
shell: pwsh
run: |
$arch = "${{ matrix.arch }}"
$issContent = @"
[Setup]
AppName=Damus Notedeck
AppVersion=0.1
DefaultDirName={pf}\Notedeck
DefaultGroupName=Damus Notedeck
OutputDir=..\packages\$arch
OutputBaseFilename=DamusNotedeckInstaller
Compression=lzma
SolidCompression=yes
[Files]
Source: "..\target\$arch-pc-windows-msvc\release\notedeck.exe"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\Damus Notedeck"; Filename: "{app}\notedeck.exe"
[Run]
Filename: "{app}\notedeck.exe"; Description: "Launch Damus Notedeck"; Flags: nowait postinstall skipifsilent
"@
Set-Content -Path "scripts/windows-installer-$arch.iss" -Value $issContent
# Build Installer
- name: Run Inno Setup Script
run: |
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "scripts\windows-installer-${{ matrix.arch }}.iss"
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "scripts\windows-installer.iss"
# List outputs
- name: List Inno Script outputs
run: dir packages
# Move output
- name: Move Inno Script outputs to architecture-specific folder
run: |
New-Item -ItemType Directory -Force -Path packages\${{ matrix.arch }}
Move-Item -Path packages\${{ matrix.arch }}\DamusNotedeckInstaller.exe -Destination packages\${{ matrix.arch }}\DamusNotedeckInstaller.exe
# Upload the installer as an artifact
- name: Upload Installer
uses: actions/upload-artifact@v4
with:
name: DamusNotedeckInstaller-${{ matrix.arch }}.exe
path: packages\${{ matrix.arch }}\DamusNotedeckInstaller.exe
upload-artifacts:
name: Upload Artifacts to Server
runs-on: ubuntu-22.04
needs: [packaging, macos-dmg, windows-installer]
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/ci'
steps:
- name: Download all Artifacts
uses: actions/download-artifact@v4
- name: Setup SSH and Upload
run: |
eval "$(ssh-agent -s)"
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "${{ secrets.DEPLOY_SFTP_KEY }}" | tr -d '\r' | ssh-add -
echo "${{ secrets.DEPLOY_IP }} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEN65pj1cNMqlf96jZLr1i9+mnHIN4jjRPPTDix6sRnt" >> ~/.ssh/known_hosts
ls -la /home/runner/work/notedeck/notedeck/notedeck-x86_64.rpm
export ARTIFACTS=/home/runner/work/notedeck/notedeck
sftp ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_IP }} <<EOF
cd upload/artifacts
put $ARTIFACTS/notedeck-x86_64.rpm/*
put $ARTIFACTS/notedeck-x86_64.deb/*
put $ARTIFACTS/notedeck-x86_64.dmg/*
put $ARTIFACTS/notedeck-aarch64.rpm/*
put $ARTIFACTS/notedeck-aarch64.deb/*
put $ARTIFACTS/notedeck-aarch64.dmg/*
put $ARTIFACTS/DamusNotedeckInstaller-x86_64.exe/*
put $ARTIFACTS/DamusNotedeckInstaller-aarch64.exe/*
bye
EOF
name: DamusNotedeckInstaller.exe
path: packages\DamusNotedeckInstaller.exe

24
.gitignore vendored
View File

@@ -1,25 +1,17 @@
.DS_Store
.build-result
.buildcmd
TODO.bak
android-config.json
logcat.txt
build.log
rusty-tags.vi
crates/notedeck_chrome/android/app/build
perf.data
perf.data.old
.privenv
*.so
*.swp
*.jar
target
.gradle
queries/damus-notifs.json
.git
cache
/dist
/packages
.direnv/
src/camera.rs
scripts/macos_build_secrets.sh
*.patch
*.txt
/tags
.zed
.lsp
.idea
local.properties
*.mdb

View File

@@ -1 +0,0 @@
*.ftl

View File

@@ -1,68 +0,0 @@
# Notedeck Beta - v0.4 - 2025-05-05
# Added
- Dave nostr ai assistant app
- GIFs!
- Fulltext note search
- Add full screen images, add zoom & pan
- Zaps! NWC/ Wallet ui
- Introduce last note per pubkey feed (experimental)
- Allow multiple media uploads per selection
- Major Android improvements (still wip)
- Added notedeck app sidebar
- User Tagging
- Note truncation
- Local network note broadcast, broadcast notes to other notedeck notes while you're offline
- Mute list support (reading)
- Relay list support
- Ctrl-enter to send notes
- Added relay indexing (relay columns soon)
- Click hashtags to open hashtag timeline
# Fixed
- Fix timelines sometimes not updating (stale feeds)
- Fix ui bounciness when loading profile pictures
- Fix unselectable post replies
# Notedeck Alpha 2 - v0.3 - 2025-01-31
## Added
- Clicking a mention now opens profile page (William Casarin)
- Note previews when hovering reply descriptions (William Casarin)
- Media uploads (kernelkind)
- Profile editing (kernelkind)
- Add hashtags to posts (Daniel Saxton)
- Enhanced command-line interface for user interactions (Ken Sedgwick)
- Various Android updates and compatibility improvements (Ken Sedgwick, William Casarin)
- Debug features for user relay-list and mute list synchronization (Ken Sedgwick)
## Changed
- Add confirmation when deleting columns (kernelkind)
- Enhance Android build and performance (Ken Sedgwick)
- Image cache handling using sha256 hash (kieran)
- Introduction of decks_cache and improvements (kernelkind)
- Migrated to egui v0.29.1 (William Casarin)
- Only show column delete button when not navigating (William Casarin)
- Show profile pictures in column headers (William Casarin)
- Show usernames in user columns (William Casarin)
- Switch to only notes & replies on some tabs (William Casarin)
- Tombstone muted notes (Ken)
- Pointer interactions enhancements in UI (William Casarin)
- Persistent theme setup across sessions (kernelkind)
- Increased ping intervals for network performance (William Casarin)
- Nostrdb update for async support (Ken Sedgwick)
## Fixed
- Fix GIT_COMMIT_HASH compilation issue (William Casarin)
- Fix avatar alignment in profile previews (William Casarin)
- Fix broken quote repost hitbox (William Casarin)
- Fix crash when navigating in debug mode (William Casarin)
- Fix long delays when reconnecting (William Casarin)
- Fix repost button size (William Casarin)
- Fixed since kind filters (kernelkind)
- Clippy warnings resolved (Dimitris Apostolou)
## Refactoring & Improvements
- Numerous internal structural improvements and modularization (William Casarin, Ken Sedgwick)

4950
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,94 +1,78 @@
[workspace]
resolver = "2"
package.version = "0.6.0"
members = [
"crates/notedeck",
"crates/notedeck_chrome",
"crates/notedeck_columns",
"crates/notedeck_dave",
"crates/notedeck_notebook",
"crates/notedeck_ui",
"crates/notedeck_clndash",
[package]
name = "notedeck"
version = "0.2.0"
authors = ["William Casarin <jb55@jb55.com>"]
edition = "2021"
default-run = "notedeck"
#rust-version = "1.60"
license = "GPLv3"
description = "A multiplatform nostr client"
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui", "crates/notedeck_clndash",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["lib", "cdylib"]
[workspace.dependencies]
opener = "0.8.2"
chrono = "0.4.40"
base32 = "0.4.0"
base64 = "0.22.1"
rmpv = "1.3.0"
bech32 = { version = "0.11", default-features = false }
bitflags = "2.5.0"
dirs = "5.0.1"
eframe = { version = "0.31.1", default-features = false, features = [ "wgpu", "wayland", "x11", "android-game-activity" ] }
egui = { version = "0.31.1", features = ["serde"] }
egui-wgpu = "0.31.1"
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "de6e2d51892478fdd516df166f866e64dedbae07" }
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
#egui_virtual_list = "0.6.0"
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
ehttp = "0.5.0"
enostr = { path = "crates/enostr" }
ewebsock = { version = "0.2.0", features = ["tls"] }
fluent = "0.17.0"
fluent-resmgr = "0.0.8"
fluent-langneg = "0.13"
hex = { version = "0.4.3", features = ["serde"] }
egui = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", features = ["serde"] }
eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "eframe", default-features = false, features = [ "wgpu", "wayland", "x11", "android-native-activity" ] }
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "71154e4100775f6932ee517da4350c433ba14ec7" }
[dependencies]
#egui-android = { git = "https://github.com/jb55/egui-android.git" }
egui = { workspace = true }
eframe = { workspace = true }
egui_extras = { workspace = true }
ehttp = "0.2.0"
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "fd0900bdff4be35709372e921f2b49f68b261469" }
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" }
reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] }
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
indexmap = "2.6.0"
log = "0.4.17"
md5 = "0.7.0"
nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] }
nwc = "0.39.0"
mio = { version = "1.0.3", features = ["os-poll", "net"] }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3" }
#nostrdb = "0.6.1"
notedeck = { path = "crates/notedeck" }
notedeck_chrome = { path = "crates/notedeck_chrome" }
notedeck_clndash = { path = "crates/notedeck_clndash" }
notedeck_columns = { path = "crates/notedeck_columns" }
notedeck_dave = { path = "crates/notedeck_dave" }
notedeck_notebook = { path = "crates/notedeck_notebook" }
notedeck_ui = { path = "crates/notedeck_ui" }
tokenator = { path = "crates/tokenator" }
once_cell = "1.19.0"
robius-open = "0.1"
poll-promise = { version = "0.3.0", features = ["tokio"] }
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
serde = { version = "1", features = ["derive"] } # You only need this if you want app persistence
serde_derive = "1"
serde = { version = "1", features = ["derive"] } # You only need this if you want app persistence
tracing = "0.1.40"
#wasm-bindgen = "0.2.83"
nostrdb = { workspace = true }
#nostrdb = { path = "/Users/jb55/dev/github/damus-io/nostrdb-rs" }
#nostrdb = "0.3.4"
enostr = { path = "enostr" }
serde_json = "1.0.89"
env_logger = "0.10.0"
puffin_egui = { version = "0.27.0", optional = true }
puffin = { version = "0.19.0", optional = true }
hex = "0.4.3"
base32 = "0.4.0"
strum = "0.26"
strum_macros = "0.26"
thiserror = "2.0.7"
tokio = { version = "1.16", features = ["macros", "rt-multi-thread", "fs"] }
tracing = { version = "0.1.40", features = ["log"] }
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tempfile = "3.13.0"
unic-langid = { version = "0.9.6", features = ["macros"] }
url = "2.5.2"
urlencoding = "2.1.3"
bitflags = "2.5.0"
uuid = { version = "1.10.0", features = ["v4"] }
sha2 = "0.10.8"
bincode = "1.3.3"
mime_guess = "2.0.5"
pretty_assertions = "1.4.1"
jni = "0.21.1"
profiling = "1.0"
lightning-invoice = { version = "0.33.1", features = ["serde"] }
secp256k1 = "0.30.0"
hashbrown = "0.15.2"
openai-api-rs = "6.0.3"
re_memory = "0.23.4"
oot_bitset = "0.1.1"
blurhash = "0.2.3"
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "092a83b747937a2890ac219617a4252c001842ea", features = [ "game-activity" ] }
indexmap = "2.6.0"
dirs = "5.0.1"
tracing-appender = "0.2.3"
urlencoding = "2.1.3"
open = "5.3.0"
url = "2.5"
fluent-resmgr = "0.0.7"
fluent-fallback = "0.7.1"
fluent = "0.16.1"
unic-langid = "0.9.5"
[dev-dependencies]
tempfile = "3.13.0"
[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "2.11.0"
[features]
default = []
profiling = ["puffin", "puffin_egui", "eframe/puffin"]
debug-widget-callstack = ["egui/callstack"]
debug-interactive-widgets = []
[profile.small]
inherits = 'release'
@@ -98,23 +82,72 @@ codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = 'abort' # Abort on panic
strip = true # Strip symbols from binary*
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6"
tracing-wasm = "0.2"
wasm-bindgen-futures = "0.4"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.16", features = ["macros", "rt-multi-thread", "fs"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11.1"
android-activity = { version = "0.4", features = [ "native-activity" ] }
#winit = "0.28.6"
winit = { version = "0.29", features = [ "android-native-activity" ] }
#winit = { git="https://github.com/rust-windowing/winit.git", rev = "2a58b785fed2a3746f7c7eebce95bce67ddfd27c", features = ["android-native-activity"] }
[package.metadata.bundle]
identifier = "com.damus.notedeck"
icon = ["assets/app_icon.icns"]
[package.metadata.android]
package = "com.damus.app"
apk_name = "damus"
#assets = "assets"
[[package.metadata.android.uses_feature]]
name = "android.hardware.vulkan.level"
required = true
version = 1
[[package.metadata.android.uses_permission]]
name = "android.permission.WRITE_EXTERNAL_STORAGE"
max_sdk_version = 18
[[package.metadata.android.uses_permission]]
name = "android.permission.READ_EXTERNAL_STORAGE"
max_sdk_version = 18
[package.metadata.android.signing.release]
path = "damus.keystore"
keystore_password = "damuskeystore"
[[package.metadata.android.uses_permission]]
name = "android.permission.INTERNET"
[package.metadata.android.application]
label = "Damus"
[package.metadata.generate-rpm]
assets = [
{ source = "target/release/notedeck", dest = "/usr/bin/notedeck", mode = "755" },
]
[[bin]]
name = "notedeck"
path = "src/bin/notedeck.rs"
[[bin]]
name = "ui_preview"
path = "src/ui_preview/main.rs"
[patch.crates-io]
#egui = { path = "/home/jb55/dev/github/emilk/egui/crates/egui" }
#eframe = { path = "/home/jb55/dev/github/emilk/egui/crates/eframe" }
#egui-winit = { path = "/home/jb55/dev/github/emilk/egui/crates/egui-winit" }
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
egui = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
eframe = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
egui-winit = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
egui_extras = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
epaint = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
#winit = { git = "https://github.com/damus-io/winit", rev = "701a43d3c6479b0a3869acd2cebbfd410d399a59" }
#winit = { path = "/home/jb55/dev/github/rust-windowing/winit" }
#android-activity = { git = "https://github.com/damus-io/android-activity", rev = "f56c974aa5182d5fbd361879f5899eb8f11a37ec" }
#android-activity = { path = "/home/jb55/dev/github/rust-mobile/android-activity/android-activity" }
egui = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb" }
eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "eframe" }
emath = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "emath" }
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras" }

View File

@@ -1,30 +1,8 @@
.DEFAULT_GOAL := check
.PHONY: fake
ANDROID_DIR := crates/notedeck_chrome/android
check:
all:
cargo check
tags: fake
rusty-tags vi
find . -type d -name target -prune -o -type f -name '*.rs' -print | xargs ctags
jni: fake
cargo ndk --target arm64-v8a -o $(ANDROID_DIR)/app/src/main/jniLibs/ build --profile release
jni-check: fake
cargo ndk --target arm64-v8a check
apk: jni
cd $(ANDROID_DIR) && ./gradlew build
gradle:
cd $(ANDROID_DIR) && ./gradlew build
push-android-config:
adb push android-config.json /sdcard/Android/data/com.damus.notedeck/files/android-config.json
android: jni
cd $(ANDROID_DIR) && ./gradlew installDebug
adb shell am start -n com.damus.notedeck/.MainActivity
adb logcat -v color -s GameActivity -s RustStdoutStderr -s threaded_app | tee logcat.txt
.PHONY: fake

186
README.md
View File

@@ -1,148 +1,96 @@
# Notedeck
# Damus Notedeck
[![CI](https://github.com/damus-io/notedeck/actions/workflows/rust.yml/badge.svg)](https://github.com/damus-io/notedeck/actions/workflows/rust.yml)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/damus-io/notedeck)
[![CI](https://github.com/damus-io/notedeck/actions/workflows/rust.yml/badge.svg)](https://github.com/damus-io/notedeck/actions/workflows/rust.yml)
A modern, multiplatform Nostr client built with Rust. Notedeck provides a feature-rich experience for interacting with the Nostr protocol on both desktop and Android platforms.
A multiplatform nostr client. Works on android and desktop
<p align="center">
<img src="https://cdn.jb55.com/s/6130555f03db55b2.png" alt="Notedeck Desktop Screenshot" width="700">
</p>
The desktop client is called notedeck:
## ✨ Features
![notedeck](https://cdn.jb55.com/s/6130555f03db55b2.png)
- **Multi-column Layout**: TweetDeck-style interface for viewing different Nostr content
- **Dave AI Assistant**: AI-powered assistant that can search and analyze Nostr content
- **Profile Management**: View and edit Nostr profiles
- **Media Support**: View and upload images with GIF support
- **Lightning Integration**: Zap (tip) content creators with Bitcoin Lightning
- **Cross-platform**: Works on desktop (Linux, macOS, Windows) and Android
## Android
## 📱 Mobile Support
Look it actually runs on android!
Notedeck runs smoothly on Android devices with a responsive interface:
<img src="https://cdn.jb55.com/s/bebeeadf7001fae1.png" height="500px" />
<p align="center">
<img src="https://cdn.jb55.com/s/bebeeadf7001fae1.png" alt="Notedeck Android Screenshot" height="500px">
</p>
## 🏗️ Project Structure
```
notedeck
├── crates
│ ├── notedeck - Core library with shared functionality
│ ├── notedeck_chrome - UI container and navigation framework
│ ├── notedeck_columns - TweetDeck-style column interface
│ ├── notedeck_dave - AI assistant for Nostr
│ ├── notedeck_ui - Shared UI components
│ └── tokenator - String token parsing library
```
## 🚀 Getting Started
### Desktop
To run on desktop platforms:
## Usage
```bash
# Development build
cargo run -- --debug
# Release build
cargo run --release
$ ./target/release/notedeck
```
### Android
# Developer Setup
For Android devices:
## Desktop (Linux/MacOS, Windows?)
If you're running debian-based machine like Ubuntu or ElementaryOS, all you need is to install [rustup] and run `sudo apt install build-essential`.
```bash
# Install required target
rustup target add aarch64-linux-android
# Build and install on connected device
cargo apk run --release -p notedeck_chrome
$ cargo run --release
```
### Android Emulator
## Android
1. Install [Android Studio](https://developer.android.com/studio)
2. Open 'Device Manager' and create a device with API level `34` and ABI `arm64-v8a`
3. Start the emulator
4. Run: `cargo apk run --release -p notedeck_chrome`
The dev shell should also have all of the android-sdk dependencies needed for development, but you still need the `aarch64-linux-android` rustup target installed:
## 🧪 Development
```
$ rustup target add aarch64-linux-android
```
### Android Configuration
To run on a real device, just type:
Customize Android views for testing:
```bash
$ cargo apk run --release
```
1. Copy `example-android-config.json` to `android-config.json`
2. Run `make push-android-config` to deploy to your device
## Android Emulator
### Setting Up Developer Environment
- Install [Android Studio](https://developer.android.com/studio)
- Open 'Device Manager' in Android Studio
- Add a new device with API level `34` and ABI `arm64-v8a` (even though the app uses 30, the 30 emulator can't find the vulkan adapter, but 34 works fine)
- Start up the emulator
while the emulator is running, run:
```bash
cargo apk run --release
```
The app should appear on the emulator
[direnv]: https://direnv.net/
## Previews
You can preview individual widgets and views by running the preview script:
```bash
./preview RelayView
./preview ProfilePreview
# ... etc
```
When adding new previews you need to implement the Preview trait for your
view/widget and then add it to the `src/ui_preview/main.rs` bin:
```rust
previews!(runner, name,
RelayView,
AccountLoginView,
ProfilePreview,
);
```
## Contributing
Configure the developer environment:
```bash
./scripts/dev_setup.sh
```
This adds pre-commit hooks for proper code formatting.
This will add the pre-commit hook to your local repository to suggest proper formatting before commits.
## 📚 Documentation
Detailed developer documentation is available in each crate:
- [Notedeck Core](./crates/notedeck/DEVELOPER.md)
- [Notedeck Chrome](./crates/notedeck_chrome/DEVELOPER.md)
- [Notedeck Columns](./crates/notedeck_columns/DEVELOPER.md)
- [Dave AI Assistant](./crates/notedeck_dave/docs/README.md)
- [UI Components](./crates/notedeck_ui/docs/components.md)
## 🔄 Release Status
Notedeck is currently in **BETA** status. For the latest changes, see the [CHANGELOG](./CHANGELOG.md).
## Future
Notedeck allows for app development built on top of the performant, built specifically for nostr database [nostrdb][nostrdb]. An example app written on notedeck is [Dave](./crates/notedeck_dave)
Building on notedeck dev documentation is also on the roadmap.
## 🤝 Contributing
### Developers
Contributions are welcome! Please check the developer documentation and follow these guidelines:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
### Translators
Help us bring Notedeck to non-English speakers!
Request to join the Notedeck translations team through [Crowdin](https://crowdin.com/project/notedeck).
If you do not have a Crowdin account, sign up for one.
If you do not see your language, please request it in Crowdin.
## 🔒 Security
For security issues, please refer to our [Security Policy](./SECURITY.md).
## 📄 License
This project is licensed under the GPL - see license information in individual crates.
## 👥 Authors
- William Casarin <jb55@jb55.com>
- kernelkind <kernelkind@gmail.com>
- And [contributors](https://github.com/damus-io/notedeck/graphs/contributors)
[nostrdb]: https://github.com/damus-io/nostrdb
[rustup]: https://rustup.rs/

0
TODO
View File

11
android
View File

@@ -1,11 +0,0 @@
#!/usr/bin/env bash
root_dir=$PWD
cargo ndk --target arm64-v8a -o ./crates/notedeck_chrome/android/app/src/main/jniLibs/ build --profile release
cd ./crates/notedeck_chrome/android
./gradlew build && ./gradlew installDebug
cd $root_dir

View File

@@ -1,186 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256mm"
height="256mm"
viewBox="0 0 256 256"
version="1.1"
id="svg5"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
sodipodi:docname="damus-bg.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:blackoutopacity="0.0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.5946522"
inkscape:cx="407.8014"
inkscape:cy="491.88416"
inkscape:window-width="1296"
inkscape:window-height="916"
inkscape:window-x="222"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="svg5"
inkscape:showpageshadow="2"
inkscape:deskcolor="#d1d1d1" />
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient39361">
<stop
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
offset="0"
id="stop39357" />
<stop
style="stop-color:#d600fc;stop-opacity:0.95433789;"
offset="1"
id="stop39359" />
</linearGradient>
<inkscape:path-effect
effect="bspline"
id="path-effect255"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<linearGradient
inkscape:collect="always"
id="linearGradient2119">
<stop
style="stop-color:#1c55ff;stop-opacity:1;"
offset="0"
id="stop2115" />
<stop
style="stop-color:#7f35ab;stop-opacity:1;"
offset="0.5"
id="stop2123" />
<stop
style="stop-color:#ff0bd6;stop-opacity:1;"
offset="1"
id="stop2117" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2119"
id="linearGradient2121"
x1="10.067794"
y1="248.81357"
x2="246.56145"
y2="7.1864405"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient39361"
id="linearGradient39367"
x1="62.104473"
y1="128.78963"
x2="208.25758"
y2="128.78963"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Background"
inkscape:groupmode="layer"
id="layer1"
sodipodi:insensitive="true"
style="display:inline">
<rect
style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
id="rect61"
width="256"
height="256"
x="-5.3875166e-08"
y="-1.0775033e-07"
ry="0"
inkscape:label="Gradient"
sodipodi:insensitive="true" />
</g>
<g
id="g407"
inkscape:label="Logo"
style="display:none">
<g
id="layer2"
inkscape:label="LogoStroke"
style="display:inline">
<path
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
id="path253" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Poly">
<path
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
id="path4648" />
<path
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
id="path9299" />
<path
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
id="path9301" />
<path
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
id="path9368" />
<path
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
id="path9370" />
<path
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
id="path9372" />
<path
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
id="path9374" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Vertices">
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path27764"
cx="106.86934"
cy="142.38014"
r="2.0022209" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle28773"
cx="111.54119"
cy="99.221161"
r="2.0022209" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle29091"
cx="165.90784"
cy="101.36163"
r="2.0022209" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -1,186 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256mm"
height="256mm"
viewBox="0 0 256 256"
version="1.1"
id="svg5"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
sodipodi:docname="damusfg.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:blackoutopacity="0.0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.5946522"
inkscape:cx="407.8014"
inkscape:cy="491.88416"
inkscape:window-width="1296"
inkscape:window-height="916"
inkscape:window-x="222"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="svg5"
inkscape:showpageshadow="2"
inkscape:deskcolor="#d1d1d1" />
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient39361">
<stop
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
offset="0"
id="stop39357" />
<stop
style="stop-color:#d600fc;stop-opacity:0.95433789;"
offset="1"
id="stop39359" />
</linearGradient>
<inkscape:path-effect
effect="bspline"
id="path-effect255"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<linearGradient
inkscape:collect="always"
id="linearGradient2119">
<stop
style="stop-color:#1c55ff;stop-opacity:1;"
offset="0"
id="stop2115" />
<stop
style="stop-color:#7f35ab;stop-opacity:1;"
offset="0.5"
id="stop2123" />
<stop
style="stop-color:#ff0bd6;stop-opacity:1;"
offset="1"
id="stop2117" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2119"
id="linearGradient2121"
x1="10.067794"
y1="248.81357"
x2="246.56145"
y2="7.1864405"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient39361"
id="linearGradient39367"
x1="62.104473"
y1="128.78963"
x2="208.25758"
y2="128.78963"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Background"
inkscape:groupmode="layer"
id="layer1"
sodipodi:insensitive="true"
style="display:none">
<rect
style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
id="rect61"
width="256"
height="256"
x="-5.3875166e-08"
y="-1.0775033e-07"
ry="0"
inkscape:label="Gradient"
sodipodi:insensitive="true" />
</g>
<g
id="g407"
inkscape:label="Logo"
transform="matrix(0.61641471,0,0,0.61641471,51.853453,49.401806)">
<g
id="layer2"
inkscape:label="LogoStroke"
style="display:inline">
<path
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
id="path253" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Poly">
<path
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
id="path4648" />
<path
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
id="path9299" />
<path
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
id="path9301" />
<path
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
id="path9368" />
<path
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
id="path9370" />
<path
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
id="path9372" />
<path
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
id="path9374" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Vertices">
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path27764"
cx="106.86934"
cy="142.38014"
r="2.0022209" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle28773"
cx="111.54119"
cy="99.221161"
r="2.0022209" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle29091"
cx="165.90784"
cy="101.36163"
r="2.0022209" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -1,12 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="16" fill="#2C2C2C"/>
<g clip-path="url(#clip0_3568_3937)">
<path opacity="0.12" d="M15.9998 21.3334C18.9454 21.3334 21.3332 18.9456 21.3332 16.0001C21.3332 13.0546 18.9454 10.6667 15.9998 10.6667C13.0543 10.6667 10.6665 13.0546 10.6665 16.0001C10.6665 18.9456 13.0543 21.3334 15.9998 21.3334Z" fill="white"/>
<path d="M21.3335 15.9999C21.3335 18.9455 18.9457 21.3333 16.0002 21.3333M21.3335 15.9999C21.3335 13.0544 18.9457 10.6666 16.0002 10.6666M21.3335 15.9999H10.6668M16.0002 21.3333C13.0546 21.3333 10.6668 18.9455 10.6668 15.9999M16.0002 21.3333C17.3342 19.8728 18.0927 17.9775 18.1339 15.9999C18.0927 14.0223 17.3342 12.127 16.0002 10.6666M16.0002 21.3333C14.6661 19.8728 13.9084 17.9775 13.8672 15.9999C13.9084 14.0223 14.6661 12.127 16.0002 10.6666M16.0002 10.6666C13.0546 10.6666 10.6668 13.0544 10.6668 15.9999M12.0002 21.3333C12.0002 22.0697 11.4032 22.6666 10.6668 22.6666C9.93045 22.6666 9.3335 22.0697 9.3335 21.3333C9.3335 20.5969 9.93045 19.9999 10.6668 19.9999C11.4032 19.9999 12.0002 20.5969 12.0002 21.3333ZM22.6668 21.3333C22.6668 22.0697 22.0699 22.6666 21.3335 22.6666C20.5971 22.6666 20.0002 22.0697 20.0002 21.3333C20.0002 20.5969 20.5971 19.9999 21.3335 19.9999C22.0699 19.9999 22.6668 20.5969 22.6668 21.3333ZM12.0002 10.6666C12.0002 11.403 11.4032 11.9999 10.6668 11.9999C9.93045 11.9999 9.3335 11.403 9.3335 10.6666C9.3335 9.93021 9.93045 9.33325 10.6668 9.33325C11.4032 9.33325 12.0002 9.93021 12.0002 10.6666ZM22.6668 10.6666C22.6668 11.403 22.0699 11.9999 21.3335 11.9999C20.5971 11.9999 20.0002 11.403 20.0002 10.6666C20.0002 9.93021 20.5971 9.33325 21.3335 9.33325C22.0699 9.33325 22.6668 9.93021 22.6668 10.6666Z" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_3568_3937">
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256mm"
height="256mm"
viewBox="0 0 256 256"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="clnlogo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.078823"
inkscape:cx="396.72867"
inkscape:cy="561.25984"
inkscape:window-width="2020"
inkscape:window-height="1420"
inkscape:window-x="270"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="matrix(1.0800571,0,0,1.0347966,-2.6149197,-3.0116377)"
style="display:inline">
<g
id="g4"
transform="matrix(0.43515072,0,0,0.43515072,68.289343,9.0200629)">
<path
class="st1"
d="M 214.6,0 2.2,285.8 246.4,222.3 100.1,222.4 Z"
id="path3"
style="fill:#f0d003" />
<path
fill="#fffae6"
d="M 31.8,550.7 244.1,264.9 0,328.4 146.3,328.3 Z"
id="path4" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,9 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M68 144.8C68 117.917 68 104.476 73.2317 94.2085C77.8336 85.1767 85.1767 77.8336 94.2085 73.2317C104.476 68 117.917 68 144.8 68H367.2C394.083 68 407.524 68 417.792 73.2317C426.823 77.8336 434.166 85.1767 438.768 94.2085C444 104.476 444 117.917 444 144.8V367.2C444 394.083 444 407.524 438.768 417.792C434.166 426.823 426.823 434.166 417.792 438.768C407.524 444 394.083 444 367.2 444H144.8C117.917 444 104.476 444 94.2085 438.768C85.1767 434.166 77.8336 426.823 73.2317 417.792C68 407.524 68 394.083 68 367.2V144.8ZM88 139.2C88 121.278 88 112.317 91.4878 105.472C94.5557 99.4511 99.4511 94.5557 105.472 91.4878C112.317 88 121.278 88 139.2 88H188C199.201 88 204.802 88 209.08 90.1799C212.843 92.0973 215.903 95.1569 217.82 98.9202C220 103.198 220 108.799 220 120V392C220 403.201 220 408.802 217.82 413.08C215.903 416.843 212.843 419.903 209.08 421.82C204.802 424 199.201 424 188 424H139.2C121.278 424 112.317 424 105.472 420.512C99.4511 417.444 94.5557 412.549 91.4878 406.528C88 399.683 88 390.722 88 372.8V139.2ZM242.18 98.9202C240 103.198 240 108.799 240 120V392C240 403.201 240 408.802 242.18 413.08C244.097 416.843 247.157 419.903 250.92 421.82C255.198 424 260.799 424 272 424H295C306.201 424 311.802 424 316.08 421.82C319.843 419.903 322.903 416.843 324.82 413.08C327 408.802 327 403.201 327 392V120C327 108.799 327 103.198 324.82 98.9202C322.903 95.1569 319.843 92.0973 316.08 90.1799C311.802 88 306.201 88 295 88H272C260.799 88 255.198 88 250.92 90.1799C247.157 92.0973 244.097 95.1569 242.18 98.9202Z" fill="url(#paint0_linear_19_1273)"/>
<defs>
<linearGradient id="paint0_linear_19_1273" x1="444" y1="444" x2="-5.21356" y2="206.447" gradientUnits="userSpaceOnUse">
<stop stop-color="#DACAA0"/>
<stop offset="1" stop-color="#8C93D7"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,3 +0,0 @@
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.32844 1.4159C8.36511 1.12223 8.20384 0.839518 7.93237 0.721671C7.66091 0.603828 7.34424 0.679064 7.15478 0.906418L1.20178 8.04995C1.0989 8.17335 0.994695 8.29835 0.918828 8.40822C0.847082 8.51208 0.716075 8.71635 0.712028 8.98475C0.707388 9.29208 0.844335 9.58449 1.08338 9.77762C1.2922 9.94635 1.53297 9.97649 1.65871 9.98788C1.79166 9.99995 1.95438 9.99988 2.11504 9.99988H6.24504L5.67204 14.5838C5.63533 14.8775 5.79664 15.1602 6.06811 15.2781C6.33958 15.3959 6.65624 15.3207 6.84571 15.0933L12.7987 7.94975C12.9016 7.82635 13.0058 7.70135 13.0816 7.59149C13.1534 7.48762 13.2844 7.28335 13.2884 7.01495C13.293 6.70762 13.1561 6.41525 12.9171 6.22207C12.7082 6.05333 12.4675 6.02321 12.3418 6.01183C12.2088 5.99979 12.046 5.99982 11.8854 5.99985L7.75544 5.99986L8.32844 1.41588V1.4159Z" fill="#FFB757"/>
</svg>

Before

Width:  |  Height:  |  Size: 922 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.12" d="M6 14V8H10V14" fill="white"/>
<path d="M5.99992 14V9.06667C5.99992 8.69327 5.99992 8.5066 6.07259 8.364C6.1365 8.23853 6.23849 8.1366 6.36392 8.07267C6.50654 8 6.69325 8 7.06659 8H8.93325C9.30665 8 9.49332 8 9.63592 8.07267C9.76139 8.1366 9.86332 8.23853 9.92725 8.364C9.99992 8.5066 9.99992 8.69327 9.99992 9.06667V14M1.33325 6.33333L7.35992 1.81333C7.58945 1.64121 7.70419 1.55514 7.83019 1.52196C7.94145 1.49268 8.05838 1.49268 8.16965 1.52197C8.29565 1.55514 8.41038 1.64121 8.63992 1.81333L14.6666 6.33333M2.66659 5.33333V11.8667C2.66659 12.6134 2.66659 12.9868 2.81191 13.272C2.93975 13.5229 3.14372 13.7269 3.3946 13.8547C3.67982 14 4.05318 14 4.79992 14H11.1999C11.9467 14 12.3201 14 12.6053 13.8547C12.8561 13.7269 13.0601 13.5229 13.1879 13.272C13.3333 12.9868 13.3333 12.6134 13.3333 11.8667V5.33333L9.27992 2.29333C8.82092 1.94907 8.59138 1.77695 8.33932 1.71059C8.11685 1.65203 7.88298 1.65203 7.66052 1.71059C7.40845 1.77695 7.17892 1.94907 6.71992 2.29333L2.66659 5.33333Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="#8a8a8a" xmlns="http://www.w3.org/2000/svg" class="icon-xl-heavy"><path d="M15.6729 3.91287C16.8918 2.69392 18.8682 2.69392 20.0871 3.91287C21.3061 5.13182 21.3061 7.10813 20.0871 8.32708L14.1499 14.2643C13.3849 15.0293 12.3925 15.5255 11.3215 15.6785L9.14142 15.9899C8.82983 16.0344 8.51546 15.9297 8.29289 15.7071C8.07033 15.4845 7.96554 15.1701 8.01005 14.8586L8.32149 12.6785C8.47449 11.6075 8.97072 10.615 9.7357 9.85006L15.6729 3.91287ZM18.6729 5.32708C18.235 4.88918 17.525 4.88918 17.0871 5.32708L11.1499 11.2643C10.6909 11.7233 10.3932 12.3187 10.3014 12.9613L10.1785 13.8215L11.0386 13.6986C11.6812 13.6068 12.2767 13.3091 12.7357 12.8501L18.6729 6.91287C19.1108 6.47497 19.1108 5.76499 18.6729 5.32708ZM11 3.99929C11.0004 4.55157 10.5531 4.99963 10.0008 5.00007C9.00227 5.00084 8.29769 5.00827 7.74651 5.06064C7.20685 5.11191 6.88488 5.20117 6.63803 5.32695C6.07354 5.61457 5.6146 6.07351 5.32698 6.63799C5.19279 6.90135 5.10062 7.24904 5.05118 7.8542C5.00078 8.47105 5 9.26336 5 10.4V13.6C5 14.7366 5.00078 15.5289 5.05118 16.1457C5.10062 16.7509 5.19279 17.0986 5.32698 17.3619C5.6146 17.9264 6.07354 18.3854 6.63803 18.673C6.90138 18.8072 7.24907 18.8993 7.85424 18.9488C8.47108 18.9992 9.26339 19 10.4 19H13.6C14.7366 19 15.5289 18.9992 16.1458 18.9488C16.7509 18.8993 17.0986 18.8072 17.362 18.673C17.9265 18.3854 18.3854 17.9264 18.673 17.3619C18.7988 17.1151 18.8881 16.7931 18.9393 16.2535C18.9917 15.7023 18.9991 14.9977 18.9999 13.9992C19.0003 13.4469 19.4484 12.9995 20.0007 13C20.553 13.0004 21.0003 13.4485 20.9999 14.0007C20.9991 14.9789 20.9932 15.7808 20.9304 16.4426C20.8664 17.116 20.7385 17.7136 20.455 18.2699C19.9757 19.2107 19.2108 19.9756 18.27 20.455C17.6777 20.7568 17.0375 20.8826 16.3086 20.9421C15.6008 21 14.7266 21 13.6428 21H10.3572C9.27339 21 8.39925 21 7.69138 20.9421C6.96253 20.8826 6.32234 20.7568 5.73005 20.455C4.78924 19.9756 4.02433 19.2107 3.54497 18.2699C3.24318 17.6776 3.11737 17.0374 3.05782 16.3086C2.99998 15.6007 2.99999 14.7266 3 13.6428V10.3572C2.99999 9.27337 2.99998 8.39922 3.05782 7.69134C3.11737 6.96249 3.24318 6.3223 3.54497 5.73001C4.02433 4.7892 4.78924 4.0243 5.73005 3.54493C6.28633 3.26149 6.88399 3.13358 7.55735 3.06961C8.21919 3.00673 9.02103 3.00083 9.99922 3.00007C10.5515 2.99964 10.9996 3.447 11 3.99929Z" fill="#8a8a8a"></path></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.12" d="M12.0001 5.33337C12.0001 4.27251 11.5787 3.25509 10.8286 2.50495C10.0784 1.7548 9.06095 1.33337 8.00008 1.33337C6.93922 1.33337 5.92182 1.7548 5.17167 2.50495C4.42152 3.25509 4.0001 4.27251 4.0001 5.33337C4.0001 7.39351 3.48041 8.80404 2.89987 9.73697C2.41018 10.524 2.16534 10.9174 2.17431 11.0272C2.18426 11.1488 2.21 11.1951 2.30794 11.2678C2.3964 11.3334 2.79516 11.3334 3.59266 11.3334H12.4075C13.205 11.3334 13.6038 11.3334 13.6922 11.2678C13.7902 11.1951 13.8159 11.1488 13.8259 11.0272C13.8349 10.9174 13.59 10.524 13.1003 9.73697C12.5197 8.80404 12.0001 7.39351 12.0001 5.33337Z" fill="white"/>
<path d="M9.33342 14H6.66675M12.0001 5.33337C12.0001 4.27251 11.5787 3.25509 10.8286 2.50495C10.0784 1.7548 9.06095 1.33337 8.00008 1.33337C6.93922 1.33337 5.92182 1.7548 5.17167 2.50495C4.42152 3.25509 4.0001 4.27251 4.0001 5.33337C4.0001 7.39351 3.48041 8.80404 2.89987 9.73697C2.41018 10.524 2.16534 10.9174 2.17431 11.0272C2.18426 11.1488 2.21 11.1951 2.30794 11.2678C2.3964 11.3334 2.79516 11.3334 3.59266 11.3334H12.4075C13.205 11.3334 13.6038 11.3334 13.6922 11.2678C13.7902 11.1951 13.8159 11.1488 13.8259 11.0272C13.8349 10.9174 13.59 10.524 13.1003 9.73697C12.5197 8.80404 12.0001 7.39351 12.0001 5.33337Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,11 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_11_352)">
<path opacity="1.0" d="M8.66663 2L9.82276 5.00591C10.0108 5.49473 10.1048 5.73914 10.251 5.94473C10.3805 6.12693 10.5397 6.28613 10.7219 6.41569C10.9275 6.56187 11.1719 6.65587 11.6607 6.84387L14.6666 8L11.6607 9.15613C11.1719 9.34413 10.9275 9.43813 10.7219 9.58433C10.5397 9.71387 10.3805 9.87307 10.251 10.0553C10.1048 10.2609 10.0108 10.5053 9.82276 10.9941L8.66663 14L7.51049 10.9941C7.32249 10.5053 7.22849 10.2609 7.08229 10.0553C6.95276 9.87307 6.79356 9.71387 6.61135 9.58433C6.40577 9.43813 6.16135 9.34413 5.67253 9.15613L2.66663 8L5.67253 6.84387C6.16135 6.65587 6.40577 6.56187 6.61135 6.41569C6.79356 6.28613 6.95276 6.12693 7.08229 5.94473C7.22849 5.73914 7.32249 5.49473 7.51049 5.00591L8.66663 2Z" fill="white"/>
<path d="M3.00004 14.6667V11.3334M3.00004 4.66671V1.33337M1.33337 3.00004H4.66671M1.33337 13H4.66671M8.66671 2.00004L7.51057 5.00595C7.32257 5.49477 7.22857 5.73918 7.08237 5.94477C6.95284 6.12697 6.79364 6.28617 6.61143 6.41573C6.40585 6.56191 6.16143 6.65591 5.67261 6.84391L2.66671 8.00004L5.67261 9.15617C6.16143 9.34417 6.40585 9.43817 6.61143 9.58437C6.79364 9.71391 6.95284 9.87311 7.08237 10.0553C7.22857 10.2609 7.32257 10.5053 7.51057 10.9941L8.66671 14L9.82284 10.9941C10.0108 10.5053 10.1048 10.2609 10.251 10.0553C10.3806 9.87311 10.5398 9.71391 10.722 9.58437C10.9276 9.43817 11.172 9.34417 11.6608 9.15617L14.6667 8.00004L11.6608 6.84391C11.172 6.65591 10.9276 6.56191 10.722 6.41573C10.5398 6.28617 10.3806 6.12697 10.251 5.94477C10.1048 5.73918 10.0108 5.49477 9.82284 5.00595L8.66671 2.00004Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_11_352">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.12" d="M11.8667 14C12.6134 14 12.9868 14 13.272 13.8547C13.5229 13.7269 13.7269 13.5229 13.8547 13.272C14 12.9868 14 12.6134 14 11.8667V7.46671C14 6.71997 14 6.3466 13.8547 6.06139C13.7269 5.81051 13.5229 5.60653 13.272 5.4787C12.9868 5.33337 12.6134 5.33337 11.8667 5.33337H4.13333C3.3866 5.33337 3.01323 5.33337 2.72801 5.4787C2.47713 5.60653 2.27315 5.8105 2.14533 6.06139C2 6.3466 2 6.71997 2 7.46671V11.8667C2 12.6134 2 12.9868 2.14533 13.272C2.27315 13.5229 2.47713 13.7269 2.72801 13.8547C3.01323 14 3.38659 14 4.13333 14H11.8667Z" fill="white"/>
<path d="M10.6667 5.33322V3.00032C10.6667 2.44583 10.6667 2.16858 10.5499 1.9982C10.4478 1.84934 10.2897 1.74821 10.1119 1.71794C9.9082 1.68329 9.65647 1.79947 9.153 2.03183L3.23934 4.76121C2.79034 4.96845 2.56583 5.07207 2.40141 5.23277C2.25604 5.37483 2.14508 5.54825 2.077 5.73977C2 5.95641 2 6.20367 2 6.6982V9.99987M11 9.66653H11.0067M2 7.46653V11.8665C2 12.6133 2 12.9867 2.14533 13.2719C2.27315 13.5227 2.47713 13.7267 2.72801 13.8545C3.01323 13.9999 3.38659 13.9999 4.13333 13.9999H11.8667C12.6134 13.9999 12.9868 13.9999 13.272 13.8545C13.5229 13.7267 13.7269 13.5227 13.8547 13.2719C14 12.9867 14 12.6133 14 11.8665V7.46653C14 6.7198 14 6.34645 13.8547 6.06123C13.7269 5.81035 13.5229 5.60637 13.272 5.47855C12.9868 5.33322 12.6134 5.33322 11.8667 5.33322H4.13333C3.3866 5.33322 3.01323 5.33322 2.72801 5.47854C2.47713 5.60637 2.27315 5.81035 2.14533 6.06123C2 6.34645 2 6.7198 2 7.46653ZM11.3333 9.66653C11.3333 9.85067 11.1841 9.99987 11 9.99987C10.8159 9.99987 10.6667 9.85067 10.6667 9.66653C10.6667 9.48247 10.8159 9.3332 11 9.3332C11.1841 9.3332 11.3333 9.48247 11.3333 9.66653Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 866 B

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
MIPMAP="../crates/notedeck_chrome/android/app/src/main/res/mipmap-"
function mkicon() {
local name="$1"
echo "making icon $name"
mkdir -p "${MIPMAP}/{l,m,h,xh,xxh,xxxh}dpi"
inkscape "$name".svg -w 36 -h 36 -o ${MIPMAP}ldpi/"$name".png &
inkscape "$name".svg -w 48 -h 48 -o ${MIPMAP}mdpi/"$name".png &
inkscape "$name".svg -w 72 -h 72 -o ${MIPMAP}hdpi/"$name".png &
inkscape "$name".svg -w 96 -h 96 -o ${MIPMAP}xhdpi/"$name".png &
inkscape "$name".svg -w 144 -h 144 -o ${MIPMAP}xxhdpi/"$name".png &
inkscape "$name".svg -w 192 -h 192 -o ${MIPMAP}xxxhdpi/"$name".png &
wait
}
mkicon "damusfg"
mkicon "damusbg"

View File

@@ -1,412 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Über mich
# Column title for account management
Accounts_f018 = Konten
# Button label to add a relay
Add_269d = Hinzufügen
# Label for add column button
Add_47df = Hinzufügen
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Eine andere Wallet hinzufügen, die nur für dieses Konto verwendet wird
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Wallet hinzufügen um fortzufahren
# Button label to add a new account
Add_account_1cfc = Konto hinzufügen
# Column title for adding new account
Add_Account_d06c = Konto hinzufügen
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Algorithmus-Spalte hinzufügen
# Column title for adding new column
Add_Column_c764 = Spalte hinzufügen
# Column title for adding new deck
Add_Deck_fabf = Deck hinzufügen
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Externe Benachrichtigungsspalte hinzufügen
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Hashtag-Spalte hinzufügen
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Letzte Notizen-Spalte hinzufügen
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Benachrichtigungs-Spalte hinzufügen
# Button label to add a relay
Add_relay_269d = Relay hinzufügen
# Button label to add a wallet
Add_Wallet_d1be = Wallet hinzufügen
# Title for algorithmic feeds column
Algo_2452 = Algorithmus
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmische Feeds zur Hilfe bei der Entdeckung von Notizen
# Label for zap amount input field
Amount_70f0 = Menge
# Label for appearance settings section
Appearance_4c7f = Darstellung
# Button to send message to Dave AI assistant
Ask_b7f4 = Fragen
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Frage Dave etwas...
# Profile banner URL field label
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Senden
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Lokal senden
# Button label to cancel an action
Cancel_ed3b = Abbrechen
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Abbrechen
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Zwischenspeicher leeren
# Hover text for editable zap amount
Click_to_edit_0414 = Zum Bearbeiten anklicken
# Column title for note composition
Compose_Note_c094 = Notiz erstellen
# Label for configure relays, settings section
Configure_relays_d156 = Relays konfigurieren
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Bestätigen
# Button label to confirm an action
Confirm_f8a6 = Bestätigen
# Status label for connected relay
Connected_f8cc = Verbunden
# Status label for connecting relay
Connecting_6b7e = Verbinde...
# Title for contact list column
Contact_List_f85a = Kontaktliste
# Column title for contact lists
Contacts_7533 = Kontakte
# Column title for last notes per contact
Contacts__last_notes_3f84 = Kontakte (letzte Notizen)
# Button label to copy logs
Copy_a688 = Kopieren
# Button to copy media link to clipboard
Copy_Link_dc7c = Link kopieren
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Notiz-ID kopieren
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Notiz-JSON kopieren
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Pubkey kopieren
# Copy the text content of the note to clipboard
Copy_Text_f81c = Text kopieren
# Relative time in days
count_d_b9be = { $count }T
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }min
# Relative time in months
count_mo_7aba = { $count }M
# Relative time in seconds
count_s_aa26 = { $count }s
# Relative time in weeks
count_w_7468 = { $count }W
# Relative time in years
count_y_9408 = { $count }J
# Button to create a new account
Create_Account_6994 = Konto erstellen
# Button label to create a new deck
Create_Deck_16b7 = Deck erstellen
# Column title for custom timelines
Custom_a69e = Benutzerdefiniert
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
# Column title for support page
Damus_Support_27c0 = Damus Support
# Label for Theme Dark, Appearance settings section
Dark_85fe = Dunkel
# Label for deck name input field
Deck_name_cd32 = Deck-Name
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Standardbetrag pro Zap:
# Name of the default deck feed
Default_Deck_fcca = Standard-Deck
# Button label to delete a deck
Delete_Deck_bb29 = Deck löschen
# Tooltip for deleting a column
Delete_this_column_8d5a = Diese Spalte löschen
# Button label to delete a wallet
Delete_Wallet_d1d4 = Wallet löschen
# Profile display name field label
Display_name_f9d9 = Anzeigename
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" wird zur Identifikation verwendet
# Column title for editing deck
Edit_Deck_4018 = Deck bearbeiten
# Button label to edit a deck
Edit_Deck_fd93 = Deck bearbeiten
# Button label to edit user profile
Edit_Profile_49e6 = Profil bearbeiten
# Column title for profile editing
Edit_Profile_8ad4 = Profil bearbeiten
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Gewünschte Hashtags hier eingeben (für mehrere, durch Leerzeichen trennen)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Relay hier eingeben
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Hier den Benutzerschlüssel (npub, hex, nip05) eingeben...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Gib deinen Schlüssel ein
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 =
Gib deinen öffentlichen Schlüssel (npub), eine Nostr-Adresse (z.B. {$address}) oder deinen privaten Schlüssel (nsec) ein.
Für das Veröffentlichen von Beiträgen und andere Aktionen ist dein privater Schlüssel erforderlich.
# Label for find user button
Find_User_bd12 = Profil finden
# Label for font size, Appearance settings section
Font_size_dd73 = Schriftgröße:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Startseite
# Label for deck icon selection
Icon_b0ab = Symbol
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Bildcache Größe:
# Title for individual user column
Individual_b776 = Individuell
# Error message for invalid zap amount
Invalid_amount_6630 = Ungültiger Betrag
# Error message for invalid key input
Invalid_key_4726 = Ungültiger Schlüssel
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = Ungültige NWC URI
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Behalte den Überblick über deine Notizen & Antworten
# Label for language, Appearance settings section
Language_e264 = Sprache:
# Title for last note per user column
Last_Note_per_User_17ad = Letzte Notiz pro Profil
# Label for Theme Light, Appearance settings section
Light_7475 = Hell
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
# Login page title
Login_9eef = Anmelden
# Login button text
Login_now___let_s_do_this_5630 = Jetzt anmelden — auf geht's!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Medien von einem Profil, dem du nicht folgst
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Verschiebt diese Spalte an eine andere Position
# Title for the user's deck
My_Deck_4ac5 = Mein Deck
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Neu bei Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Nostr-Adresse (NIP-05-Identität)
# Default username when profile is not available
nostrich_df29 = Nostrich
# Status label for disconnected relay
Not_Connected_6292 = Nicht verbunden
# Link text for note references
note_cad6 = Notiz
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck ist ein Beta-Produkt. Erwarte Fehler und kontaktiere uns, wenn Probleme oder Fehler auftreten.
# Filter label for notes only view
Notes_03fb = Notizen
# Label for notes-only filter
Notes_60d2 = Notizen
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notizen & Antworten
# Label for notes and replies filter
Notes___Replies_6e3b = Notizen & Antworten
# Column title for notifications
Notifications_d673 = Benachrichtigungen
# Title for notifications column
Notifications_ef56 = Benachrichtigungen
# Relative time for very recent events (less than 3 seconds)
now_2181 = Gerade eben
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = An
# Button label to open email client
Open_Email_25e9 = E-Mail öffnen
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Öffne deinen Standard-E-Mail-Client, um Hilfe vom Damus-Team zu erhalten
# Label for others settings section
Others_7267 = Andere
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Füge hier deine NWC-URI ein...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Bitte erstelle einen Namen für das Deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Bitte erstelle einen Namen für das Deck und wähle ein Symbol aus.
# Error message for missing deck icon
Please_select_an_icon_655b = Bitte wählen ein Symbol aus.
# Button label to post a note
Post_now_8a49 = Jetzt veröffentlichen
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Drücke die Schaltfläche unten, um deine neuesten Protokolle in die Zwischenablage deines Systems zu kopieren. Dann füge sie in deine E-Mail ein.
# Profile picture URL field label
Profile_picture_81ff = Profilbild
# Column title for quote composition
Quote_475c = Zitat
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Zitat von unbekannter Notiz
# Label for read-only profile mode
Read_only_82ff = Nur Lesezugriff
# Column title for relay management
Relays_9d89 = Relays
# Label for relay list section
Relays_ad5e = Relays
# Column title for reply composition
Reply_3bf1 = Antwort
# Hover text for reply button
Reply_to_this_note_f5de = Auf diese Notiz antworten
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Antwort auf unbekannte Notiz
# Fallback template for replying to user
replying_to__user_15ab = Antwort an { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = Antwort an { $user } im Beitrag von jemandem
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = Antwort auf { $user }'s { $note } in { $thread_user }'s { $thread }
# Template for replying to user's note
replying_to__user__s__note_ccba = Antwort auf { $user }'s { $note }
# Template for replying to root thread
replying_to__user__s__thread_444d = Antwort auf { $user }'s { $thread }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = Antwort auf eine Notiz
# Hover text for repost button
Repost_this_note_8e56 = Diese Notiz teilen
# Label for reposted notes
Reposted_61c8 = Teilen
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Zurücksetzen
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Zurücksetzen
# Heading for support section
Running_into_a_bug_1796 = Ein Fehler aufgetreten?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = Sats
# Button to save default zap amount
Save_6f7c = Speichern
# Button label to save profile changes
Save_changes_00db = Änderungen speichern
# Column title for search page
Search_c573 = Suche
# Placeholder for search notes input field
Search_notes_42a6 = Notizen suchen...
# Search in progress message
Searching_for___query_5d18 = Suche nach '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Notizen von deinen Kontakten ansehen
# Description for universe column
See_the_whole_nostr_universe_7694 = Sieh dir das ganze Nostr-Universum an
# Button label to send a zap
Send_1ea4 = Senden
# Column title for app settings
Settings_7a4f = Einstellungen
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Zeige die letzte Notiz für jedes Profil aus einer Liste
# Button label to sign out of account
Sign_out_337b = Abmelden
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notizen anderer Profile
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Mitteilungen anderer Profile
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Neueste Antworten zuerst sortieren:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Die letzte Notiz für jedes Profil aus deiner Kontaktliste anzeigen
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mit einem bestimmten Hashtag auf dem Laufenden bleiben
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Bleibe auf dem Laufenden mit Benachrichtigungen und Erwähnungen
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Bleib auf dem Laufenden bei den Notizen & Antworten anderer
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Bleib bei den Benachrichtigungen und Erwähnungen anderer auf dem Laufenden
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Bleib bei den Notizen & Antworten eines anderen auf dem Laufenden
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Bleib bei deinen Benachrichtigungen und Erwähnungen auf dem Laufenden
# Step 1 label in support instructions
Step_1_8656 = Schritt 1
# Step 2 label in support instructions
Step_2_d08d = Schritt 2
# Label for storage settings section
Storage_ed65 = Speicher
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Abonniere die Notizen von jemandem
# Support email address
Support_email_44d9 = E-Mail Support:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Zum Hellmodus wechseln
# Button text to load blurred media
Tap_to_Load_4b05 = Zum Laden antippen
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Die Testphase des Dave Nostr KI-Assistenten ist beendet :(. Vielen Dank fürs Ausprobieren! Zap-fähiger Dave kommt bald!
# Label for theme, Appearance settings section
Theme_4aac = Design:
# Column title for note thread view
Thread_0f20 = Unterhaltung
# Link text for thread references
thread_ad1f = Unterhaltung
# Title for universe column
Universe_e01e = Weltraum
# Column title for universe feed
Universe_ffaa = Weltraum
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Diese Wallet nur für das aktuelle Konto verwenden
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" bei "{ $domain }" wird für die Identifikation verwendet werden
# Profile username field label
Username_daa7 = Benutzername
# Label for view folder button, Storage settings section
View_folder_9742 = Ordner anzeigen
# Column title for wallet management
Wallet_5e50 = Wallet
# Hint for deck name input field
We_recommend_short_names_083e = Wir empfehlen kurze Namen
# Profile website field label
Website_7980 = Website
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Schreib hier eine richtig coole Notiz...
# Placeholder text for key input field
Your_key_here_81bd = Dein Schlüssel hier...
# Title for your notes column
Your_Notes_f6db = Deine Notizen
# Title for your notifications column
Your_Notifications_080d = Deine Benachrichtigungen
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zappe diese Notiz
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Zoomstufe:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] { $count } Ergebnis für '{ $query } gefunden'
*[other] { $count } Ergebnisse für '{ $query } gefunden'
}

View File

@@ -1,605 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = About
# Column title for account management
Accounts_f018 = Accounts
# Button label to add a relay
Add_269d = Add
# Label for add column button
Add_47df = Add
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Add a different wallet that will only be used for this account
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Add a wallet to continue
# Button label to add a new account
Add_account_1cfc = Add account
# Column title for adding new account
Add_Account_d06c = Add Account
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Add Algo Column
# Column title for adding new column
Add_Column_c764 = Add Column
# Column title for adding new deck
Add_Deck_fabf = Add Deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Add External Notifications Column
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Add Hashtag Column
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Add Last Notes Column
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Add Notifications Column
# Button label to add a relay
Add_relay_269d = Add relay
# Button label to add a wallet
Add_Wallet_d1be = Add Wallet
# Title for algorithmic feeds column
Algo_2452 = Algo
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmic feeds to aid in note discovery
# Label for zap amount input field
Amount_70f0 = Amount
# Label for appearance settings section
Appearance_4c7f = Appearance
# Button to send message to Dave AI assistant
Ask_b7f4 = Ask
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Ask dave anything...
# Profile banner URL field label
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Broadcast
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Broadcast Local
# Button label to cancel an action
Cancel_ed3b = Cancel
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancel
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Clear cache
# Hover text for editable zap amount
Click_to_edit_0414 = Click to edit
# Column title for note composition
Compose_Note_c094 = Compose Note
# Label for configure relays, settings section
Configure_relays_d156 = Configure relays
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirm
# Button label to confirm an action
Confirm_f8a6 = Confirm
# Status label for connected relay
Connected_f8cc = Connected
# Status label for connecting relay
Connecting_6b7e = Connecting...
# Title for contact list column
Contact_List_f85a = Contact List
# Column title for contact lists
Contacts_7533 = Contacts
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contacts (last notes)
# Button label to copy logs
Copy_a688 = Copy
# Button to copy media link to clipboard
Copy_Link_dc7c = Copy Link
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copy Note ID
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copy Note JSON
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copy Pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copy Text
# Relative time in days
count_d_b9be = {$count}d
# Relative time in hours
count_h_3ecb = {$count}h
# Relative time in minutes
count_m_b41e = {$count}m
# Relative time in months
count_mo_7aba = {$count}mo
# Relative time in seconds
count_s_aa26 = {$count}s
# Relative time in weeks
count_w_7468 = {$count}w
# Relative time in years
count_y_9408 = {$count}y
# Button to create a new account
Create_Account_6994 = Create Account
# Button label to create a new deck
Create_Deck_16b7 = Create Deck
# Column title for custom timelines
Custom_a69e = Custom
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Customize Zap Amount
# Column title for support page
Damus_Support_27c0 = Damus Support
# Label for Theme Dark, Appearance settings section
Dark_85fe = Dark
# Label for deck name input field
Deck_name_cd32 = Deck name
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Default amount per zap:
# Name of the default deck feed
Default_Deck_fcca = Default Deck
# Button label to delete a deck
Delete_Deck_bb29 = Delete Deck
# Tooltip for deleting a column
Delete_this_column_8d5a = Delete this column
# Button label to delete a wallet
Delete_Wallet_d1d4 = Delete Wallet
# Profile display name field label
Display_name_f9d9 = Display name
# Domain identification message
domain___will_be_used_for_identification_b67e = "{$domain}" will be used for identification
# Column title for editing deck
Edit_Deck_4018 = Edit Deck
# Button label to edit a deck
Edit_Deck_fd93 = Edit Deck
# Button label to edit user profile
Edit_Profile_49e6 = Edit Profile
# Column title for profile editing
Edit_Profile_8ad4 = Edit Profile
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Enter the desired hashtags here (for multiple space-separated)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Enter the relay here
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Enter the user's key (npub, hex, nip05) here...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Enter your key
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Enter your public key (npub), nostr address (e.g. {$address}), or private key (nsec). You must enter your private key to be able to post, reply, etc.
# Label for find user button
Find_User_bd12 = Find User
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Home
# Label for deck icon selection
Icon_b0ab = Icon
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Image cache size:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Invalid amount
# Error message for invalid key input
Invalid_key_4726 = Invalid key.
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = Invalid NWC URI
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Keep track of your notes & replies
# Label for language, Appearance settings section
Language_e264 = Language:
# Title for last note per user column
Last_Note_per_User_17ad = Last Note per User
# Label for Theme Light, Appearance settings section
Light_7475 = Light
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Lightning network address (lud16)
# Login page title
Login_9eef = Login
# Login button text
Login_now___let_s_do_this_5630 = Login now — let's do this!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Media from someone you don't follow
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Moves this column to another position
# Title for the user's deck
My_Deck_4ac5 = My Deck
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = New to Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Nostr address (NIP-05 identity)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = Not Connected
# Link text for note references
note_cad6 = note
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck is a beta product. Expect bugs and contact us when you run into issues.
# Filter label for notes only view
Notes_03fb = Notes
# Label for notes-only filter
Notes_60d2 = Notes
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notes & Replies
# Label for notes and replies filter
Notes___Replies_6e3b = Notes & Replies
# Column title for notifications
Notifications_d673 = Notifications
# Title for notifications column
Notifications_ef56 = Notifications
# Relative time for very recent events (less than 3 seconds)
now_2181 = now
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = Open Email
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Open your default email client to get help from the Damus team
# Label for others settings section
Others_7267 = Others
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Paste your NWC URI here...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Please create a name for the deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Please create a name for the deck and select an icon.
# Error message for missing deck icon
Please_select_an_icon_655b = Please select an icon.
# Button label to post a note
Post_now_8a49 = Post now
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Press the button below to copy your most recent logs to your system's clipboard. Then paste it into your email.
# Profile picture URL field label
Profile_picture_81ff = Profile picture
# Column title for quote composition
Quote_475c = Quote
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Quote of unknown note
# Label for read-only profile mode
Read_only_82ff = Read only
# Column title for relay management
Relays_9d89 = Relays
# Label for relay list section
Relays_ad5e = Relays
# Column title for reply composition
Reply_3bf1 = Reply
# Hover text for reply button
Reply_to_this_note_f5de = Reply to this note
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Reply to unknown note
# Fallback template for replying to user
replying_to__user_15ab = replying to {$user}
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = replying to {$user} in someone's thread
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = replying to {$user}'s {$note} in {$thread_user}'s {$thread}
# Template for replying to user's note
replying_to__user__s__note_ccba = replying to {$user}'s {$note}
# Template for replying to root thread
replying_to__user__s__thread_444d = replying to {$user}'s {$thread}
# Fallback text when reply note is not found
replying_to_a_note_e0bc = replying to a note
# Hover text for repost button
Repost_this_note_8e56 = Repost this note
# Label for reposted notes
Reposted_61c8 = Reposted
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Reset
# Heading for support section
Running_into_a_bug_1796 = Running into a bug?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Save
# Button label to save profile changes
Save_changes_00db = Save changes
# Column title for search page
Search_c573 = Search
# Placeholder for search notes input field
Search_notes_42a6 = Search notes...
# Search in progress message
Searching_for___query_5d18 = Searching for '{$query}'
# Description for Home column
See_notes_from_your_contacts_ac16 = See notes from your contacts
# Description for universe column
See_the_whole_nostr_universe_7694 = See the whole nostr universe
# Button label to send a zap
Send_1ea4 = Send
# Column title for app settings
Settings_7a4f = Settings
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Show the last note for each user from a list
# Button label to sign out of account
Sign_out_337b = Sign out
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Someone else's Notes
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Someone else's Notifications
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source the last note for each user in your contact list
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Stay up to date with a certain hashtag
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Stay up to date with notifications and mentions
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Stay up to date with someone else's notes & replies
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Stay up to date with someone else's notifications and mentions
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Stay up to date with someone's notes & replies
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Stay up to date with your notifications and mentions
# Step 1 label in support instructions
Step_1_8656 = Step 1
# Step 2 label in support instructions
Step_2_d08d = Step 2
# Label for storage settings section
Storage_ed65 = Storage
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Subscribe to someone else's notes
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Subscribe to someone's notes
# Support email address
Support_email_44d9 = Support email:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Switch to dark mode
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Switch to light mode
# Button text to load blurred media
Tap_to_Load_4b05 = Tap to Load
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon!
# Label for theme, Appearance settings section
Theme_4aac = Theme:
# Column title for note thread view
Thread_0f20 = Thread
# Link text for thread references
thread_ad1f = thread
# Title for universe column
Universe_e01e = Universe
# Column title for universe feed
Universe_ffaa = Universe
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Use this wallet for the current account only
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{$username}" at "{$domain}" will be used for identification
# Profile username field label
Username_daa7 = Username
# Label for view folder button, Storage settings section
View_folder_9742 = View folder
# Column title for wallet management
Wallet_5e50 = Wallet
# Hint for deck name input field
We_recommend_short_names_083e = We recommend short names
# Profile website field label
Website_7980 = Website
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Write a banger note here...
# Placeholder text for key input field
Your_key_here_81bd = Your key here...
# Title for your notes column
Your_Notes_f6db = Your Notes
# Title for your notifications column
Your_Notifications_080d = Your Notifications
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap this note
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Zoom Level:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] Got {$count} result for '{$query}'
*[other] Got {$count} results for '{$query}'
}

View File

@@ -0,0 +1 @@
universe-title = Universe with Fluent Translation

View File

@@ -1,605 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = {"["}Àbóút{"]"}
# Column title for account management
Accounts_f018 = {"["}Àççóúñts{"]"}
# Button label to add a relay
Add_269d = {"["}Àdd{"]"}
# Label for add column button
Add_47df = {"["}Àdd{"]"}
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = {"["}Àdd à dífféréñt wàllét thàt wíll óñly bé úséd fór thís àççóúñt{"]"}
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = {"["}Àdd à wàllét tó çóñtíñúé{"]"}
# Button label to add a new account
Add_account_1cfc = {"["}Àdd àççóúñt{"]"}
# Column title for adding new account
Add_Account_d06c = {"["}Àdd Àççóúñt{"]"}
# Column title for adding algorithm column
Add_Algo_Column_0d75 = {"["}Àdd Àlgó Çólúmñ{"]"}
# Column title for adding new column
Add_Column_c764 = {"["}Àdd Çólúmñ{"]"}
# Column title for adding new deck
Add_Deck_fabf = {"["}Àdd Déçk{"]"}
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = {"["}Àdd Éxtérñàl Ñótífíçàtíóñs Çólúmñ{"]"}
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = {"["}Àdd Hàshtàg Çólúmñ{"]"}
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = {"["}Àdd Làst Ñótés Çólúmñ{"]"}
# Column title for adding notifications column
Add_Notifications_Column_79f8 = {"["}Àdd Ñótífíçàtíóñs Çólúmñ{"]"}
# Button label to add a relay
Add_relay_269d = {"["}Àdd rélày{"]"}
# Button label to add a wallet
Add_Wallet_d1be = {"["}Àdd Wàllét{"]"}
# Title for algorithmic feeds column
Algo_2452 = {"["}Àlgó{"]"}
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = {"["}Àlgóríthmíç fééds tó àíd íñ ñóté dísçóvéry{"]"}
# Label for zap amount input field
Amount_70f0 = {"["}Àmóúñt{"]"}
# Label for appearance settings section
Appearance_4c7f = {"["}Àppéàràñçé{"]"}
# Button to send message to Dave AI assistant
Ask_b7f4 = {"["}Àsk{"]"}
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = {"["}Àsk dàvé àñythíñg...{"]"}
# Profile banner URL field label
Banner_52ef = {"["}Bàññér{"]"}
# Beta version label
BETA_8e5d = {"["}BÉTÀ{"]"}
# Broadcast the note to all connected relays
Broadcast_fe43 = {"["}Bróàdçàst{"]"}
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = {"["}Bróàdçàst Lóçàl{"]"}
# Button label to cancel an action
Cancel_ed3b = {"["}Çàñçél{"]"}
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = {"["}Çàñçél{"]"}
# Label for clear cache button, Storage settings section
Clear_cache_dccb = {"["}Çléàr çàçhé{"]"}
# Hover text for editable zap amount
Click_to_edit_0414 = {"["}Çlíçk tó édít{"]"}
# Column title for note composition
Compose_Note_c094 = {"["}Çómpósé Ñóté{"]"}
# Label for configure relays, settings section
Configure_relays_d156 = {"["}Çóñfígúré rélàys{"]"}
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = {"["}Çóñfírm{"]"}
# Button label to confirm an action
Confirm_f8a6 = {"["}Çóñfírm{"]"}
# Status label for connected relay
Connected_f8cc = {"["}Çóññéçtéd{"]"}
# Status label for connecting relay
Connecting_6b7e = {"["}Çóññéçtíñg...{"]"}
# Title for contact list column
Contact_List_f85a = {"["}Çóñtàçt Líst{"]"}
# Column title for contact lists
Contacts_7533 = {"["}Çóñtàçts{"]"}
# Column title for last notes per contact
Contacts__last_notes_3f84 = {"["}Çóñtàçts (làst ñótés){"]"}
# Button label to copy logs
Copy_a688 = {"["}Çópy{"]"}
# Button to copy media link to clipboard
Copy_Link_dc7c = {"["}Çópy Líñk{"]"}
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = {"["}Çópy Ñóté ÍD{"]"}
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = {"["}Çópy Ñóté JSÓÑ{"]"}
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"}
# Copy the text content of the note to clipboard
Copy_Text_f81c = {"["}Çópy Téxt{"]"}
# Relative time in days
count_d_b9be = {"["}{$count}d{"]"}
# Relative time in hours
count_h_3ecb = {"["}{$count}h{"]"}
# Relative time in minutes
count_m_b41e = {"["}{$count}m{"]"}
# Relative time in months
count_mo_7aba = {"["}{$count}mó{"]"}
# Relative time in seconds
count_s_aa26 = {"["}{$count}s{"]"}
# Relative time in weeks
count_w_7468 = {"["}{$count}w{"]"}
# Relative time in years
count_y_9408 = {"["}{$count}y{"]"}
# Button to create a new account
Create_Account_6994 = {"["}Çréàté Àççóúñt{"]"}
# Button label to create a new deck
Create_Deck_16b7 = {"["}Çréàté Déçk{"]"}
# Column title for custom timelines
Custom_a69e = {"["}Çústóm{"]"}
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = {"["}Çústómízé Zàp Àmóúñt{"]"}
# Column title for support page
Damus_Support_27c0 = {"["}Dàmús Súppórt{"]"}
# Label for Theme Dark, Appearance settings section
Dark_85fe = {"["}Dàrk{"]"}
# Label for deck name input field
Deck_name_cd32 = {"["}Déçk ñàmé{"]"}
# Label for decks section in side panel
DECKS_1fad = {"["}DÉÇKS{"]"}
# Label for default zap amount input
Default_amount_per_zap_399d = {"["}Défàúlt àmóúñt pér zàp:{"]"}
# Name of the default deck feed
Default_Deck_fcca = {"["}Défàúlt Déçk{"]"}
# Button label to delete a deck
Delete_Deck_bb29 = {"["}Délété Déçk{"]"}
# Tooltip for deleting a column
Delete_this_column_8d5a = {"["}Délété thís çólúmñ{"]"}
# Button label to delete a wallet
Delete_Wallet_d1d4 = {"["}Délété Wàllét{"]"}
# Profile display name field label
Display_name_f9d9 = {"["}Dísplày ñàmé{"]"}
# Domain identification message
domain___will_be_used_for_identification_b67e = {"["}"{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"}
# Column title for editing deck
Edit_Deck_4018 = {"["}Édít Déçk{"]"}
# Button label to edit a deck
Edit_Deck_fd93 = {"["}Édít Déçk{"]"}
# Button label to edit user profile
Edit_Profile_49e6 = {"["}Édít Prófílé{"]"}
# Column title for profile editing
Edit_Profile_8ad4 = {"["}Édít Prófílé{"]"}
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = {"["}Éñtér thé désíréd hàshtàgs héré (fór múltíplé spàçé-sépàràtéd){"]"}
# Placeholder for relay input field
Enter_the_relay_here_1c8b = {"["}Éñtér thé rélày héré{"]"}
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = {"["}Éñtér thé úsér's kéy (ñpúb, héx, ñíp05) héré...{"]"}
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = {"["}Éñtér yóúr kéy{"]"}
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = {"["}Éñtér yóúr públíç kéy (ñpúb), ñóstr àddréss (é.g. {$address}), ór prívàté kéy (ñséç). Yóú múst éñtér yóúr prívàté kéy tó bé àblé tó póst, réply, étç.{"]"}
# Label for find user button
Find_User_bd12 = {"["}Fíñd Úsér{"]"}
# Label for font size, Appearance settings section
Font_size_dd73 = {"["}Fóñt sízé:{"]"}
# Title for hashtags column
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
# Title for Home column
Home_8c19 = {"["}Hómé{"]"}
# Label for deck icon selection
Icon_b0ab = {"["}Íçóñ{"]"}
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = {"["}Ímàgé çàçhé sízé:{"]"}
# Title for individual user column
Individual_b776 = {"["}Íñdívídúàl{"]"}
# Error message for invalid zap amount
Invalid_amount_6630 = {"["}Íñvàlíd àmóúñt{"]"}
# Error message for invalid key input
Invalid_key_4726 = {"["}Íñvàlíd kéy.{"]"}
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = {"["}Íñvàlíd ÑWÇ ÚRÍ{"]"}
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = {"["}100K{"]"}
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = {"["}10K{"]"}
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = {"["}20K{"]"}
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = {"["}50K{"]"}
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = {"["}5K{"]"}
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = {"["}Kéép tràçk óf yóúr ñótés & réplíés{"]"}
# Label for language, Appearance settings section
Language_e264 = {"["}Làñgúàgé:{"]"}
# Title for last note per user column
Last_Note_per_User_17ad = {"["}Làst Ñóté pér Úsér{"]"}
# Label for Theme Light, Appearance settings section
Light_7475 = {"["}Líght{"]"}
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = {"["}Líghtñíñg ñétwórk àddréss (lúd16){"]"}
# Login page title
Login_9eef = {"["}Lógíñ{"]"}
# Login button text
Login_now___let_s_do_this_5630 = {"["}Lógíñ ñów — lét's dó thís!{"]"}
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = {"["}Médíà fróm sóméóñé yóú dóñ't fóllów{"]"}
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = {"["}Móvés thís çólúmñ tó àñóthér pósítíóñ{"]"}
# Title for the user's deck
My_Deck_4ac5 = {"["}My Déçk{"]"}
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = {"["}Ñéw tó Ñóstr?{"]"}
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = {"["}Ñóstr àddréss (ÑÍP-05 ídéñtíty){"]"}
# Default username when profile is not available
nostrich_df29 = {"["}ñóstríçh{"]"}
# Status label for disconnected relay
Not_Connected_6292 = {"["}Ñót Çóññéçtéd{"]"}
# Link text for note references
note_cad6 = {"["}ñóté{"]"}
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = {"["}Ñótédéçk ís à bétà pródúçt. Éxpéçt búgs àñd çóñtàçt ús whéñ yóú rúñ íñtó íssúés.{"]"}
# Filter label for notes only view
Notes_03fb = {"["}Ñótés{"]"}
# Label for notes-only filter
Notes_60d2 = {"["}Ñótés{"]"}
# Filter label for notes and replies view
Notes___Replies_1ec2 = {"["}Ñótés & Réplíés{"]"}
# Label for notes and replies filter
Notes___Replies_6e3b = {"["}Ñótés & Réplíés{"]"}
# Column title for notifications
Notifications_d673 = {"["}Ñótífíçàtíóñs{"]"}
# Title for notifications column
Notifications_ef56 = {"["}Ñótífíçàtíóñs{"]"}
# Relative time for very recent events (less than 3 seconds)
now_2181 = {"["}ñów{"]"}
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = {"["}Óñ{"]"}
# Button label to open email client
Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"}
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = {"["}Ópéñ yóúr défàúlt émàíl çlíéñt tó gét hélp fróm thé Dàmús téàm{"]"}
# Label for others settings section
Others_7267 = {"["}Óthérs{"]"}
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = {"["}Pàsté yóúr ÑWÇ ÚRÍ héré...{"]"}
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = {"["}Pléàsé çréàté à ñàmé fór thé déçk.{"]"}
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = {"["}Pléàsé çréàté à ñàmé fór thé déçk àñd séléçt àñ íçóñ.{"]"}
# Error message for missing deck icon
Please_select_an_icon_655b = {"["}Pléàsé séléçt àñ íçóñ.{"]"}
# Button label to post a note
Post_now_8a49 = {"["}Póst ñów{"]"}
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = {"["}Préss thé búttóñ bélów tó çópy yóúr móst réçéñt lógs tó yóúr systém's çlípbóàrd. Théñ pàsté ít íñtó yóúr émàíl.{"]"}
# Profile picture URL field label
Profile_picture_81ff = {"["}Prófílé píçtúré{"]"}
# Column title for quote composition
Quote_475c = {"["}Qúóté{"]"}
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = {"["}Qúóté óf úñkñówñ ñóté{"]"}
# Label for read-only profile mode
Read_only_82ff = {"["}Réàd óñly{"]"}
# Column title for relay management
Relays_9d89 = {"["}Rélàys{"]"}
# Label for relay list section
Relays_ad5e = {"["}Rélàys{"]"}
# Column title for reply composition
Reply_3bf1 = {"["}Réply{"]"}
# Hover text for reply button
Reply_to_this_note_f5de = {"["}Réply tó thís ñóté{"]"}
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = {"["}Réply tó úñkñówñ ñóté{"]"}
# Fallback template for replying to user
replying_to__user_15ab = {"["}réplyíñg tó {$user}{"]"}
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = {"["}réplyíñg tó {$user} íñ sóméóñé's thréàd{"]"}
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = {"["}réplyíñg tó {$user}'s {$note} íñ {$thread_user}'s {$thread}{"]"}
# Template for replying to user's note
replying_to__user__s__note_ccba = {"["}réplyíñg tó {$user}'s {$note}{"]"}
# Template for replying to root thread
replying_to__user__s__thread_444d = {"["}réplyíñg tó {$user}'s {$thread}{"]"}
# Fallback text when reply note is not found
replying_to_a_note_e0bc = {"["}réplyíñg tó à ñóté{"]"}
# Hover text for repost button
Repost_this_note_8e56 = {"["}Répóst thís ñóté{"]"}
# Label for reposted notes
Reposted_61c8 = {"["}Répóstéd{"]"}
# Label for reset note body font size, Appearance settings section
Reset_4e60 = {"["}Rését{"]"}
# Label for reset zoom level, Appearance settings section
Reset_62d4 = {"["}Rését{"]"}
# Heading for support section
Running_into_a_bug_1796 = {"["}Rúññíñg íñtó à búg?{"]"}
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = {"["}SÀTS{"]"}
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = {"["}sàts{"]"}
# Button to save default zap amount
Save_6f7c = {"["}Sàvé{"]"}
# Button label to save profile changes
Save_changes_00db = {"["}Sàvé çhàñgés{"]"}
# Column title for search page
Search_c573 = {"["}Séàrçh{"]"}
# Placeholder for search notes input field
Search_notes_42a6 = {"["}Séàrçh ñótés...{"]"}
# Search in progress message
Searching_for___query_5d18 = {"["}Séàrçhíñg fór '{$query}'{"]"}
# Description for Home column
See_notes_from_your_contacts_ac16 = {"["}Séé ñótés fróm yóúr çóñtàçts{"]"}
# Description for universe column
See_the_whole_nostr_universe_7694 = {"["}Séé thé whólé ñóstr úñívérsé{"]"}
# Button label to send a zap
Send_1ea4 = {"["}Séñd{"]"}
# Column title for app settings
Settings_7a4f = {"["}Séttíñgs{"]"}
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = {"["}Shów thé làst ñóté fór éàçh úsér fróm à líst{"]"}
# Button label to sign out of account
Sign_out_337b = {"["}Sígñ óút{"]"}
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = {"["}Sóméóñé élsé's Ñótés{"]"}
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = {"["}Sóméóñé élsé's Ñótífíçàtíóñs{"]"}
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = {"["}Sórt réplíés ñéwést fírst:{"]"}
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = {"["}Sóúrçé thé làst ñóté fór éàçh úsér íñ yóúr çóñtàçt líst{"]"}
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = {"["}Stày úp tó dàté wíth à çértàíñ hàshtàg{"]"}
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = {"["}Stày úp tó dàté wíth ñótífíçàtíóñs àñd méñtíóñs{"]"}
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = {"["}Stày úp tó dàté wíth sóméóñé élsé's ñótés & réplíés{"]"}
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = {"["}Stày úp tó dàté wíth sóméóñé élsé's ñótífíçàtíóñs àñd méñtíóñs{"]"}
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = {"["}Stày úp tó dàté wíth sóméóñé's ñótés & réplíés{"]"}
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = {"["}Stày úp tó dàté wíth yóúr ñótífíçàtíóñs àñd méñtíóñs{"]"}
# Step 1 label in support instructions
Step_1_8656 = {"["}Stép 1{"]"}
# Step 2 label in support instructions
Step_2_d08d = {"["}Stép 2{"]"}
# Label for storage settings section
Storage_ed65 = {"["}Stóràgé{"]"}
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = {"["}Súbsçríbé tó sóméóñé élsé's ñótés{"]"}
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = {"["}Súbsçríbé tó sóméóñé's ñótés{"]"}
# Support email address
Support_email_44d9 = {"["}Súppórt émàíl:{"]"}
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = {"["}Swítçh tó dàrk módé{"]"}
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = {"["}Swítçh tó líght módé{"]"}
# Button text to load blurred media
Tap_to_Load_4b05 = {"["}Tàp tó Lóàd{"]"}
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = {"["}Thé Dàvé Ñóstr ÀÍ àssístàñt tríàl hàs éñdéd :(. Thàñks fór téstíñg! Zàp-éñàbléd Dàvé çómíñg sóóñ!{"]"}
# Label for theme, Appearance settings section
Theme_4aac = {"["}Thémé:{"]"}
# Column title for note thread view
Thread_0f20 = {"["}Thréàd{"]"}
# Link text for thread references
thread_ad1f = {"["}thréàd{"]"}
# Title for universe column
Universe_e01e = {"["}Úñívérsé{"]"}
# Column title for universe feed
Universe_ffaa = {"["}Úñívérsé{"]"}
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = {"["}Úsé thís wàllét fór thé çúrréñt àççóúñt óñly{"]"}
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = {"["}"{$username}" àt "{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"}
# Profile username field label
Username_daa7 = {"["}Úsérñàmé{"]"}
# Label for view folder button, Storage settings section
View_folder_9742 = {"["}Víéw fóldér{"]"}
# Column title for wallet management
Wallet_5e50 = {"["}Wàllét{"]"}
# Hint for deck name input field
We_recommend_short_names_083e = {"["}Wé réçómméñd shórt ñàmés{"]"}
# Profile website field label
Website_7980 = {"["}Wébsíté{"]"}
# Placeholder for note input field
Write_a_banger_note_here_bad2 = {"["}Wríté à bàñgér ñóté héré...{"]"}
# Placeholder text for key input field
Your_key_here_81bd = {"["}Yóúr kéy héré...{"]"}
# Title for your notes column
Your_Notes_f6db = {"["}Yóúr Ñótés{"]"}
# Title for your notifications column
Your_Notifications_080d = {"["}Yóúr Ñótífíçàtíóñs{"]"}
# Heading for zap (tip) action
Zap_16b4 = {"["}Zàp{"]"}
# Hover text for zap button
Zap_this_note_42b2 = {"["}Zàp thís ñóté{"]"}
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = {"["}Zóóm Lévél:{"]"}
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] {"["}Gót {$count} résúlt fór '{$query}'{"]"}
*[other] {"["}Gót {$count} résúlts fór '{$query}'{"]"}
}

View File

@@ -1,410 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Información
# Column title for account management
Accounts_f018 = Cuentas
# Button label to add a relay
Add_269d = Agregar
# Label for add column button
Add_47df = Agregar
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Agregar una billetera diferente que solo se utilizará para esta cuenta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Agregar una billetera para continuar
# Button label to add a new account
Add_account_1cfc = Agregar cuenta
# Column title for adding new account
Add_Account_d06c = Agregar cuenta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Agregar columna algorítmica
# Column title for adding new column
Add_Column_c764 = Agregar columna
# Column title for adding new deck
Add_Deck_fabf = Agregar deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Agregar columna de notificaciones externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Agregar columna de hashtags
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Agregar columna de últimas notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Agregar columna de notificaciones
# Button label to add a relay
Add_relay_269d = Agregar relé
# Button label to add a wallet
Add_Wallet_d1be = Agregar billetera
# Title for algorithmic feeds column
Algo_2452 = Algo
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
# Label for zap amount input field
Amount_70f0 = Cantidad
# Label for appearance settings section
Appearance_4c7f = Aspecto
# Button to send message to Dave AI assistant
Ask_b7f4 = Preguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Pregúntale cualquier cosa a Dave...
# Profile banner URL field label
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Transmitir
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Transmitir localmente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpiar caché
# Hover text for editable zap amount
Click_to_edit_0414 = Haz clic para editar
# Column title for note composition
Compose_Note_c094 = Redactar nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar relés
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectado
# Status label for connecting relay
Connecting_6b7e = Conectando...
# Title for contact list column
Contact_List_f85a = Lista de contactos
# Column title for contact lists
Contacts_7533 = Contactos
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contactos (últimas notas)
# Button label to copy logs
Copy_a688 = Copiar
# Button to copy media link to clipboard
Copy_Link_dc7c = Copiar enlace
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copiar ID de nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON de nota
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
count_d_b9be = { $count }d
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }m
# Relative time in months
count_mo_7aba = { $count }mes
# Relative time in seconds
count_s_aa26 = { $count }s
# Relative time in weeks
count_w_7468 = { $count }sem
# Relative time in years
count_y_9408 = { $count }a
# Button to create a new account
Create_Account_6994 = Crear cuenta
# Button label to create a new deck
Create_Deck_16b7 = Crear deck
# Column title for custom timelines
Custom_a69e = Personalizado
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
# Column title for support page
Damus_Support_27c0 = Ayuda de Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = Oscuro
# Label for deck name input field
Deck_name_cd32 = Nombre del deck
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Cantidad predeterminada por zap:
# Name of the default deck feed
Default_Deck_fcca = Deck predeterminado
# Button label to delete a deck
Delete_Deck_bb29 = Eliminar deck
# Tooltip for deleting a column
Delete_this_column_8d5a = Eliminar esta columna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Eliminar billetera
# Profile display name field label
Display_name_f9d9 = Nombre para mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
# Column title for editing deck
Edit_Deck_4018 = Editar deck
# Button label to edit a deck
Edit_Deck_fd93 = Editar deck
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
Edit_Profile_8ad4 = Editar perfil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Ingresa aquí los hashtags deseados (si son varios, sepáralos con un espacio)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Ingresa el relé aquí
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Ingresa la clave del usuario (npub, hex, nip05) aquí...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Ingresa tu clave
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Buscar usuario
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
Icon_b0ab = Ícono
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamaño de caché de imágenes:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Cantidad no válida
# Error message for invalid key input
Invalid_key_4726 = Clave no válida.
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI no válido
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100.000
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10.000
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20.000
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50.000
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5.000
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por usuario
# Label for Theme Light, Appearance settings section
Light_7475 = Claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
# Login page title
Login_9eef = Inicio de sesión
# Login button text
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que no sigues
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
# Title for the user's deck
My_Deck_4ac5 = Mi deck
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Dirección de Nostr (identidad NIP-05)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = No conectado
# Link text for note references
note_cad6 = nota
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck es un producto en fase beta. Es posible que haya errores, así que ponte en contacto con nosotros si tienes algún problema.
# Filter label for notes only view
Notes_03fb = Notas
# Label for notes-only filter
Notes_60d2 = Notas
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notas y respuestas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas y respuestas
# Column title for notifications
Notifications_d673 = Notificaciones
# Title for notifications column
Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
# Label for others settings section
Others_7267 = Otros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el deck y selecciona un ícono.
# Error message for missing deck icon
Please_select_an_icon_655b = Selecciona un ícono.
# Button label to post a note
Post_now_8a49 = Publicar ahora
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
# Profile picture URL field label
Profile_picture_81ff = Imagen de perfil
# Column title for quote composition
Quote_475c = Citar
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Cita de nota desconocida
# Label for read-only profile mode
Read_only_82ff = Solo lectura
# Column title for relay management
Relays_9d89 = Relés
# Label for relay list section
Relays_ad5e = Relés
# Column title for reply composition
Reply_3bf1 = Respuesta
# Hover text for reply button
Reply_to_this_note_f5de = Responder a esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder a nota desconocida
# Fallback template for replying to user
replying_to__user_15ab = respondiendo a { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = respondiendo a { $user } en la conversación de alguien
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondiendo a { $note } de { $user } en { $thread } de { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = respondiendo a { $note } de { $user }
# Template for replying to root thread
replying_to__user__s__thread_444d = respondiendo a { $thread } de { $user }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = respondiendo a una nota
# Hover text for repost button
Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer
# Heading for support section
Running_into_a_bug_1796 = ¿Encontraste un error?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Guardar
# Button label to save profile changes
Save_changes_00db = Guardar cambios
# Column title for search page
Search_c573 = Búsqueda
# Placeholder for search notes input field
Search_notes_42a6 = Buscar notas...
# Search in progress message
Searching_for___query_5d18 = Buscando '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
# Description for universe column
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configuración
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
# Button label to sign out of account
Sign_out_337b = Cerrar sesión
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notas de otra persona
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantente al día con un hashtag específico
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Mantente al día con notificaciones y menciones
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantente al día con las notas y respuestas de otra persona
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantente al día con las notificaciones y menciones de otra persona
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantente al día con las notas y respuestas de alguien
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con tus notificaciones y menciones
# Step 1 label in support instructions
Step_1_8656 = Paso 1
# Step 2 label in support instructions
Step_2_d08d = Paso 2
# Label for storage settings section
Storage_ed65 = Almacenamiento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Support email address
Support_email_44d9 = Support email:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Cambiar a modo claro
# Button text to load blurred media
Tap_to_Load_4b05 = Toca para cargar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr finalizó :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# Column title for note thread view
Thread_0f20 = Conversación
# Link text for thread references
thread_ad1f = conversación
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
Universe_ffaa = Universo
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Usar esta billetera solo para la cuenta actual
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
# Profile username field label
Username_daa7 = Nombre de usuario
# Label for view folder button, Storage settings section
View_folder_9742 = Ver carpeta
# Column title for wallet management
Wallet_5e50 = Billetera
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nombres cortos
# Profile website field label
Website_7980 = Sitio web
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escribe aquí una nota impactante...
# Placeholder text for key input field
Your_key_here_81bd = Tu clave aquí...
# Title for your notes column
Your_Notes_f6db = Tus notas
# Title for your notifications column
Your_Notifications_080d = Tus notificaciones
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Enviar un zap a esta nota
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nivel de zoom:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[uno] Obtuvo { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados para '{ $query }'
}

View File

@@ -1,410 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Información
# Column title for account management
Accounts_f018 = Cuentas
# Button label to add a relay
Add_269d = Añadir
# Label for add column button
Add_47df = Añadir
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Añadir un monedero diferente que solo se utilizará para esta cuenta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Añadir un monedero para continuar
# Button label to add a new account
Add_account_1cfc = Añadir cuenta
# Column title for adding new account
Add_Account_d06c = Añadir cuenta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Añadir columna algorítmica
# Column title for adding new column
Add_Column_c764 = Añadir columna
# Column title for adding new deck
Add_Deck_fabf = Añadir deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Añadir columna de notificaciones externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Añadir columna de hashtags
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Añadir columna de últimas notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Añadir columna de notificaciones
# Button label to add a relay
Add_relay_269d = Añadir relé
# Button label to add a wallet
Add_Wallet_d1be = Añadir monedero
# Title for algorithmic feeds column
Algo_2452 = Algo
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
# Label for zap amount input field
Amount_70f0 = Cantidad
# Label for appearance settings section
Appearance_4c7f = Aspecto
# Button to send message to Dave AI assistant
Ask_b7f4 = Preguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Pregúntale cualquier cosa a Dave...
# Profile banner URL field label
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Transmitir
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Transmitir localmente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpiar caché
# Hover text for editable zap amount
Click_to_edit_0414 = Haz clic para editar
# Column title for note composition
Compose_Note_c094 = Redactar nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar relés
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectado
# Status label for connecting relay
Connecting_6b7e = Conectando...
# Title for contact list column
Contact_List_f85a = Lista de contactos
# Column title for contact lists
Contacts_7533 = Contactos
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contactos (últimas notas)
# Button label to copy logs
Copy_a688 = Copiar
# Button to copy media link to clipboard
Copy_Link_dc7c = Copiar enlace
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copiar ID de nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON de nota
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
count_d_b9be = { $count }d
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }m
# Relative time in months
count_mo_7aba = { $count }mes
# Relative time in seconds
count_s_aa26 = { $count }s
# Relative time in weeks
count_w_7468 = { $count }sem
# Relative time in years
count_y_9408 = { $count }a
# Button to create a new account
Create_Account_6994 = Crear cuenta
# Button label to create a new deck
Create_Deck_16b7 = Crear deck
# Column title for custom timelines
Custom_a69e = Personalizado
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
# Column title for support page
Damus_Support_27c0 = Ayuda de Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = Oscuro
# Label for deck name input field
Deck_name_cd32 = Nombre del deck
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Cantidad predeterminada por zap:
# Name of the default deck feed
Default_Deck_fcca = Deck predeterminado
# Button label to delete a deck
Delete_Deck_bb29 = Eliminar deck
# Tooltip for deleting a column
Delete_this_column_8d5a = Eliminar esta columna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Eliminar monedero
# Profile display name field label
Display_name_f9d9 = Nombre para mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
# Column title for editing deck
Edit_Deck_4018 = Editar deck
# Button label to edit a deck
Edit_Deck_fd93 = Editar deck
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
Edit_Profile_8ad4 = Editar perfil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Ingresa aquí los hashtags deseados (si son varios, sepáralos con un espacio)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Ingresa el relé aquí
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Ingresa la clave del usuario (npub, hex, nip05) aquí...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Ingresa tu clave
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Buscar usuario
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
Icon_b0ab = Icono
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamaño de caché de imágenes:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Cantidad no válida
# Error message for invalid key input
Invalid_key_4726 = Clave no válida.
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI no válido
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100.000
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10.000
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20.000
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50.000
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5.000
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por usuario
# Label for Theme Light, Appearance settings section
Light_7475 = Claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
# Login page title
Login_9eef = Inicio de sesión
# Login button text
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que no sigues
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
# Title for the user's deck
My_Deck_4ac5 = Mi deck
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Dirección de Nostr (identidad NIP-05)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = No conectado
# Link text for note references
note_cad6 = nota
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck es un producto en fase beta. Es posible que haya errores, así que ponte en contacto con nosotros si tienes algún problema.
# Filter label for notes only view
Notes_03fb = Notas
# Label for notes-only filter
Notes_60d2 = Notas
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notas y respuestas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas y respuestas
# Column title for notifications
Notifications_d673 = Notificaciones
# Title for notifications column
Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
# Label for others settings section
Others_7267 = Otros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el Deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el deck y selecciona un icono.
# Error message for missing deck icon
Please_select_an_icon_655b = Selecciona un icono.
# Button label to post a note
Post_now_8a49 = Publicar ahora
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
# Profile picture URL field label
Profile_picture_81ff = Imagen de perfil
# Column title for quote composition
Quote_475c = Citar
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Cita de nota desconocida
# Label for read-only profile mode
Read_only_82ff = Solo lectura
# Column title for relay management
Relays_9d89 = Relés
# Label for relay list section
Relays_ad5e = Relés
# Column title for reply composition
Reply_3bf1 = Respuesta
# Hover text for reply button
Reply_to_this_note_f5de = Responder a esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder a nota desconocida
# Fallback template for replying to user
replying_to__user_15ab = respondiendo a { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = respondiendo a { $user } en la conversación de alguien
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondiendo a { $note } de { $user } en { $thread } de { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = respondiendo a { $note } de { $user }
# Template for replying to root thread
replying_to__user__s__thread_444d = respondiendo a { $thread } de { $user }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = respondiendo a una nota
# Hover text for repost button
Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer
# Heading for support section
Running_into_a_bug_1796 = ¿Has encontrado un error?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Guardar
# Button label to save profile changes
Save_changes_00db = Guardar cambios
# Column title for search page
Search_c573 = Búsqueda
# Placeholder for search notes input field
Search_notes_42a6 = Buscar notas...
# Search in progress message
Searching_for___query_5d18 = Buscando '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
# Description for universe column
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configuración
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
# Button label to sign out of account
Sign_out_337b = Cerrar sesión
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notas de otra persona
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantente al día con un hashtag específico
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Mantente al día con notificaciones y menciones
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantente al día con las notas y respuestas de otra persona
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantente al día con las notificaciones y menciones de otra persona
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantente al día con las notas y respuestas de alguien
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con tus notificaciones y menciones
# Step 1 label in support instructions
Step_1_8656 = Paso 1
# Step 2 label in support instructions
Step_2_d08d = Paso 2
# Label for storage settings section
Storage_ed65 = Almacenamiento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Support email address
Support_email_44d9 = Support email:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Cambiar a modo claro
# Button text to load blurred media
Tap_to_Load_4b05 = Toca para cargar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr ha finalizado :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# Column title for note thread view
Thread_0f20 = Conversación
# Link text for thread references
thread_ad1f = conversación
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
Universe_ffaa = Universo
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Usar este monedero solo para la cuenta actual
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
# Profile username field label
Username_daa7 = Nombre de usuario
# Label for view folder button, Storage settings section
View_folder_9742 = Ver carpeta
# Column title for wallet management
Wallet_5e50 = Monedero
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nombres cortos
# Profile website field label
Website_7980 = Sitio web
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escribe aquí una nota impactante...
# Placeholder text for key input field
Your_key_here_81bd = Tu clave aquí...
# Title for your notes column
Your_Notes_f6db = Tus notas
# Title for your notifications column
Your_Notifications_080d = Tus notificaciones
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Enviar un zap a esta nota
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nivel de zoom:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[uno] Obtuvo { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados para '{ $query }'
}

View File

@@ -1,410 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = A propos
# Column title for account management
Accounts_f018 = Comptes
# Button label to add a relay
Add_269d = Ajouter
# Label for add column button
Add_47df = Ajouter
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Ajouter un portefeuille différent qui ne sera utilisé que pour ce compte
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Ajouter un portefeuille pour continuer
# Button label to add a new account
Add_account_1cfc = Ajouter un compte
# Column title for adding new account
Add_Account_d06c = Ajouter un compte
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Ajouter une colonne Algo
# Column title for adding new column
Add_Column_c764 = Ajouter une colonne
# Column title for adding new deck
Add_Deck_fabf = Ajouter un deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Ajouter une colonne pour les notifications externes
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Ajouter une colonne Hashtag
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Ajouter une colonne pour les dernières notes
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Ajouter une colonne pour les notifications
# Button label to add a relay
Add_relay_269d = Ajouter un relai
# Button label to add a wallet
Add_Wallet_d1be = Ajouter un portefeuille
# Title for algorithmic feeds column
Algo_2452 = Algo
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Des fils algorithmiques pour faciliter la découverte de notes
# Label for zap amount input field
Amount_70f0 = Montant
# Label for appearance settings section
Appearance_4c7f = Apparence
# Button to send message to Dave AI assistant
Ask_b7f4 = Demander
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Demandez à Dave n'importe quoi...
# Profile banner URL field label
Banner_52ef = Bannière
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Diffusion
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Diffusion locale
# Button label to cancel an action
Cancel_ed3b = Annuler
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Annuler
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Vider le cache
# Hover text for editable zap amount
Click_to_edit_0414 = Cliquer pour modifier
# Column title for note composition
Compose_Note_c094 = Ecrire une note
# Label for configure relays, settings section
Configure_relays_d156 = Configurer les relais
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmer
# Button label to confirm an action
Confirm_f8a6 = Confirmer
# Status label for connected relay
Connected_f8cc = Connecté
# Status label for connecting relay
Connecting_6b7e = Connexion...
# Title for contact list column
Contact_List_f85a = Liste de contacts
# Column title for contact lists
Contacts_7533 = Contacts
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contacts (dernières notes)
# Button label to copy logs
Copy_a688 = Copier
# Button to copy media link to clipboard
Copy_Link_dc7c = Copier le lien
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copier l'ID de la note
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copier le JSON de la note
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copier la Pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copier le texte
# Relative time in days
count_d_b9be = { $count }j
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }min
# Relative time in months
count_mo_7aba = { $count }m
# Relative time in seconds
count_s_aa26 = { $count }s
# Relative time in weeks
count_w_7468 = { $count }sem
# Relative time in years
count_y_9408 = { $count }a
# Button to create a new account
Create_Account_6994 = Créer un compte
# Button label to create a new deck
Create_Deck_16b7 = Créer un deck
# Column title for custom timelines
Custom_a69e = Personnaliser
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personnaliser le montant du Zap
# Column title for support page
Damus_Support_27c0 = Assistance Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = Sombre
# Label for deck name input field
Deck_name_cd32 = Nom du deck
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Montant par défaut pour un Zap :
# Name of the default deck feed
Default_Deck_fcca = Deck par défaut
# Button label to delete a deck
Delete_Deck_bb29 = Supprimer le deck
# Tooltip for deleting a column
Delete_this_column_8d5a = Supprimer cette colonne
# Button label to delete a wallet
Delete_Wallet_d1d4 = Supprimer le portefeuille
# Profile display name field label
Display_name_f9d9 = Nom d'utilisateur
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" sera utilisé pour l'identification
# Column title for editing deck
Edit_Deck_4018 = Modifier le deck
# Button label to edit a deck
Edit_Deck_fd93 = Modifier le deck
# Button label to edit user profile
Edit_Profile_49e6 = Modifier le profil
# Column title for profile editing
Edit_Profile_8ad4 = Modifier le profil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Entrez les hashtags souhaités ici (séparez-les avec un espace)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Entrer un relai ici
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Entrer ici la clé de l'utilisateur (npub, hex, nip05)...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Entrez votre clé
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Entrez votre clé publique (npub), votre adresse nostr (par exemple { $address }), ou votre clé privée (nsec). Vous devez entrer votre clé privée pour pouvoir poster, répondre, etc.
# Label for find user button
Find_User_bd12 = Trouver un utilisateur
# Label for font size, Appearance settings section
Font_size_dd73 = Taille du texte :
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Accueil
# Label for deck icon selection
Icon_b0ab = Icone
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Taille du cache des images :
# Title for individual user column
Individual_b776 = Individuel
# Error message for invalid zap amount
Invalid_amount_6630 = Montant invalide
# Error message for invalid key input
Invalid_key_4726 = Clé non valide.
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = Invalide NWC URI
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Gardez une trace de vos notes & réponses
# Label for language, Appearance settings section
Language_e264 = Langue :
# Title for last note per user column
Last_Note_per_User_17ad = Dernière note par utilisateur
# Label for Theme Light, Appearance settings section
Light_7475 = Clair
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Adresse réseau Lightning (lud16)
# Login page title
Login_9eef = Se connecter
# Login button text
Login_now___let_s_do_this_5630 = Se connecter maintenant - c'est parti !
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Média d'une personne que vous ne suivez pas
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Déplace cette colonne vers une autre position
# Title for the user's deck
My_Deck_4ac5 = Mon deck
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Nouveau sur Nostr ?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Adresse Nostr (NIP-05 identité)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = Non connecté
# Link text for note references
note_cad6 = note
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck est un produit en phase beta. Attendez-vous à des bugs et contactez-nous si vous rencontrez des problèmes.
# Filter label for notes only view
Notes_03fb = Notes
# Label for notes-only filter
Notes_60d2 = Notes
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notes & Réponses
# Label for notes and replies filter
Notes___Replies_6e3b = Notes & Réponses
# Column title for notifications
Notifications_d673 = Notifications
# Title for notifications column
Notifications_ef56 = Notifications
# Relative time for very recent events (less than 3 seconds)
now_2181 = maintenant
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = Activé
# Button label to open email client
Open_Email_25e9 = Ouvrir Email
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Ouvrez votre service d'email par défaut pour obtenir de l'aide de l'équipe Damus
# Label for others settings section
Others_7267 = Autres
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Collez ici votre NWC URI...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Veuillez créer un nom pour le deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Veuillez créer un nom pour le deck et sélectionner une icône.
# Error message for missing deck icon
Please_select_an_icon_655b = Veuillez choisir une icône.
# Button label to post a note
Post_now_8a49 = Publier maintenant
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Cliquez sur le bouton ci-dessous pour copier vos données les plus récentes dans le presse-papiers de votre système. Collez-les ensuite dans votre courrier électronique.
# Profile picture URL field label
Profile_picture_81ff = Photo de profil
# Column title for quote composition
Quote_475c = Citation
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Citation d'une note inconnue
# Label for read-only profile mode
Read_only_82ff = En lecture seule
# Column title for relay management
Relays_9d89 = Relais
# Label for relay list section
Relays_ad5e = Relais
# Column title for reply composition
Reply_3bf1 = Répondre
# Hover text for reply button
Reply_to_this_note_f5de = Répondre à cette note
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Répondre à la note inconnue
# Fallback template for replying to user
replying_to__user_15ab = répondre à { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = répondre à { $user } dans le fil de discussion
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = répondre à la { $note } de { $user } dans le { $thread } sur le { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = répondre à la { $note } de { $user }
# Template for replying to root thread
replying_to__user__s__thread_444d = répondre dans le { $thread } de { $user }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = répondre à une note
# Hover text for repost button
Repost_this_note_8e56 = Republier cette note
# Label for reposted notes
Reposted_61c8 = Republier
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Réinitialiser
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Réinitialiser
# Heading for support section
Running_into_a_bug_1796 = Vous rencontrez un problème ?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Enregistrer
# Button label to save profile changes
Save_changes_00db = Enregistrer les modifications
# Column title for search page
Search_c573 = Rechercher
# Placeholder for search notes input field
Search_notes_42a6 = Rechercher des notes...
# Search in progress message
Searching_for___query_5d18 = Recherche par '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts
# Description for universe column
See_the_whole_nostr_universe_7694 = Voir l'ensemble de l'univers nostr
# Button label to send a zap
Send_1ea4 = Envoyer
# Column title for app settings
Settings_7a4f = Paramètres
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Afficher la dernière note de chaque utilisateur à partir d'une liste
# Button label to sign out of account
Sign_out_337b = Se déconnecter
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notes de quelqu'un d'autre
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notifications de quelqu'un d'autre
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Trier les réponses les plus récentes en premier :
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source de la dernière note pour chaque utilisateur de votre liste de contacts
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Restez informé sur un hashtag
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Restez informé avec les notifications et les mentions
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Restez informé des notes et des réponses de quelqu'un d'autre
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Restez informé des notifications et mentions de quelqu'un d'autre
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Restez informé des notes et réponses de quelqu'un
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Restez informé pour vos notifications et mentions
# Step 1 label in support instructions
Step_1_8656 = Etape 1
# Step 2 label in support instructions
Step_2_d08d = Etape 2
# Label for storage settings section
Storage_ed65 = Stockage
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = S'abonner aux notes de quelqu'un d'autre
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = S'abonner aux notes de quelqu'un
# Support email address
Support_email_44d9 = Adresse email de l'assistance :
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Passer en mode sombre
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Passer en mode clair
# Button text to load blurred media
Tap_to_Load_4b05 = Appuyer pour charger
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La période d'essai de l'assistant IA Dave Nostr est terminée :(. Merci de l'avoir testé ! Un Dave compatible-Zap sera bientôt disponible !
# Label for theme, Appearance settings section
Theme_4aac = Thème :
# Column title for note thread view
Thread_0f20 = Fil
# Link text for thread references
thread_ad1f = fil
# Title for universe column
Universe_e01e = Universel
# Column title for universe feed
Universe_ffaa = Universel
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Utiliser ce portefeuille pour le compte actuel
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" à "{ $domain }" sera utilisé pour l'identification
# Profile username field label
Username_daa7 = Nom d'utilisateur
# Label for view folder button, Storage settings section
View_folder_9742 = Voir le dossier
# Column title for wallet management
Wallet_5e50 = Portefeuille
# Hint for deck name input field
We_recommend_short_names_083e = Nous recommandons des noms courts
# Profile website field label
Website_7980 = Site web
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Écrivez une note banger ici...
# Placeholder text for key input field
Your_key_here_81bd = Votre clé ici...
# Title for your notes column
Your_Notes_f6db = Vos Notes
# Title for your notifications column
Your_Notifications_080d = Vos notifications
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap cette note
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Niveau de zoom :
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] A obtenu { $count } pour '{ $query }'
*[other] A obtenu { $count } pour '{ $query }'
}

View File

@@ -1,410 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = 概要
# Column title for account management
Accounts_f018 = アカウント
# Button label to add a relay
Add_269d = 追加
# Label for add column button
Add_47df = 追加
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = このアカウントでのみ使用される別のウォレットを追加
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = 続行するにはウォレットを追加してください
# Button label to add a new account
Add_account_1cfc = アカウントを追加
# Column title for adding new account
Add_Account_d06c = アカウントの追加
# Column title for adding algorithm column
Add_Algo_Column_0d75 = アルゴカラムの追加
# Column title for adding new column
Add_Column_c764 = カラムの追加
# Column title for adding new deck
Add_Deck_fabf = デッキの追加
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = 外部通知カラムの追加
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = ハッシュタグカラムの追加
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = 最後の投稿カラムの追加
# Column title for adding notifications column
Add_Notifications_Column_79f8 = 外部通知カラムの追加
# Button label to add a relay
Add_relay_269d = リレーを追加
# Button label to add a wallet
Add_Wallet_d1be = ウォレットを追加
# Title for algorithmic feeds column
Algo_2452 = アルゴ
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 投稿の発見に役立つアルゴリズムフィードです
# Label for zap amount input field
Amount_70f0 = 金額
# Label for appearance settings section
Appearance_4c7f = 外観
# Button to send message to Dave AI assistant
Ask_b7f4 = 質問
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Dave に何でも質問してみましょう…
# Profile banner URL field label
Banner_52ef = バナー
# Beta version label
BETA_8e5d = ベータ
# Broadcast the note to all connected relays
Broadcast_fe43 = ブロードキャスト
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = ローカルにブロードキャスト
# Button label to cancel an action
Cancel_ed3b = キャンセル
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = キャンセル
# Label for clear cache button, Storage settings section
Clear_cache_dccb = キャッシュを消去
# Hover text for editable zap amount
Click_to_edit_0414 = クリックして編集
# Column title for note composition
Compose_Note_c094 = メモの作成
# Label for configure relays, settings section
Configure_relays_d156 = リレーを設定
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = 決定
# Button label to confirm an action
Confirm_f8a6 = 決定
# Status label for connected relay
Connected_f8cc = 接続済
# Status label for connecting relay
Connecting_6b7e = 接続中…
# Title for contact list column
Contact_List_f85a = フォロイーリスト
# Column title for contact lists
Contacts_7533 = フォロー
# Column title for last notes per contact
Contacts__last_notes_3f84 = フォロー (最後の投稿)
# Button label to copy logs
Copy_a688 = コピー
# Button to copy media link to clipboard
Copy_Link_dc7c = リンクをコピー
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = 投稿 ID をコピー
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = 投稿の JSON をコピー
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = 公開鍵をコピー
# Copy the text content of the note to clipboard
Copy_Text_f81c = テキストをコピー
# Relative time in days
count_d_b9be = { $count }日
# Relative time in hours
count_h_3ecb = { $count }時間
# Relative time in minutes
count_m_b41e = { $count }分
# Relative time in months
count_mo_7aba = { $count }ヶ月
# Relative time in seconds
count_s_aa26 = { $count }秒
# Relative time in weeks
count_w_7468 = { $count }週間
# Relative time in years
count_y_9408 = { $count }年
# Button to create a new account
Create_Account_6994 = アカウントを作成
# Button label to create a new deck
Create_Deck_16b7 = デッキを作成
# Column title for custom timelines
Custom_a69e = カスタマイズ
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Zap 金額をカスタマイズ
# Column title for support page
Damus_Support_27c0 = Damus サポート
# Label for Theme Dark, Appearance settings section
Dark_85fe = ダーク
# Label for deck name input field
Deck_name_cd32 = デッキ名
# Label for decks section in side panel
DECKS_1fad = デッキ
# Label for default zap amount input
Default_amount_per_zap_399d = Zap ごとのデフォルトの金額:
# Name of the default deck feed
Default_Deck_fcca = 既定のデッキ
# Button label to delete a deck
Delete_Deck_bb29 = デッキを削除
# Tooltip for deleting a column
Delete_this_column_8d5a = このカラムを削除します
# Button label to delete a wallet
Delete_Wallet_d1d4 = ウォレットを削除
# Profile display name field label
Display_name_f9d9 = 表示名
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" が識別に使用されます
# Column title for editing deck
Edit_Deck_4018 = デッキの編集
# Button label to edit a deck
Edit_Deck_fd93 = デッキを編集
# Button label to edit user profile
Edit_Profile_49e6 = プロファイルを編集
# Column title for profile editing
Edit_Profile_8ad4 = プロファイルの編集
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = 必要なハッシュタグをここに入力してください (複数スペースで区切る場合)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = ここにリレーを入力してください
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = ユーザーの鍵 (npub, hex, nip05) を入力してください...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = 鍵を入力してください
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 公開鍵 (npub)、nostr アドレス (例: { $address })、秘密鍵 (nsec) を入力してください。 投稿、返信などを行うには秘密鍵を入力する必要があります。
# Label for find user button
Find_User_bd12 = ユーザーを探す
# Label for font size, Appearance settings section
Font_size_dd73 = フォントサイズ:
# Title for hashtags column
Hashtags_f8e0 = ハッシュタグ
# Title for Home column
Home_8c19 = ホーム
# Label for deck icon selection
Icon_b0ab = アイコン
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = 画像キャッシュのサイズ:
# Title for individual user column
Individual_b776 = 個人用
# Error message for invalid zap amount
Invalid_amount_6630 = 無効な金額です
# Error message for invalid key input
Invalid_key_4726 = 無効な鍵です。
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = 無効な NWC URI です
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = 投稿と返信を記録します
# Label for language, Appearance settings section
Language_e264 = 言語:
# Title for last note per user column
Last_Note_per_User_17ad = ユーザーごとの最後の投稿
# Label for Theme Light, Appearance settings section
Light_7475 = ライト
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = ライトニングネットワークアドレス (lud16)
# Login page title
Login_9eef = ログイン
# Login button text
Login_now___let_s_do_this_5630 = 今すぐログイン — レッツゴー!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = フォローしていない人のメディアです
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = このカラムを別の位置に移動します
# Title for the user's deck
My_Deck_4ac5 = あなたのデッキ
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Nostr は初めてですか?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Nostr アドレス (NIP-05)
# Default username when profile is not available
nostrich_df29 = ノス民
# Status label for disconnected relay
Not_Connected_6292 = 未接続
# Link text for note references
note_cad6 = 投稿
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck はベータ製品です。問題が発生した場合はサポートに問い合わせてください。
# Filter label for notes only view
Notes_03fb = 投稿
# Label for notes-only filter
Notes_60d2 = 投稿
# Filter label for notes and replies view
Notes___Replies_1ec2 = 投稿 & 返信
# Label for notes and replies filter
Notes___Replies_6e3b = 投稿 & 返信
# Column title for notifications
Notifications_d673 = 通知
# Title for notifications column
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = たった今
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 有効
# Button label to open email client
Open_Email_25e9 = メールを開く
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = デフォルトのメールクライアントを開いて、Damus チームのヘルプを表示しましょう。
# Label for others settings section
Others_7267 = その他
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = ここに NWC の URI を貼り付けてください...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = デッキの名前を作成してください。
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = デッキの名前を作成してアイコンを選択してください。
# Error message for missing deck icon
Please_select_an_icon_655b = アイコンを選択してください。
# Button label to post a note
Post_now_8a49 = すぐに投稿
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = 下のボタンを押して、最新のログをシステムのクリップボードにコピーします。その後、メールに貼り付けてください。
# Profile picture URL field label
Profile_picture_81ff = プロフィール写真
# Column title for quote composition
Quote_475c = 引用
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = 不明な投稿の引用です
# Label for read-only profile mode
Read_only_82ff = 読み取り専用
# Column title for relay management
Relays_9d89 = リレー
# Label for relay list section
Relays_ad5e = リレー
# Column title for reply composition
Reply_3bf1 = 返信
# Hover text for reply button
Reply_to_this_note_f5de = この投稿に返信
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = 不明な投稿に返信しています
# Fallback template for replying to user
replying_to__user_15ab = { $user } に返信
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = 誰かのスレッドで { $user } に返信
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = { $user }の { $note } の { $thread_user }の { $thread } に返信
# Template for replying to user's note
replying_to__user__s__note_ccba = { $user }の { $note } に返信
# Template for replying to root thread
replying_to__user__s__thread_444d = { $user }の { $thread } に返信
# Fallback text when reply note is not found
replying_to_a_note_e0bc = 投稿に返信
# Hover text for repost button
Repost_this_note_8e56 = このメモを再投稿
# Label for reposted notes
Reposted_61c8 = 再投稿
# Label for reset note body font size, Appearance settings section
Reset_4e60 = リセット
# Label for reset zoom level, Appearance settings section
Reset_62d4 = リセット
# Heading for support section
Running_into_a_bug_1796 = バグに遭遇しましたか?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = 保存
# Button label to save profile changes
Save_changes_00db = 変更を保存
# Column title for search page
Search_c573 = 検索
# Placeholder for search notes input field
Search_notes_42a6 = 投稿を検索しましょう...
# Search in progress message
Searching_for___query_5d18 = 「{ $query }」を検索中
# Description for Home column
See_notes_from_your_contacts_ac16 = フォローしている人の投稿を表示
# Description for universe column
See_the_whole_nostr_universe_7694 = 全ユニバースを表示します
# Button label to send a zap
Send_1ea4 = 送信
# Column title for app settings
Settings_7a4f = 設定
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 一覧から各ユーザーの最後の投稿を表示する
# Button label to sign out of account
Sign_out_337b = サインアウト
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = 他の人の投稿
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 他の人の通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 最新の返信を最初に並べ替え:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = フォローリストにある各ユーザーの最後の投稿を取得します
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = 特定のハッシュタグで最新の情報を受け取ります
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = 通知とメンションの最新の情報を受け取ります
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = 他のユーザーの投稿と返信の最新の情報を受け取ります
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = 他のユーザーの投稿と返信の最新の情報を受け取ります
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = 投稿と返信の最新の情報を受け取ります
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = あなたの通知とメンションの最新の情報を受け取ります
# Step 1 label in support instructions
Step_1_8656 = ステップ 1
# Step 2 label in support instructions
Step_2_d08d = ステップ 2
# Label for storage settings section
Storage_ed65 = ストレージ
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = 他のユーザー投稿の購読
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 投稿の購読
# Support email address
Support_email_44d9 = サポートメール:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = ダークモードに切り替える
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = ライトモードに切り替える
# Button text to load blurred media
Tap_to_Load_4b05 = タップして読み込む
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI アシスタントトライアルが終了しました: (テストしていただきありがとうございます! Zap 対応デイブは近日公開予定です!
# Label for theme, Appearance settings section
Theme_4aac = テーマ:
# Column title for note thread view
Thread_0f20 = スレッド
# Link text for thread references
thread_ad1f = スレッド
# Title for universe column
Universe_e01e = ユニバース
# Column title for universe feed
Universe_ffaa = ユニバース
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = このウォレットを現在のアカウントにのみ使用する
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $domain }" の "{ $username }" が識別に使用されます
# Profile username field label
Username_daa7 = ユーザー名
# Label for view folder button, Storage settings section
View_folder_9742 = フォルダを表示
# Column title for wallet management
Wallet_5e50 = ウォレット
# Hint for deck name input field
We_recommend_short_names_083e = 短い名前を推奨しています
# Profile website field label
Website_7980 = Web サイト
# Placeholder for note input field
Write_a_banger_note_here_bad2 = アツい一言をどうぞ...
# Placeholder text for key input field
Your_key_here_81bd = ここに鍵を入力...
# Title for your notes column
Your_Notes_f6db = 投稿
# Title for your notifications column
Your_Notifications_080d = 通知
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = この投稿に Zap
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = 拡大率:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] { $query } の結果を '{ $count }' 件取得しました
*[other] ' { $query } の結果を '{ $count }' 件取得しました
}

View File

@@ -1,410 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Sobre
# Column title for account management
Accounts_f018 = Contas
# Button label to add a relay
Add_269d = Transmitir
# Label for add column button
Add_47df = Adicionar coluna
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Adicionar outra carteira a ser usada apenas nesta conta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Obrigatório adicionar carteira
# Button label to add a new account
Add_account_1cfc = Adicionar conta nova aqui
# Column title for adding new account
Add_Account_d06c = Adicionar nova conta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Adicionar coluna de algoritmo
# Column title for adding new column
Add_Column_c764 = Adicionar coluna
# Column title for adding new deck
Add_Deck_fabf = Adicionar aba
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Adicionar coluna de notificações externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Adicionar coluna de #
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Adicionar última coluna de notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Adicionar coluna de notificações
# Button label to add a relay
Add_relay_269d = Adicionar transmissão
# Button label to add a wallet
Add_Wallet_d1be = Adicionar carteira
# Title for algorithmic feeds column
Algo_2452 = Algoritmos
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algoritmos para pesquisar notas
# Label for zap amount input field
Amount_70f0 = Valor
# Label for appearance settings section
Appearance_4c7f = Aparência
# Button to send message to Dave AI assistant
Ask_b7f4 = Perguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Perguntar ao Dave
# Profile banner URL field label
Banner_52ef = Destaque
# Beta version label
BETA_8e5d = Beta
# Broadcast the note to all connected relays
Broadcast_fe43 = Encaminhar
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Encaminhar especificamente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpar cache
# Hover text for editable zap amount
Click_to_edit_0414 = Editar valor
# Column title for note composition
Compose_Note_c094 = Compor nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar canais
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectar
# Status label for connecting relay
Connecting_6b7e = Conectando...
# Title for contact list column
Contact_List_f85a = Lista de contatos
# Column title for contact lists
Contacts_7533 = Contatos
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contatos (últimas notas)
# Button label to copy logs
Copy_a688 = Copiar
# Button to copy media link to clipboard
Copy_Link_dc7c = Copiar link
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copiar ID da nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar nota "JSON"
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar chave pública
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
count_d_b9be = { $count }D
# Relative time in hours
count_h_3ecb = { $count }H
# Relative time in minutes
count_m_b41e = { $count }M
# Relative time in months
count_mo_7aba = { $count }Mes
# Relative time in seconds
count_s_aa26 = { $count }S
# Relative time in weeks
count_w_7468 = { $count }Sem
# Relative time in years
count_y_9408 = { $count }A
# Button to create a new account
Create_Account_6994 = Criar conta
# Button label to create a new deck
Create_Deck_16b7 = Criar aba
# Column title for custom timelines
Custom_a69e = Personalizar
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar valor do ZAP
# Column title for support page
Damus_Support_27c0 = Ajuda
# Label for Theme Dark, Appearance settings section
Dark_85fe = Modo escuro
# Label for deck name input field
Deck_name_cd32 = Nome da aba
# Label for decks section in side panel
DECKS_1fad = ABAS
# Label for default zap amount input
Default_amount_per_zap_399d = Valor padrão de ZAP
# Name of the default deck feed
Default_Deck_fcca = Nome padrão de abas
# Button label to delete a deck
Delete_Deck_bb29 = Deletar aba
# Tooltip for deleting a column
Delete_this_column_8d5a = Deletar esta coluna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Deletar carteira
# Profile display name field label
Display_name_f9d9 = Nome de exibição
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" será utilizado para identificação
# Column title for editing deck
Edit_Deck_4018 = Editar aba
# Button label to edit a deck
Edit_Deck_fd93 = Editar
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
Edit_Profile_8ad4 = Editar perfil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Digite as # desejadas aqui (para múltiplos espaços separados)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Insira a retransmissão aqui
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Digite a chave do usuário (npub, hex, nip05) aqui...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Sua chave aqui
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insira sua chave pública (npub), endereço do Nostr (e.g. { $address }), ou chave privada (nsec). Você deve digitar sua chave privada para conseguir publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Pesquisar usuário
# Label for font size, Appearance settings section
Font_size_dd73 = Tamanho da letra
# Title for hashtags column
Hashtags_f8e0 = #
# Title for Home column
Home_8c19 = Início
# Label for deck icon selection
Icon_b0ab = Ícone
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamanho do cache de imagem:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Quantia inválida
# Error message for invalid key input
Invalid_key_4726 = Chave inválida
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI Inválido
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100 mil
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10 mil
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20 mil
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50 mil
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5 mil
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Acompanhe suas notas e respostas
# Label for language, Appearance settings section
Language_e264 = Idioma
# Title for last note per user column
Last_Note_per_User_17ad = Última Nota por Usuário
# Label for Theme Light, Appearance settings section
Light_7475 = Modo claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Endereço de rede de eletrização (lud16)
# Login page title
Login_9eef = Entrar
# Login button text
Login_now___let_s_do_this_5630 = Entrar agora! Vamos nessa!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Conteúdo de pessoas que você não segue
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mover esta coluna
# Title for the user's deck
My_Deck_4ac5 = Minha aba
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Novo no Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Endereço Nostr (Identidade NIP-05)
# Default username when profile is not available
nostrich_df29 = Nostrich
# Status label for disconnected relay
Not_Connected_6292 = Desconectado
# Link text for note references
note_cad6 = Nota
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck é um produto beta. Espere erros e entre em contato conosco quando tiver problemas.
# Filter label for notes only view
Notes_03fb = Notas
# Label for notes-only filter
Notes_60d2 = Notas
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notas e respostas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas e respostas
# Column title for notifications
Notifications_d673 = Notificações
# Title for notifications column
Notifications_ef56 = Notificações
# Relative time for very recent events (less than 3 seconds)
now_2181 = Agora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = Ligar
# Button label to open email client
Open_Email_25e9 = Abrir E-mail
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abra o seu cliente de e-mail padrão para obter ajuda do time Damus
# Label for others settings section
Others_7267 = Outros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Cole seu URI NWC aqui...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Por favor, crie um nome para a aba.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Por favor, crie um nome para a aba e selecione um ícone.
# Error message for missing deck icon
Please_select_an_icon_655b = Favor selecionar um ícone.
# Button label to post a note
Post_now_8a49 = Postar
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Clique abaixo para copiar seus registros mais recentes para a área de transferência do seu sistema. Em seguida, cole-os no seu E-mail.
# Profile picture URL field label
Profile_picture_81ff = Foto de perfil
# Column title for quote composition
Quote_475c = Citação
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Citação de nota desconhecida
# Label for read-only profile mode
Read_only_82ff = Modo leitura
# Column title for relay management
Relays_9d89 = Canais
# Label for relay list section
Relays_ad5e = Canais
# Column title for reply composition
Reply_3bf1 = Responder
# Hover text for reply button
Reply_to_this_note_f5de = Responder esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder nota desconhecida
# Fallback template for replying to user
replying_to__user_15ab = Respondendo { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = Respondendo { $user } no tópico de alguém
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = Resposta { $user }de { $note } em { $thread_user }' { $thread }
# Template for replying to user's note
replying_to__user__s__note_ccba = Respondendo { $user }de { $note }
# Template for replying to root thread
replying_to__user__s__thread_444d = Respondendo { $user }de { $thread }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = Respondendo nota
# Hover text for repost button
Repost_this_note_8e56 = Republicar nota
# Label for reposted notes
Reposted_61c8 = Publicada
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Redefinir
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Resetar
# Heading for support section
Running_into_a_bug_1796 = Precisa de ajuda?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Salvar
# Button label to save profile changes
Save_changes_00db = Salvo
# Column title for search page
Search_c573 = Pesquisar
# Placeholder for search notes input field
Search_notes_42a6 = Pesquisar notas...
# Search in progress message
Searching_for___query_5d18 = Pesquisando por '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Veja notas dos seus contatos
# Description for universe column
See_the_whole_nostr_universe_7694 = Veja todo o universo Nostr
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configurações
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada usuário de uma lista
# Button label to sign out of account
Sign_out_337b = Sair
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notas de outra pessoa
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Ordenar respostas mais recentes primeiro:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Fonte da última nota para cada usuário em sua lista de contatos
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantenha-se atualizado com uma certa hashtag
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Ficar atualizado com notificações e menções
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantenha-se atualizado com as notas e respostas de alguém
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantenha-se atualizado com as notificações e menções de alguém
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantenha-se atualizado com as notas e respostas de alguém
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantenha-se atualizado com suas notificações e menções
# Step 1 label in support instructions
Step_1_8656 = Passo 1
# Step 2 label in support instructions
Step_2_d08d = Passo 2
# Label for storage settings section
Storage_ed65 = Armazenamento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Inscrever-se em notas de outra pessoa
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Inscrever-se nas notas de alguém
# Support email address
Support_email_44d9 = E-mail de suporte
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Mudar para modo escuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Mudar para modo claro
# Button text to load blurred media
Tap_to_Load_4b05 = Toque para carregar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = O teste do assistente de IA Dave Nostr terminou :(. Obrigado por testar! Em breve teremos Dave habilitado para Zap
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# Column title for note thread view
Thread_0f20 = Fio
# Link text for thread references
thread_ad1f = Fio
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
Universe_ffaa = Universo
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Use esta carteira apenas para a conta atual
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = d = "{ $username }" em "{ $domain }" será usado para identificação
# Profile username field label
Username_daa7 = Usuário
# Label for view folder button, Storage settings section
View_folder_9742 = Visualizar pasta
# Column title for wallet management
Wallet_5e50 = Carteira
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nomes pequenos
# Profile website field label
Website_7980 = Site
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escreva uma nota criativa aqui.
# Placeholder text for key input field
Your_key_here_81bd = Sua chave aqui...
# Title for your notes column
Your_Notes_f6db = Suas notas
# Title for your notifications column
Your_Notifications_080d = Suas notificações
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap esta nota
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nível de zoom:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] Obteve um resultado { $count } para '{ $query }'
*[other] Obteve { $count } resultados para '{ $query }'
}

View File

@@ -1,410 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Sobre
# Column title for account management
Accounts_f018 = Contas
# Button label to add a relay
Add_269d = Adicionar
# Label for add column button
Add_47df = Adicionar
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Adicionar uma carteira diferente que será usada apenas para esta conta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Adicionar uma carteira para continuar
# Button label to add a new account
Add_account_1cfc = Adicionar conta
# Column title for adding new account
Add_Account_d06c = Adicionar conta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Adicionar coluna de algoritmo
# Column title for adding new column
Add_Column_c764 = Adicionar coluna
# Column title for adding new deck
Add_Deck_fabf = Adicionar aba
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Adicionar coluna de notificações externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Adicionar coluna de marcadores
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Adicionar coluna de últimas notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Adicionar coluna de notificações
# Button label to add a relay
Add_relay_269d = Adicionar relay
# Button label to add a wallet
Add_Wallet_d1be = Adicionar carteira
# Title for algorithmic feeds column
Algo_2452 = Algoritmo
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Fontes de algoritmo para ajudar na descoberta de notas
# Label for zap amount input field
Amount_70f0 = Quantia
# Label for appearance settings section
Appearance_4c7f = Aparência
# Button to send message to Dave AI assistant
Ask_b7f4 = Perguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Perguntar qualquer coisa...
# Profile banner URL field label
Banner_52ef = Faixa
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Transmissão
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Transmissão local
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpar cache
# Hover text for editable zap amount
Click_to_edit_0414 = Clica para editar
# Column title for note composition
Compose_Note_c094 = Compor nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar relays
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectado
# Status label for connecting relay
Connecting_6b7e = A conectar...
# Title for contact list column
Contact_List_f85a = Lista de contactos
# Column title for contact lists
Contacts_7533 = Contactos
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contactos (últimas notas)
# Button label to copy logs
Copy_a688 = Copiar
# Button to copy media link to clipboard
Copy_Link_dc7c = Copiar link
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copiar ID da nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON da nota
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar chave pública
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
count_d_b9be = { $count }d
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }m
# Relative time in months
count_mo_7aba = { $count } mês(es)
# Relative time in seconds
count_s_aa26 = { $count } s
# Relative time in weeks
count_w_7468 = { $count } semana(s)
# Relative time in years
count_y_9408 = { $count } ano(s)
# Button to create a new account
Create_Account_6994 = Criar conta
# Button label to create a new deck
Create_Deck_16b7 = Criar aba
# Column title for custom timelines
Custom_a69e = Personalizadas
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar valor do zap
# Column title for support page
Damus_Support_27c0 = Suporte Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = Modo escuro
# Label for deck name input field
Deck_name_cd32 = Nome da aba
# Label for decks section in side panel
DECKS_1fad = ABAS
# Label for default zap amount input
Default_amount_per_zap_399d = Valor padrão por zap:
# Name of the default deck feed
Default_Deck_fcca = Aba padrão
# Button label to delete a deck
Delete_Deck_bb29 = Excluir aba
# Tooltip for deleting a column
Delete_this_column_8d5a = Apagar esta coluna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Eliminar carteira
# Profile display name field label
Display_name_f9d9 = Nome a mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" será usado para identificação
# Column title for editing deck
Edit_Deck_4018 = Editar aba
# Button label to edit a deck
Edit_Deck_fd93 = Editar aba
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
Edit_Profile_8ad4 = Editar perfil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Insere aqui os marcadores desejados (para múltiplos com espaços separados)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Insere aqui o relay
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Insere aqui a chave de utilizador (npub, hex, nip05)
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Insere a tua chave
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insere a tua chave públca (npub), endereço nostr (por exemplo { $address }), ou chave privada (nsec). Tens de inserir a tua chave pública para publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Encontrar utilizador
# Label for font size, Appearance settings section
Font_size_dd73 = Tamanho da letra:
# Title for hashtags column
Hashtags_f8e0 = Marcadores
# Title for Home column
Home_8c19 = Início
# Label for deck icon selection
Icon_b0ab = Ícone
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamanho do cache da imagem:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Quantia inválida
# Error message for invalid key input
Invalid_key_4726 = Chave inválida.
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI inválido.
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Acompanha as tuas notas e respostas
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por utilizador
# Label for Theme Light, Appearance settings section
Light_7475 = Modo claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Endereço da rede Lightning (lud16)
# Login page title
Login_9eef = Iniciar sessão
# Login button text
Login_now___let_s_do_this_5630 = Entra agora — vamos fazer isto!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Conteúdo de alguém que não segues
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mover esta coluna para outra posição
# Title for the user's deck
My_Deck_4ac5 = Minha aba
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Nov@ no Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Endereço Nostr (identificação NIP-05)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = Não conectado
# Link text for note references
note_cad6 = nota
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck é um produto beta. Espere bugs e contacte-nos quando tiver problemas.
# Filter label for notes only view
Notes_03fb = Notas
# Label for notes-only filter
Notes_60d2 = Notas
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notas e respostas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas e respostas
# Column title for notifications
Notifications_d673 = Notificações
# Title for notifications column
Notifications_ef56 = Notificações
# Relative time for very recent events (less than 3 seconds)
now_2181 = agora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = Ativado
# Button label to open email client
Open_Email_25e9 = Abrir e-mail
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre o teu cliente de e-mail padrão para obteres ajuda da equipa Damus
# Label for others settings section
Others_7267 = Outros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Cola o teu NWC URI aqui...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Cria um nome para a aba.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Cria um nome para a aba e seleciona um ícone.
# Error message for missing deck icon
Please_select_an_icon_655b = Seleciona um ícone.
# Button label to post a note
Post_now_8a49 = Publicar agora
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Prime o botão abaixo para copiar os teus registos mais recentes para a área de transferência do teu sistema. Depois cola-os no teu e-mail.
# Profile picture URL field label
Profile_picture_81ff = Foto de perfil
# Column title for quote composition
Quote_475c = Citação
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Citação de nota desconhecida
# Label for read-only profile mode
Read_only_82ff = Somente leitura
# Column title for relay management
Relays_9d89 = Relays
# Label for relay list section
Relays_ad5e = Relays
# Column title for reply composition
Reply_3bf1 = Responder
# Hover text for reply button
Reply_to_this_note_f5de = Responder a esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder a nota desconhecida
# Fallback template for replying to user
replying_to__user_15ab = responder a { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = responder a { $user } no tópico de alguém
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondendo à { $note } de { $user } no { $thread } de { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = respondendo à { $note } de { $user }
# Template for replying to root thread
replying_to__user__s__thread_444d = respondendo ao { $thread } de { $user }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = respondendo a uma nota
# Hover text for repost button
Repost_this_note_8e56 = Republicar esta nota
# Label for reposted notes
Reposted_61c8 = Republicado
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Redefinir
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Redefinir
# Heading for support section
Running_into_a_bug_1796 = Encontraste um bug?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Guardar
# Button label to save profile changes
Save_changes_00db = Guardar alterações
# Column title for search page
Search_c573 = Procurar
# Placeholder for search notes input field
Search_notes_42a6 = Procurar notas...
# Search in progress message
Searching_for___query_5d18 = Procurando por '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas dos meus contactos
# Description for universe column
See_the_whole_nostr_universe_7694 = Ver notas de todo o universo nostr
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configurações
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada utilizador a partir de uma lista
# Button label to sign out of account
Sign_out_337b = Terminar sessão
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notas de outra pessoa
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Ordenar respostas mais recentes antes:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Origem da última nota para cada utilizador na minha lista
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Atualizações com um dado marcador
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Atualizações com notificações e menções
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Atualizar-me de notas e respostas de outra pessoa
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Atualizar-me de notificações e menções de outra pessoa
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Atualizar-me de notas e respostas de outra pessoa
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Atualizar-me de notificações e menções
# Step 1 label in support instructions
Step_1_8656 = Passo 1
# Step 2 label in support instructions
Step_2_d08d = Passo 2
# Label for storage settings section
Storage_ed65 = Armazenamento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Subscrever as notas de outra pessoa
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Subscrever as notas de alguém
# Support email address
Support_email_44d9 = E-mail de suporte:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Mudar para o modo escuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Mudar para o modo claro
# Button text to load blurred media
Tap_to_Load_4b05 = Toca para carregar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = O teste do assistente de IA Dave Nost terminou :(. Obrigado por testares! Dave com ativação de ZAPS em breve!
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# Column title for note thread view
Thread_0f20 = Tópico
# Link text for thread references
thread_ad1f = tópico
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
Universe_ffaa = Universo
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Usar esta carteira apenas para a conta atual
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" em "{ $domain }" será usado para identificação
# Profile username field label
Username_daa7 = Nome de utilizador
# Label for view folder button, Storage settings section
View_folder_9742 = Ver pasta
# Column title for wallet management
Wallet_5e50 = Carteira
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nomes curtos
# Profile website field label
Website_7980 = Website
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escreve uma nota sonante aqui...
# Placeholder text for key input field
Your_key_here_81bd = A tua chave aqui...
# Title for your notes column
Your_Notes_f6db = Minhas notas
# Title for your notifications column
Your_Notifications_080d = Minhas notificações
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Enviar zaps a esta nota
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nível de zoom:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] { $count } resultado obtido para '{ $query }'
*[other] { $count } resultados obtidos para '{ $query }'
}

View File

@@ -1,412 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = เกี่ยวกับเรา
# Column title for account management
Accounts_f018 = บัญชีผู้ใช้
# Button label to add a relay
Add_269d = เพิ่ม
# Label for add column button
Add_47df = เพิ่ม
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = เพิ่มวอลเล็ตอื่นเพื่อใช้สำหรับบัญชีนี้โดยเฉพาะ
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = พิ่มวอลเล็ตเพื่อดำเนินการต่อ
# Button label to add a new account
Add_account_1cfc = เพิ่มบัญชี
# Column title for adding new account
Add_Account_d06c = เพิ่มบัญชี
# Column title for adding algorithm column
Add_Algo_Column_0d75 = เพิ่มคอลัมน์อัลกอฯ
# Column title for adding new column
Add_Column_c764 = เพิ่มคอลัมน์
# Column title for adding new deck
Add_Deck_fabf = เพิ่ม Deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = เพิ่มคอลัมน์การแจ้งเตือนภายนอก
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = เพิ่มคอลัมน์แฮชแท็ก
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = เพิ่มคอลัมน์โน้ตล่าสุด
# Column title for adding notifications column
Add_Notifications_Column_79f8 = เพิ่มคอลัมน์การแจ้งเตือน
# Button label to add a relay
Add_relay_269d = เพิ่มรีเลย์
# Button label to add a wallet
Add_Wallet_d1be = เพิ่มวอลเล็ต
# Title for algorithmic feeds column
Algo_2452 = อัลกอฯ
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = ฟีดแบบอัลกอริทึมที่ช่วยในการค้นหาโน้ต
# Label for zap amount input field
Amount_70f0 = จำนวน
# Label for appearance settings section
Appearance_4c7f = รูปลักษณ์
# Button to send message to Dave AI assistant
Ask_b7f4 = ถาม
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = ถามเดฟได้ทุกเรื่อง...
# Profile banner URL field label
Banner_52ef = ภาพปก
# Beta version label
BETA_8e5d = เบต้า
# Broadcast the note to all connected relays
Broadcast_fe43 = เผยแพร่
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = เผยแพร่เฉพาะที่
# Button label to cancel an action
Cancel_ed3b = ยกเลิก
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = ยกเลิก
# Label for clear cache button, Storage settings section
Clear_cache_dccb = ล้างแคช
# Hover text for editable zap amount
Click_to_edit_0414 = คลิกเพื่อแก้ไข
# Column title for note composition
Compose_Note_c094 = เขียนโน้ต
# Label for configure relays, settings section
Configure_relays_d156 = กำหนดค่ารีเลย์
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = ยืนยัน
# Button label to confirm an action
Confirm_f8a6 = ยืนยัน
# Status label for connected relay
Connected_f8cc = เชื่อมต่อแล้ว
# Status label for connecting relay
Connecting_6b7e = กำลังเชื่อมต่อ...
# Title for contact list column
Contact_List_f85a = รายชื่อผู้ติดต่อ
# Column title for contact lists
Contacts_7533 = ผู้ติดต่อ
# Column title for last notes per contact
Contacts__last_notes_3f84 = ผู้ติดต่อ (โน้ตล่าสุด)
# Button label to copy logs
Copy_a688 = คัดลอก
# Button to copy media link to clipboard
Copy_Link_dc7c = คัดลอกลิงก์
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = คัดลอก ID โน้ต
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = คัดลอก JSON ของโน้ต
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = คัดลอก Pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = คัดลอกข้อความ
# Relative time in days
count_d_b9be = { $count }d
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }m
# Relative time in months
count_mo_7aba = { $count }mo
# Relative time in seconds
count_s_aa26 = { $count }s
# Relative time in weeks
count_w_7468 = { $count }w
# Relative time in years
count_y_9408 = { $count }y
# Button to create a new account
Create_Account_6994 = สร้างบัญชี
# Button label to create a new deck
Create_Deck_16b7 = สร้าง Deck
# Column title for custom timelines
Custom_a69e = กำหนดเอง
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = กำหนดจำนวน Zap
# Column title for support page
Damus_Support_27c0 = ฝ่ายสนับสนุน Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = มืด
# Label for deck name input field
Deck_name_cd32 = ชื่อ Deck
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = ยอด Zap เริ่มต้น
# Name of the default deck feed
Default_Deck_fcca = Deck หลัก
# Button label to delete a deck
Delete_Deck_bb29 = ลบ Deck
# Tooltip for deleting a column
Delete_this_column_8d5a = ลบคอลัมน์นี้
# Button label to delete a wallet
Delete_Wallet_d1d4 = ลบวอลเล็ต
# Profile display name field label
Display_name_f9d9 = ชื่อที่แสดง
# Domain identification message
domain___will_be_used_for_identification_b67e = { $domain } จะใช้สำหรับการระบุตัวตน
# Column title for editing deck
Edit_Deck_4018 = แก้ไข Deck
# Button label to edit a deck
Edit_Deck_fd93 = แก้ไข Deck
# Button label to edit user profile
Edit_Profile_49e6 = แก้ไขโปรไฟล์
# Column title for profile editing
Edit_Profile_8ad4 = แก้ไขโปรไฟล์
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = ใส่แฮชแท็กที่ต้องการ (หากมีหลายอัน ให้คั่นด้วยการเว้นวรรค)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = ใส่รีเลย์ที่นี่
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = ใส่คีย์ของผู้ใช้ (npub, hex, nip05)...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = ใส่คีย์ของคุณ
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 =
โปรดใส่คีย์สาธารณะ (npub), ที่อยู่ Nostr (เช่น { $address }) หรือคีย์ส่วนตัว (nsec)
คุณจำเป็นต้องใส่คีย์ส่วนตัวเพื่อทำการโพสต์, ตอบกลับ และอื่นๆ
# Label for find user button
Find_User_bd12 = ค้นหาผู้ใช้
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = แฮชแท็ก
# Title for Home column
Home_8c19 = หน้าแรก
# Label for deck icon selection
Icon_b0ab = ไอคอน
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = ขนาดแคชรูปภาพ:
# Title for individual user column
Individual_b776 = ปัจเจคบุคคล
# Error message for invalid zap amount
Invalid_amount_6630 = จำนวนเงินไม่ถูกต้อง
# Error message for invalid key input
Invalid_key_4726 = คีย์ไม่ถูกต้อง
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI ไม่ถูกต้อง
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = ติดตามโน้ตและการตอบกลับของคุณ
# Label for language, Appearance settings section
Language_e264 = ภาษา:
# Title for last note per user column
Last_Note_per_User_17ad = โน้ตล่าสุดของผู้ใช้แต่ละคน
# Label for Theme Light, Appearance settings section
Light_7475 = สว่าง
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = ที่อยู่ Lightning Network (lud16)
# Login page title
Login_9eef = เข้าสู่ระบบ
# Login button text
Login_now___let_s_do_this_5630 = เข้าสู่ระบบเลย — มาเริ่มกัน!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = สื่อจากคนที่คุณไม่ได้ติดตาม
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = ย้ายคอลัมน์นี้ไปยังตำแหน่งอื่น
# Title for the user's deck
My_Deck_4ac5 = Deck ของฉัน
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = มือใหม่สำหรับ Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = ที่อยู่ Nostr (NIP-05)
# Default username when profile is not available
nostrich_df29 = นกม่วง
# Status label for disconnected relay
Not_Connected_6292 = ไม่ได้เชื่อมต่อ
# Link text for note references
note_cad6 = โน้ต
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck เป็นผลิตภัณฑ์รุ่นเบต้า ซึ่งอาจมีข้อบกพร่องเกิดขึ้นได้ โปรดติดต่อเราเมื่อคุณพบปัญหา
# Filter label for notes only view
Notes_03fb = โน้ต
# Label for notes-only filter
Notes_60d2 = โน้ต
# Filter label for notes and replies view
Notes___Replies_1ec2 = โน้ตและการตอบกลับ
# Label for notes and replies filter
Notes___Replies_6e3b = โน้ตและการตอบกลับ
# Column title for notifications
Notifications_d673 = การแจ้งเตือน
# Title for notifications column
Notifications_ef56 = การแจ้งเตือน
# Relative time for very recent events (less than 3 seconds)
now_2181 = เมื่อสักครู่
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = เปิดอีเมล
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = เปิดโปรแกรมอีเมลของคุณเพื่อรับความช่วยเหลือจากทีม Damus
# Label for others settings section
Others_7267 = อื่นๆ
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = วาง NWC URI ของคุณที่นี่...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = กรุณาตั้งชื่อ Deck
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = กรุณาตั้งชื่อ Deck และเลือกไอคอน
# Error message for missing deck icon
Please_select_an_icon_655b = กรุณาเลือกไอคอน
# Button label to post a note
Post_now_8a49 = โพสต์เลย
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = กดปุ่มด้านล่างเพื่อคัดลอกข้อมูลบันทึกล่าสุด ไปยังคลิปบอร์ด จากนั้นนำไปวางในอีเมลของคุณ
# Profile picture URL field label
Profile_picture_81ff = รูปโปรไฟล์
# Column title for quote composition
Quote_475c = อ้างอิง
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = อ้างอิงโน้ตที่ไม่รู้จัก
# Label for read-only profile mode
Read_only_82ff = อ่านอย่างเดียว
# Column title for relay management
Relays_9d89 = รีเลย์
# Label for relay list section
Relays_ad5e = รีเลย์
# Column title for reply composition
Reply_3bf1 = ตอบกลับ
# Hover text for reply button
Reply_to_this_note_f5de = ตอบกลับโน้ตนี้
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = ตอบกลับโน้ตที่ไม่รู้จัก
# Fallback template for replying to user
replying_to__user_15ab = ตอบกลับ { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = ตอบกลับ { $user } ในเธรดของผู้อื่น
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = ตอบกลับโน้ต { $note } ของ { $user } ในเธรด { $thread } ของ { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = ตอบกลับโน้ต { $user } ใน { $note }
# Template for replying to root thread
replying_to__user__s__thread_444d = ตอบกลับ { $user } ใน { $thread }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = ตอบกลับโน้ต
# Hover text for repost button
Repost_this_note_8e56 = รีโพสต์โน้ตนี้
# Label for reposted notes
Reposted_61c8 = รีโพสต์แล้ว
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = รีเซ็ต
# Heading for support section
Running_into_a_bug_1796 = พบปัญหาในการใช้งานใช่ไหม?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = บันทึก
# Button label to save profile changes
Save_changes_00db = บันทึกการเปลี่ยนแปลง
# Column title for search page
Search_c573 = ค้นหา
# Placeholder for search notes input field
Search_notes_42a6 = ค้นหาโน้ต...
# Search in progress message
Searching_for___query_5d18 = กำลังค้นหา '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = ดูโน้ตจากผู้ติดต่อของคุณ
# Description for universe column
See_the_whole_nostr_universe_7694 = ท่องจักรวาล Nostr ทั้งหมด
# Button label to send a zap
Send_1ea4 = ส่ง
# Column title for app settings
Settings_7a4f = การตั้งค่า
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = แสดงโน้ตล่าสุดของผู้ใช้แต่ละคนจากรายการ
# Button label to sign out of account
Sign_out_337b = ออกจากระบบ
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = โน้ตของผู้อื่น
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = การแจ้งเตือนของผู้อื่น
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = ดึงโน้ตล่าสุดของผู้ใช้แต่ละคนในรายชื่อผู้ติดต่อ
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = ติดตามความเคลื่อนไหวของแฮชแท็ก
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = ติดตามการแจ้งเตือนและการกล่าวถึง
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = ติดตามโน้ตและการตอบกลับของผู้อื่น
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = ติดตามการแจ้งเตือนและการกล่าวถึงของผู้อื่น
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = ติดตามโน้ตและการตอบกลับของผู้อื่น
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = ติดตามการแจ้งเตือนและการกล่าวถึงของคุณ
# Step 1 label in support instructions
Step_1_8656 = ขั้นตอนที่ 1
# Step 2 label in support instructions
Step_2_d08d = ขั้นตอนที่ 2
# Label for storage settings section
Storage_ed65 = พื้นที่จัดเก็บ
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = ติดตามโน้ตของผู้อื่น
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = ติดตามโน้ตของผู้อื่น
# Support email address
Support_email_44d9 = Support email:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = เปลี่ยนเป็นโหมดมืด
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = เปลี่ยนเป็นโหมดสว่าง
# Button text to load blurred media
Tap_to_Load_4b05 = แตะเพื่อโหลด
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = ช่วงทดลองใช้ผู้ช่วย AI 'Dave Nostr' ได้สิ้นสุดลงแล้ว :( ขอบคุณที่ร่วมทดสอบ! Dave ที่รองรับการ Zap กำลังจะมาเร็วๆ นี้!
# Label for theme, Appearance settings section
Theme_4aac = ธีม:
# Column title for note thread view
Thread_0f20 = เธรด
# Link text for thread references
thread_ad1f = เธรด
# Title for universe column
Universe_e01e = จักรวาล
# Column title for universe feed
Universe_ffaa = จักรวาล
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = ใช้วอลเล็ตนี้สำหรับบัญชีปัจจุบันเท่านั้น
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" ที่ "{ $domain }" จะถูกใช้สำหรับการระบุตัวตน
# Profile username field label
Username_daa7 = ชื่อผู้ใช้
# Label for view folder button, Storage settings section
View_folder_9742 = ดูโฟลเดอร์
# Column title for wallet management
Wallet_5e50 = วอลเล็ต
# Hint for deck name input field
We_recommend_short_names_083e = เราแนะนำให้ใช้ชื่อสั้นๆ
# Profile website field label
Website_7980 = เว็บไซต์
# Placeholder for note input field
Write_a_banger_note_here_bad2 = เขียนโน้ตปังๆ ที่นี่...
# Placeholder text for key input field
Your_key_here_81bd = ใส่คีย์ของคุณที่นี่...
# Title for your notes column
Your_Notes_f6db = โน้ตของคุณ
# Title for your notifications column
Your_Notifications_080d = การแจ้งเตือนของคุณ
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap โน้ตนี้
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = ระดับการซูม:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] ผลการค้นหา '{ $query }': พบ { $count } รายการ
*[other] ผลการค้นหา '{ $query }': พบ { $count } รายการ
}

View File

@@ -1,411 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = 关于
# Column title for account management
Accounts_f018 = 帐户
# Button label to add a relay
Add_269d = 添加
# Label for add column button
Add_47df = 添加
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = 添加一个仅用于此帐户的不同钱包
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = 添加钱包以继续
# Button label to add a new account
Add_account_1cfc = 添加帐户
# Column title for adding new account
Add_Account_d06c = 添加帐户
# Column title for adding algorithm column
Add_Algo_Column_0d75 = 添加算法列
# Column title for adding new column
Add_Column_c764 = 添加列
# Column title for adding new deck
Add_Deck_fabf = 添加仪表板
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = 添加外部通知列
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = 添加标签列
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = 添加最新笔记列
# Column title for adding notifications column
Add_Notifications_Column_79f8 = 添加通知列
# Button label to add a relay
Add_relay_269d = 添加中继器
# Button label to add a wallet
Add_Wallet_d1be = 添加钱包
# Title for algorithmic feeds column
Algo_2452 = 算法
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 用于帮助发现笔记的算法源
# Label for zap amount input field
Amount_70f0 = 金额
# Label for appearance settings section
Appearance_4c7f = 外观
# Button to send message to Dave AI assistant
Ask_b7f4 = 询问
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = 向 Dave 提问任何问题…
# Profile banner URL field label
Banner_52ef = 横幅
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = 广播
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = 仅广播至本地中继
# Button label to cancel an action
Cancel_ed3b = 取消
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = 取消
# Label for clear cache button, Storage settings section
Clear_cache_dccb = 清除缓存
# Hover text for editable zap amount
Click_to_edit_0414 = 点击以编辑
# Column title for note composition
Compose_Note_c094 = 撰写笔记
# Label for configure relays, settings section
Configure_relays_d156 = 配置中继器
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = 确认
# Button label to confirm an action
Confirm_f8a6 = 确认
# Status label for connected relay
Connected_f8cc = 已连接
# Status label for connecting relay
Connecting_6b7e = 正在连接...
# Title for contact list column
Contact_List_f85a = 联系人列表
# Column title for contact lists
Contacts_7533 = 联系人
# Column title for last notes per contact
Contacts__last_notes_3f84 = 联系人(最新笔记)
# Button label to copy logs
Copy_a688 = 复制
# Button to copy media link to clipboard
Copy_Link_dc7c = 复制链接
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = 复制笔记 ID
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = 复制笔记 JSON
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = 复制公钥
# Copy the text content of the note to clipboard
Copy_Text_f81c = 复制文本
# Relative time in days
count_d_b9be = { $count }天
# Relative time in hours
count_h_3ecb = { $count }小时
# Relative time in minutes
count_m_b41e = { $count }分钟
# Relative time in months
count_mo_7aba = { $count }月
# Relative time in seconds
count_s_aa26 = { $count }秒
# Relative time in weeks
count_w_7468 = { $count }周
# Relative time in years
count_y_9408 = { $count }年
# Button to create a new account
Create_Account_6994 = 创建帐户
# Button label to create a new deck
Create_Deck_16b7 = 创建仪表板
# Column title for custom timelines
Custom_a69e = 自定义
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = 自定义打闪金额
# Column title for support page
Damus_Support_27c0 = 达摩支持
# Label for Theme Dark, Appearance settings section
Dark_85fe = 暗色
# Label for deck name input field
Deck_name_cd32 = 仪表板名称
# Label for decks section in side panel
DECKS_1fad = 仪表板
# Label for default zap amount input
Default_amount_per_zap_399d = 打闪默认金额:
# Name of the default deck feed
Default_Deck_fcca = 默认仪表板
# Button label to delete a deck
Delete_Deck_bb29 = 删除仪表板
# Tooltip for deleting a column
Delete_this_column_8d5a = 删除此列
# Button label to delete a wallet
Delete_Wallet_d1d4 = 删除钱包
# Profile display name field label
Display_name_f9d9 = 显示名称
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" 将用于身份识别
# Column title for editing deck
Edit_Deck_4018 = 编辑仪表板
# Button label to edit a deck
Edit_Deck_fd93 = 编辑仪表板
# Button label to edit user profile
Edit_Profile_49e6 = 编辑个人档案
# Column title for profile editing
Edit_Profile_8ad4 = 编辑个人档案
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = 在此输入所需的标签 (用于多个时以空格分隔)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = 在此输入中继器
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = 在此输入用户的密钥npub、hex、nip05...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = 请输入你的密钥
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 请输入你的公钥npub、nostr 地址(如 { $address })、或私钥(nsec)。 你必须输入你的私钥才能发帖、回复等等。
# Label for find user button
Find_User_bd12 = 查找用户
# Label for font size, Appearance settings section
Font_size_dd73 = 字体大小:
# Title for hashtags column
Hashtags_f8e0 = 标签
# Title for Home column
Home_8c19 = 主页
# Label for deck icon selection
Icon_b0ab = 图标
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = 图像缓存大小:
# Title for individual user column
Individual_b776 = 个人
# Error message for invalid zap amount
Invalid_amount_6630 = 无效金额
# Error message for invalid key input
Invalid_key_4726 = 无效密钥。
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = 无效 NWC URI
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 10万
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 1万
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 2万
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 5万
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5千
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = 随时查看你的笔记和回复
# Label for language, Appearance settings section
Language_e264 = 语言:
# Title for last note per user column
Last_Note_per_User_17ad = 每个用户的最新笔记
# Label for Theme Light, Appearance settings section
Light_7475 = 亮色
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = 闪电网络地址lud16
# Login page title
Login_9eef = 登录
# Login button text
Login_now___let_s_do_this_5630 = 立即登录——让我们开始吧!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = 来自你不关注的用户的媒体
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = 将此列移动到其他位置
# Title for the user's deck
My_Deck_4ac5 = 我的仪表板
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = 第一次使用 Nostr
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Nostr 地址 (NIP-05 标识符)
# Default username when profile is not available
nostrich_df29 = nostr 用户
# Status label for disconnected relay
Not_Connected_6292 = 未连接
# Link text for note references
note_cad6 = 笔记
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck目前是测试版产品。可能会出现故障如果遇到问题请及时联系我们。
# Filter label for notes only view
Notes_03fb = 笔记
# Label for notes-only filter
Notes_60d2 = 笔记
# Filter label for notes and replies view
Notes___Replies_1ec2 = 笔记和回复
# Label for notes and replies filter
Notes___Replies_6e3b = 笔记和回复
# Column title for notifications
Notifications_d673 = 通知
# Title for notifications column
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = 刚刚
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 开启
# Button label to open email client
Open_Email_25e9 = 打开电子邮箱
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打开你的默认电子邮件客户端以获得达摩团队的帮助
# Label for others settings section
Others_7267 = 其它
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = 在此粘贴你的 NWC URI...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = 请为仪表板创建一个名称。
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = 请为仪表板创建一个名称并选择一个图标。
# Error message for missing deck icon
Please_select_an_icon_655b = 请选择一个图标。
# Button label to post a note
Post_now_8a49 = 立即发布
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = 请按下面的按钮将你最近的日志复制到系统剪贴板,然后将其粘贴到你的电子邮件。
# Profile picture URL field label
Profile_picture_81ff = 头像图片
# Column title for quote composition
Quote_475c = 引用
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = 引用未知笔记
# Label for read-only profile mode
Read_only_82ff = 只读
# Column title for relay management
Relays_9d89 = 中继器
# Label for relay list section
Relays_ad5e = 中继器
# Column title for reply composition
Reply_3bf1 = 回复
# Hover text for reply button
Reply_to_this_note_f5de = 回复此笔记
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = 回复未知笔记
# Fallback template for replying to user
replying_to__user_15ab = 正在回复{ $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = 正在回复某人帖子中的{ $user }
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = 正在回复在{ $thread_user }的{ $thread }中的{ $user }的{ $note }
# Template for replying to user's note
replying_to__user__s__note_ccba = 正在回复{ $user }的{ $note }
# Template for replying to root thread
replying_to__user__s__thread_444d = 正在回复{ $user }的{ $thread }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = 正在回复笔记
# Hover text for repost button
Repost_this_note_8e56 = 转发此笔记
# Label for reposted notes
Reposted_61c8 = 已转发
# Label for reset note body font size, Appearance settings section
Reset_4e60 = 重置
# Label for reset zoom level, Appearance settings section
Reset_62d4 = 重置
# Heading for support section
Running_into_a_bug_1796 = 遇到故障了吗?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = 聪
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = 聪
# Button to save default zap amount
Save_6f7c = 保存
# Button label to save profile changes
Save_changes_00db = 保存变更
# Column title for search page
Search_c573 = 搜索
# Placeholder for search notes input field
Search_notes_42a6 = 搜索笔记...
# Search in progress message
Searching_for___query_5d18 = 正在搜索'{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = 查看来自你的联系人的笔记
# Description for universe column
See_the_whole_nostr_universe_7694 = 查看整个 nostr 宇宙
# Button label to send a zap
Send_1ea4 = 发送
# Column title for app settings
Settings_7a4f = 设置
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 显示列表中每个用户的最新一条笔记
# Button label to sign out of account
Sign_out_337b = 登出
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = 其他人的笔记
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 其他人的通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 按最新排序回复:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = 获取你的联系人列表中每个用户的最新一条笔记
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = 获取某个标签的最新动态
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = 获取通知和提及的最新动态
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = 获取其他用户的笔记和回复的最新动态
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = 获取其他用户的通知和提及的最新动态
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = 获取某人的笔记和回复的最新动态
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = 获取你的通知和提及的最新动态
# Step 1 label in support instructions
Step_1_8656 = 第一步
# Step 2 label in support instructions
Step_2_d08d = 第二步
# Label for storage settings section
Storage_ed65 = 存储
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = 订阅他人的笔记
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 订阅某人的笔记
# Support email address
Support_email_44d9 = 支持电子邮件:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = 切换到暗色模式
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = 切换到亮色模式
# Button text to load blurred media
Tap_to_Load_4b05 = 点击加载
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI 助手试用期已经结束 :(。感谢测试!可打闪付款的 Dave 即将来临!
# Label for theme, Appearance settings section
Theme_4aac = 主题:
# Column title for note thread view
Thread_0f20 = 帖子
# Link text for thread references
thread_ad1f = 帖子
# Title for universe column
Universe_e01e = 宇宙
# Column title for universe feed
Universe_ffaa = 宇宙
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = 此钱包仅限用于当前帐户
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" 于 "{ $domain }" 将被用于身份识别
# Profile username field label
Username_daa7 = 用户名
# Label for view folder button, Storage settings section
View_folder_9742 = 查看文件夹
# Column title for wallet management
Wallet_5e50 = 钱包
# Hint for deck name input field
We_recommend_short_names_083e = 我们推荐使用简短的名称
# Profile website field label
Website_7980 = 网站
# Placeholder for note input field
Write_a_banger_note_here_bad2 = 在这里写条超赞的笔记...
# Placeholder text for key input field
Your_key_here_81bd = 在此输入你的密钥...
# Title for your notes column
Your_Notes_f6db = 你的笔记
# Title for your notifications column
Your_Notifications_080d = 你的通知
# Heading for zap (tip) action
Zap_16b4 = 打闪
# Hover text for zap button
Zap_this_note_42b2 = 打闪此笔记
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = 缩放大小:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{
$count ->
[one] 查询"{ $query }"得到{ $count }条结果
*[other] 查询"{ $query }"得到{ $count }条结果
}

View File

@@ -1,411 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = 關於
# Column title for account management
Accounts_f018 = 帳戶
# Button label to add a relay
Add_269d = 添加
# Label for add column button
Add_47df = 添加
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = 添加一個僅用於此帳戶的不同錢包
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = 添加錢包以繼續
# Button label to add a new account
Add_account_1cfc = 新增帳戶
# Column title for adding new account
Add_Account_d06c = 新增帳戶
# Column title for adding algorithm column
Add_Algo_Column_0d75 = 添加算法列
# Column title for adding new column
Add_Column_c764 = 添加列
# Column title for adding new deck
Add_Deck_fabf = 添加儀表板
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = 添加外部通知列
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = 添加標籤列
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = 添加最新筆記列
# Column title for adding notifications column
Add_Notifications_Column_79f8 = 添加通知列
# Button label to add a relay
Add_relay_269d = 新增中繼器
# Button label to add a wallet
Add_Wallet_d1be = 新增錢包
# Title for algorithmic feeds column
Algo_2452 = 算法
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 用於幫助發現筆記的算法源
# Label for zap amount input field
Amount_70f0 = 金額
# Label for appearance settings section
Appearance_4c7f = 外觀
# Button to send message to Dave AI assistant
Ask_b7f4 = 詢問
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = 向 Dave 提問任何問題...
# Profile banner URL field label
Banner_52ef = 橫幅
# Beta version label
BETA_8e5d = 測試版
# Broadcast the note to all connected relays
Broadcast_fe43 = 廣播
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = 僅廣播至本地中繼
# Button label to cancel an action
Cancel_ed3b = 取消
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = 取消
# Label for clear cache button, Storage settings section
Clear_cache_dccb = 清除快取
# Hover text for editable zap amount
Click_to_edit_0414 = 點擊編輯
# Column title for note composition
Compose_Note_c094 = 撰寫筆記
# Label for configure relays, settings section
Configure_relays_d156 = 配置中繼器
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = 確認
# Button label to confirm an action
Confirm_f8a6 = 確認
# Status label for connected relay
Connected_f8cc = 已連接
# Status label for connecting relay
Connecting_6b7e = 正在連接 ...
# Title for contact list column
Contact_List_f85a = 聯絡人列表
# Column title for contact lists
Contacts_7533 = 聯絡人
# Column title for last notes per contact
Contacts__last_notes_3f84 = 聯絡人(最新筆記)
# Button label to copy logs
Copy_a688 = 複製
# Button to copy media link to clipboard
Copy_Link_dc7c = 複製鏈接
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = 複製筆記 ID
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = 複製筆記 JSON
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = 複製公鑰
# Copy the text content of the note to clipboard
Copy_Text_f81c = 複製文字
# Relative time in days
count_d_b9be = { $count }天
# Relative time in hours
count_h_3ecb = { $count }小時
# Relative time in minutes
count_m_b41e = { $count }分鐘
# Relative time in months
count_mo_7aba = { $count }月
# Relative time in seconds
count_s_aa26 = { $count }秒
# Relative time in weeks
count_w_7468 = { $count }週
# Relative time in years
count_y_9408 = { $count }年
# Button to create a new account
Create_Account_6994 = 創建帳戶
# Button label to create a new deck
Create_Deck_16b7 = 創建儀表板
# Column title for custom timelines
Custom_a69e = 自訂
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = 自訂打閃金額
# Column title for support page
Damus_Support_27c0 = 達摩支持
# Label for Theme Dark, Appearance settings section
Dark_85fe = 暗色
# Label for deck name input field
Deck_name_cd32 = 儀表板名稱
# Label for decks section in side panel
DECKS_1fad = 儀表板
# Label for default zap amount input
Default_amount_per_zap_399d = 默認打閃金額:
# Name of the default deck feed
Default_Deck_fcca = 默認儀表板
# Button label to delete a deck
Delete_Deck_bb29 = 刪除儀表板
# Tooltip for deleting a column
Delete_this_column_8d5a = 刪除此列
# Button label to delete a wallet
Delete_Wallet_d1d4 = 刪除錢包
# Profile display name field label
Display_name_f9d9 = 顯示名稱
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" 將用於身份識別
# Column title for editing deck
Edit_Deck_4018 = 編輯儀表板
# Button label to edit a deck
Edit_Deck_fd93 = 編輯儀表板
# Button label to edit user profile
Edit_Profile_49e6 = 編輯個人檔案
# Column title for profile editing
Edit_Profile_8ad4 = 編輯個人檔案
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = 在此輸入所需的標籤(用於多個時以空格分隔)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = 在此輸入中繼器
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = 請輸入用戶的密鑰npub、hex、nip05...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = 請輸入你的密鑰
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 請輸入你的公鑰npub、nostr 地址(如 { $address }、或私鑰nsec。你必須輸入你的私鑰才能發貼、回覆等等。
# Label for find user button
Find_User_bd12 = 查找用戶
# Label for font size, Appearance settings section
Font_size_dd73 = 字體大小:
# Title for hashtags column
Hashtags_f8e0 = 標籤
# Title for Home column
Home_8c19 = 主頁
# Label for deck icon selection
Icon_b0ab = 圖標
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = 圖像快取大小:
# Title for individual user column
Individual_b776 = 個人
# Error message for invalid zap amount
Invalid_amount_6630 = 無效金額
# Error message for invalid key input
Invalid_key_4726 = 無效密鑰。
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = 無效 NWC URI
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 10萬
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 1萬
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 2萬
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 5萬
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5千
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = 隨時查看你的筆記和回覆
# Label for language, Appearance settings section
Language_e264 = 語言:
# Title for last note per user column
Last_Note_per_User_17ad = 每個用戶的最新筆記
# Label for Theme Light, Appearance settings section
Light_7475 = 亮色
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = 閃電網絡地址lud16
# Login page title
Login_9eef = 登錄
# Login button text
Login_now___let_s_do_this_5630 = 立即登錄——讓我們開始吧!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = 來自你不關注的用戶的媒體
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = 將此列移動到其他位置
# Title for the user's deck
My_Deck_4ac5 = 我的儀表板
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = 第一次使用 Nostr
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Nostr 地址NIP-05 標識符)
# Default username when profile is not available
nostrich_df29 = nostr 用戶
# Status label for disconnected relay
Not_Connected_6292 = 未連接
# Link text for note references
note_cad6 = 筆記
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck 目前是測試版產品。可能會出現故障,如果遇到問題請及時聯繫我們。
# Filter label for notes only view
Notes_03fb = 筆記
# Label for notes-only filter
Notes_60d2 = 筆記
# Filter label for notes and replies view
Notes___Replies_1ec2 = 筆記和回覆
# Label for notes and replies filter
Notes___Replies_6e3b = 筆記和回覆
# Column title for notifications
Notifications_d673 = 通知
# Title for notifications column
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = 剛剛
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 開啟
# Button label to open email client
Open_Email_25e9 = 打開電子郵箱
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打開你的默認電子郵件客戶端以獲得達摩團隊的幫助
# Label for others settings section
Others_7267 = 其他
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = 在此貼上你的 NWC URI...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = 請為儀表板創建一個名稱。
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = 請為儀表板創建一個名稱並選擇一個圖標。
# Error message for missing deck icon
Please_select_an_icon_655b = 請選擇一個圖標。
# Button label to post a note
Post_now_8a49 = 立即發布
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = 請按下面的按鈕將你最近的日誌複製到剪貼板,然後將其粘貼到你的電子郵件。
# Profile picture URL field label
Profile_picture_81ff = 頭像圖片
# Column title for quote composition
Quote_475c = 引用
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = 引用未知筆記
# Label for read-only profile mode
Read_only_82ff = 只讀
# Column title for relay management
Relays_9d89 = 中繼器
# Label for relay list section
Relays_ad5e = 中繼器
# Column title for reply composition
Reply_3bf1 = 回覆
# Hover text for reply button
Reply_to_this_note_f5de = 回覆此筆記
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = 回覆未知筆記
# Fallback template for replying to user
replying_to__user_15ab = 正在回覆{ $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = 正在回覆某人帖子中的{ $user }
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = 正在回覆在{ $thread_user }的{ $thread }中的{ $user }的{ $note }
# Template for replying to user's note
replying_to__user__s__note_ccba = 正在回覆{ $user }的{ $note }
# Template for replying to root thread
replying_to__user__s__thread_444d = 正在回覆{ $user }的{ $thread }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = 正在回覆筆記
# Hover text for repost button
Repost_this_note_8e56 = 轉發此筆記
# Label for reposted notes
Reposted_61c8 = 已轉發
# Label for reset note body font size, Appearance settings section
Reset_4e60 = 重置
# Label for reset zoom level, Appearance settings section
Reset_62d4 = 重置
# Heading for support section
Running_into_a_bug_1796 = 遇到故障了嗎?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = 聰
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = 聰
# Button to save default zap amount
Save_6f7c = 保存
# Button label to save profile changes
Save_changes_00db = 保存變更
# Column title for search page
Search_c573 = 搜索
# Placeholder for search notes input field
Search_notes_42a6 = 搜索筆記...
# Search in progress message
Searching_for___query_5d18 = 正在搜索「{ $query }」
# Description for Home column
See_notes_from_your_contacts_ac16 = 查看來自你的聯繫人的筆記
# Description for universe column
See_the_whole_nostr_universe_7694 = 查看整個 nostr 宇宙
# Button label to send a zap
Send_1ea4 = 發送
# Column title for app settings
Settings_7a4f = 設置
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 顯示列表中每個用戶的最後一條筆記
# Button label to sign out of account
Sign_out_337b = 登出
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = 其他人的筆記
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 其他人的通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 按最新排序回覆:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = 獲取你的聯繫人列表中每個用戶的最新一條筆記
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = 獲取某個標籤的最新動態
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = 獲取通知和提及的最新動態
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = 獲取其他用戶的筆記和回覆的最新動態
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = 獲取其他用戶的通知和提及的最新動態
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = 獲取某人的筆記和回覆的最新動態
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = 獲取你的通知和提及的最新動態
# Step 1 label in support instructions
Step_1_8656 = 第一步
# Step 2 label in support instructions
Step_2_d08d = 第二步
# Label for storage settings section
Storage_ed65 = 儲存
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = 訂閱他人的筆記
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 訂閱某人的筆記
# Support email address
Support_email_44d9 = 支持電子郵件:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = 切換到暗色模式
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = 切換到亮色模式
# Button text to load blurred media
Tap_to_Load_4b05 = 點擊加載
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI 助手試用期已經結束 :(。感謝測試!可打閃付款的 Dave 即將來臨!
# Label for theme, Appearance settings section
Theme_4aac = 主題:
# Column title for note thread view
Thread_0f20 = 串文
# Link text for thread references
thread_ad1f = 串文
# Title for universe column
Universe_e01e = 宇宙
# Column title for universe feed
Universe_ffaa = 宇宙
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = 此錢包僅限用於當前帳戶
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" 於 "{ $domain }" 將被用於身份識別
# Profile username field label
Username_daa7 = 用戶名
# Label for view folder button, Storage settings section
View_folder_9742 = 查看文件夾
# Column title for wallet management
Wallet_5e50 = 錢包
# Hint for deck name input field
We_recommend_short_names_083e = 我們推薦使用簡短的名稱
# Profile website field label
Website_7980 = 網站
# Placeholder for note input field
Write_a_banger_note_here_bad2 = 在這裡寫條超讚的筆記...
# Placeholder text for key input field
Your_key_here_81bd = 在此輸入你的密鑰...
# Title for your notes column
Your_Notes_f6db = 你的筆記
# Title for your notifications column
Your_Notifications_080d = 你的通知
# Heading for zap (tip) action
Zap_16b4 = 打閃
# Hover text for zap button
Zap_this_note_42b2 = 打閃此筆記
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = 縮放大小:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{
$count ->
[one] 查詢"{ $query }"得到{ $count }條結果
*[other] 查詢"{ $query }"得到{ $count }條結果
}

10
build.rs Normal file
View File

@@ -0,0 +1,10 @@
use std::process::Command;
fn main() {
if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
if output.status.success() {
let hash = String::from_utf8_lossy(&output.stdout);
println!("cargo:rustc-env=GIT_COMMIT_HASH={}", hash.trim());
}
}
}

View File

@@ -1,23 +0,0 @@
[package]
name = "enostr"
version = "0.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ewebsock = { version = "0.8.0", features = ["tls"] }
serde_derive = { workspace = true }
serde = { workspace = true, features = ["derive"] } # You only need this if you want app persistence
serde_json = { workspace = true }
nostr = { workspace = true }
bech32 = { workspace = true }
nostrdb = { workspace = true }
hex = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
mio = { workspace = true }
tokio = { workspace = true }
tokenator = { workspace = true }
hashbrown = { workspace = true }

View File

@@ -1,3 +0,0 @@
mod message;
pub use message::{ClientMessage, EventClientMessage};

View File

@@ -1,61 +0,0 @@
//use nostr::prelude::secp256k1;
use std::array::TryFromSliceError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("message is empty")]
Empty,
#[error("decoding failed: {0}")]
DecodeFailed(String),
#[error("hex decoding failed")]
HexDecodeFailed,
#[error("invalid bech32")]
InvalidBech32,
#[error("invalid byte size")]
InvalidByteSize,
#[error("invalid signature")]
InvalidSignature,
#[error("invalid public key")]
InvalidPublicKey,
#[error("invalid relay url")]
InvalidRelayUrl,
// Secp(secp256k1::Error),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("nostrdb error: {0}")]
Nostrdb(#[from] nostrdb::Error),
#[error("{0}")]
Generic(String),
}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Generic(s)
}
}
impl From<TryFromSliceError> for Error {
fn from(_e: TryFromSliceError) -> Self {
Error::InvalidByteSize
}
}
impl From<hex::FromHexError> for Error {
fn from(_e: hex::FromHexError) -> Self {
Error::HexDecodeFailed
}
}

View File

@@ -1,291 +0,0 @@
use nostr::nips::nip19::FromBech32;
use nostr::nips::nip19::ToBech32;
use nostr::nips::nip49::EncryptedSecretKey;
use serde::Deserialize;
use serde::Serialize;
use tokenator::ParseError;
use tokenator::TokenParser;
use tokenator::TokenSerializable;
use crate::Pubkey;
use crate::SecretKey;
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Keypair {
pub pubkey: Pubkey,
pub secret_key: Option<SecretKey>,
}
pub struct KeypairUnowned<'a> {
pub pubkey: &'a Pubkey,
pub secret_key: Option<&'a SecretKey>,
}
impl<'a> From<&'a Keypair> for KeypairUnowned<'a> {
fn from(value: &'a Keypair) -> Self {
Self {
pubkey: &value.pubkey,
secret_key: value.secret_key.as_ref(),
}
}
}
impl Keypair {
pub fn from_secret(secret_key: SecretKey) -> Self {
let cloned_secret_key = secret_key.clone();
let nostr_keys = nostr::Keys::new(secret_key);
Keypair {
pubkey: Pubkey::new(nostr_keys.public_key().to_bytes()),
secret_key: Some(cloned_secret_key),
}
}
pub fn new(pubkey: Pubkey, secret_key: Option<SecretKey>) -> Self {
Keypair { pubkey, secret_key }
}
pub fn only_pubkey(pubkey: Pubkey) -> Self {
Keypair {
pubkey,
secret_key: None,
}
}
pub fn to_full(&self) -> Option<FilledKeypair<'_>> {
self.secret_key.as_ref().map(|secret_key| FilledKeypair {
pubkey: &self.pubkey,
secret_key,
})
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct FullKeypair {
pub pubkey: Pubkey,
pub secret_key: SecretKey,
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub struct FilledKeypair<'a> {
pub pubkey: &'a Pubkey,
pub secret_key: &'a SecretKey,
}
impl<'a> FilledKeypair<'a> {
pub fn new(pubkey: &'a Pubkey, secret_key: &'a SecretKey) -> Self {
FilledKeypair { pubkey, secret_key }
}
pub fn to_full(&self) -> FullKeypair {
FullKeypair {
pubkey: self.pubkey.to_owned(),
secret_key: self.secret_key.to_owned(),
}
}
}
impl<'a> From<&'a FilledKeypair<'a>> for KeypairUnowned<'a> {
fn from(value: &'a FilledKeypair<'a>) -> Self {
Self {
pubkey: value.pubkey,
secret_key: Some(value.secret_key),
}
}
}
impl FullKeypair {
pub fn new(pubkey: Pubkey, secret_key: SecretKey) -> Self {
FullKeypair { pubkey, secret_key }
}
pub fn to_filled(&self) -> FilledKeypair<'_> {
FilledKeypair::new(&self.pubkey, &self.secret_key)
}
pub fn generate() -> Self {
let mut rng = nostr::secp256k1::rand::rngs::OsRng;
let (secret_key, _) = &nostr::SECP256K1.generate_keypair(&mut rng);
let (xopk, _) = secret_key.x_only_public_key(&nostr::SECP256K1);
let secret_key = nostr::SecretKey::from(*secret_key);
FullKeypair {
pubkey: Pubkey::new(xopk.serialize()),
secret_key,
}
}
pub fn to_keypair(self) -> Keypair {
Keypair {
pubkey: self.pubkey,
secret_key: Some(self.secret_key),
}
}
}
impl std::fmt::Display for Keypair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Keypair:\n\tpublic: {}\n\tsecret: {}",
self.pubkey,
match self.secret_key {
Some(_) => "Some(<hidden>)",
None => "None",
}
)
}
}
impl std::fmt::Display for FullKeypair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Keypair:\n\tpublic: {}\n\tsecret: <hidden>", self.pubkey)
}
}
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SerializableKeypair {
pub pubkey: Pubkey,
pub encrypted_secret_key: Option<EncryptedSecretKey>,
}
impl SerializableKeypair {
pub fn from_keypair(kp: &Keypair, pass: &str, log_n: u8) -> Self {
Self {
pubkey: kp.pubkey,
encrypted_secret_key: kp.secret_key.clone().and_then(|s| {
EncryptedSecretKey::new(&s, pass, log_n, nostr::nips::nip49::KeySecurity::Weak).ok()
}),
}
}
pub fn to_keypair(&self, pass: &str) -> Keypair {
Keypair::new(
self.pubkey,
self.encrypted_secret_key
.and_then(|e| e.to_secret_key(pass).ok()),
)
}
}
impl TokenSerializable for Pubkey {
fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result<Self, ParseError<'a>> {
parser.parse_token(PUBKEY_TOKEN)?;
let raw = parser.pull_token()?;
let pubkey =
Pubkey::try_from_bech32_string(raw, true).map_err(|_| ParseError::DecodeFailed)?;
Ok(pubkey)
}
fn serialize_tokens(&self, writer: &mut tokenator::TokenWriter) {
writer.write_token(PUBKEY_TOKEN);
let Some(bech) = self.npub() else {
tracing::error!("Could not convert pubkey to bech: {}", self.hex());
return;
};
writer.write_token(&bech);
}
}
impl TokenSerializable for Keypair {
fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result<Self, ParseError<'a>> {
TokenParser::alt(
parser,
&[
|p| Ok(Keypair::only_pubkey(Pubkey::parse_from_tokens(p)?)),
|p| Ok(Keypair::from_secret(parse_seckey(p)?)),
],
)
}
fn serialize_tokens(&self, writer: &mut tokenator::TokenWriter) {
if let Some(seckey) = &self.secret_key {
writer.write_token(ESECKEY_TOKEN);
let maybe_eseckey = EncryptedSecretKey::new(
seckey,
ESECKEY_PASS,
7,
nostr::nips::nip49::KeySecurity::Unknown,
);
let Ok(eseckey) = maybe_eseckey else {
tracing::error!("Could not convert seckey to EncryptedSecretKey");
return;
};
let Ok(serialized) = eseckey.to_bech32() else {
tracing::error!("Could not serialize ncryptsec");
return;
};
writer.write_token(&serialized);
} else {
self.pubkey.serialize_tokens(writer);
}
}
}
const ESECKEY_TOKEN: &str = "eseckey";
const ESECKEY_PASS: &str = "notedeck";
const PUBKEY_TOKEN: &str = "pubkey";
fn parse_seckey<'a>(parser: &mut TokenParser<'a>) -> Result<SecretKey, ParseError<'a>> {
parser.parse_token(ESECKEY_TOKEN)?;
let raw = parser.pull_token()?;
let eseckey = EncryptedSecretKey::from_bech32(raw).map_err(|_| ParseError::DecodeFailed)?;
let seckey = eseckey
.to_secret_key(ESECKEY_PASS)
.map_err(|_| ParseError::DecodeFailed)?;
Ok(seckey)
}
#[cfg(test)]
mod tests {
use tokenator::{TokenParser, TokenSerializable, TokenWriter};
use super::{FullKeypair, Keypair};
#[test]
fn test_token_eseckey_serialize_deserialize() {
let kp = FullKeypair::generate();
let mut writer = TokenWriter::new("\t");
kp.clone().to_keypair().serialize_tokens(&mut writer);
let serialized = writer.str();
let data = &serialized.split("\t").collect::<Vec<&str>>();
let mut parser = TokenParser::new(data);
let m_new_kp = Keypair::parse_from_tokens(&mut parser);
assert!(m_new_kp.is_ok());
let new_kp = m_new_kp.unwrap();
assert_eq!(kp, new_kp.to_full().unwrap().to_full());
}
#[test]
fn test_token_pubkey_serialize_deserialize() {
let kp = Keypair::only_pubkey(FullKeypair::generate().pubkey);
let mut writer = TokenWriter::new("\t");
kp.clone().serialize_tokens(&mut writer);
let serialized = writer.str();
let data = &serialized.split("\t").collect::<Vec<&str>>();
let mut parser = TokenParser::new(data);
let m_new_kp = Keypair::parse_from_tokens(&mut parser);
assert!(m_new_kp.is_ok());
let new_kp = m_new_kp.unwrap();
assert_eq!(kp, new_kp);
}
}

View File

@@ -1,110 +0,0 @@
use serde_json::{Map, Value};
#[derive(Debug, Clone)]
pub struct ProfileState(Value);
impl Default for ProfileState {
fn default() -> Self {
ProfileState::new(Map::default())
}
}
impl ProfileState {
pub fn new(value: Map<String, Value>) -> Self {
Self(Value::Object(value))
}
pub fn get_str(&self, name: &str) -> Option<&str> {
self.0.get(name).and_then(|v| v.as_str())
}
pub fn values_mut(&mut self) -> &mut Map<String, Value> {
self.0.as_object_mut().unwrap()
}
/// Insert or overwrite an existing value with a string
pub fn str_mut(&mut self, name: &str) -> &mut String {
let val = self
.values_mut()
.entry(name)
.or_insert(Value::String("".to_string()));
// if its not a string, make it one. this will overrwrite
// the old value, so be careful
if !val.is_string() {
*val = Value::String("".to_string());
}
match val {
Value::String(s) => s,
// SAFETY: we replace it above, so its impossible to be something
// other than a string
_ => panic!("impossible"),
}
}
pub fn value(&self) -> &Value {
&self.0
}
pub fn to_json(&self) -> String {
// SAFETY: serializing a value should be irrefutable
serde_json::to_string(self.value()).unwrap()
}
#[inline]
pub fn name(&self) -> Option<&str> {
self.get_str("name")
}
#[inline]
pub fn banner(&self) -> Option<&str> {
self.get_str("name")
}
#[inline]
pub fn display_name(&self) -> Option<&str> {
self.get_str("display_name")
}
#[inline]
pub fn lud06(&self) -> Option<&str> {
self.get_str("lud06")
}
#[inline]
pub fn nip05(&self) -> Option<&str> {
self.get_str("nip05")
}
#[inline]
pub fn lud16(&self) -> Option<&str> {
self.get_str("lud16")
}
#[inline]
pub fn about(&self) -> Option<&str> {
self.get_str("about")
}
#[inline]
pub fn picture(&self) -> Option<&str> {
self.get_str("picture")
}
#[inline]
pub fn website(&self) -> Option<&str> {
self.get_str("website")
}
pub fn from_note_contents(contents: &str) -> Self {
let json = serde_json::from_str(contents);
let data = if let Ok(Value::Object(data)) = json {
data
} else {
Map::new()
};
Self::new(data)
}
}

View File

@@ -1,225 +0,0 @@
use ewebsock::{Options, WsEvent, WsMessage, WsReceiver, WsSender};
use mio::net::UdpSocket;
use std::io;
use std::net::IpAddr;
use std::net::{SocketAddr, SocketAddrV4};
use std::time::{Duration, Instant};
use crate::{ClientMessage, EventClientMessage, Result};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::net::Ipv4Addr;
use tracing::{debug, error};
pub mod message;
pub mod pool;
pub mod subs_debug;
#[derive(Debug, Copy, Clone)]
pub enum RelayStatus {
Connected,
Connecting,
Disconnected,
}
pub struct MulticastRelay {
last_join: Instant,
status: RelayStatus,
address: SocketAddrV4,
socket: UdpSocket,
interface: Ipv4Addr,
}
impl MulticastRelay {
pub fn new(address: SocketAddrV4, socket: UdpSocket, interface: Ipv4Addr) -> Self {
let last_join = Instant::now();
let status = RelayStatus::Connected;
MulticastRelay {
status,
address,
socket,
interface,
last_join,
}
}
/// Multicast seems to fail every 260 seconds. We force a rejoin every 200 seconds or
/// so to ensure we are always in the group
pub fn rejoin(&mut self) -> Result<()> {
self.last_join = Instant::now();
self.status = RelayStatus::Disconnected;
self.socket
.leave_multicast_v4(self.address.ip(), &self.interface)?;
self.socket
.join_multicast_v4(self.address.ip(), &self.interface)?;
self.status = RelayStatus::Connected;
Ok(())
}
pub fn should_rejoin(&self) -> bool {
(Instant::now() - self.last_join) >= Duration::from_secs(200)
}
pub fn try_recv(&self) -> Option<WsEvent> {
let mut buffer = [0u8; 65535];
// Read the size header
match self.socket.recv_from(&mut buffer) {
Ok((size, src)) => {
let parsed_size = u32::from_be_bytes(buffer[0..4].try_into().ok()?) as usize;
debug!("multicast: read size {} from start of header", size - 4);
if size != parsed_size + 4 {
error!(
"multicast: partial data received: expected {}, got {}",
parsed_size, size
);
return None;
}
let text = String::from_utf8_lossy(&buffer[4..size]);
debug!("multicast: received {} bytes from {}: {}", size, src, &text);
Some(WsEvent::Message(WsMessage::Text(text.to_string())))
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
// No data available, continue
None
}
Err(e) => {
error!("multicast: error receiving data: {}", e);
None
}
}
}
pub fn send(&self, msg: &EventClientMessage) -> Result<()> {
let json = msg.to_json();
let len = json.len();
debug!("writing to multicast relay");
let mut buf: Vec<u8> = Vec::with_capacity(4 + len);
// Write the length of the message as 4 bytes (big-endian)
buf.extend_from_slice(&(len as u32).to_be_bytes());
// Append the JSON message bytes
buf.extend_from_slice(json.as_bytes());
self.socket.send_to(&buf, SocketAddr::V4(self.address))?;
Ok(())
}
}
pub fn setup_multicast_relay(
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) -> Result<MulticastRelay> {
use mio::{Events, Interest, Poll, Token};
let port = 9797;
let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), port);
let multicast_ip = Ipv4Addr::new(239, 19, 88, 1);
let mut socket = UdpSocket::bind(address)?;
let interface = Ipv4Addr::UNSPECIFIED;
let multicast_address = SocketAddrV4::new(multicast_ip, port);
socket.join_multicast_v4(&multicast_ip, &interface)?;
let mut poll = Poll::new()?;
poll.registry().register(
&mut socket,
Token(0),
Interest::READABLE | Interest::WRITABLE,
)?;
// wakeup our render thread when we have new stuff on the socket
std::thread::spawn(move || {
let mut events = Events::with_capacity(1);
loop {
if let Err(err) = poll.poll(&mut events, None) {
error!("multicast socket poll error: {err}. ending multicast poller.");
return;
}
wakeup();
std::thread::yield_now();
}
});
Ok(MulticastRelay::new(multicast_address, socket, interface))
}
pub struct Relay {
pub url: nostr::RelayUrl,
pub status: RelayStatus,
pub sender: WsSender,
pub receiver: WsReceiver,
}
impl fmt::Debug for Relay {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Relay")
.field("url", &self.url)
.field("status", &self.status)
.finish()
}
}
impl Hash for Relay {
fn hash<H: Hasher>(&self, state: &mut H) {
// Hashes the Relay by hashing the URL
self.url.hash(state);
}
}
impl PartialEq for Relay {
fn eq(&self, other: &Self) -> bool {
self.url == other.url
}
}
impl Eq for Relay {}
impl Relay {
pub fn new(url: nostr::RelayUrl, wakeup: impl Fn() + Send + Sync + 'static) -> Result<Self> {
let status = RelayStatus::Connecting;
let (sender, receiver) =
ewebsock::connect_with_wakeup(url.as_str(), Options::default(), wakeup)?;
Ok(Self {
url,
sender,
receiver,
status,
})
}
pub fn send(&mut self, msg: &ClientMessage) {
let json = match msg.to_json() {
Ok(json) => {
debug!("sending {} to {}", json, self.url);
json
}
Err(e) => {
error!("error serializing json for filter: {e}");
return;
}
};
let txt = WsMessage::Text(json);
self.sender.send(txt);
}
pub fn connect(&mut self, wakeup: impl Fn() + Send + Sync + 'static) -> Result<()> {
let (sender, receiver) =
ewebsock::connect_with_wakeup(self.url.as_str(), Options::default(), wakeup)?;
self.status = RelayStatus::Connecting;
self.sender = sender;
self.receiver = receiver;
Ok(())
}
pub fn ping(&mut self) {
let msg = WsMessage::Ping(vec![]);
self.sender.send(msg);
}
}

View File

@@ -1,411 +0,0 @@
use crate::relay::{setup_multicast_relay, MulticastRelay, Relay, RelayStatus};
use crate::{ClientMessage, Error, Result};
use nostrdb::Filter;
use std::collections::BTreeSet;
use std::time::{Duration, Instant};
use url::Url;
#[cfg(not(target_arch = "wasm32"))]
use ewebsock::{WsEvent, WsMessage};
#[cfg(not(target_arch = "wasm32"))]
use tracing::{debug, error};
use super::subs_debug::SubsDebug;
#[derive(Debug)]
pub struct PoolEvent<'a> {
pub relay: &'a str,
pub event: ewebsock::WsEvent,
}
impl PoolEvent<'_> {
pub fn into_owned(self) -> PoolEventBuf {
PoolEventBuf {
relay: self.relay.to_owned(),
event: self.event,
}
}
}
pub struct PoolEventBuf {
pub relay: String,
pub event: ewebsock::WsEvent,
}
pub enum PoolRelay {
Websocket(WebsocketRelay),
Multicast(MulticastRelay),
}
pub struct WebsocketRelay {
pub relay: Relay,
pub last_ping: Instant,
pub last_connect_attempt: Instant,
pub retry_connect_after: Duration,
}
impl PoolRelay {
pub fn url(&self) -> &str {
match self {
Self::Websocket(wsr) => wsr.relay.url.as_str(),
Self::Multicast(_wsr) => "multicast",
}
}
pub fn set_status(&mut self, status: RelayStatus) {
match self {
Self::Websocket(wsr) => {
wsr.relay.status = status;
}
Self::Multicast(_mcr) => {}
}
}
pub fn try_recv(&self) -> Option<WsEvent> {
match self {
Self::Websocket(recvr) => recvr.relay.receiver.try_recv(),
Self::Multicast(recvr) => recvr.try_recv(),
}
}
pub fn status(&self) -> RelayStatus {
match self {
Self::Websocket(wsr) => wsr.relay.status,
Self::Multicast(mcr) => mcr.status,
}
}
pub fn send(&mut self, msg: &ClientMessage) -> Result<()> {
match self {
Self::Websocket(wsr) => {
wsr.relay.send(msg);
Ok(())
}
Self::Multicast(mcr) => {
// we only send event client messages at the moment
if let ClientMessage::Event(ecm) = msg {
mcr.send(ecm)?;
}
Ok(())
}
}
}
pub fn subscribe(&mut self, subid: String, filter: Vec<Filter>) -> Result<()> {
self.send(&ClientMessage::req(subid, filter))
}
pub fn websocket(relay: Relay) -> Self {
Self::Websocket(WebsocketRelay::new(relay))
}
pub fn multicast(wakeup: impl Fn() + Send + Sync + Clone + 'static) -> Result<Self> {
Ok(Self::Multicast(setup_multicast_relay(wakeup)?))
}
}
impl WebsocketRelay {
pub fn new(relay: Relay) -> Self {
Self {
relay,
last_ping: Instant::now(),
last_connect_attempt: Instant::now(),
retry_connect_after: Self::initial_reconnect_duration(),
}
}
pub fn initial_reconnect_duration() -> Duration {
Duration::from_secs(5)
}
}
pub struct RelayPool {
pub relays: Vec<PoolRelay>,
pub ping_rate: Duration,
pub debug: Option<SubsDebug>,
}
impl Default for RelayPool {
fn default() -> Self {
RelayPool::new()
}
}
impl RelayPool {
// Constructs a new, empty RelayPool.
pub fn new() -> RelayPool {
RelayPool {
relays: vec![],
ping_rate: Duration::from_secs(45),
debug: None,
}
}
pub fn add_multicast_relay(
&mut self,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) -> Result<()> {
let multicast_relay = PoolRelay::multicast(wakeup)?;
self.relays.push(multicast_relay);
Ok(())
}
pub fn use_debug(&mut self) {
self.debug = Some(SubsDebug::default());
}
pub fn ping_rate(&mut self, duration: Duration) -> &mut Self {
self.ping_rate = duration;
self
}
pub fn has(&self, url: &str) -> bool {
for relay in &self.relays {
if relay.url() == url {
return true;
}
}
false
}
pub fn urls(&self) -> BTreeSet<String> {
self.relays
.iter()
.map(|pool_relay| pool_relay.url().to_string())
.collect()
}
pub fn send(&mut self, cmd: &ClientMessage) {
for relay in &mut self.relays {
if let Some(debug) = &mut self.debug {
debug.send_cmd(relay.url().to_owned(), cmd);
}
if let Err(err) = relay.send(cmd) {
error!("error sending {:?} to {}: {err}", cmd, relay.url());
}
}
}
pub fn unsubscribe(&mut self, subid: String) {
for relay in &mut self.relays {
let cmd = ClientMessage::close(subid.clone());
if let Some(debug) = &mut self.debug {
debug.send_cmd(relay.url().to_owned(), &cmd);
}
if let Err(err) = relay.send(&cmd) {
error!(
"error unsubscribing from {} on {}: {err}",
&subid,
relay.url()
);
}
}
}
pub fn subscribe(&mut self, subid: String, filter: Vec<Filter>) {
for relay in &mut self.relays {
if let Some(debug) = &mut self.debug {
debug.send_cmd(
relay.url().to_owned(),
&ClientMessage::req(subid.clone(), filter.clone()),
);
}
if let Err(err) = relay.send(&ClientMessage::req(subid.clone(), filter.clone())) {
error!("error subscribing to {}: {err}", relay.url());
}
}
}
/// Keep relay connectiongs alive by pinging relays that haven't been
/// pinged in awhile. Adjust ping rate with [`ping_rate`].
pub fn keepalive_ping(&mut self, wakeup: impl Fn() + Send + Sync + Clone + 'static) {
for relay in &mut self.relays {
let now = std::time::Instant::now();
match relay {
PoolRelay::Multicast(_) => {}
PoolRelay::Websocket(relay) => {
match relay.relay.status {
RelayStatus::Disconnected => {
let reconnect_at =
relay.last_connect_attempt + relay.retry_connect_after;
if now > reconnect_at {
relay.last_connect_attempt = now;
let next_duration = Duration::from_millis(3000);
debug!(
"bumping reconnect duration from {:?} to {:?} and retrying connect",
relay.retry_connect_after, next_duration
);
relay.retry_connect_after = next_duration;
if let Err(err) = relay.relay.connect(wakeup.clone()) {
error!("error connecting to relay: {}", err);
}
} else {
// let's wait a bit before we try again
}
}
RelayStatus::Connected => {
relay.retry_connect_after =
WebsocketRelay::initial_reconnect_duration();
let should_ping = now - relay.last_ping > self.ping_rate;
if should_ping {
debug!("pinging {}", relay.relay.url);
relay.relay.ping();
relay.last_ping = Instant::now();
}
}
RelayStatus::Connecting => {
// cool story bro
}
}
}
}
}
}
pub fn send_to(&mut self, cmd: &ClientMessage, relay_url: &str) {
for relay in &mut self.relays {
if relay.url() == relay_url {
if let Some(debug) = &mut self.debug {
debug.send_cmd(relay.url().to_owned(), cmd);
}
if let Err(err) = relay.send(cmd) {
error!("send_to err: {err}");
}
return;
}
}
}
/// check whether a relay url is valid to add
pub fn is_valid_url(&self, url: &str) -> bool {
if url.is_empty() {
return false;
}
let url = match Url::parse(url) {
Ok(parsed_url) => parsed_url.to_string(),
Err(_err) => {
// debug!("bad relay url \"{}\": {:?}", url, err);
return false;
}
};
if self.has(&url) {
return false;
}
true
}
// Adds a websocket url to the RelayPool.
pub fn add_url(
&mut self,
url: String,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) -> Result<()> {
let url = Self::canonicalize_url(url);
// Check if the URL already exists in the pool.
if self.has(&url) {
return Ok(());
}
let relay = Relay::new(
nostr::RelayUrl::parse(url).map_err(|_| Error::InvalidRelayUrl)?,
wakeup,
)?;
let pool_relay = PoolRelay::websocket(relay);
self.relays.push(pool_relay);
Ok(())
}
pub fn add_urls(
&mut self,
urls: BTreeSet<String>,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) -> Result<()> {
for url in urls {
self.add_url(url, wakeup.clone())?;
}
Ok(())
}
pub fn remove_urls(&mut self, urls: &BTreeSet<String>) {
self.relays
.retain(|pool_relay| !urls.contains(pool_relay.url()));
}
// standardize the format (ie, trailing slashes)
fn canonicalize_url(url: String) -> String {
match Url::parse(&url) {
Ok(parsed_url) => parsed_url.to_string(),
Err(_) => url, // If parsing fails, return the original URL.
}
}
/// Attempts to receive a pool event from a list of relays. The
/// function searches each relay in the list in order, attempting to
/// receive a message from each. If a message is received, return it.
/// If no message is received from any relays, None is returned.
pub fn try_recv(&mut self) -> Option<PoolEvent<'_>> {
for relay in &mut self.relays {
if let PoolRelay::Multicast(mcr) = relay {
// try rejoin on multicast
if mcr.should_rejoin() {
if let Err(err) = mcr.rejoin() {
error!("multicast: rejoin error: {err}");
}
}
}
if let Some(event) = relay.try_recv() {
match &event {
WsEvent::Opened => {
relay.set_status(RelayStatus::Connected);
}
WsEvent::Closed => {
relay.set_status(RelayStatus::Disconnected);
}
WsEvent::Error(err) => {
error!("{:?}", err);
relay.set_status(RelayStatus::Disconnected);
}
WsEvent::Message(ev) => {
// let's just handle pongs here.
// We only need to do this natively.
#[cfg(not(target_arch = "wasm32"))]
if let WsMessage::Ping(ref bs) = ev {
debug!("pong {}", relay.url());
match relay {
PoolRelay::Websocket(wsr) => {
wsr.relay.sender.send(WsMessage::Pong(bs.to_owned()));
}
PoolRelay::Multicast(_mcr) => {}
}
}
}
}
if let Some(debug) = &mut self.debug {
debug.receive_cmd(relay.url().to_owned(), (&event).into());
}
let pool_event = PoolEvent {
event,
relay: relay.url(),
};
return Some(pool_event);
}
}
None
}
}

View File

@@ -1,267 +0,0 @@
use std::{collections::HashMap, mem, time::SystemTime};
use ewebsock::WsMessage;
use nostrdb::Filter;
use crate::{ClientMessage, Error, RelayEvent, RelayMessage};
use super::message::calculate_command_result_size;
type RelayId = String;
type SubId = String;
pub struct SubsDebug {
data: HashMap<RelayId, RelayStats>,
time_incd: SystemTime,
pub relay_events_selection: Option<RelayId>,
}
#[derive(Default)]
pub struct RelayStats {
pub count: TransferStats,
pub events: Vec<RelayLogEvent>,
pub sub_data: HashMap<SubId, SubStats>,
}
#[derive(Clone)]
pub enum RelayLogEvent {
Send(ClientMessage),
Recieve(OwnedRelayEvent),
}
#[derive(Clone)]
pub enum OwnedRelayEvent {
Opened,
Closed,
Other(String),
Error(String),
Message(String),
}
impl From<RelayEvent<'_>> for OwnedRelayEvent {
fn from(value: RelayEvent<'_>) -> Self {
match value {
RelayEvent::Opened => OwnedRelayEvent::Opened,
RelayEvent::Closed => OwnedRelayEvent::Closed,
RelayEvent::Other(ws_message) => {
let ws_str = match ws_message {
WsMessage::Binary(_) => "Binary".to_owned(),
WsMessage::Text(t) => format!("Text:{t}"),
WsMessage::Unknown(u) => format!("Unknown:{u}"),
WsMessage::Ping(_) => "Ping".to_owned(),
WsMessage::Pong(_) => "Pong".to_owned(),
};
OwnedRelayEvent::Other(ws_str)
}
RelayEvent::Error(error) => OwnedRelayEvent::Error(error.to_string()),
RelayEvent::Message(relay_message) => {
let relay_msg = match relay_message {
RelayMessage::OK(_) => "OK".to_owned(),
RelayMessage::Eose(s) => format!("EOSE:{s}"),
RelayMessage::Event(_, s) => format!("EVENT:{s}"),
RelayMessage::Notice(s) => format!("NOTICE:{s}"),
};
OwnedRelayEvent::Message(relay_msg)
}
}
}
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct _RelaySub {
pub(crate) subid: String,
pub(crate) filter: String,
}
#[derive(Default)]
pub struct SubStats {
pub filter: String,
pub count: TransferStats,
}
#[derive(Default)]
pub struct TransferStats {
pub up_total: usize,
pub down_total: usize,
// 1 sec < last tick < 2 sec
pub up_sec_prior: usize,
pub down_sec_prior: usize,
// < 1 sec since last tick
up_sec_cur: usize,
down_sec_cur: usize,
}
impl Default for SubsDebug {
fn default() -> Self {
Self {
data: Default::default(),
time_incd: SystemTime::now(),
relay_events_selection: None,
}
}
}
impl SubsDebug {
pub fn get_data(&self) -> &HashMap<RelayId, RelayStats> {
&self.data
}
pub(crate) fn send_cmd(&mut self, relay: String, cmd: &ClientMessage) {
let data = self.data.entry(relay).or_default();
let msg_num_bytes = calculate_client_message_size(cmd);
match cmd {
ClientMessage::Req { sub_id, filters } => {
data.sub_data.insert(
sub_id.to_string(),
SubStats {
filter: filters_to_string(filters),
count: Default::default(),
},
);
}
ClientMessage::Close { sub_id } => {
data.sub_data.remove(sub_id);
}
_ => {}
}
data.count.up_sec_cur += msg_num_bytes;
data.events.push(RelayLogEvent::Send(cmd.clone()));
}
pub(crate) fn receive_cmd(&mut self, relay: String, cmd: RelayEvent) {
let data = self.data.entry(relay).or_default();
let msg_num_bytes = calculate_relay_event_size(&cmd);
if let RelayEvent::Message(RelayMessage::Event(sid, _)) = cmd {
if let Some(sub_data) = data.sub_data.get_mut(sid) {
let c = &mut sub_data.count;
c.down_sec_cur += msg_num_bytes;
}
};
data.count.down_sec_cur += msg_num_bytes;
data.events.push(RelayLogEvent::Recieve(cmd.into()));
}
pub fn try_increment_stats(&mut self) {
let cur_time = SystemTime::now();
if let Ok(dur) = cur_time.duration_since(self.time_incd) {
if dur.as_secs() >= 1 {
self.time_incd = cur_time;
self.internal_inc_stats();
}
}
}
fn internal_inc_stats(&mut self) {
for relay_data in self.data.values_mut() {
let c = &mut relay_data.count;
inc_data_count(c);
for sub in relay_data.sub_data.values_mut() {
inc_data_count(&mut sub.count);
}
}
}
}
fn inc_data_count(c: &mut TransferStats) {
c.up_total += c.up_sec_cur;
c.up_sec_prior = c.up_sec_cur;
c.down_total += c.down_sec_cur;
c.down_sec_prior = c.down_sec_cur;
c.up_sec_cur = 0;
c.down_sec_cur = 0;
}
fn calculate_client_message_size(message: &ClientMessage) -> usize {
match message {
ClientMessage::Event(note) => note.note_json.len() + 10, // 10 is ["EVENT",]
ClientMessage::Req { sub_id, filters } => {
mem::size_of_val(message)
+ mem::size_of_val(sub_id)
+ sub_id.len()
+ filters.iter().map(mem::size_of_val).sum::<usize>()
}
ClientMessage::Close { sub_id } => {
mem::size_of_val(message) + mem::size_of_val(sub_id) + sub_id.len()
}
ClientMessage::Raw(data) => mem::size_of_val(message) + data.len(),
}
}
fn calculate_relay_event_size(event: &RelayEvent<'_>) -> usize {
let base_size = mem::size_of_val(event); // Size of the enum on the stack
let variant_size = match event {
RelayEvent::Opened | RelayEvent::Closed => 0, // No additional data
RelayEvent::Other(ws_message) => calculate_ws_message_size(ws_message),
RelayEvent::Error(error) => calculate_error_size(error),
RelayEvent::Message(message) => calculate_relay_message_size(message),
};
base_size + variant_size
}
fn calculate_ws_message_size(message: &WsMessage) -> usize {
match message {
WsMessage::Binary(vec) | WsMessage::Ping(vec) | WsMessage::Pong(vec) => {
mem::size_of_val(message) + vec.len()
}
WsMessage::Text(string) | WsMessage::Unknown(string) => {
mem::size_of_val(message) + string.len()
}
}
}
fn calculate_error_size(error: &Error) -> usize {
match error {
Error::Empty
| Error::HexDecodeFailed
| Error::InvalidBech32
| Error::InvalidByteSize
| Error::InvalidSignature
| Error::InvalidRelayUrl
| Error::Io(_)
| Error::InvalidPublicKey => mem::size_of_val(error), // No heap usage
Error::DecodeFailed(string) => mem::size_of_val(error) + string.len(),
Error::Json(json_err) => mem::size_of_val(error) + json_err.to_string().len(),
Error::Nostrdb(nostrdb_err) => mem::size_of_val(error) + nostrdb_err.to_string().len(),
Error::Generic(string) => mem::size_of_val(error) + string.len(),
}
}
fn calculate_relay_message_size(message: &RelayMessage) -> usize {
match message {
RelayMessage::OK(result) => calculate_command_result_size(result),
RelayMessage::Eose(str_ref)
| RelayMessage::Event(str_ref, _)
| RelayMessage::Notice(str_ref) => mem::size_of_val(message) + str_ref.len(),
}
}
fn filters_to_string(f: &Vec<Filter>) -> String {
let mut cur_str = String::new();
for filter in f {
if let Ok(json) = filter.json() {
if !cur_str.is_empty() {
cur_str.push_str(", ");
}
cur_str.push_str(&json);
}
}
cur_str
}

View File

@@ -1,63 +0,0 @@
[package]
name = "notedeck"
version = { workspace = true }
edition = "2021"
description = "The APIs and data structures used by notedeck apps"
[dependencies]
nostrdb = { workspace = true }
jni = { workspace = true }
url = { workspace = true }
strum = { workspace = true }
blurhash = { workspace = true }
strum_macros = { workspace = true }
dirs = { workspace = true }
enostr = { workspace = true }
nostr = { workspace = true }
egui = { workspace = true }
egui_extras = { workspace = true }
eframe = { workspace = true }
image = { workspace = true }
base32 = { workspace = true }
poll-promise = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
hex = { workspace = true }
thiserror = { workspace = true }
puffin = { workspace = true, optional = true }
puffin_egui = { workspace = true, optional = true }
sha2 = { workspace = true }
bincode = { workspace = true }
ehttp = {workspace = true }
mime_guess = { workspace = true }
egui-winit = { workspace = true }
tokenator = { workspace = true }
profiling = { workspace = true }
nwc = { workspace = true }
tokio = { workspace = true }
bech32 = { workspace = true }
lightning-invoice = { workspace = true }
secp256k1 = { workspace = true }
hashbrown = { workspace = true }
fluent = { workspace = true }
fluent-resmgr = { workspace = true }
fluent-langneg = { workspace = true }
unic-langid = { workspace = true }
once_cell = { workspace = true }
md5 = { workspace = true }
bitflags = { workspace = true }
regex = "1"
chrono = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }
tokio = { workspace = true }
[target.'cfg(target_os = "android")'.dependencies]
jni = { workspace = true }
android-activity = { workspace = true }
[features]
puffin = ["puffin_egui", "dep:puffin"]

View File

@@ -1,396 +0,0 @@
# Notedeck Developer Documentation
This document provides technical details and guidance for developers working with the Notedeck crate.
## Architecture Overview
Notedeck is built around a modular architecture that separates concerns into distinct components:
### Core Components
1. **App Framework (`app.rs`)**
- `Notedeck` - The main application framework that ties everything together
- `App` - The trait that specific applications must implement
2. **Data Layer**
- `Ndb` - NostrDB database for efficient storage and querying
- `NoteCache` - In-memory cache for expensive-to-compute note data like nip10 structure
- `Images` - Image and GIF cache management
3. **Network Layer**
- `RelayPool` - Manages connections to Nostr relays
- `UnknownIds` - Tracks and resolves unknown profiles and notes
4. **User Accounts**
- `Accounts` - Manages user keypairs and account information
- `AccountStorage` - Handles persistent storage of account data
5. **Wallet Integration**
- `Wallet` - Lightning wallet integration
- `Zaps` - Handles Nostr zap functionality
6. **UI Components**
- `NotedeckTextStyle` - Text styling utilities
- `ColorTheme` - Theme management
- Various UI helpers
7. **Localization System**
- `LocalizationManager` - Core localization functionality
- `LocalizationContext` - Thread-safe context for sharing localization
- Fluent-based translation system
## Key Concepts
### Note Context and Actions
Notes have associated context and actions that define how users can interact with them:
```rust
pub enum NoteAction {
Reply(NoteId), // Reply to a note
Quote(NoteId), // Quote a note
Hashtag(String), // Click on a hashtag
Profile(Pubkey), // View a profile
Note(NoteId), // View a note
Context(ContextSelection), // Context menu options
Zap(ZapAction), // Zap (tip) interaction
}
```
### Relay Management
Notedeck handles relays through the `RelaySpec` structure, which implements NIP-65 functionality for marking relays as read or write.
### Filtering and Subscriptions
The `FilterState` enum manages the state of subscriptions to Nostr relays:
```rust
pub enum FilterState {
NeedsRemote(Vec<Filter>),
FetchingRemote(UnifiedSubscription),
GotRemote(Subscription),
Ready(Vec<Filter>),
Broken(FilterError),
}
```
## Development Workflow
### Setting Up Your Environment
1. Clone the repository
2. Build with `cargo build`
3. Test with `cargo test`
### Creating a New Notedeck App
1. Import the notedeck crate
2. Implement the `App` trait
3. Use the `Notedeck` struct as your application framework
Example:
```rust
use notedeck::{App, Notedeck, AppContext};
struct MyNostrApp {
// Your app-specific state
}
impl App for MyNostrApp {
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) {
// Your app's UI and logic here
}
}
fn main() {
let notedeck = Notedeck::new(...).app(MyNostrApp { /* ... */ });
// Run your app
}
```
### Working with Notes
Notes are the core data structure in Nostr. Here's how to work with them:
```rust
// Get a note by ID
let txn = Transaction::new(&ndb).expect("txn");
if let Ok(note) = ndb.get_note_by_id(&txn, note_id.bytes()) {
// Process the note
}
// Create a cached note
let cached_note = note_cache.cached_note_or_insert(note_key, &note);
```
### Adding Account Management
Account management is handled through the `Accounts` struct:
```rust
// Add a new account
let action = accounts.add_account(keypair);
action.process_action(&mut unknown_ids, &ndb, &txn);
// Get the current account
if let Some(account) = accounts.get_selected_account() {
// Use the account
}
```
## Advanced Topics
### Zaps Implementation
Notedeck implements the zap (tipping) functionality according to the Nostr protocol:
1. Creates a zap request note (kind 9734)
2. Fetches a Lightning invoice via LNURL or LUD-16
3. Pays the invoice using a connected wallet
4. Tracks the zap state
### Image Caching
The image caching system efficiently manages images and animated GIFs:
1. Downloads images from URLs
2. Stores them in a local cache
3. Handles conversion between formats
4. Manages memory usage
### Persistent Storage
Notedeck provides several persistence mechanisms:
- `AccountStorage` - For user accounts
- `TimedSerializer` - For settings that need to be saved after a delay
- Various handlers for specific settings (zoom, theme, app size)
### Localization System
Notedeck includes a comprehensive internationalization system built on the [Fluent](https://projectfluent.org/) translation framework. The system is designed for performance and developer experience.
#### Architecture
The localization system consists of several key components:
1. **LocalizationManager** - Core functionality for managing locales and translations
2. **LocalizationContext** - Thread-safe context for sharing localization across the application
3. **Fluent Resources** - Translation files in `.ftl` format stored in `assets/translations/`
#### Key Features
- **Efficient Caching**: Parsed Fluent resources and formatted strings are cached for performance
- **Thread Safety**: Uses `RwLock` for safe concurrent access
- **Dynamic Locale Switching**: Change languages at runtime without restarting
- **Argument Support**: Localized strings can include dynamic arguments
- **Development Tools**: Pseudolocale support for testing UI layout
#### Using the tr! and tr_plural! Macros
The `tr!` and `tr_plural!` macros are the primary way to use localization in Notedeck code. They provide a convenient, type-safe interface for getting localized strings.
##### The tr! Macro
```rust
use notedeck::tr;
// Simple string with comment
let welcome = tr!("Welcome to Notedeck!", "Main welcome message");
let cancel = tr!("Cancel", "Button label to cancel an action");
// String with parameters
let greeting = tr!("Hello, {name}!", "Greeting message", name="Alice");
// Multiple parameters
let message = tr!(
"Welcome {name} to {app}!",
"Welcome message with app name",
name="Alice",
app="Notedeck"
);
// In UI components
ui.button(tr!("Reply to {user}", "Reply button text", user="alice@example.com"));
```
##### The tr_plural! Macro
Use tr_plural! when there can be multiple variations of the same string depending on
some numeric count.
Not all languages follow the same pluralization rules
```rust
use notedeck::tr_plural;
// Simple pluralization
let count = 5;
let message = tr_plural!(
"You have {count} note", // Singular form
"You have {count} notes", // Plural form
"Note count message", // Comment
count // Count value
);
// With additional parameters
let user = "Alice";
let message = tr_plural!(
"{user} has {count} note", // Singular
"{user} has {count} notes", // Plural
"User note count message", // Comment
count, // Count
user=user // Additional parameter
);
```
##### Key Features
- **Automatic Key Normalization**: Converts messages and comments into valid FTL keys
- **Fallback Handling**: Falls back to original message if translation not found
- **Parameter Interpolation**: Automatically handles named parameters
- **Comment Context**: Provides context for translators
##### Best Practices
1. **Always Include Comments**: Comments provide context for translators
```rust
// Good
tr!("Add", "Button label to add something")
// Bad
tr!("Add", "")
```
2. **Use Descriptive Comments**: Make comments specific and helpful
```rust
// Good
tr!("Reply", "Button to reply to a note")
// Bad
tr!("Reply", "Reply")
```
3. **Consistent Parameter Names**: Use consistent parameter names across related strings
```rust
// Consistent
tr!("Follow {user}", "Follow button", user="alice")
tr!("Unfollow {user}", "Unfollow button", user="alice")
```
4. **Always use tr_plural! for plural strings**: Not all languages follow English pluralization rules
```rust
// Good
// Each language can have more (or less) than just two pluralization forms.
// Let the translators and the localization system help you figure that out implicitly.
let message = tr_plural!(
"You have {count} note", // Singular form
"You have {count} notes", // Plural form
"Note count message", // Comment
count // Count value
);
// Bad
// Not all languages follow pluralization rules of English.
// Some languages can have more (or less) than two variations!
if count == 1 {
tr!("You have 1 note", "Note count message")
} else {
tr!("You have {count} notes", "Note count message")
}
```
#### Translation File Format
Translation files use the [Fluent](https://projectfluent.org/) format (`.ftl`).
Developers should never create their own `.ftl` files. Whenever user-facing strings are changed in code, run `python3 scripts/export_source_strings.py`. This script will generate `assets/translations/en-US/main.ftl` and `assets/translations/en-XA/main.ftl`. The format of the files look like the following:
```ftl
# Simple string
welcome_message = Welcome to Notedeck!
# String with arguments
welcome_user = Welcome {$name}!
# String with pluralization
note_count = {$count ->
[1] One note
*[other] {$count} notes
}
```
#### Adding New Languages
TODO
#### Development with Pseudolocale (en-XA)
For testing that all user-facing strings are going through the localization system and that the
UI layout renders well with different language translations, enable the pseudolocale:
```bash
cargo run -- --debug --locale en-XA
```
The pseudolocale (`en-XA`) transforms English text in a way that is still readable but makes adjustments obvious enough that they are different from the original text (such as replacing English letters with accented equivalents), helping identify potential UI layout issues once it gets translated
to other languages.
Example transformations:
- "Add relay" → "[Àdd rélày]"
- "Cancel" → "[Çàñçél]"
- "Confirm" → "[Çóñfírm]"
#### Performance Considerations
- **Resource Caching**: Parsed Fluent resources are cached per locale
- **String Caching**: Simple strings (without arguments) are cached for repeated access
- **Cache Management**: Caches are automatically cleared when switching locales
- **Memory Limits**: String cache size can be limited to prevent memory growth
#### Testing Localization
The localization system includes comprehensive tests:
```bash
# Run localization tests
cargo test i18n
```
## Troubleshooting
### Common Issues
1. **Relay Connection Issues**
- Check network connectivity
- Verify relay URLs are correct
- Look for relay debug messages
2. **Database Errors**
- Ensure the database path is writable
- Check for database corruption
- Increase map size if needed
3. **Performance Issues**
- Monitor the frame history
- Check for large image caches
- Consider reducing the number of active subscriptions
4. **Localization Issues**
- Verify translation files exist in the correct directory structure
- Check that locale codes are valid (e.g., `en-US`, `es-ES`)
- Ensure FTL files are properly formatted
- Look for missing translation keys in logs
## Contributing
When contributing to Notedeck:
1. Follow the existing code style
2. Add tests for new functionality
3. Update documentation as needed
4. Keep performance in mind, especially for mobile targets
5. For UI changes, test with pseudolocale enabled
6. When adding new strings, ensure they are properly localized

View File

@@ -1,30 +0,0 @@
# Notedeck
Notedeck is a shared Rust library that provides the core functionality for building Nostr client applications. It serves as the foundation for various Notedeck applications like notedeck_chrome, notedeck_columns, and notedeck_dave.
## Overview
The Notedeck crate implements common data types, utilities, and logic used across all Notedeck applications. It provides a unified interface for interacting with the Nostr protocol, managing accounts, handling note data, and rendering UI components.
Key features include:
- **Nostr Protocol Integration**: Connect to relays, subscribe to events, publish notes
- **Account Management**: Handle user accounts, keypairs, and profiles
- **Note Handling**: Cache and process notes efficiently
- **UI Components**: Common UI elements and styles
- **Image Caching**: Efficient image and GIF caching system
- **Wallet Integration**: Lightning wallet support with zaps functionality
- **Theme Support**: Customizable themes and styles
- **Storage**: Persistent storage for settings and data
## Applications
This crate serves as the foundation for several Notedeck applications:
- **notedeck_chrome** - The browser chrome, manages a toolbar for switching between different clients
- **notedeck_columns** - A column-based Nostr client interface
- **notedeck_dave** - A nostr ai assistant
## License
GPLv2

View File

@@ -1,526 +0,0 @@
use uuid::Uuid;
use crate::account::cache::AccountCache;
use crate::account::contacts::Contacts;
use crate::account::mute::AccountMutedData;
use crate::account::relay::{
modify_advertised_relays, update_relay_configuration, AccountRelayData, RelayAction,
RelayDefaults,
};
use crate::storage::AccountStorageWriter;
use crate::user_account::UserAccountSerializable;
use crate::{
AccountStorage, MuteFun, SingleUnkIdAction, UnifiedSubscription, UnknownIds, UserAccount,
ZapWallet,
};
use enostr::{ClientMessage, FilledKeypair, Keypair, Pubkey, RelayPool};
use nostrdb::{Ndb, Note, Transaction};
// TODO: remove this
use std::sync::Arc;
/// The interface for managing the user's accounts.
/// Represents all user-facing operations related to account management.
pub struct Accounts {
pub cache: AccountCache,
storage_writer: Option<AccountStorageWriter>,
relay_defaults: RelayDefaults,
subs: AccountSubs,
}
impl Accounts {
#[allow(clippy::too_many_arguments)]
pub fn new(
key_store: Option<AccountStorage>,
forced_relays: Vec<String>,
fallback: Pubkey,
ndb: &mut Ndb,
txn: &Transaction,
pool: &mut RelayPool,
ctx: &egui::Context,
unknown_ids: &mut UnknownIds,
) -> Self {
let (mut cache, unknown_id) = AccountCache::new(UserAccount::new(
Keypair::only_pubkey(fallback),
AccountData::new(fallback.bytes()),
));
unknown_id.process_action(unknown_ids, ndb, txn);
let mut storage_writer = None;
if let Some(keystore) = key_store {
let (reader, writer) = keystore.rw();
match reader.get_accounts() {
Ok(accounts) => {
for account in accounts {
add_account_from_storage(&mut cache, account).process_action(
unknown_ids,
ndb,
txn,
)
}
}
Err(e) => {
tracing::error!("could not get keys: {e}");
}
}
if let Some(selected) = reader.get_selected_key().ok().flatten() {
cache.select(selected);
}
storage_writer = Some(writer);
};
let relay_defaults = RelayDefaults::new(forced_relays);
let selected = cache.selected_mut();
let selected_data = &mut selected.data;
selected_data.query(ndb, txn);
let subs = {
AccountSubs::new(
ndb,
pool,
&relay_defaults,
&selected.key.pubkey,
selected_data,
create_wakeup(ctx),
)
};
Accounts {
cache,
storage_writer,
relay_defaults,
subs,
}
}
pub fn remove_account(
&mut self,
pk: &Pubkey,
ndb: &mut Ndb,
pool: &mut RelayPool,
ctx: &egui::Context,
) -> bool {
let Some(resp) = self.cache.remove(pk) else {
return false;
};
if pk != self.cache.fallback() {
if let Some(key_store) = &self.storage_writer {
if let Err(e) = key_store.remove_key(&resp.deleted) {
tracing::error!("Could not remove account {pk}: {e}");
}
}
}
if let Some(swap_to) = resp.swap_to {
let txn = Transaction::new(ndb).expect("txn");
self.select_account_internal(&swap_to, ndb, &txn, pool, ctx);
}
true
}
pub fn contains_full_kp(&self, pubkey: &enostr::Pubkey) -> bool {
self.cache
.get(pubkey)
.is_some_and(|u| u.key.secret_key.is_some())
}
#[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"]
pub fn add_account(&mut self, kp: Keypair) -> Option<AddAccountResponse> {
let acc = if let Some(acc) = self.cache.get_mut(&kp.pubkey) {
if kp.secret_key.is_none() || acc.key.secret_key.is_some() {
tracing::info!("Already have account, not adding");
return None;
}
acc.key = kp.clone();
AccType::Acc(&*acc)
} else {
let new_account_data = AccountData::new(kp.pubkey.bytes());
AccType::Entry(
self.cache
.add(UserAccount::new(kp.clone(), new_account_data)),
)
};
if let Some(key_store) = &self.storage_writer {
if let Err(e) = key_store.write_account(&acc.get_acc().into()) {
tracing::error!("Could not add key for {:?}: {e}", kp.pubkey);
}
}
Some(AddAccountResponse {
switch_to: kp.pubkey,
unk_id_action: SingleUnkIdAction::pubkey(kp.pubkey),
})
}
/// Update the `UserAccount` via callback and save the result to disk.
/// return true if the update was successful
pub fn update_current_account(&mut self, update: impl FnOnce(&mut UserAccount)) -> bool {
let cur_account = self.get_selected_account_mut();
update(cur_account);
let cur_acc = self.get_selected_account();
let Some(key_store) = &self.storage_writer else {
return false;
};
if let Err(err) = key_store.write_account(&cur_acc.into()) {
tracing::error!("Could not add account {:?} to storage: {err}", cur_acc.key);
return false;
}
true
}
pub fn selected_filled(&self) -> Option<FilledKeypair<'_>> {
self.get_selected_account().key.to_full()
}
/// Get the selected account's pubkey as bytes. Common operation so
/// we make it a helper here.
pub fn selected_account_pubkey_bytes(&self) -> &[u8; 32] {
self.get_selected_account().key.pubkey.bytes()
}
pub fn selected_account_pubkey(&self) -> &Pubkey {
&self.get_selected_account().key.pubkey
}
pub fn get_selected_account(&self) -> &UserAccount {
self.cache.selected()
}
pub fn selected_account_has_wallet(&self) -> bool {
self.get_selected_account().wallet.is_some()
}
fn get_selected_account_mut(&mut self) -> &mut UserAccount {
self.cache.selected_mut()
}
pub fn get_selected_wallet(&self) -> Option<&ZapWallet> {
self.cache.selected().wallet.as_ref()
}
pub fn get_selected_wallet_mut(&mut self) -> Option<&mut ZapWallet> {
self.cache.selected_mut().wallet.as_mut()
}
fn get_selected_account_data(&self) -> &AccountData {
&self.cache.selected().data
}
pub fn select_account(
&mut self,
pk_to_select: &Pubkey,
ndb: &mut Ndb,
txn: &Transaction,
pool: &mut RelayPool,
ctx: &egui::Context,
) {
if !self.cache.select(*pk_to_select) {
return;
}
self.select_account_internal(pk_to_select, ndb, txn, pool, ctx);
}
/// Have already selected in `AccountCache`, updating other things
fn select_account_internal(
&mut self,
pk_to_select: &Pubkey,
ndb: &mut Ndb,
txn: &Transaction,
pool: &mut RelayPool,
ctx: &egui::Context,
) {
if let Some(key_store) = &self.storage_writer {
if let Err(e) = key_store.select_key(Some(*pk_to_select)) {
tracing::error!("Could not select key {:?}: {e}", pk_to_select);
}
}
self.get_selected_account_mut().data.query(ndb, txn);
self.subs.swap_to(
ndb,
pool,
&self.relay_defaults,
pk_to_select,
&self.cache.selected().data,
create_wakeup(ctx),
);
}
pub fn mutefun(&self) -> Box<MuteFun> {
let account_data = self.get_selected_account_data();
let muted = Arc::clone(&account_data.muted.muted);
Box::new(move |note: &Note, thread: &[u8; 32]| muted.is_muted(note, thread))
}
pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) {
let data = &self.get_selected_account().data;
// send the active account's relay list subscription
pool.send_to(
&ClientMessage::req(
self.subs.relay.remote.clone(),
vec![data.relay.filter.clone()],
),
relay_url,
);
// send the active account's muted subscription
pool.send_to(
&ClientMessage::req(
self.subs.mute.remote.clone(),
vec![data.muted.filter.clone()],
),
relay_url,
);
pool.send_to(
&ClientMessage::req(
self.subs.contacts.remote.clone(),
vec![data.contacts.filter.clone()],
),
relay_url,
);
}
pub fn update(&mut self, ndb: &mut Ndb, pool: &mut RelayPool, ctx: &egui::Context) {
// IMPORTANT - This function is called in the UI update loop,
// make sure it is fast when idle
let Some(update) = self
.cache
.selected_mut()
.data
.poll_for_updates(ndb, &self.subs)
else {
return;
};
match update {
// If needed, update the relay configuration
AccountDataUpdate::Relay => {
let acc = self.cache.selected();
update_relay_configuration(
pool,
&self.relay_defaults,
&acc.key.pubkey,
&acc.data.relay,
create_wakeup(ctx),
);
}
}
}
pub fn get_full<'a>(&'a self, pubkey: &Pubkey) -> Option<FilledKeypair<'a>> {
self.cache.get(pubkey).and_then(|r| r.key.to_full())
}
pub fn process_relay_action(
&mut self,
ctx: &egui::Context,
pool: &mut RelayPool,
action: RelayAction,
) {
let acc = self.cache.selected_mut();
modify_advertised_relays(&acc.key, action, pool, &self.relay_defaults, &mut acc.data);
update_relay_configuration(
pool,
&self.relay_defaults,
&acc.key.pubkey,
&acc.data.relay,
create_wakeup(ctx),
);
}
pub fn get_subs(&self) -> &AccountSubs {
&self.subs
}
}
enum AccType<'a> {
Entry(hashbrown::hash_map::OccupiedEntry<'a, Pubkey, UserAccount>),
Acc(&'a UserAccount),
}
impl<'a> AccType<'a> {
fn get_acc(&'a self) -> &'a UserAccount {
match self {
AccType::Entry(occupied_entry) => occupied_entry.get(),
AccType::Acc(user_account) => user_account,
}
}
}
fn create_wakeup(ctx: &egui::Context) -> impl Fn() + Send + Sync + Clone + 'static {
let ctx = ctx.clone();
move || {
ctx.request_repaint();
}
}
fn add_account_from_storage(
cache: &mut AccountCache,
user_account_serializable: UserAccountSerializable,
) -> SingleUnkIdAction {
let Some(acc) = get_acc_from_storage(user_account_serializable) else {
return SingleUnkIdAction::NoAction;
};
let pk = acc.key.pubkey;
cache.add(acc);
SingleUnkIdAction::pubkey(pk)
}
fn get_acc_from_storage(user_account_serializable: UserAccountSerializable) -> Option<UserAccount> {
let keypair = user_account_serializable.key;
let new_account_data = AccountData::new(keypair.pubkey.bytes());
let mut wallet = None;
if let Some(wallet_s) = user_account_serializable.wallet {
let m_wallet: Result<crate::ZapWallet, crate::Error> = wallet_s.into();
match m_wallet {
Ok(w) => wallet = Some(w),
Err(e) => {
tracing::error!("Problem creating wallet from disk: {e}");
}
};
}
Some(UserAccount {
key: keypair,
wallet,
data: new_account_data,
})
}
#[derive(Clone)]
pub struct AccountData {
pub(crate) relay: AccountRelayData,
pub(crate) muted: AccountMutedData,
pub contacts: Contacts,
}
impl AccountData {
pub fn new(pubkey: &[u8; 32]) -> Self {
Self {
relay: AccountRelayData::new(pubkey),
muted: AccountMutedData::new(pubkey),
contacts: Contacts::new(pubkey),
}
}
pub(super) fn poll_for_updates(
&mut self,
ndb: &Ndb,
subs: &AccountSubs,
) -> Option<AccountDataUpdate> {
let txn = Transaction::new(ndb).expect("txn");
let mut resp = None;
if self.relay.poll_for_updates(ndb, &txn, subs.relay.local) {
resp = Some(AccountDataUpdate::Relay);
}
self.muted.poll_for_updates(ndb, &txn, subs.mute.local);
self.contacts
.poll_for_updates(ndb, &txn, subs.contacts.local);
resp
}
/// Note: query should be called as close to the subscription as possible
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
self.relay.query(ndb, txn);
self.muted.query(ndb, txn);
self.contacts.query(ndb, txn);
}
}
pub(super) enum AccountDataUpdate {
Relay,
}
pub struct AddAccountResponse {
pub switch_to: Pubkey,
pub unk_id_action: SingleUnkIdAction,
}
pub struct AccountSubs {
relay: UnifiedSubscription,
mute: UnifiedSubscription,
pub contacts: UnifiedSubscription,
}
impl AccountSubs {
pub(super) fn new(
ndb: &mut Ndb,
pool: &mut RelayPool,
relay_defaults: &RelayDefaults,
pk: &Pubkey,
data: &AccountData,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) -> Self {
let relay = subscribe(ndb, pool, &data.relay.filter);
let mute = subscribe(ndb, pool, &data.muted.filter);
let contacts = subscribe(ndb, pool, &data.contacts.filter);
update_relay_configuration(pool, relay_defaults, pk, &data.relay, wakeup);
Self {
relay,
mute,
contacts,
}
}
pub(super) fn swap_to(
&mut self,
ndb: &mut Ndb,
pool: &mut RelayPool,
relay_defaults: &RelayDefaults,
pk: &Pubkey,
new_selection_data: &AccountData,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) {
unsubscribe(ndb, pool, &self.relay);
unsubscribe(ndb, pool, &self.mute);
unsubscribe(ndb, pool, &self.contacts);
*self = AccountSubs::new(ndb, pool, relay_defaults, pk, new_selection_data, wakeup);
}
}
fn subscribe(ndb: &Ndb, pool: &mut RelayPool, filter: &nostrdb::Filter) -> UnifiedSubscription {
let filters = vec![filter.clone()];
let sub = ndb
.subscribe(&filters)
.expect("ndb relay list subscription");
// remote subscription
let subid = Uuid::new_v4().to_string();
pool.subscribe(subid.clone(), filters);
UnifiedSubscription {
local: sub,
remote: subid,
}
}
fn unsubscribe(ndb: &mut Ndb, pool: &mut RelayPool, sub: &UnifiedSubscription) {
pool.unsubscribe(sub.remote.clone());
// local subscription
ndb.unsubscribe(sub.local)
.expect("ndb relay list unsubscribe");
}

View File

@@ -1,126 +0,0 @@
use enostr::Pubkey;
use hashbrown::{hash_map::OccupiedEntry, HashMap};
use crate::{SingleUnkIdAction, UserAccount};
pub struct AccountCache {
selected: Pubkey,
fallback: Pubkey,
fallback_account: UserAccount,
// never empty at rest
accounts: HashMap<Pubkey, UserAccount>,
}
impl AccountCache {
pub(super) fn new(fallback: UserAccount) -> (Self, SingleUnkIdAction) {
let mut accounts = HashMap::with_capacity(1);
let pk = fallback.key.pubkey;
accounts.insert(pk, fallback.clone());
(
Self {
selected: pk,
fallback: pk,
fallback_account: fallback,
accounts,
},
SingleUnkIdAction::pubkey(pk),
)
}
pub fn get(&self, pk: &Pubkey) -> Option<&UserAccount> {
self.accounts.get(pk)
}
pub fn get_bytes(&self, pk: &[u8; 32]) -> Option<&UserAccount> {
self.accounts.get(pk)
}
pub(super) fn get_mut(&mut self, pk: &Pubkey) -> Option<&mut UserAccount> {
self.accounts.get_mut(pk)
}
pub(super) fn add<'a>(
&'a mut self,
account: UserAccount,
) -> OccupiedEntry<'a, Pubkey, UserAccount> {
let pk = account.key.pubkey;
self.accounts.entry(pk).insert(account)
}
pub(super) fn remove(&mut self, pk: &Pubkey) -> Option<AccountDeletionResponse> {
if *pk == self.fallback && self.accounts.len() == 1 {
// no point in removing it since it'll just get re-added anyway
return None;
}
let removed = self.accounts.remove(pk)?;
if self.accounts.is_empty() {
self.accounts
.insert(self.fallback, self.fallback_account.clone());
}
if self.selected == *pk {
// TODO(kernelkind): choose next better
let (next, _) = self
.accounts
.iter()
.next()
.expect("accounts can never be empty");
self.selected = *next;
return Some(AccountDeletionResponse {
deleted: removed.key,
swap_to: Some(*next),
});
}
Some(AccountDeletionResponse {
deleted: removed.key,
swap_to: None,
})
}
/// guarenteed that all selected exist in accounts
pub(super) fn select(&mut self, pk: Pubkey) -> bool {
if !self.accounts.contains_key(&pk) {
return false;
}
self.selected = pk;
true
}
pub fn selected(&self) -> &UserAccount {
self.accounts
.get(&self.selected)
.expect("guarenteed that selected exists in accounts")
}
pub(super) fn selected_mut(&mut self) -> &mut UserAccount {
self.accounts
.get_mut(&self.selected)
.expect("guarenteed that selected exists in accounts")
}
pub fn fallback(&self) -> &Pubkey {
&self.fallback
}
}
impl<'a> IntoIterator for &'a AccountCache {
type Item = (&'a Pubkey, &'a UserAccount);
type IntoIter = hashbrown::hash_map::Iter<'a, Pubkey, UserAccount>;
fn into_iter(self) -> Self::IntoIter {
self.accounts.iter()
}
}
pub struct AccountDeletionResponse {
pub deleted: enostr::Keypair,
pub swap_to: Option<Pubkey>,
}

View File

@@ -1,167 +0,0 @@
use std::collections::HashSet;
use enostr::Pubkey;
use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction};
#[derive(Clone)]
pub struct Contacts {
pub filter: Filter,
pub(super) state: ContactState,
}
#[derive(Clone)]
pub enum ContactState {
Unreceived,
Received {
contacts: HashSet<Pubkey>,
note_key: NoteKey,
timestamp: u64,
},
}
#[derive(Eq, PartialEq, Debug, Clone, Copy)]
pub enum IsFollowing {
/// We don't have the contact list, so we don't know
Unknown,
/// We are follow
Yes,
No,
}
impl Contacts {
pub fn new(pubkey: &[u8; 32]) -> Self {
let filter = Filter::new().authors([pubkey]).kinds([3]).limit(1).build();
Self {
filter,
state: ContactState::Unreceived,
}
}
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
let binding = ndb
.query(txn, std::slice::from_ref(&self.filter), 1)
.expect("query user relays results");
let Some(res) = binding.first() else {
return;
};
update_state(&mut self.state, &res.note, res.note_key);
}
pub fn is_following(&self, other_pubkey: &[u8; 32]) -> IsFollowing {
match &self.state {
ContactState::Unreceived => IsFollowing::Unknown,
ContactState::Received {
contacts,
note_key: _,
timestamp: _,
} => {
if contacts.contains(other_pubkey) {
IsFollowing::Yes
} else {
IsFollowing::No
}
}
}
}
pub(super) fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) {
let nks = ndb.poll_for_notes(sub, 1);
let Some(key) = nks.first() else {
return;
};
let note = match ndb.get_note_by_key(txn, *key) {
Ok(note) => note,
Err(e) => {
tracing::error!("Could not find note at key {:?}: {e}", key);
return;
}
};
if let ContactState::Received {
contacts: _,
note_key: _,
timestamp,
} = self.get_state()
{
if *timestamp > note.created_at() {
// the current contact list is more up to date than the one we just received. ignore it.
return;
}
}
update_state(&mut self.state, &note, *key);
}
pub fn get_state(&self) -> &ContactState {
&self.state
}
}
fn update_state(state: &mut ContactState, note: &Note, key: NoteKey) {
match state {
ContactState::Unreceived => {
*state = ContactState::Received {
contacts: get_contacts_owned(note),
note_key: key,
timestamp: note.created_at(),
};
}
ContactState::Received {
contacts,
note_key,
timestamp,
} => {
update_contacts(contacts, note);
*note_key = key;
*timestamp = note.created_at();
}
};
}
fn get_contacts<'a>(note: &Note<'a>) -> HashSet<&'a [u8; 32]> {
let mut contacts = HashSet::with_capacity(note.tags().count().into());
for tag in note.tags() {
if tag.count() < 2 {
continue;
}
let Some("p") = tag.get_str(0) else {
continue;
};
let Some(cur_id) = tag.get_id(1) else {
continue;
};
contacts.insert(cur_id);
}
contacts
}
fn get_contacts_owned(note: &Note<'_>) -> HashSet<Pubkey> {
get_contacts(note)
.iter()
.map(|p| Pubkey::new(**p))
.collect()
}
fn update_contacts(cur: &mut HashSet<Pubkey>, new: &Note<'_>) {
let new_contacts = get_contacts(new);
cur.retain(|pk| new_contacts.contains(pk.bytes()));
new_contacts.iter().for_each(|c| {
if !cur.contains(*c) {
cur.insert(Pubkey::new(**c));
}
});
}

View File

@@ -1,12 +0,0 @@
pub mod accounts;
pub mod cache;
pub mod contacts;
pub mod mute;
pub mod relay;
pub const FALLBACK_PUBKEY: fn() -> enostr::Pubkey = || {
enostr::Pubkey::new([
170, 115, 48, 129, 228, 240, 247, 157, 212, 48, 35, 216, 152, 50, 101, 89, 63, 43, 65, 169,
136, 103, 28, 252, 239, 63, 72, 155, 145, 173, 147, 254,
])
};

View File

@@ -1,99 +0,0 @@
use std::sync::Arc;
use nostrdb::{Filter, Ndb, NoteKey, Subscription, Transaction};
use tracing::{debug, error};
use crate::Muted;
#[derive(Clone)]
pub(crate) struct AccountMutedData {
pub filter: Filter,
pub muted: Arc<Muted>,
}
impl AccountMutedData {
pub fn new(pubkey: &[u8; 32]) -> Self {
// Construct a filter for the user's NIP-51 muted list
let filter = Filter::new()
.authors([pubkey])
.kinds([10000])
.limit(1)
.build();
AccountMutedData {
filter,
muted: Arc::new(Muted::default()),
}
}
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
// Query the ndb immediately to see if the user's muted list is already there
let lim = self
.filter
.limit()
.unwrap_or(crate::filter::default_limit()) as i32;
let nks = ndb
.query(txn, std::slice::from_ref(&self.filter), lim)
.expect("query user muted results")
.iter()
.map(|qr| qr.note_key)
.collect::<Vec<NoteKey>>();
let muted = Self::harvest_nip51_muted(ndb, txn, &nks);
debug!("initial muted {:?}", muted);
self.muted = Arc::new(muted);
}
pub(crate) fn harvest_nip51_muted(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Muted {
let mut muted = Muted::default();
for nk in nks.iter() {
if let Ok(note) = ndb.get_note_by_key(txn, *nk) {
for tag in note.tags() {
match tag.get(0).and_then(|t| t.variant().str()) {
Some("p") => {
if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) {
muted.pubkeys.insert(*id);
}
}
Some("t") => {
if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) {
muted.hashtags.insert(str.to_string());
}
}
Some("word") => {
if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) {
muted.words.insert(str.to_string());
}
}
Some("e") => {
if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) {
muted.threads.insert(*id);
}
}
Some("alt") => {
// maybe we can ignore these?
}
Some(x) => error!("query_nip51_muted: unexpected tag: {}", x),
None => error!(
"query_nip51_muted: bad tag value: {:?}",
tag.get_unchecked(0).variant()
),
}
}
}
}
muted
}
pub(super) fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) {
let nks = ndb.poll_for_notes(sub, 1);
if nks.is_empty() {
return;
}
let muted = AccountMutedData::harvest_nip51_muted(ndb, txn, &nks);
debug!("updated muted {:?}", muted);
self.muted = Arc::new(muted);
}
}

View File

@@ -1,261 +0,0 @@
use std::collections::BTreeSet;
use crate::{AccountData, RelaySpec};
use enostr::{Keypair, Pubkey, RelayPool};
use nostrdb::{Filter, Ndb, NoteBuilder, NoteKey, Subscription, Transaction};
use tracing::{debug, error, info};
use url::Url;
#[derive(Clone)]
pub(crate) struct AccountRelayData {
pub filter: Filter,
pub local: BTreeSet<RelaySpec>, // used locally but not advertised
pub advertised: BTreeSet<RelaySpec>, // advertised via NIP-65
}
impl AccountRelayData {
pub fn new(pubkey: &[u8; 32]) -> Self {
// Construct a filter for the user's NIP-65 relay list
let filter = Filter::new()
.authors([pubkey])
.kinds([10002])
.limit(1)
.build();
AccountRelayData {
filter,
local: BTreeSet::new(),
advertised: BTreeSet::new(),
}
}
pub fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
// Query the ndb immediately to see if the user list is already there
let lim = self
.filter
.limit()
.unwrap_or(crate::filter::default_limit()) as i32;
let nks = ndb
.query(txn, std::slice::from_ref(&self.filter), lim)
.expect("query user relays results")
.iter()
.map(|qr| qr.note_key)
.collect::<Vec<NoteKey>>();
let relays = Self::harvest_nip65_relays(ndb, txn, &nks);
debug!("initial relays {:?}", relays);
self.advertised = relays.into_iter().collect()
}
// standardize the format (ie, trailing slashes) to avoid dups
pub fn canonicalize_url(url: &str) -> String {
match Url::parse(url) {
Ok(parsed_url) => parsed_url.to_string(),
Err(_) => url.to_owned(), // If parsing fails, return the original URL.
}
}
pub(crate) fn harvest_nip65_relays(
ndb: &Ndb,
txn: &Transaction,
nks: &[NoteKey],
) -> Vec<RelaySpec> {
let mut relays = Vec::new();
for nk in nks.iter() {
if let Ok(note) = ndb.get_note_by_key(txn, *nk) {
for tag in note.tags() {
match tag.get(0).and_then(|t| t.variant().str()) {
Some("r") => {
if let Some(url) = tag.get(1).and_then(|f| f.variant().str()) {
let has_read_marker = tag
.get(2)
.is_some_and(|m| m.variant().str() == Some("read"));
let has_write_marker = tag
.get(2)
.is_some_and(|m| m.variant().str() == Some("write"));
relays.push(RelaySpec::new(
Self::canonicalize_url(url),
has_read_marker,
has_write_marker,
));
}
}
Some("alt") => {
// ignore for now
}
Some(x) => {
error!("harvest_nip65_relays: unexpected tag type: {}", x);
}
None => {
error!("harvest_nip65_relays: invalid tag");
}
}
}
}
}
relays
}
pub fn publish_nip65_relays(&self, seckey: &[u8; 32], pool: &mut RelayPool) {
let mut builder = NoteBuilder::new().kind(10002).content("");
for rs in &self.advertised {
builder = builder.start_tag().tag_str("r").tag_str(&rs.url);
if rs.has_read_marker {
builder = builder.tag_str("read");
} else if rs.has_write_marker {
builder = builder.tag_str("write");
}
}
let note = builder.sign(seckey).build().expect("note build");
pool.send(&enostr::ClientMessage::event(&note).expect("note client message"));
}
pub fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) -> bool {
let nks = ndb.poll_for_notes(sub, 1);
if nks.is_empty() {
return false;
}
let relays = AccountRelayData::harvest_nip65_relays(ndb, txn, &nks);
debug!("updated relays {:?}", relays);
self.advertised = relays.into_iter().collect();
true
}
}
pub(crate) struct RelayDefaults {
pub forced_relays: BTreeSet<RelaySpec>,
pub bootstrap_relays: BTreeSet<RelaySpec>,
}
impl RelayDefaults {
pub(crate) fn new(forced_relays: Vec<String>) -> Self {
let forced_relays: BTreeSet<RelaySpec> = forced_relays
.into_iter()
.map(|u| RelaySpec::new(AccountRelayData::canonicalize_url(&u), false, false))
.collect();
let bootstrap_relays = [
"wss://relay.damus.io",
// "wss://pyramid.fiatjaf.com", // Uncomment if needed
"wss://nos.lol",
"wss://nostr.wine",
"wss://purplepag.es",
]
.iter()
.map(|&url| url.to_string())
.map(|u| RelaySpec::new(AccountRelayData::canonicalize_url(&u), false, false))
.collect();
Self {
forced_relays,
bootstrap_relays,
}
}
}
pub(super) fn update_relay_configuration(
pool: &mut RelayPool,
relay_defaults: &RelayDefaults,
pk: &Pubkey,
data: &AccountRelayData,
wakeup: impl Fn() + Send + Sync + Clone + 'static,
) {
debug!(
"updating relay configuration for currently selected {:?}",
pk.hex()
);
// If forced relays are set use them only
let mut desired_relays = relay_defaults.forced_relays.clone();
// Compose the desired relay lists from the selected account
if desired_relays.is_empty() {
desired_relays.extend(data.local.iter().cloned());
desired_relays.extend(data.advertised.iter().cloned());
}
// If no relays are specified at this point use the bootstrap list
if desired_relays.is_empty() {
desired_relays = relay_defaults.bootstrap_relays.clone();
}
debug!("current relays: {:?}", pool.urls());
debug!("desired relays: {:?}", desired_relays);
let pool_specs = pool
.urls()
.iter()
.map(|url| RelaySpec::new(url.clone(), false, false))
.collect();
let add: BTreeSet<RelaySpec> = desired_relays.difference(&pool_specs).cloned().collect();
let mut sub: BTreeSet<RelaySpec> = pool_specs.difference(&desired_relays).cloned().collect();
if !add.is_empty() {
debug!("configuring added relays: {:?}", add);
let _ = pool.add_urls(add.iter().map(|r| r.url.clone()).collect(), wakeup);
}
if !sub.is_empty() {
// certain relays are persistent like the multicast relay,
// although we should probably have a way to explicitly
// disable it
sub.remove(&RelaySpec::new("multicast", false, false));
debug!("removing unwanted relays: {:?}", sub);
pool.remove_urls(&sub.iter().map(|r| r.url.clone()).collect());
}
debug!("current relays: {:?}", pool.urls());
}
pub enum RelayAction {
Add(String),
Remove(String),
}
impl RelayAction {
pub(super) fn get_url(&self) -> &str {
match self {
RelayAction::Add(url) => url,
RelayAction::Remove(url) => url,
}
}
}
pub(super) fn modify_advertised_relays(
kp: &Keypair,
action: RelayAction,
pool: &mut RelayPool,
relay_defaults: &RelayDefaults,
account_data: &mut AccountData,
) {
let relay_url = AccountRelayData::canonicalize_url(action.get_url());
match action {
RelayAction::Add(_) => info!("add advertised relay \"{}\"", relay_url),
RelayAction::Remove(_) => info!("remove advertised relay \"{}\"", relay_url),
}
// let selected = self.cache.selected_mut();
let advertised = &mut account_data.relay.advertised;
if advertised.is_empty() {
// If the selected account has no advertised relays,
// initialize with the bootstrapping set.
advertised.extend(relay_defaults.bootstrap_relays.iter().cloned());
}
match action {
RelayAction::Add(_) => {
advertised.insert(RelaySpec::new(relay_url, false, false));
}
RelayAction::Remove(_) => {
advertised.remove(&RelaySpec::new(relay_url, false, false));
}
}
// If we have the secret key publish the NIP-65 relay list
if let Some(secretkey) = &kp.secret_key {
account_data
.relay
.publish_nip65_relays(&secretkey.to_secret_bytes(), pool);
}
}

View File

@@ -1,379 +0,0 @@
use crate::account::FALLBACK_PUBKEY;
use crate::i18n::Localization;
use crate::persist::{AppSizeHandler, SettingsHandler};
use crate::wallet::GlobalWallet;
use crate::zaps::Zaps;
use crate::Error;
use crate::JobPool;
use crate::NotedeckOptions;
use crate::{
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds,
};
use egui::Margin;
use egui::ThemePreference;
use egui_winit::clipboard::Clipboard;
use enostr::RelayPool;
use nostrdb::{Config, Ndb, Transaction};
use std::cell::RefCell;
use std::collections::BTreeSet;
use std::path::Path;
use std::rc::Rc;
use tracing::{error, info};
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
#[cfg(target_os = "android")]
use android_activity::AndroidApp;
pub enum AppAction {
Note(NoteAction),
ToggleChrome,
}
pub trait App {
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction>;
}
/// Main notedeck app framework
pub struct Notedeck {
ndb: Ndb,
img_cache: Images,
unknown_ids: UnknownIds,
pool: RelayPool,
note_cache: NoteCache,
accounts: Accounts,
global_wallet: GlobalWallet,
path: DataPath,
args: Args,
settings: SettingsHandler,
app: Option<Rc<RefCell<dyn App>>>,
app_size: AppSizeHandler,
unrecognized_args: BTreeSet<String>,
clipboard: Clipboard,
zaps: Zaps,
frame_history: FrameHistory,
job_pool: JobPool,
i18n: Localization,
#[cfg(target_os = "android")]
android_app: Option<AndroidApp>,
}
/// Our chrome, which is basically nothing
fn main_panel(style: &egui::Style) -> egui::CentralPanel {
egui::CentralPanel::default().frame(egui::Frame {
inner_margin: Margin::ZERO,
fill: style.visuals.panel_fill,
..Default::default()
})
}
fn render_notedeck(notedeck: &mut Notedeck, ctx: &egui::Context) {
main_panel(&ctx.style()).show(ctx, |ui| {
// render app
let Some(app) = &notedeck.app else {
return;
};
let app = app.clone();
app.borrow_mut().update(&mut notedeck.app_context(), ui);
// Move the screen up when we have a virtual keyboard
// NOTE: actually, we only want to do this if the keyboard is covering the focused element?
/*
let keyboard_height = crate::platform::virtual_keyboard_height() as f32;
if keyboard_height > 0.0 {
ui.ctx().transform_layer_shapes(
ui.layer_id(),
egui::emath::TSTransform::from_translation(egui::Vec2::new(0.0, -(keyboard_height/2.0))),
);
}
*/
});
}
impl eframe::App for Notedeck {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
profiling::finish_frame!();
self.frame_history
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
// handle account updates
self.accounts.update(&mut self.ndb, &mut self.pool, ctx);
self.zaps
.process(&mut self.accounts, &mut self.global_wallet, &self.ndb);
render_notedeck(self, ctx);
self.settings.update_batch(|settings| {
settings.zoom_factor = ctx.zoom_factor();
settings.locale = self.i18n.get_current_locale().to_string();
settings.theme = if ctx.style().visuals.dark_mode {
ThemePreference::Dark
} else {
ThemePreference::Light
};
});
self.app_size.try_save_app_size(ctx);
if self.args.options.contains(NotedeckOptions::RelayDebug) {
if self.pool.debug.is_none() {
self.pool.use_debug();
}
if let Some(debug) = &mut self.pool.debug {
RelayDebugView::window(ctx, debug);
}
}
#[cfg(feature = "puffin")]
puffin_egui::profiler_window(ctx);
}
/// Called by the framework to save state before shutdown.
fn save(&mut self, _storage: &mut dyn eframe::Storage) {
//eframe::set_value(storage, eframe::APP_KEY, self);
}
}
#[cfg(feature = "puffin")]
fn setup_puffin() {
info!("setting up puffin");
puffin::set_scopes_on(true); // tell puffin to collect data
}
impl Notedeck {
#[cfg(target_os = "android")]
pub fn set_android_context(&mut self, context: AndroidApp) {
self.android_app = Some(context);
}
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self {
#[cfg(feature = "puffin")]
setup_puffin();
// Skip the first argument, which is the program name.
let (parsed_args, unrecognized_args) = Args::parse(&args[1..]);
let data_path = parsed_args
.datapath
.clone()
.unwrap_or(data_path.as_ref().to_str().expect("db path ok").to_string());
let path = DataPath::new(&data_path);
let dbpath_str = parsed_args
.dbpath
.clone()
.unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string());
let _ = std::fs::create_dir_all(&dbpath_str);
let img_cache_dir = path.path(DataPathType::Cache);
let _ = std::fs::create_dir_all(img_cache_dir.clone());
let map_size = if cfg!(target_os = "windows") {
// 16 Gib on windows because it actually creates the file
1024usize * 1024usize * 1024usize * 16usize
} else {
// 1 TiB for everything else since its just virtually mapped
1024usize * 1024usize * 1024usize * 1024usize
};
let settings = SettingsHandler::new(&path).load();
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
let keystore = if parsed_args.options.contains(NotedeckOptions::UseKeystore) {
let keys_path = path.path(DataPathType::Keys);
let selected_key_path = path.path(DataPathType::SelectedKey);
Some(AccountStorage::new(
Directory::new(keys_path),
Directory::new(selected_key_path),
))
} else {
None
};
// AccountManager will setup the pool on first update
let mut pool = RelayPool::new();
{
let ctx = ctx.clone();
if let Err(err) = pool.add_multicast_relay(move || ctx.request_repaint()) {
error!("error setting up multicast relay: {err}");
}
}
let mut unknown_ids = UnknownIds::default();
let mut ndb = Ndb::new(&dbpath_str, &config).expect("ndb");
let txn = Transaction::new(&ndb).expect("txn");
let mut accounts = Accounts::new(
keystore,
parsed_args.relays.clone(),
FALLBACK_PUBKEY(),
&mut ndb,
&txn,
&mut pool,
ctx,
&mut unknown_ids,
);
{
for key in &parsed_args.keys {
info!("adding account: {}", &key.pubkey);
if let Some(resp) = accounts.add_account(key.clone()) {
resp.unk_id_action
.process_action(&mut unknown_ids, &ndb, &txn);
}
}
}
if let Some(first) = parsed_args.keys.first() {
accounts.select_account(&first.pubkey, &mut ndb, &txn, &mut pool, ctx);
}
let img_cache = Images::new(img_cache_dir);
let note_cache = NoteCache::default();
let app_size = AppSizeHandler::new(&path);
// migrate
if let Err(e) = img_cache.migrate_v0() {
error!("error migrating image cache: {e}");
}
let global_wallet = GlobalWallet::new(&path);
let zaps = Zaps::default();
let job_pool = JobPool::default();
// Initialize localization
let mut i18n = Localization::new();
let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
settings.locale().parse();
if let Ok(setting_locale) = setting_locale {
if let Err(err) = i18n.set_locale(setting_locale) {
error!("{err}");
}
}
if let Some(locale) = &parsed_args.locale {
if let Err(err) = i18n.set_locale(locale.to_owned()) {
error!("{err}");
}
}
Self {
ndb,
img_cache,
unknown_ids,
pool,
note_cache,
accounts,
global_wallet,
path: path.clone(),
args: parsed_args,
settings,
app: None,
app_size,
unrecognized_args,
frame_history: FrameHistory::default(),
clipboard: Clipboard::new(None),
zaps,
job_pool,
i18n,
#[cfg(target_os = "android")]
android_app: None,
}
}
/// Setup egui context
pub fn setup(&self, ctx: &egui::Context) {
// Initialize global i18n context
//crate::i18n::init_global_i18n(i18n.clone());
crate::setup::setup_egui_context(
ctx,
self.args.options,
self.theme(),
self.note_body_font_size(),
self.zoom_factor(),
);
}
/// ensure we recognized all the arguments
pub fn check_args(&self, other_app_args: &BTreeSet<String>) -> Result<(), Error> {
let completely_unrecognized: Vec<String> = self
.unrecognized_args()
.intersection(other_app_args)
.cloned()
.collect();
if !completely_unrecognized.is_empty() {
let err = format!("Unrecognized arguments: {completely_unrecognized:?}");
tracing::error!("{}", &err);
return Err(Error::Generic(err));
}
Ok(())
}
#[inline]
pub fn options(&self) -> NotedeckOptions {
self.args.options
}
pub fn has_option(&self, option: NotedeckOptions) -> bool {
self.options().contains(option)
}
pub fn app<A: App + 'static>(mut self, app: A) -> Self {
self.set_app(app);
self
}
pub fn app_context(&mut self) -> AppContext<'_> {
AppContext {
ndb: &mut self.ndb,
img_cache: &mut self.img_cache,
unknown_ids: &mut self.unknown_ids,
pool: &mut self.pool,
note_cache: &mut self.note_cache,
accounts: &mut self.accounts,
global_wallet: &mut self.global_wallet,
path: &self.path,
args: &self.args,
settings: &mut self.settings,
clipboard: &mut self.clipboard,
zaps: &mut self.zaps,
frame_history: &mut self.frame_history,
job_pool: &mut self.job_pool,
i18n: &mut self.i18n,
#[cfg(target_os = "android")]
android: self.android_app.as_ref().unwrap().clone(),
}
}
pub fn set_app<T: App + 'static>(&mut self, app: T) {
self.app = Some(Rc::new(RefCell::new(app)));
}
pub fn args(&self) -> &Args {
&self.args
}
pub fn theme(&self) -> ThemePreference {
self.settings.theme()
}
pub fn note_body_font_size(&self) -> f32 {
self.settings.note_body_font_size()
}
pub fn zoom_factor(&self) -> f32 {
self.settings.zoom_factor()
}
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
&self.unrecognized_args
}
}

View File

@@ -1,140 +0,0 @@
use std::collections::BTreeSet;
use crate::NotedeckOptions;
use enostr::{Keypair, Pubkey, SecretKey};
use tracing::error;
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
pub struct Args {
pub relays: Vec<String>,
pub locale: Option<LanguageIdentifier>,
pub keys: Vec<Keypair>,
pub options: NotedeckOptions,
pub dbpath: Option<String>,
pub datapath: Option<String>,
}
impl Args {
// parse arguments, return set of unrecognized args
pub fn parse(args: &[String]) -> (Self, BTreeSet<String>) {
let mut unrecognized_args = BTreeSet::new();
let mut res = Args {
relays: vec![],
keys: vec![],
options: NotedeckOptions::default(),
dbpath: None,
datapath: None,
locale: None,
};
let mut i = 0;
let len = args.len();
while i < len {
let arg = &args[i];
if arg == "--mobile" {
res.options.set(NotedeckOptions::Mobile, true);
} else if arg == "--light" {
res.options.set(NotedeckOptions::LightTheme, true);
} else if arg == "--locale" {
i += 1;
let Some(locale) = args.get(i) else {
panic!("locale argument missing?");
};
let parsed: Result<LanguageIdentifier, LanguageIdentifierError> = locale.parse();
match parsed {
Err(err) => {
panic!("locale failed to parse: {err}");
}
Ok(locale) => {
tracing::info!(
"parsed locale '{locale}' from args, not sure if we have it yet though."
);
res.locale = Some(locale);
}
}
} else if arg == "--dark" {
res.options.set(NotedeckOptions::LightTheme, false);
} else if arg == "--debug" {
res.options.set(NotedeckOptions::Debug, true);
} else if arg == "--testrunner" {
res.options.set(NotedeckOptions::Tests, true);
} else if arg == "--pub" || arg == "--npub" {
i += 1;
let pubstr = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("sec argument missing?");
continue;
};
if let Ok(pk) = Pubkey::parse(pubstr) {
res.keys.push(Keypair::only_pubkey(pk));
} else {
error!(
"failed to parse {} argument. Make sure to use hex or npub.",
arg
);
}
} else if arg == "--sec" || arg == "--nsec" {
i += 1;
let secstr = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("sec argument missing?");
continue;
};
if let Ok(sec) = SecretKey::parse(secstr) {
res.keys.push(Keypair::from_secret(sec));
} else {
error!(
"failed to parse {} argument. Make sure to use hex or nsec.",
arg
);
}
} else if arg == "--dbpath" {
i += 1;
let path = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("dbpath argument missing?");
continue;
};
res.dbpath = Some(path.clone());
} else if arg == "--datapath" {
i += 1;
let path = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("datapath argument missing?");
continue;
};
res.datapath = Some(path.clone());
} else if arg == "-r" || arg == "--relay" {
i += 1;
let relay = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("relay argument missing?");
continue;
};
res.relays.push(relay.clone());
} else if arg == "--no-keystore" {
res.options.set(NotedeckOptions::UseKeystore, true);
} else if arg == "--relay-debug" {
res.options.set(NotedeckOptions::RelayDebug, true);
} else if arg == "--notebook" {
res.options.set(NotedeckOptions::FeatureNotebook, true);
} else if arg == "--clndash" {
res.options.set(NotedeckOptions::FeatureClnDash, true);
} else {
unrecognized_args.insert(arg.clone());
}
i += 1;
}
(res, unrecognized_args)
}
}

View File

@@ -1,24 +0,0 @@
use crate::{
filter::{self, HybridFilter},
Error,
};
use nostrdb::{Filter, Note};
pub fn contacts_filter(pk: &[u8; 32]) -> Filter {
Filter::new().authors([pk]).kinds([3]).limit(1).build()
}
/// Contact filters have an additional kind0 in the remote filter so it can fetch profiles as well
/// we don't need this in the local filter since we only care about the kind1 results
pub fn hybrid_contacts_filter(
note: &Note,
add_pk: Option<&[u8; 32]>,
with_hashtags: bool,
) -> Result<HybridFilter, Error> {
let local = filter::filter_from_tags(note, add_pk, with_hashtags)?
.into_filter([1], filter::default_limit());
let remote = filter::filter_from_tags(note, add_pk, with_hashtags)?
.into_filter([1, 0], filter::default_remote_limit());
Ok(HybridFilter::split(local, remote))
}

View File

@@ -1,90 +0,0 @@
use crate::{
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, SettingsHandler,
UnknownIds,
};
use egui_winit::clipboard::Clipboard;
use enostr::RelayPool;
use nostrdb::Ndb;
#[cfg(target_os = "android")]
use android_activity::AndroidApp;
use egui::{Pos2, Rect};
// TODO: make this interface more sandboxed
pub struct AppContext<'a> {
pub ndb: &'a mut Ndb,
pub img_cache: &'a mut Images,
pub unknown_ids: &'a mut UnknownIds,
pub pool: &'a mut RelayPool,
pub note_cache: &'a mut NoteCache,
pub accounts: &'a mut Accounts,
pub global_wallet: &'a mut GlobalWallet,
pub path: &'a DataPath,
pub args: &'a Args,
pub settings: &'a mut SettingsHandler,
pub clipboard: &'a mut Clipboard,
pub zaps: &'a mut Zaps,
pub frame_history: &'a mut FrameHistory,
pub job_pool: &'a mut JobPool,
pub i18n: &'a mut Localization,
#[cfg(target_os = "android")]
pub android: AndroidApp,
}
#[derive(Debug, Clone)]
pub enum SoftKeyboardContext {
Virtual,
Platform { ppp: f32 },
}
impl SoftKeyboardContext {
pub fn platform(context: &egui::Context) -> Self {
Self::Platform {
ppp: context.pixels_per_point(),
}
}
}
impl<'a> AppContext<'a> {
pub fn soft_keyboard_rect(&self, screen_rect: Rect, ctx: SoftKeyboardContext) -> Option<Rect> {
match ctx {
SoftKeyboardContext::Virtual => {
let height = 400.0;
skb_rect_from_screen_rect(screen_rect, height)
}
#[allow(unused_variables)]
SoftKeyboardContext::Platform { ppp } => {
#[cfg(target_os = "android")]
{
use android_activity::InsetType;
// not sure why I need this, it seems to be consistently off by some amount of
// pixels ?
let fudge = 0.0;
let inset = self.android.get_window_insets(InsetType::Ime);
let height = (inset.bottom as f32 / ppp) - fudge;
skb_rect_from_screen_rect(screen_rect, height)
}
#[cfg(not(target_os = "android"))]
{
None
}
}
}
}
}
#[inline]
fn skb_rect_from_screen_rect(screen_rect: Rect, height: f32) -> Option<Rect> {
if height == 0.0 {
return None;
}
let min = Pos2::new(0.0, screen_rect.max.y - height);
Some(Rect::from_min_max(min, screen_rect.max))
}

View File

@@ -1,35 +0,0 @@
use std::time::{Duration, Instant};
/// A simple debouncer that tracks when an action was last performed
/// and determines if enough time has passed to perform it again.
#[derive(Debug)]
pub struct Debouncer {
delay: Duration,
last_action: Instant,
}
impl Debouncer {
/// Creates a new Debouncer with the specified delay
pub fn new(delay: Duration) -> Self {
Self {
delay,
last_action: Instant::now() - delay, // Start ready to act
}
}
/// Sets a new delay value and returns self for method chaining
pub fn with_delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
/// Checks if enough time has passed since the last action
pub fn should_act(&self) -> bool {
self.last_action.elapsed() >= self.delay
}
/// Marks an action as performed, updating the timestamp
pub fn bounce(&mut self) {
self.last_action = Instant::now();
}
}

View File

@@ -1,94 +0,0 @@
use std::io;
/// App related errors
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("image error: {0}")]
Image(#[from] image::error::ImageError),
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("subscription error: {0}")]
SubscriptionError(SubscriptionError),
#[error("filter error: {0}")]
Filter(FilterError),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("io error: {0}")]
Nostrdb(#[from] nostrdb::Error),
#[error("generic error: {0}")]
Generic(String),
#[error("zaps error: {0}")]
Zap(#[from] ZapError),
}
#[derive(Debug, thiserror::Error, Clone)]
pub enum ZapError {
#[error("invalid lud16")]
InvalidLud16(String),
#[error("invalid endpoint response")]
EndpointError(String),
#[error("bech encoding/decoding error")]
Bech(String),
#[error("serialization/deserialization problem")]
Serialization(String),
#[error("nwc error")]
NWC(String),
}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Generic(s)
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
pub enum FilterError {
#[error("empty contact list")]
EmptyContactList,
#[error("filter not ready")]
FilterNotReady,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone, thiserror::Error)]
pub enum SubscriptionError {
#[error("no active subscriptions")]
NoActive,
/// When a timeline has an unexpected number
/// of active subscriptions. Should only happen if there
/// is a bug in notedeck
#[error("unexpected subscription count")]
UnexpectedSubscriptionCount(i32),
}
impl Error {
pub fn unexpected_sub_count(c: i32) -> Self {
Error::SubscriptionError(SubscriptionError::UnexpectedSubscriptionCount(c))
}
pub fn no_active_sub() -> Self {
Error::SubscriptionError(SubscriptionError::NoActive)
}
pub fn empty_contact_list() -> Self {
Error::Filter(FilterError::EmptyContactList)
}
}
pub fn show_one_error_message(ui: &mut egui::Ui, message: &str) {
let id = ui.id().with(("error", message));
let res: Option<()> = ui.ctx().data(|d| d.get_temp(id));
if res.is_none() {
ui.ctx().data_mut(|d| d.insert_temp(id, ()));
tracing::error!(message);
}
}

View File

@@ -1,24 +0,0 @@
use super::IntlKeyBuf;
use unic_langid::LanguageIdentifier;
/// App related errors
#[derive(thiserror::Error, Debug)]
pub enum IntlError {
#[error("message not found: {0}")]
NotFound(IntlKeyBuf),
#[error("message has no value: {0}")]
NoValue(IntlKeyBuf),
#[error("Locale({0}) parse error: {1}")]
LocaleParse(LanguageIdentifier, String),
#[error("locale not available: {0}")]
LocaleNotAvailable(LanguageIdentifier),
#[error("FTL for '{0}' is not available")]
NoFtl(LanguageIdentifier),
#[error("Bundle for '{0}' is not available")]
NoBundle(LanguageIdentifier),
}

View File

@@ -1,47 +0,0 @@
use std::fmt;
/// An owned key used to lookup i18n translations. Mostly used for errors
#[derive(Eq, PartialEq, Clone, Debug)]
pub struct IntlKeyBuf(String);
/// A key used to lookup i18n translations
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
pub struct IntlKey<'a>(&'a str);
impl fmt::Display for IntlKey<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Use `self.number` to refer to each positional data point.
write!(f, "{}", self.0)
}
}
impl fmt::Display for IntlKeyBuf {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Use `self.number` to refer to each positional data point.
write!(f, "{}", &self.0)
}
}
impl IntlKeyBuf {
pub fn new(string: impl Into<String>) -> Self {
IntlKeyBuf(string.into())
}
pub fn borrow<'a>(&'a self) -> IntlKey<'a> {
IntlKey::new(&self.0)
}
}
impl<'a> IntlKey<'a> {
pub fn new(string: &'a str) -> IntlKey<'a> {
IntlKey(string)
}
pub fn to_owned(&self) -> IntlKeyBuf {
IntlKeyBuf::new(self.0)
}
pub fn as_str(&self) -> &'a str {
self.0
}
}

View File

@@ -1,710 +0,0 @@
use super::{IntlError, IntlKey, IntlKeyBuf};
use fluent::{FluentArgs, FluentBundle, FluentResource};
use fluent_langneg::negotiate_languages;
use std::borrow::Cow;
use std::collections::HashMap;
use unic_langid::{langid, LanguageIdentifier};
const EN_US: LanguageIdentifier = langid!("en-US");
const EN_XA: LanguageIdentifier = langid!("en-XA");
const DE: LanguageIdentifier = langid!("de");
const ES_419: LanguageIdentifier = langid!("es-419");
const ES_ES: LanguageIdentifier = langid!("es-ES");
const FR: LanguageIdentifier = langid!("fr");
const JA: LanguageIdentifier = langid!("ja");
const PT_BR: LanguageIdentifier = langid!("pt-BR");
const PT_PT: LanguageIdentifier = langid!("pt-PT");
const TH: LanguageIdentifier = langid!("th");
const ZH_CN: LanguageIdentifier = langid!("zh-CN");
const ZH_TW: LanguageIdentifier = langid!("zh-TW");
const NUM_FTLS: usize = 12;
const EN_US_NATIVE_NAME: &str = "English (US)";
const EN_XA_NATIVE_NAME: &str = "Éñglísh (Pséúdólóçàlé)";
const DE_NATIVE_NAME: &str = "Deutsch";
const ES_419_NATIVE_NAME: &str = "Español (Latinoamérica)";
const ES_ES_NATIVE_NAME: &str = "Español (España)";
const FR_NATIVE_NAME: &str = "Français";
const JA_NATIVE_NAME: &str = "日本語";
const PT_BR_NATIVE_NAME: &str = "Português (Brasil)";
const PT_PT_NATIVE_NAME: &str = "Português (Portugal)";
const TH_NATIVE_NAME: &str = "ภาษาไทย";
const ZH_CN_NATIVE_NAME: &str = "简体中文";
const ZH_TW_NATIVE_NAME: &str = "繁體中文";
struct StaticBundle {
identifier: LanguageIdentifier,
ftl: &'static str,
}
const FTLS: [StaticBundle; NUM_FTLS] = [
StaticBundle {
identifier: EN_US,
ftl: include_str!("../../../../assets/translations/en-US/main.ftl"),
},
StaticBundle {
identifier: EN_XA,
ftl: include_str!("../../../../assets/translations/en-XA/main.ftl"),
},
StaticBundle {
identifier: DE,
ftl: include_str!("../../../../assets/translations/de/main.ftl"),
},
StaticBundle {
identifier: ES_419,
ftl: include_str!("../../../../assets/translations/es-419/main.ftl"),
},
StaticBundle {
identifier: ES_ES,
ftl: include_str!("../../../../assets/translations/es-ES/main.ftl"),
},
StaticBundle {
identifier: FR,
ftl: include_str!("../../../../assets/translations/fr/main.ftl"),
},
StaticBundle {
identifier: JA,
ftl: include_str!("../../../../assets/translations/ja/main.ftl"),
},
StaticBundle {
identifier: PT_BR,
ftl: include_str!("../../../../assets/translations/pt-BR/main.ftl"),
},
StaticBundle {
identifier: PT_PT,
ftl: include_str!("../../../../assets/translations/pt-PT/main.ftl"),
},
StaticBundle {
identifier: TH,
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
},
StaticBundle {
identifier: ZH_CN,
ftl: include_str!("../../../../assets/translations/zh-CN/main.ftl"),
},
StaticBundle {
identifier: ZH_TW,
ftl: include_str!("../../../../assets/translations/zh-TW/main.ftl"),
},
];
type Bundle = FluentBundle<FluentResource>;
/// Manages localization resources and provides localized strings
pub struct Localization {
/// Current locale
current_locale: LanguageIdentifier,
/// Available locales
available_locales: Vec<LanguageIdentifier>,
/// Fallback locale
fallback_locale: LanguageIdentifier,
/// Native names for locales
locale_native_names: HashMap<LanguageIdentifier, String>,
/// Cached string results per locale (only for strings without arguments)
string_cache: HashMap<LanguageIdentifier, HashMap<String, String>>,
/// Cached normalized keys
normalized_key_cache: HashMap<String, IntlKeyBuf>,
/// Bundles
bundles: HashMap<LanguageIdentifier, Bundle>,
use_isolating: bool,
}
impl Default for Localization {
fn default() -> Self {
// Default to English (US)
let default_locale = &EN_US;
let fallback_locale = default_locale.to_owned();
// Build available locales list
let available_locales = vec![
EN_US.clone(),
EN_XA.clone(),
DE.clone(),
ES_419.clone(),
ES_ES.clone(),
FR.clone(),
JA.clone(),
PT_BR.clone(),
PT_PT.clone(),
TH.clone(),
ZH_CN.clone(),
ZH_TW.clone(),
];
let locale_native_names = HashMap::from([
(EN_US, EN_US_NATIVE_NAME.to_owned()),
(EN_XA, EN_XA_NATIVE_NAME.to_owned()),
(DE, DE_NATIVE_NAME.to_owned()),
(ES_419, ES_419_NATIVE_NAME.to_owned()),
(ES_ES, ES_ES_NATIVE_NAME.to_owned()),
(FR, FR_NATIVE_NAME.to_owned()),
(JA, JA_NATIVE_NAME.to_owned()),
(PT_BR, PT_BR_NATIVE_NAME.to_owned()),
(PT_PT, PT_PT_NATIVE_NAME.to_owned()),
(TH, TH_NATIVE_NAME.to_owned()),
(ZH_CN, ZH_CN_NATIVE_NAME.to_owned()),
(ZH_TW, ZH_TW_NATIVE_NAME.to_owned()),
]);
Self {
current_locale: default_locale.to_owned(),
available_locales,
fallback_locale,
locale_native_names,
use_isolating: true,
normalized_key_cache: HashMap::new(),
string_cache: HashMap::new(),
bundles: HashMap::new(),
}
}
}
impl Localization {
/// Creates a new Localization with the specified resource directory
pub fn new() -> Self {
Localization::default()
}
/// Disable bidirectional isolation markers. mostly useful for tests
pub fn no_bidi() -> Self {
Localization {
use_isolating: false,
..Localization::default()
}
}
/// Gets a localized string by its ID
pub fn get_string(&mut self, id: IntlKey<'_>) -> Result<String, IntlError> {
self.get_cached_string(id, None)
}
/// Load a fluent bundle given a language identifier. Only looks in the static
/// ftl files baked into the binary
fn load_bundle(lang: &LanguageIdentifier) -> Result<Bundle, IntlError> {
for ftl in &FTLS {
if &ftl.identifier == lang {
let mut bundle = FluentBundle::new(vec![lang.to_owned()]);
let resource = FluentResource::try_new(ftl.ftl.to_string());
match resource {
Err((resource, errors)) => {
for error in errors {
tracing::error!("load_bundle ({lang}): {error}");
}
tracing::warn!("load_bundle ({}: loading bundle with errors", lang);
if let Err(errs) = bundle.add_resource(resource) {
for err in errs {
tracing::error!("adding resource: {err}");
}
}
}
Ok(resource) => {
tracing::info!("loaded {} bundle OK!", lang);
if let Err(errs) = bundle.add_resource(resource) {
for err in errs {
tracing::error!("adding resource 2: {err}");
}
}
}
}
return Ok(bundle);
}
}
// no static ftl for this LanguageIdentifier
Err(IntlError::NoFtl(lang.to_owned()))
}
fn get_bundle<'a>(&'a self, lang: &LanguageIdentifier) -> &'a Bundle {
self.bundles
.get(lang)
.expect("make sure to call ensure_bundle!")
}
fn has_bundle(&self, lang: &LanguageIdentifier) -> bool {
self.bundles.contains_key(lang)
}
fn try_load_bundle(&mut self, lang: &LanguageIdentifier) -> Result<(), IntlError> {
let mut bundle = Self::load_bundle(lang)?;
if !self.use_isolating {
bundle.set_use_isolating(false);
}
self.bundles.insert(lang.to_owned(), bundle);
Ok(())
}
pub fn normalized_ftl_key(&mut self, key: &str, comment: &str) -> IntlKeyBuf {
match self.get_ftl_key(key) {
Some(intl_key) => intl_key,
None => {
self.insert_ftl_key(key, comment);
self.get_ftl_key(key).unwrap()
}
}
}
fn get_ftl_key(&self, cache_key: &str) -> Option<IntlKeyBuf> {
self.normalized_key_cache.get(cache_key).cloned()
}
fn insert_ftl_key(&mut self, cache_key: &str, comment: &str) {
let mut result = fixup_key(cache_key);
// Ensure the key starts with a letter (Fluent requirement)
if result.is_empty() || !result.chars().next().unwrap().is_ascii_alphabetic() {
result = format!("k_{result}");
}
// If we have a comment, append a hash of it to reduce collisions
let hash_str = format!("_{}", simple_hash(comment));
result.push_str(&hash_str);
tracing::debug!(
"normalize_ftl_key: original='{}', final='{}'",
cache_key,
result
);
self.normalized_key_cache
.insert(cache_key.to_owned(), IntlKeyBuf::new(result));
}
fn get_cached_string_no_args<'key>(
&'key self,
lang: &LanguageIdentifier,
id: IntlKey<'key>,
) -> Result<Cow<'key, str>, IntlError> {
// Try to get from string cache first
if let Some(locale_cache) = self.string_cache.get(lang) {
if let Some(cached_string) = locale_cache.get(id.as_str()) {
/*
tracing::trace!(
"Using cached string result for '{}' in locale: {}",
id,
&lang
);
*/
return Ok(Cow::Borrowed(cached_string));
}
}
Err(IntlError::NotFound(id.to_owned()))
}
fn ensure_bundle(&mut self) -> Result<(), IntlError> {
let locale = self.current_locale.clone();
if !self.has_bundle(&locale) {
match self.try_load_bundle(&locale) {
Err(err) => {
tracing::warn!(
"tried to load bundle {} but failed with '{err}'. using fallback {}",
&locale,
&self.fallback_locale
);
self.try_load_bundle(&locale)
.expect("failed to load fallback bundle!?");
Ok(())
}
Ok(()) => Ok(()),
}
} else {
Ok(())
}
}
fn get_current_bundle(&self) -> &Bundle {
if self.has_bundle(&self.current_locale) {
return self.get_bundle(&self.current_locale);
}
self.get_bundle(&self.fallback_locale)
}
/// Gets cached string result, or formats it and caches the result
pub fn get_cached_string(
&mut self,
id: IntlKey<'_>,
args: Option<&FluentArgs>,
) -> Result<String, IntlError> {
self.ensure_bundle()?;
if args.is_none() {
if let Ok(result) = self.get_cached_string_no_args(&self.current_locale, id) {
return Ok(result.to_string());
}
}
let result = {
let bundle = self.get_current_bundle();
let message = bundle
.get_message(id.as_str())
.ok_or_else(|| IntlError::NotFound(id.to_owned()))?;
let pattern = message
.value()
.ok_or_else(|| IntlError::NoValue(id.to_owned()))?;
let mut errors = Vec::with_capacity(0);
let result = bundle.format_pattern(pattern, args, &mut errors);
if !errors.is_empty() {
tracing::warn!("Localization errors for {}: {:?}", id, &errors);
}
result.to_string()
};
// Only cache simple strings without arguments
// This prevents caching issues when the same message ID is used with different arguments
if args.is_none() {
self.cache_string(self.current_locale.clone(), id, result.as_str());
tracing::debug!(
"Cached string result for '{}' in locale: {}",
id,
&self.current_locale
);
} else {
tracing::trace!("Not caching string '{}' due to arguments", id);
}
Ok(result)
}
pub fn cache_string<'a>(&mut self, locale: LanguageIdentifier, id: IntlKey<'a>, result: &str) {
tracing::debug!("Cached string result for '{}' in locale: {}", id, &locale);
let locale_cache = self.string_cache.entry(locale).or_default();
locale_cache.insert(id.to_owned().to_string(), result.to_owned());
}
/// Sets the current locale
pub fn set_locale(&mut self, locale: LanguageIdentifier) -> Result<(), IntlError> {
tracing::info!("Attempting to set locale to: {}", locale);
tracing::info!("Available locales: {:?}", self.available_locales);
// Validate that the locale is available
if !self.available_locales.contains(&locale) {
tracing::error!(
"Locale {} is not available. Available locales: {:?}",
locale,
self.available_locales
);
return Err(IntlError::LocaleNotAvailable(locale));
}
tracing::info!(
"Switching locale from {} to {}",
&self.current_locale,
&locale
);
self.current_locale = locale;
// Clear caches when locale changes since they are locale-specific
self.string_cache.clear();
tracing::debug!("String cache cleared due to locale change");
Ok(())
}
/// Clears the parsed FluentResource cache (useful for development when FTL files change)
pub fn clear_cache(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.bundles.clear();
tracing::debug!("Parsed FluentResource cache cleared");
self.string_cache.clear();
tracing::debug!("String result cache cleared");
Ok(())
}
/// Gets the current locale
pub fn get_current_locale(&self) -> &LanguageIdentifier {
&self.current_locale
}
/// Gets all available locales
pub fn get_available_locales(&self) -> &[LanguageIdentifier] {
&self.available_locales
}
/// Gets the fallback locale
pub fn get_fallback_locale(&self) -> &LanguageIdentifier {
&self.fallback_locale
}
pub fn get_locale_native_name(&self, locale: &LanguageIdentifier) -> Option<&str> {
self.locale_native_names.get(locale).map(|s| s.as_str())
}
/// Gets cache statistics for monitoring performance
pub fn get_cache_stats(&self) -> Result<CacheStats, Box<dyn std::error::Error + Send + Sync>> {
let mut total_strings = 0;
for locale_cache in self.string_cache.values() {
total_strings += locale_cache.len();
}
Ok(CacheStats {
resource_cache_size: self.bundles.len(),
string_cache_size: total_strings,
cached_locales: self.bundles.keys().cloned().collect(),
})
}
/// Limits the string cache size to prevent memory growth
pub fn limit_string_cache_size(
&mut self,
max_strings_per_locale: usize,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
for locale_cache in self.string_cache.values_mut() {
if locale_cache.len() > max_strings_per_locale {
// Remove oldest entries (simple approach: just clear and let it rebuild)
// In a more sophisticated implementation, you might use an LRU cache
locale_cache.clear();
tracing::debug!("Cleared string cache for locale due to size limit");
}
}
Ok(())
}
/// Negotiates the best locale from a list of preferred locales
pub fn negotiate_locale(&self, preferred: &[LanguageIdentifier]) -> LanguageIdentifier {
let available = self.available_locales.clone();
let negotiated = negotiate_languages(
preferred,
&available,
Some(&self.fallback_locale),
fluent_langneg::NegotiationStrategy::Filtering,
);
negotiated
.first()
.map_or(self.fallback_locale.clone(), |v| (*v).clone())
}
}
/// Statistics about cache usage
#[derive(Debug, Clone)]
pub struct CacheStats {
pub resource_cache_size: usize,
pub string_cache_size: usize,
pub cached_locales: Vec<LanguageIdentifier>,
}
#[cfg(test)]
mod tests {
//
// TODO(jb55): write tests that work, i broke all these during the refacto
//
/*
use super::*;
#[test]
fn test_locale_management() {
let i18n = Localization::default();
// Test default locale
let current = i18n.get_current_locale();
assert_eq!(current.to_string(), "en-US");
// Test available locales
let available = i18n.get_available_locales();
assert_eq!(available.len(), 2);
assert_eq!(available[0].to_string(), "en-US");
assert_eq!(available[1].to_string(), "en-XA");
}
#[test]
fn test_cache_clearing() {
let mut i18n = Localization::default();
// Load and cache the FTL content
let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result1.is_ok());
// Clear the cache
let clear_result = i18n.clear_cache();
assert!(clear_result.is_ok());
// Should still work after clearing cache (will reload)
let result2 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
}
#[test]
fn test_context_caching() {
let mut i18n = Localization::default();
// Debug: check what the normalized key should be
let normalized_key = i18n.normalized_ftl_key("test_key", "comment");
println!("Normalized key: '{}'", normalized_key);
// First call should load and cache the FTL content
let result1 = i18n.get_string(normalized_key.borrow());
println!("First result: {:?}", result1);
assert!(result1.is_ok());
assert_eq!(result1.unwrap(), "Test Value");
// Second call should use cached FTL content
let result2 = i18n.get_string(normalized_key.borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
// Test cache clearing through context
let clear_result = i18n.clear_cache();
assert!(clear_result.is_ok());
// Should still work after clearing cache
let result3 = i18n.get_string(normalized_key.borrow());
assert!(result3.is_ok());
assert_eq!(result3.unwrap(), "Test Value");
}
#[test]
fn test_ftl_caching() {
let mut i18n = Localization::default();
// First call should load and cache the FTL content
let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result1.is_ok());
assert_eq!(result1.as_ref().unwrap(), "Test Value");
// Second call should use cached FTL content
let result2 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
// Test another key from the same FTL content
let result3 = i18n.get_string(IntlKeyBuf::new("another_key").borrow());
assert!(result3.is_ok());
assert_eq!(result3.unwrap(), "Another Value");
}
#[test]
fn test_bundle_caching() {
let mut i18n = Localization::default();
// First call should create bundle and cache the resource
let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result1.is_ok());
assert_eq!(result1.unwrap(), "Test Value");
// Second call should use cached resource but create new bundle
let result2 = i18n.get_string(IntlKeyBuf::new("another_key").borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Another Value");
// Check cache stats
let stats = i18n.get_cache_stats().unwrap();
assert_eq!(stats.resource_cache_size, 1);
assert_eq!(stats.string_cache_size, 2); // Both strings should be cached
}
#[test]
fn test_string_caching() {
let mut i18n = Localization::default();
let key = i18n.normalized_ftl_key("test_key", "comment");
// First call should format and cache the string
let result1 = i18n.get_string(key.borrow());
assert!(result1.is_ok());
assert_eq!(result1.unwrap(), "Test Value");
// Second call should use cached string
let result2 = i18n.get_string(key.borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
// Check cache stats
let stats = i18n.get_cache_stats().unwrap();
assert_eq!(stats.string_cache_size, 1);
}
#[test]
fn test_string_caching_with_arguments() {
let mut manager = Localization::default();
// First call with arguments should not be cached
let mut args = fluent::FluentArgs::new();
args.set("name", "Alice");
let key = IntlKeyBuf::new("welcome_message");
let result1 = manager
.get_cached_string(key.borrow(), Some(&args))
.unwrap();
assert!(result1.contains("Alice"));
// Check that it's not in the string cache
let stats1 = manager.get_cache_stats().unwrap();
assert_eq!(stats1.string_cache_size, 0);
// Second call with different arguments should work correctly
let mut args2 = fluent::FluentArgs::new();
args2.set("name", "Bob");
let result2 = manager.get_cached_string(key.borrow(), Some(&args2));
assert!(result2.is_ok());
let result2_str = result2.unwrap();
assert!(result2_str.contains("Bob"));
// Check that it's still not in the string cache
let stats2 = manager.get_cache_stats().unwrap();
assert_eq!(stats2.string_cache_size, 0);
// Clear cache to start fresh
manager.clear_cache().unwrap();
let result3 = manager.get_string(key.borrow());
assert!(result3.is_ok());
assert_eq!(result3.unwrap(), "Hello World");
// Check that simple string is cached
let stats3 = manager.get_cache_stats().unwrap();
assert_eq!(stats3.string_cache_size, 1);
}
#[test]
fn test_cache_clearing_on_locale_change() {
let mut i18n = Localization::default();
// Check that caches are populated
let stats1 = i18n.get_cache_stats().unwrap();
assert!(stats1.resource_cache_size > 0);
assert!(stats1.string_cache_size > 0);
// Switch to en-XA
let en_xa: LanguageIdentifier = langid!("en-XA");
i18n.set_locale(en_xa).unwrap();
// Check that string cache is cleared (resource cache remains for both locales)
let stats2 = i18n.get_cache_stats().unwrap();
assert_eq!(stats2.string_cache_size, 0);
}
*/
}
/// Replace each invalid character with exactly one underscore
/// This matches the behavior of the Python extraction script
pub fn fixup_key(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' => out.push(ch),
_ => out.push('_'), // always push
}
}
let trimmed = out.trim_matches('_');
trimmed.to_owned()
}
fn simple_hash(s: &str) -> String {
let digest = md5::compute(s.as_bytes());
// Take the first 2 bytes and convert to 4 hex characters
format!("{:02x}{:02x}", digest[0], digest[1])
}

View File

@@ -1,107 +0,0 @@
//! Internationalization (i18n) module for Notedeck
//!
//! This module provides localization support using fluent and fluent-resmgr.
//! It handles loading translation files, managing locales, and providing
//! localized strings throughout the application.
mod error;
mod key;
pub mod manager;
pub use error::IntlError;
pub use key::{IntlKey, IntlKeyBuf};
pub use manager::CacheStats;
pub use manager::Localization;
/// Re-export commonly used types for convenience
pub use fluent::FluentArgs;
pub use fluent::FluentValue;
pub use unic_langid::LanguageIdentifier;
/// Macro for getting localized strings with format-like syntax
///
/// Syntax: tr!("message", comment)
/// tr!("message with {param}", comment, param="value")
/// tr!("message with {first} and {second}", comment, first="value1", second="value2")
///
/// The first argument is the source message (like format!).
/// The second argument is always the comment to provide context for translators.
/// If `{name}` placeholders are found, there must be corresponding named arguments after the comment.
/// All placeholders must be named and start with a letter (a-zA-Z).
#[macro_export]
macro_rules! tr {
($i18n:expr, $message:expr, $comment:expr) => {
{
let key = $i18n.normalized_ftl_key($message, $comment);
match $i18n.get_string(key.borrow()) {
Ok(r) => r,
Err(_err) => {
$message.to_string()
}
}
}
};
// Case with named parameters: message, comment, param=value, ...
($i18n:expr, $message:expr, $comment:expr, $($param:ident = $value:expr),*) => {
{
let key = $i18n.normalized_ftl_key($message, $comment);
let mut args = $crate::i18n::FluentArgs::new();
$(
args.set(stringify!($param), $value);
)*
match $i18n.get_cached_string(key.borrow(), Some(&args)) {
Ok(r) => r,
Err(_) => {
// Fallback: replace placeholders with values
let mut result = $message.to_string();
$(
result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());
)*
result
}
}
}
};
}
/// Macro for getting localized pluralized strings with count and named arguments
///
/// Syntax: tr_plural!(one, other, comment, count, param1=..., param2=...)
/// - one: Message for the singular ("one") plural rule
/// - other: Message for the "other" plural rule
/// - comment: Context for translators
/// - count: The count value
/// - named arguments: Any additional named parameters for interpolation
#[macro_export]
macro_rules! tr_plural {
// With named parameters
($i18n:expr, $one:expr, $other:expr, $comment:expr, $count:expr, $($param:ident = $value:expr),*) => {{
let norm_key = $i18n.normalized_ftl_key($other, $comment);
let mut args = $crate::i18n::FluentArgs::new();
args.set("count", $count);
$(args.set(stringify!($param), $value);)*
match $i18n.get_cached_string(norm_key.borrow(), Some(&args)) {
Ok(s) => s,
Err(_) => {
// Fallback: use simple pluralization
if $count == 1 {
let mut result = $one.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
} else {
let mut result = $other.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
}
}
}
}};
// Without named parameters
($one:expr, $other:expr, $comment:expr, $count:expr) => {{
$crate::tr_plural!($one, $other, $comment, $count, )
}};
}

View File

@@ -1,573 +0,0 @@
use crate::media::gif::ensure_latest_texture_from_cache;
use crate::media::images::ImageType;
use crate::media::AnimationMode;
use crate::urls::{UrlCache, UrlMimes};
use crate::ImageMetadata;
use crate::ObfuscationType;
use crate::RenderableMedia;
use crate::Result;
use egui::TextureHandle;
use image::{Delay, Frame};
use poll_promise::Promise;
use egui::ColorImage;
use std::collections::HashMap;
use std::fs::{self, create_dir_all, File};
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime};
use std::{io, thread};
use hex::ToHex;
use sha2::Digest;
use std::path::PathBuf;
use std::path::{self, Path};
use tracing::warn;
#[derive(Default)]
pub struct TexturesCache {
pub cache: hashbrown::HashMap<String, TextureStateInternal>,
}
impl TexturesCache {
pub fn handle_and_get_or_insert_loadable(
&mut self,
url: &str,
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
) -> LoadableTextureState<'_> {
let internal = self.handle_and_get_state_internal(url, true, closure);
internal.into()
}
pub fn handle_and_get_or_insert(
&mut self,
url: &str,
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
) -> TextureState<'_> {
let internal = self.handle_and_get_state_internal(url, false, closure);
internal.into()
}
fn handle_and_get_state_internal(
&mut self,
url: &str,
use_loading: bool,
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
) -> &mut TextureStateInternal {
let state = match self.cache.raw_entry_mut().from_key(url) {
hashbrown::hash_map::RawEntryMut::Occupied(entry) => {
let state = entry.into_mut();
handle_occupied(state, use_loading);
state
}
hashbrown::hash_map::RawEntryMut::Vacant(entry) => {
let res = closure();
let (_, state) = entry.insert(url.to_owned(), TextureStateInternal::Pending(res));
state
}
};
state
}
pub fn insert_pending(&mut self, url: &str, promise: Promise<Option<Result<TexturedImage>>>) {
self.cache
.insert(url.to_owned(), TextureStateInternal::Pending(promise));
}
pub fn move_to_loaded(&mut self, url: &str) {
let hashbrown::hash_map::RawEntryMut::Occupied(entry) =
self.cache.raw_entry_mut().from_key(url)
else {
return;
};
entry.replace_entry_with(|_, v| {
let TextureStateInternal::Loading(textured) = v else {
return None;
};
Some(TextureStateInternal::Loaded(textured))
});
}
pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState<'_>> {
self.cache.get_mut(url).map(|state| {
handle_occupied(state, true);
state.into()
})
}
}
fn handle_occupied(state: &mut TextureStateInternal, use_loading: bool) {
let TextureStateInternal::Pending(promise) = state else {
return;
};
let Some(res) = promise.ready_mut() else {
return;
};
let Some(res) = res.take() else {
tracing::error!("Failed to take the promise");
*state =
TextureStateInternal::Error(crate::Error::Generic("Promise already taken".to_owned()));
return;
};
match res {
Ok(textured) => {
*state = if use_loading {
TextureStateInternal::Loading(textured)
} else {
TextureStateInternal::Loaded(textured)
}
}
Err(e) => *state = TextureStateInternal::Error(e),
}
}
pub enum LoadableTextureState<'a> {
Pending,
Error(&'a crate::Error),
Loading {
actual_image_tex: &'a mut TexturedImage,
}, // the texture is in the loading state, for transitioning between the pending and loaded states
Loaded(&'a mut TexturedImage),
}
pub enum TextureState<'a> {
Pending,
Error(&'a crate::Error),
Loaded(&'a mut TexturedImage),
}
impl<'a> TextureState<'a> {
pub fn is_loaded(&self) -> bool {
matches!(self, Self::Loaded(_))
}
}
impl<'a> From<&'a mut TextureStateInternal> for TextureState<'a> {
fn from(value: &'a mut TextureStateInternal) -> Self {
match value {
TextureStateInternal::Pending(_) => TextureState::Pending,
TextureStateInternal::Error(error) => TextureState::Error(error),
TextureStateInternal::Loading(textured_image) => TextureState::Loaded(textured_image),
TextureStateInternal::Loaded(textured_image) => TextureState::Loaded(textured_image),
}
}
}
pub enum TextureStateInternal {
Pending(Promise<Option<Result<TexturedImage>>>),
Error(crate::Error),
Loading(TexturedImage), // the image is in the loading state, for transitioning between blur and image
Loaded(TexturedImage),
}
impl<'a> From<&'a mut TextureStateInternal> for LoadableTextureState<'a> {
fn from(value: &'a mut TextureStateInternal) -> Self {
match value {
TextureStateInternal::Pending(_) => LoadableTextureState::Pending,
TextureStateInternal::Error(error) => LoadableTextureState::Error(error),
TextureStateInternal::Loading(textured_image) => LoadableTextureState::Loading {
actual_image_tex: textured_image,
},
TextureStateInternal::Loaded(textured_image) => {
LoadableTextureState::Loaded(textured_image)
}
}
}
}
pub enum TexturedImage {
Static(TextureHandle),
Animated(Animation),
}
impl TexturedImage {
pub fn get_first_texture(&self) -> &TextureHandle {
match self {
TexturedImage::Static(texture_handle) => texture_handle,
TexturedImage::Animated(animation) => &animation.first_frame.texture,
}
}
}
pub struct Animation {
pub first_frame: TextureFrame,
pub other_frames: Vec<TextureFrame>,
pub receiver: Option<Receiver<TextureFrame>>,
}
impl Animation {
pub fn get_frame(&self, index: usize) -> Option<&TextureFrame> {
if index == 0 {
Some(&self.first_frame)
} else {
self.other_frames.get(index - 1)
}
}
pub fn num_frames(&self) -> usize {
self.other_frames.len() + 1
}
}
pub struct TextureFrame {
pub delay: Duration,
pub texture: TextureHandle,
}
pub struct ImageFrame {
pub delay: Duration,
pub image: ColorImage,
}
pub struct MediaCache {
pub cache_dir: path::PathBuf,
pub textures_cache: TexturesCache,
pub cache_type: MediaCacheType,
pub cache_size: Arc<Mutex<Option<u64>>>,
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum MediaCacheType {
Image,
Gif,
}
impl MediaCache {
pub fn new(parent_dir: &Path, cache_type: MediaCacheType) -> Self {
let cache_dir = parent_dir.join(Self::rel_dir(cache_type));
let cache_dir_clone = cache_dir.clone();
let cache_size = Arc::new(Mutex::new(None));
let cache_size_clone = Arc::clone(&cache_size);
thread::spawn(move || {
let mut last_checked = Instant::now() - Duration::from_secs(999);
loop {
// check cache folder size every 60 s
if last_checked.elapsed() >= Duration::from_secs(60) {
let size = compute_folder_size(&cache_dir_clone);
*cache_size_clone.lock().unwrap() = Some(size);
last_checked = Instant::now();
}
thread::sleep(Duration::from_secs(5));
}
});
Self {
cache_dir,
textures_cache: TexturesCache::default(),
cache_type,
cache_size,
}
}
pub fn rel_dir(cache_type: MediaCacheType) -> &'static str {
match cache_type {
MediaCacheType::Image => "img",
MediaCacheType::Gif => "gif",
}
}
pub fn write(cache_dir: &path::Path, url: &str, data: ColorImage) -> Result<()> {
let file = Self::create_file(cache_dir, url)?;
let encoder = image::codecs::webp::WebPEncoder::new_lossless(file);
encoder.encode(
data.as_raw(),
data.size[0] as u32,
data.size[1] as u32,
image::ColorType::Rgba8.into(),
)?;
Ok(())
}
fn create_file(cache_dir: &path::Path, url: &str) -> Result<File> {
let file_path = cache_dir.join(Self::key(url));
if let Some(p) = file_path.parent() {
create_dir_all(p)?;
}
Ok(File::options()
.write(true)
.create(true)
.truncate(true)
.open(file_path)?)
}
pub fn write_gif(cache_dir: &path::Path, url: &str, data: Vec<ImageFrame>) -> Result<()> {
let file = Self::create_file(cache_dir, url)?;
let mut encoder = image::codecs::gif::GifEncoder::new(file);
for img in data {
let buf = color_image_to_rgba(img.image);
let frame = Frame::from_parts(buf, 0, 0, Delay::from_saturating_duration(img.delay));
if let Err(e) = encoder.encode_frame(frame) {
tracing::error!("problem encoding frame: {e}");
}
}
Ok(())
}
pub fn key(url: &str) -> String {
let k: String = sha2::Sha256::digest(url.as_bytes()).encode_hex();
PathBuf::from(&k[0..2])
.join(&k[2..4])
.join(k)
.to_string_lossy()
.to_string()
}
/// Migrate from base32 encoded url to sha256 url + sub-dir structure
pub fn migrate_v0(&self) -> Result<()> {
for file in std::fs::read_dir(&self.cache_dir)? {
let file = if let Ok(f) = file {
f
} else {
// not sure how this could fail, skip entry
continue;
};
if !file.path().is_file() {
continue;
}
let old_filename = file.file_name().to_string_lossy().to_string();
let old_url = if let Some(u) =
base32::decode(base32::Alphabet::Crockford, &old_filename)
.and_then(|s| String::from_utf8(s).ok())
{
u
} else {
warn!("Invalid base32 filename: {}", &old_filename);
continue;
};
let new_path = self.cache_dir.join(Self::key(&old_url));
if let Some(p) = new_path.parent() {
create_dir_all(p)?;
}
if let Err(e) = std::fs::rename(file.path(), &new_path) {
warn!(
"Failed to migrate file from {} to {}: {:?}",
file.path().display(),
new_path.display(),
e
);
}
}
Ok(())
}
fn clear(&mut self) {
self.textures_cache.cache.clear();
*self.cache_size.try_lock().unwrap() = Some(0);
}
}
fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
let width = color_image.width() as u32;
let height = color_image.height() as u32;
let rgba_pixels: Vec<u8> = color_image
.pixels
.iter()
.flat_map(|color| color.to_array()) // Convert Color32 to `[u8; 4]`
.collect();
image::RgbaImage::from_raw(width, height, rgba_pixels)
.expect("Failed to create RgbaImage from ColorImage")
}
fn compute_folder_size<P: AsRef<Path>>(path: P) -> u64 {
fn walk(path: &Path) -> u64 {
let mut size = 0;
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if let Ok(metadata) = entry.metadata() {
if metadata.is_file() {
size += metadata.len();
} else if metadata.is_dir() {
size += walk(&path);
}
}
}
}
size
}
walk(path.as_ref())
}
pub struct Images {
pub base_path: path::PathBuf,
pub static_imgs: MediaCache,
pub gifs: MediaCache,
pub urls: UrlMimes,
/// cached imeta data
pub metadata: HashMap<String, ImageMetadata>,
pub gif_states: GifStateMap,
}
impl Images {
/// path to directory to place [`MediaCache`]s
pub fn new(path: path::PathBuf) -> Self {
Self {
base_path: path.clone(),
static_imgs: MediaCache::new(&path, MediaCacheType::Image),
gifs: MediaCache::new(&path, MediaCacheType::Gif),
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
gif_states: Default::default(),
metadata: Default::default(),
}
}
pub fn migrate_v0(&self) -> Result<()> {
self.static_imgs.migrate_v0()?;
self.gifs.migrate_v0()
}
pub fn get_renderable_media(&mut self, url: &str) -> Option<RenderableMedia> {
Self::find_renderable_media(&mut self.urls, &self.metadata, url)
}
pub fn find_renderable_media(
urls: &mut UrlMimes,
imeta: &HashMap<String, ImageMetadata>,
url: &str,
) -> Option<RenderableMedia> {
let media_type = crate::urls::supported_mime_hosted_at_url(urls, url)?;
let obfuscation_type = match imeta.get(url) {
Some(blur) => ObfuscationType::Blurhash(blur.clone()),
None => ObfuscationType::Default,
};
Some(RenderableMedia {
url: url.to_string(),
media_type,
obfuscation_type,
})
}
pub fn latest_texture(
&mut self,
ui: &mut egui::Ui,
url: &str,
img_type: ImageType,
animation_mode: AnimationMode,
) -> Option<TextureHandle> {
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
let cache_dir = self.get_cache(cache_type).cache_dir.clone();
let is_loaded = self
.get_cache_mut(cache_type)
.textures_cache
.handle_and_get_or_insert(url, || {
crate::media::images::fetch_img(&cache_dir, ui.ctx(), url, img_type, cache_type)
})
.is_loaded();
if !is_loaded {
return None;
}
let cache = match cache_type {
MediaCacheType::Image => &mut self.static_imgs,
MediaCacheType::Gif => &mut self.gifs,
};
ensure_latest_texture_from_cache(
ui,
url,
&mut self.gif_states,
&mut cache.textures_cache,
animation_mode,
)
}
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
match cache_type {
MediaCacheType::Image => &self.static_imgs,
MediaCacheType::Gif => &self.gifs,
}
}
pub fn get_cache_mut(&mut self, cache_type: MediaCacheType) -> &mut MediaCache {
match cache_type {
MediaCacheType::Image => &mut self.static_imgs,
MediaCacheType::Gif => &mut self.gifs,
}
}
pub fn clear_folder_contents(&mut self) -> io::Result<()> {
for entry in fs::read_dir(self.base_path.clone())? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
fs::remove_dir_all(path)?;
} else {
fs::remove_file(path)?;
}
}
self.urls.cache.clear();
self.static_imgs.clear();
self.gifs.clear();
self.gif_states.clear();
Ok(())
}
}
pub type GifStateMap = HashMap<String, GifState>;
pub struct GifState {
pub last_frame_rendered: Instant,
pub last_frame_duration: Duration,
pub next_frame_time: Option<SystemTime>,
pub last_frame_index: usize,
}
pub struct LatestTexture {
pub texture: TextureHandle,
pub request_next_repaint: Option<SystemTime>,
}
pub fn get_render_state<'a>(
ctx: &egui::Context,
images: &'a mut Images,
cache_type: MediaCacheType,
url: &str,
img_type: ImageType,
) -> RenderState<'a> {
let cache = match cache_type {
MediaCacheType::Image => &mut images.static_imgs,
MediaCacheType::Gif => &mut images.gifs,
};
let texture_state = cache.textures_cache.handle_and_get_or_insert(url, || {
crate::media::images::fetch_img(&cache.cache_dir, ctx, url, img_type, cache_type)
});
RenderState {
texture_state,
gifs: &mut images.gif_states,
}
}
pub struct RenderState<'a> {
pub texture_state: TextureState<'a>,
pub gifs: &'a mut GifStateMap,
}

View File

@@ -1,100 +0,0 @@
use std::{
future::Future,
sync::{
mpsc::{self, Sender},
Arc, Mutex,
},
};
use tokio::sync::oneshot;
type Job = Box<dyn FnOnce() + Send + 'static>;
pub struct JobPool {
tx: Sender<Job>,
}
impl Default for JobPool {
fn default() -> Self {
JobPool::new(2)
}
}
impl JobPool {
pub fn new(num_threads: usize) -> Self {
let (tx, rx) = mpsc::channel::<Job>();
// TODO(jb55) why not mpmc here !???
let arc_rx = Arc::new(Mutex::new(rx));
for _ in 0..num_threads {
let arc_rx_clone = arc_rx.clone();
std::thread::spawn(move || loop {
let job = {
let Ok(unlocked) = arc_rx_clone.lock() else {
continue;
};
let Ok(job) = unlocked.recv() else {
continue;
};
job
};
job();
});
}
Self { tx }
}
pub fn schedule<F, T>(&self, job: F) -> impl Future<Output = T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let (tx_result, rx_result) = oneshot::channel::<T>();
let job = Box::new(move || {
let output = job();
let _ = tx_result.send(output);
});
self.tx
.send(job)
.expect("receiver should not be deallocated");
async move {
rx_result.await.unwrap_or_else(|_| {
panic!("Worker thread or channel dropped before returning the result.")
})
}
}
}
#[cfg(test)]
mod tests {
use crate::job_pool::JobPool;
fn test_fn(a: u32, b: u32) -> u32 {
a + b
}
#[tokio::test]
async fn test() {
let pool = JobPool::default();
// Now each job can return different T
let future_str = pool.schedule(|| -> String { "hello from string job".into() });
let a = 5;
let b = 6;
let future_int = pool.schedule(move || -> u32 { test_fn(a, b) });
println!("(Meanwhile we can do more async work) ...");
let s = future_str.await;
let i = future_int.await;
println!("Got string: {:?}", s);
println!("Got integer: {}", i);
}
}

View File

@@ -1,153 +0,0 @@
use crate::JobPool;
use egui::TextureHandle;
use hashbrown::{hash_map::RawEntryMut, HashMap};
use poll_promise::Promise;
#[derive(Default)]
pub struct JobsCache {
jobs: HashMap<JobIdOwned, JobState>,
}
pub enum JobState {
Pending(Promise<Option<Result<Job, JobError>>>),
Error(JobError),
Completed(Job),
}
pub enum JobError {
InvalidParameters,
}
#[derive(Debug)]
pub enum JobParams<'a> {
Blurhash(BlurhashParams<'a>),
}
#[derive(Debug)]
pub enum JobParamsOwned {
Blurhash(BlurhashParamsOwned),
}
impl<'a> From<BlurhashParams<'a>> for BlurhashParamsOwned {
fn from(params: BlurhashParams<'a>) -> Self {
BlurhashParamsOwned {
blurhash: params.blurhash.to_owned(),
url: params.url.to_owned(),
ctx: params.ctx.clone(),
}
}
}
impl<'a> From<JobParams<'a>> for JobParamsOwned {
fn from(params: JobParams<'a>) -> Self {
match params {
JobParams::Blurhash(bp) => JobParamsOwned::Blurhash(bp.into()),
}
}
}
#[derive(Debug)]
pub struct BlurhashParams<'a> {
pub blurhash: &'a str,
pub url: &'a str,
pub ctx: &'a egui::Context,
}
#[derive(Debug)]
pub struct BlurhashParamsOwned {
pub blurhash: String,
pub url: String,
pub ctx: egui::Context,
}
impl JobsCache {
pub fn get_or_insert_with<
'a,
F: FnOnce(Option<JobParamsOwned>) -> Result<Job, JobError> + Send + 'static,
>(
&'a mut self,
job_pool: &mut JobPool,
jobid: &JobId,
params: Option<JobParams>,
run_job: F,
) -> &'a mut JobState {
match self.jobs.raw_entry_mut().from_key(jobid) {
RawEntryMut::Occupied(entry) => 's: {
let mut state = entry.into_mut();
let JobState::Pending(promise) = &mut state else {
break 's state;
};
let Some(res) = promise.ready_mut() else {
break 's state;
};
let Some(res) = res.take() else {
tracing::error!("Failed to take the promise for job: {:?}", jobid);
break 's state;
};
*state = match res {
Ok(j) => JobState::Completed(j),
Err(e) => JobState::Error(e),
};
state
}
RawEntryMut::Vacant(entry) => {
let owned_params = params.map(JobParams::into);
let wrapped: Box<dyn FnOnce() -> Option<Result<Job, JobError>> + Send + 'static> =
Box::new(move || Some(run_job(owned_params)));
let promise = Promise::spawn_async(job_pool.schedule(wrapped));
let (_, state) = entry.insert(jobid.into(), JobState::Pending(promise));
state
}
}
}
pub fn get(&self, jobid: &JobId) -> Option<&JobState> {
self.jobs.get(jobid)
}
}
impl<'a> From<&JobId<'a>> for JobIdOwned {
fn from(jobid: &JobId<'a>) -> Self {
match jobid {
JobId::Blurhash(s) => JobIdOwned::Blurhash(s.to_string()),
}
}
}
impl hashbrown::Equivalent<JobIdOwned> for JobId<'_> {
fn equivalent(&self, key: &JobIdOwned) -> bool {
match (self, key) {
(JobId::Blurhash(a), JobIdOwned::Blurhash(b)) => *a == b.as_str(),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
enum JobIdOwned {
Blurhash(String), // image URL
}
#[derive(Debug, Hash)]
pub enum JobId<'a> {
Blurhash(&'a str), // image URL
}
pub enum Job {
Blurhash(Option<TextureHandle>),
}
impl std::fmt::Debug for Job {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Job::Blurhash(_) => write!(f, "Blurhash"),
}
}
}

View File

@@ -1,103 +0,0 @@
pub mod abbrev;
mod account;
mod app;
mod args;
pub mod contacts;
mod context;
pub mod debouncer;
mod error;
pub mod filter;
pub mod fonts;
mod frame_history;
pub mod i18n;
mod imgcache;
mod job_pool;
mod jobs;
pub mod media;
mod muted;
pub mod name;
mod nip51_set;
pub mod note;
mod notecache;
mod options;
mod persist;
pub mod platform;
pub mod profile;
pub mod relay_debug;
pub mod relayspec;
mod result;
mod setup;
pub mod storage;
mod style;
pub mod theme;
mod time;
mod timecache;
mod timed_serializer;
pub mod ui;
mod unknowns;
mod urls;
mod user_account;
mod wallet;
mod zaps;
pub use account::accounts::{AccountData, AccountSubs, Accounts};
pub use account::contacts::{ContactState, IsFollowing};
pub use account::relay::RelayAction;
pub use account::FALLBACK_PUBKEY;
pub use app::{App, AppAction, Notedeck};
pub use args::Args;
pub use context::{AppContext, SoftKeyboardContext};
pub use error::{show_one_error_message, Error, FilterError, ZapError};
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
pub use fonts::NamedFontFamily;
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
pub use imgcache::{
get_render_state, Animation, GifState, GifStateMap, ImageFrame, Images, LatestTexture,
LoadableTextureState, MediaCache, MediaCacheType, RenderState, TextureFrame, TextureState,
TexturedImage, TexturesCache,
};
pub use job_pool::JobPool;
pub use jobs::{
BlurhashParams, Job, JobError, JobId, JobParams, JobParamsOwned, JobState, JobsCache,
};
pub use media::{
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ImageType, MediaAction,
ObfuscationType, PixelDimensions, PointDimensions, RenderableMedia,
};
pub use muted::{MuteFun, Muted};
pub use name::NostrName;
pub use nip51_set::{create_nip51_set, Nip51Set, Nip51SetCache};
pub use note::{
BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef,
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
};
pub use notecache::{CachedNote, NoteCache};
pub use options::NotedeckOptions;
pub use persist::*;
pub use profile::get_profile_url;
pub use relay_debug::RelayDebugView;
pub use relayspec::RelaySpec;
pub use result::Result;
pub use storage::{AccountStorage, DataPath, DataPathType, Directory};
pub use style::NotedeckTextStyle;
pub use theme::ColorTheme;
pub use time::time_ago_since;
pub use time::time_format;
pub use timecache::TimeCached;
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
pub use user_account::UserAccount;
pub use wallet::{
get_current_wallet, get_current_wallet_mut, get_wallet_for, GlobalWallet, Wallet, WalletError,
WalletType, WalletUIState, ZapWallet,
};
pub use zaps::{
get_current_default_msats, AnyZapState, DefaultZapError, DefaultZapMsats, NoteZapTarget,
NoteZapTargetOwned, PendingDefaultZapState, ZapTarget, ZapTargetOwned, ZappingError,
};
// export libs
pub use enostr;
pub use nostrdb;
pub use zaps::Zaps;

View File

@@ -1,127 +0,0 @@
use crate::{Images, MediaCacheType, TexturedImage};
use poll_promise::Promise;
/// Tracks where media was on the screen so that
/// we can do fun animations when opening the
/// Media Viewer
#[derive(Debug, Clone)]
pub struct MediaInfo {
/// The original screen position where it
/// was rendered from. This is not where
/// it should be rendered in the scene.
pub original_position: egui::Rect,
pub url: String,
}
/// Contains various information for when a user
/// clicks a piece of media. It contains the current
/// location on screen for each piece of media.
///
/// Viewers can use this to smoothly transition from
/// the timeline to the viewer
#[derive(Debug, Clone, Default)]
pub struct ViewMediaInfo {
pub clicked_index: usize,
pub medias: Vec<MediaInfo>,
}
impl ViewMediaInfo {
pub fn clicked_media(&self) -> &MediaInfo {
&self.medias[self.clicked_index]
}
}
/// Actions generated by media ui interactions
pub enum MediaAction {
/// An image was clicked on in a carousel, we have
/// the opportunity to open into a fullscreen media viewer
/// with a list of url values
ViewMedias(ViewMediaInfo),
FetchImage {
url: String,
cache_type: MediaCacheType,
no_pfp_promise: Promise<Option<Result<TexturedImage, crate::Error>>>,
},
DoneLoading {
url: String,
cache_type: MediaCacheType,
},
}
impl std::fmt::Debug for MediaAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ViewMedias(ViewMediaInfo {
clicked_index,
medias,
}) => f
.debug_struct("ViewMedias")
.field("clicked_index", clicked_index)
.field("media", medias)
.finish(),
Self::FetchImage {
url,
cache_type,
no_pfp_promise,
} => f
.debug_struct("FetchNoPfpImage")
.field("url", url)
.field("cache_type", cache_type)
.field("no_pfp_promise ready", &no_pfp_promise.ready().is_some())
.finish(),
Self::DoneLoading { url, cache_type } => f
.debug_struct("DoneLoading")
.field("url", url)
.field("cache_type", cache_type)
.finish(),
}
}
}
impl MediaAction {
/// Handle view media actions
pub fn on_view_media(&self, handler: impl FnOnce(&ViewMediaInfo)) {
if let MediaAction::ViewMedias(view_medias) = self {
handler(view_medias)
}
}
/// Default processing logic for Media Actions. We don't handle ViewMedias here since
/// this may be app specific ?
pub fn process_default_media_actions(self, images: &mut Images) {
match self {
MediaAction::ViewMedias(_urls) => {
// NOTE(jb55): don't assume we want to show a fullscreen
// media viewer we can use on_view_media for that. We
// also don't want to have a notedeck_ui dependency in
// the notedeck lib (MediaViewerState)
//
// In general our notedeck crate should be pretty
// agnostic to functionallity in general unless it low
// level like image rendering.
//
//mview_state.set_urls(urls);
}
MediaAction::FetchImage {
url,
cache_type,
no_pfp_promise: promise,
} => {
images
.get_cache_mut(cache_type)
.textures_cache
.insert_pending(&url, promise);
}
MediaAction::DoneLoading { url, cache_type } => {
let cache = match cache_type {
MediaCacheType::Image => &mut images.static_imgs,
MediaCacheType::Gif => &mut images.gifs,
};
cache.textures_cache.move_to_loaded(&url);
}
}
}
}

View File

@@ -1,191 +0,0 @@
use std::collections::HashMap;
use nostrdb::Note;
use crate::jobs::{Job, JobError, JobParamsOwned};
#[derive(Clone)]
pub struct ImageMetadata {
pub blurhash: String,
pub dimensions: Option<PixelDimensions>, // width and height in pixels
}
#[derive(Clone, Debug)]
pub struct PixelDimensions {
pub x: u32,
pub y: u32,
}
impl PixelDimensions {
pub fn to_points(&self, ppp: f32) -> PointDimensions {
PointDimensions {
x: (self.x as f32) / ppp,
y: (self.y as f32) / ppp,
}
}
}
#[derive(Clone, Debug)]
pub struct PointDimensions {
pub x: f32,
pub y: f32,
}
impl PointDimensions {
pub fn to_pixels(self, ui: &egui::Ui) -> PixelDimensions {
PixelDimensions {
x: (self.x * ui.pixels_per_point()).round() as u32,
y: (self.y * ui.pixels_per_point()).round() as u32,
}
}
pub fn to_vec(self) -> egui::Vec2 {
egui::Vec2::new(self.x, self.y)
}
}
impl ImageMetadata {
pub fn scaled_pixel_dimensions(
&self,
ui: &egui::Ui,
available_points: PointDimensions,
) -> PixelDimensions {
let max_pixels = available_points.to_pixels(ui);
let Some(defined_dimensions) = &self.dimensions else {
return max_pixels;
};
if defined_dimensions.x == 0 || defined_dimensions.y == 0 {
tracing::error!("The blur dimensions should not be zero");
return max_pixels;
}
if defined_dimensions.y <= max_pixels.y {
return defined_dimensions.clone();
}
let scale_factor = (max_pixels.y as f32) / (defined_dimensions.y as f32);
let max_width_scaled = scale_factor * (defined_dimensions.x as f32);
PixelDimensions {
x: (max_width_scaled.round() as u32),
y: max_pixels.y,
}
}
}
/// Find blurhashes in image metadata and update our cache
pub fn update_imeta_blurhashes(note: &Note, blurs: &mut HashMap<String, ImageMetadata>) {
for tag in note.tags() {
let mut tag_iter = tag.into_iter();
if tag_iter
.next()
.and_then(|s| s.str())
.filter(|s| *s == "imeta")
.is_none()
{
continue;
}
let Some((url, blur)) = find_blur(tag_iter) else {
continue;
};
blurs.insert(url.to_string(), blur);
}
}
fn find_blur(tag_iter: nostrdb::TagIter<'_>) -> Option<(String, ImageMetadata)> {
let mut url = None;
let mut blurhash = None;
let mut dims = None;
for tag_elem in tag_iter {
let Some(s) = tag_elem.str() else { continue };
let mut split = s.split_whitespace();
let Some(first) = split.next() else { continue };
let Some(second) = split.next() else { continue };
match first {
"url" => url = Some(second),
"blurhash" => blurhash = Some(second),
"dim" => dims = Some(second),
_ => {}
}
if url.is_some() && blurhash.is_some() && dims.is_some() {
break;
}
}
let url = url?;
let blurhash = blurhash?;
let dimensions = dims.and_then(|d| {
let mut split = d.split('x');
let width = split.next()?.parse::<u32>().ok()?;
let height = split.next()?.parse::<u32>().ok()?;
Some(PixelDimensions {
x: width,
y: height,
})
});
Some((
url.to_string(),
ImageMetadata {
blurhash: blurhash.to_string(),
dimensions,
},
))
}
#[derive(Clone)]
pub enum ObfuscationType {
Blurhash(ImageMetadata),
Default,
}
pub fn compute_blurhash(
params: Option<JobParamsOwned>,
dims: PixelDimensions,
) -> Result<Job, JobError> {
#[allow(irrefutable_let_patterns)]
let Some(JobParamsOwned::Blurhash(params)) = params
else {
return Err(JobError::InvalidParameters);
};
let maybe_handle = match generate_blurhash_texturehandle(
&params.ctx,
&params.blurhash,
&params.url,
dims.x,
dims.y,
) {
Ok(tex) => Some(tex),
Err(e) => {
tracing::error!("failed to render blurhash: {e}");
None
}
};
Ok(Job::Blurhash(maybe_handle))
}
fn generate_blurhash_texturehandle(
ctx: &egui::Context,
blurhash: &str,
url: &str,
width: u32,
height: u32,
) -> Result<egui::TextureHandle, crate::Error> {
let bytes = blurhash::decode(blurhash, width, height, 1.0)
.map_err(|e| crate::Error::Generic(e.to_string()))?;
let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes);
Ok(ctx.load_texture(url, img, Default::default()))
}

View File

@@ -1,164 +0,0 @@
use std::{
sync::mpsc::TryRecvError,
time::{Instant, SystemTime},
};
use crate::media::AnimationMode;
use crate::Animation;
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
use egui::TextureHandle;
use std::time::Duration;
pub fn ensure_latest_texture_from_cache(
ui: &egui::Ui,
url: &str,
gifs: &mut GifStateMap,
textures: &mut TexturesCache,
animation_mode: AnimationMode,
) -> Option<TextureHandle> {
let tstate = textures.cache.get_mut(url)?;
let TextureState::Loaded(img) = tstate.into() else {
return None;
};
Some(ensure_latest_texture(ui, url, gifs, img, animation_mode))
}
struct ProcessedGifFrame {
texture: TextureHandle,
maybe_new_state: Option<GifState>,
repaint_at: Option<SystemTime>,
}
/// Process a gif state frame, and optionally present a new
/// state and when to repaint it
fn process_gif_frame(
animation: &Animation,
frame_state: Option<&GifState>,
animation_mode: AnimationMode,
) -> ProcessedGifFrame {
let now = Instant::now();
match frame_state {
Some(prev_state) => {
let should_advance = animation_mode.can_animate()
&& (now - prev_state.last_frame_rendered >= prev_state.last_frame_duration);
if should_advance {
let maybe_new_index = if animation.receiver.is_some()
|| prev_state.last_frame_index < animation.num_frames() - 1
{
prev_state.last_frame_index + 1
} else {
0
};
match animation.get_frame(maybe_new_index) {
Some(frame) => {
let next_frame_time = match animation_mode {
AnimationMode::Continuous { fps } => match fps {
Some(fps) => {
let max_delay_ms = Duration::from_millis((1000.0 / fps) as u64);
SystemTime::now().checked_add(frame.delay.max(max_delay_ms))
}
None => SystemTime::now().checked_add(frame.delay),
},
AnimationMode::NoAnimation | AnimationMode::Reactive => None,
};
ProcessedGifFrame {
texture: frame.texture.clone(),
maybe_new_state: Some(GifState {
last_frame_rendered: now,
last_frame_duration: frame.delay,
next_frame_time,
last_frame_index: maybe_new_index,
}),
repaint_at: next_frame_time,
}
}
None => {
let (texture, maybe_new_state) =
match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (frame.texture.clone(), None),
None => (animation.first_frame.texture.clone(), None),
};
ProcessedGifFrame {
texture,
maybe_new_state,
repaint_at: prev_state.next_frame_time,
}
}
}
} else {
let (texture, maybe_new_state) =
match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (frame.texture.clone(), None),
None => (animation.first_frame.texture.clone(), None),
};
ProcessedGifFrame {
texture,
maybe_new_state,
repaint_at: prev_state.next_frame_time,
}
}
}
None => ProcessedGifFrame {
texture: animation.first_frame.texture.clone(),
maybe_new_state: Some(GifState {
last_frame_rendered: now,
last_frame_duration: animation.first_frame.delay,
next_frame_time: None,
last_frame_index: 0,
}),
repaint_at: None,
},
}
}
pub fn ensure_latest_texture(
ui: &egui::Ui,
url: &str,
gifs: &mut GifStateMap,
img: &mut TexturedImage,
animation_mode: AnimationMode,
) -> TextureHandle {
match img {
TexturedImage::Static(handle) => handle.clone(),
TexturedImage::Animated(animation) => {
if let Some(receiver) = &animation.receiver {
loop {
match receiver.try_recv() {
Ok(frame) => animation.other_frames.push(frame),
Err(TryRecvError::Empty) => {
break;
}
Err(TryRecvError::Disconnected) => {
animation.receiver = None;
break;
}
}
}
}
let next_state = process_gif_frame(animation, gifs.get(url), animation_mode);
if let Some(new_state) = next_state.maybe_new_state {
gifs.insert(url.to_owned(), new_state);
}
if let Some(repaint) = next_state.repaint_at {
tracing::trace!("requesting repaint for {url} after {repaint:?}");
if let Ok(dur) = repaint.duration_since(SystemTime::now()) {
ui.ctx().request_repaint_after(dur);
}
}
next_state.texture
}
}
}

View File

@@ -1,475 +0,0 @@
use crate::{Animation, ImageFrame, MediaCache, MediaCacheType, TextureFrame, TexturedImage};
use egui::{pos2, Color32, ColorImage, Context, Rect, Sense, SizeHint};
use image::codecs::gif::GifDecoder;
use image::imageops::FilterType;
use image::{AnimationDecoder, DynamicImage, FlatSamples, Frame};
use poll_promise::Promise;
use std::collections::VecDeque;
use std::io::Cursor;
use std::path::PathBuf;
use std::path::{self, Path};
use std::sync::mpsc;
use std::sync::mpsc::SyncSender;
use std::thread;
use std::time::Duration;
use tokio::fs;
// NOTE(jb55): chatgpt wrote this because I was too dumb to
pub fn aspect_fill(
ui: &mut egui::Ui,
sense: Sense,
texture_id: egui::TextureId,
aspect_ratio: f32,
) -> egui::Response {
let frame = ui.available_rect_before_wrap(); // Get the available frame space in the current layout
let frame_ratio = frame.width() / frame.height();
let (width, height) = if frame_ratio > aspect_ratio {
// Frame is wider than the content
(frame.width(), frame.width() / aspect_ratio)
} else {
// Frame is taller than the content
(frame.height() * aspect_ratio, frame.height())
};
let content_rect = Rect::from_min_size(
frame.min
+ egui::vec2(
(frame.width() - width) / 2.0,
(frame.height() - height) / 2.0,
),
egui::vec2(width, height),
);
// Set the clipping rectangle to the frame
//let clip_rect = ui.clip_rect(); // Preserve the original clipping rectangle
//ui.set_clip_rect(frame);
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
let (response, painter) = ui.allocate_painter(ui.available_size(), sense);
// Draw the texture within the calculated rect, potentially clipping it
painter.rect_filled(content_rect, 0.0, ui.ctx().style().visuals.window_fill());
painter.image(texture_id, content_rect, uv, Color32::WHITE);
// Restore the original clipping rectangle
//ui.set_clip_rect(clip_rect);
response
}
#[profiling::function]
pub fn round_image(image: &mut ColorImage) {
// The radius to the edge of of the avatar circle
let edge_radius = image.size[0] as f32 / 2.0;
let edge_radius_squared = edge_radius * edge_radius;
for (pixnum, pixel) in image.pixels.iter_mut().enumerate() {
// y coordinate
let uy = pixnum / image.size[0];
let y = uy as f32;
let y_offset = edge_radius - y;
// x coordinate
let ux = pixnum % image.size[0];
let x = ux as f32;
let x_offset = edge_radius - x;
// The radius to this pixel (may be inside or outside the circle)
let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset;
// If inside of the avatar circle
if pixel_radius_squared <= edge_radius_squared {
// squareroot to find how many pixels we are from the edge
let pixel_radius: f32 = pixel_radius_squared.sqrt();
let distance = edge_radius - pixel_radius;
// If we are within 1 pixel of the edge, we should fade, to
// antialias the edge of the circle. 1 pixel from the edge should
// be 100% of the original color, and right on the edge should be
// 0% of the original color.
if distance <= 1.0 {
*pixel = Color32::from_rgba_premultiplied(
(pixel.r() as f32 * distance) as u8,
(pixel.g() as f32 * distance) as u8,
(pixel.b() as f32 * distance) as u8,
(pixel.a() as f32 * distance) as u8,
);
}
} else {
// Outside of the avatar circle
*pixel = Color32::TRANSPARENT;
}
}
}
/// If the image's longest dimension is greater than max_edge, downscale
fn resize_image_if_too_big(
image: image::DynamicImage,
max_edge: u32,
filter: FilterType,
) -> image::DynamicImage {
// if we have no size hint, resize to something reasonable
let w = image.width();
let h = image.height();
let long = w.max(h);
if long > max_edge {
let scale = max_edge as f32 / long as f32;
let new_w = (w as f32 * scale).round() as u32;
let new_h = (h as f32 * scale).round() as u32;
image.resize(new_w, new_h, filter)
} else {
image
}
}
///
/// Process an image, resizing so we don't blow up video memory or even crash
///
/// For profile pictures, make them round and small to fit the size hint
/// For everything else, either:
///
/// - resize to the size hint
/// - keep the size if the longest dimension is less than MAX_IMG_LENGTH
/// - resize if any larger, using [`resize_image_if_too_big`]
///
#[profiling::function]
fn process_image(imgtyp: ImageType, mut image: image::DynamicImage) -> ColorImage {
const MAX_IMG_LENGTH: u32 = 2048;
const FILTER_TYPE: FilterType = FilterType::CatmullRom;
match imgtyp {
ImageType::Content(size_hint) => {
let image = match size_hint {
None => resize_image_if_too_big(image, MAX_IMG_LENGTH, FILTER_TYPE),
Some((w, h)) => image.resize(w, h, FILTER_TYPE),
};
let image_buffer = image.into_rgba8();
ColorImage::from_rgba_unmultiplied(
[
image_buffer.width() as usize,
image_buffer.height() as usize,
],
image_buffer.as_flat_samples().as_slice(),
)
}
ImageType::Profile(size) => {
// Crop square
let smaller = image.width().min(image.height());
if image.width() > smaller {
let excess = image.width() - smaller;
image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
} else if image.height() > smaller {
let excess = image.height() - smaller;
image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
}
let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
let mut color_image = ColorImage::from_rgba_unmultiplied(
[
image_buffer.width() as usize,
image_buffer.height() as usize,
],
image_buffer.as_flat_samples().as_slice(),
);
round_image(&mut color_image);
color_image
}
}
}
#[profiling::function]
fn parse_img_response(
response: ehttp::Response,
imgtyp: ImageType,
) -> Result<ColorImage, crate::Error> {
let content_type = response.content_type().unwrap_or_default();
let size_hint = match imgtyp {
ImageType::Profile(size) => SizeHint::Size(size, size),
ImageType::Content(Some((w, h))) => SizeHint::Size(w, h),
ImageType::Content(None) => SizeHint::default(),
};
if content_type.starts_with("image/svg") {
profiling::scope!("load_svg");
let mut color_image =
egui_extras::image::load_svg_bytes_with_size(&response.bytes, Some(size_hint))?;
round_image(&mut color_image);
Ok(color_image)
} else if content_type.starts_with("image/") {
profiling::scope!("load_from_memory");
let dyn_image = image::load_from_memory(&response.bytes)?;
Ok(process_image(imgtyp, dyn_image))
} else {
Err(format!("Expected image, found content-type {content_type:?}").into())
}
}
fn fetch_img_from_disk(
ctx: &egui::Context,
url: &str,
path: &path::Path,
cache_type: MediaCacheType,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
let ctx = ctx.clone();
let url = url.to_owned();
let path = path.to_owned();
Promise::spawn_async(async move {
Some(async_fetch_img_from_disk(ctx, url, &path, cache_type).await)
})
}
async fn async_fetch_img_from_disk(
ctx: egui::Context,
url: String,
path: &path::Path,
cache_type: MediaCacheType,
) -> Result<TexturedImage, crate::Error> {
match cache_type {
MediaCacheType::Image => {
let data = fs::read(path).await?;
let image_buffer = image::load_from_memory(&data).map_err(crate::Error::Image)?;
let img = buffer_to_color_image(
image_buffer.as_flat_samples_u8(),
image_buffer.width(),
image_buffer.height(),
);
Ok(TexturedImage::Static(ctx.load_texture(
&url,
img,
Default::default(),
)))
}
MediaCacheType::Gif => {
let gif_bytes = fs::read(path).await?; // Read entire file into a Vec<u8>
generate_gif(ctx, url, path, gif_bytes, false, |i| {
buffer_to_color_image(i.as_flat_samples_u8(), i.width(), i.height())
})
}
}
}
fn generate_gif(
ctx: egui::Context,
url: String,
path: &path::Path,
data: Vec<u8>,
write_to_disk: bool,
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static,
) -> Result<TexturedImage, crate::Error> {
let decoder = {
let reader = Cursor::new(data.as_slice());
GifDecoder::new(reader)?
};
let (tex_input, tex_output) = mpsc::sync_channel(4);
let (maybe_encoder_input, maybe_encoder_output) = if write_to_disk {
let (inp, out) = mpsc::sync_channel(4);
(Some(inp), Some(out))
} else {
(None, None)
};
let mut frames: VecDeque<Frame> = decoder
.into_frames()
.collect::<std::result::Result<VecDeque<_>, image::ImageError>>()
.map_err(|e| crate::Error::Generic(e.to_string()))?;
let first_frame = frames.pop_front().map(|frame| {
generate_animation_frame(
&ctx,
&url,
0,
frame,
maybe_encoder_input.as_ref(),
process_to_egui,
)
});
let cur_url = url.clone();
thread::spawn(move || {
for (index, frame) in frames.into_iter().enumerate() {
let texture_frame = generate_animation_frame(
&ctx,
&cur_url,
index,
frame,
maybe_encoder_input.as_ref(),
process_to_egui,
);
if tex_input.send(texture_frame).is_err() {
tracing::debug!("AnimationTextureFrame mpsc stopped abruptly");
break;
}
}
});
if let Some(encoder_output) = maybe_encoder_output {
let path = path.to_owned();
thread::spawn(move || {
let mut imgs = Vec::new();
while let Ok(img) = encoder_output.recv() {
imgs.push(img);
}
if let Err(e) = MediaCache::write_gif(&path, &url, imgs) {
tracing::error!("Could not write gif to disk: {e}");
}
});
}
first_frame.map_or_else(
|| {
Err(crate::Error::Generic(
"first frame not found for gif".to_owned(),
))
},
|first_frame| {
Ok(TexturedImage::Animated(Animation {
other_frames: Default::default(),
receiver: Some(tex_output),
first_frame,
}))
},
)
}
fn generate_animation_frame(
ctx: &egui::Context,
url: &str,
index: usize,
frame: image::Frame,
maybe_encoder_input: Option<&SyncSender<ImageFrame>>,
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + 'static,
) -> TextureFrame {
let delay = Duration::from(frame.delay());
let img = DynamicImage::ImageRgba8(frame.into_buffer());
let color_img = process_to_egui(img);
if let Some(sender) = maybe_encoder_input {
if let Err(e) = sender.send(ImageFrame {
delay,
image: color_img.clone(),
}) {
tracing::error!("ImageFrame mpsc unexpectedly closed: {e}");
}
}
TextureFrame {
delay,
texture: ctx.load_texture(format!("{url}{index}"), color_img, Default::default()),
}
}
fn buffer_to_color_image(
samples: Option<FlatSamples<&[u8]>>,
width: u32,
height: u32,
) -> ColorImage {
// TODO(jb55): remove unwrap here
let flat_samples = samples.unwrap();
ColorImage::from_rgba_unmultiplied([width as usize, height as usize], flat_samples.as_slice())
}
pub fn fetch_binary_from_disk(path: PathBuf) -> Result<Vec<u8>, crate::Error> {
std::fs::read(path).map_err(|e| crate::Error::Generic(e.to_string()))
}
/// Controls type-specific handling
#[derive(Debug, Clone, Copy)]
pub enum ImageType {
/// Profile Image (size)
Profile(u32),
/// Content Image with optional size hint
Content(Option<(u32, u32)>),
}
pub fn fetch_img(
img_cache_path: &Path,
ctx: &egui::Context,
url: &str,
imgtyp: ImageType,
cache_type: MediaCacheType,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
let key = MediaCache::key(url);
let path = img_cache_path.join(key);
if path.exists() {
fetch_img_from_disk(ctx, url, &path, cache_type)
} else {
fetch_img_from_net(img_cache_path, ctx, url, imgtyp, cache_type)
}
// TODO: fetch image from local cache
}
fn fetch_img_from_net(
cache_path: &path::Path,
ctx: &egui::Context,
url: &str,
imgtyp: ImageType,
cache_type: MediaCacheType,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
let (sender, promise) = Promise::new();
let request = ehttp::Request::get(url);
let ctx = ctx.clone();
let cloned_url = url.to_owned();
let cache_path = cache_path.to_owned();
ehttp::fetch(request, move |response| {
let handle = response.map_err(crate::Error::Generic).and_then(|resp| {
match cache_type {
MediaCacheType::Image => {
let img = parse_img_response(resp, imgtyp);
img.map(|img| {
let texture_handle =
ctx.load_texture(&cloned_url, img.clone(), Default::default());
// write to disk
std::thread::spawn(move || {
MediaCache::write(&cache_path, &cloned_url, img)
});
TexturedImage::Static(texture_handle)
})
}
MediaCacheType::Gif => {
let gif_bytes = resp.bytes;
generate_gif(
ctx.clone(),
cloned_url,
&cache_path,
gif_bytes,
true,
move |img| process_image(imgtyp, img),
)
}
}
});
sender.send(Some(handle)); // send the results back to the UI thread.
ctx.request_repaint();
});
promise
}
pub fn fetch_no_pfp_promise(
ctx: &Context,
cache: &MediaCache,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
crate::media::images::fetch_img(
&cache.cache_dir,
ctx,
crate::profile::no_pfp_url(),
ImageType::Profile(128),
MediaCacheType::Image,
)
}

View File

@@ -1 +0,0 @@

View File

@@ -1,32 +0,0 @@
pub mod action;
pub mod blur;
pub mod gif;
pub mod images;
pub mod imeta;
pub mod renderable;
pub use action::{MediaAction, MediaInfo, ViewMediaInfo};
pub use blur::{
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ObfuscationType, PixelDimensions,
PointDimensions,
};
pub use images::ImageType;
pub use renderable::RenderableMedia;
#[derive(Copy, Clone, Debug)]
pub enum AnimationMode {
/// Only render when scrolling, network activity, etc
Reactive,
/// Continuous with an optional target fps
Continuous { fps: Option<f32> },
/// Disable animation
NoAnimation,
}
impl AnimationMode {
pub fn can_animate(&self) -> bool {
!matches!(self, Self::NoAnimation)
}
}

View File

@@ -1,9 +0,0 @@
use super::ObfuscationType;
use crate::MediaCacheType;
/// Media that is prepared for rendering. Use [`Images::get_renderable_media`] to get these
pub struct RenderableMedia {
pub url: String,
pub media_type: MediaCacheType,
pub obfuscation_type: ObfuscationType,
}

View File

@@ -1,76 +0,0 @@
use nostrdb::ProfileRecord;
pub struct NostrName<'a> {
pub username: Option<&'a str>,
pub display_name: Option<&'a str>,
pub nip05: Option<&'a str>,
}
impl<'a> NostrName<'a> {
/// Our nostr name is usually our display_name, if we don't have
/// that then its just the username
pub fn name(&self) -> &'a str {
if let Some(name) = self.display_name {
name
} else if let Some(name) = self.username {
name
} else {
self.nip05.unwrap_or("??")
}
}
pub fn username_or_displayname(&self) -> &'a str {
if let Some(name) = self.username {
name
} else if let Some(name) = self.display_name {
name
} else {
self.nip05.unwrap_or("??")
}
}
pub fn unknown() -> Self {
Self {
username: None,
display_name: None,
nip05: None,
}
}
}
fn is_empty(s: &str) -> bool {
s.chars().all(|c| c.is_whitespace())
}
pub fn get_display_name<'a>(record: Option<&ProfileRecord<'a>>) -> NostrName<'a> {
let Some(record) = record else {
return NostrName::unknown();
};
let Some(profile) = record.record().profile() else {
return NostrName::unknown();
};
let display_name = profile.display_name().filter(|n| !is_empty(n));
let username = profile.name().filter(|n| !is_empty(n));
let nip05 = if let Some(raw_nip05) = profile.nip05() {
if let Some(at_pos) = raw_nip05.find('@') {
if raw_nip05.starts_with('_') {
raw_nip05.get(at_pos + 1..)
} else {
Some(raw_nip05)
}
} else {
None
}
} else {
None
};
NostrName {
username,
display_name,
nip05,
}
}

View File

@@ -1,195 +0,0 @@
use std::collections::HashMap;
use enostr::{Pubkey, RelayPool};
use nostrdb::{Filter, Ndb, Note, Transaction};
use uuid::Uuid;
use crate::{UnifiedSubscription, UnknownIds};
/// Keeps track of most recent NIP-51 sets
#[derive(Debug)]
pub struct Nip51SetCache {
pub sub: UnifiedSubscription,
cached_notes: HashMap<PackId, Nip51Set>,
}
type PackId = String;
impl Nip51SetCache {
pub fn new(
pool: &mut RelayPool,
ndb: &Ndb,
txn: &Transaction,
unknown_ids: &mut UnknownIds,
nip51_set_filter: Vec<Filter>,
) -> Option<Self> {
let subid = Uuid::new_v4().to_string();
let mut cached_notes = HashMap::default();
let notes: Option<Vec<Note>> = if let Ok(results) = ndb.query(txn, &nip51_set_filter, 500) {
Some(results.into_iter().map(|r| r.note).collect())
} else {
None
};
if let Some(notes) = notes {
add(notes, &mut cached_notes, ndb, txn, unknown_ids);
}
let sub = match ndb.subscribe(&nip51_set_filter) {
Ok(sub) => sub,
Err(e) => {
tracing::error!("Could not ndb subscribe: {e}");
return None;
}
};
pool.subscribe(subid.clone(), nip51_set_filter);
Some(Self {
sub: UnifiedSubscription {
local: sub,
remote: subid,
},
cached_notes,
})
}
pub fn poll_for_notes(&mut self, ndb: &Ndb, unknown_ids: &mut UnknownIds) {
let new_notes = ndb.poll_for_notes(self.sub.local, 5);
if new_notes.is_empty() {
return;
}
let txn = Transaction::new(ndb).expect("txn");
let notes: Vec<Note> = new_notes
.into_iter()
.filter_map(|new_note_key| ndb.get_note_by_key(&txn, new_note_key).ok())
.collect();
add(notes, &mut self.cached_notes, ndb, &txn, unknown_ids);
}
pub fn iter(&self) -> impl IntoIterator<Item = &Nip51Set> {
self.cached_notes.values()
}
}
fn add(
notes: Vec<Note>,
cache: &mut HashMap<PackId, Nip51Set>,
ndb: &Ndb,
txn: &Transaction,
unknown_ids: &mut UnknownIds,
) {
for note in notes {
let Some(new_pack) = create_nip51_set(note) else {
continue;
};
if let Some(cur_cached) = cache.get(&new_pack.identifier) {
if new_pack.created_at <= cur_cached.created_at {
continue;
}
}
for pk in &new_pack.pks {
unknown_ids.add_pubkey_if_missing(ndb, txn, pk);
}
cache.insert(new_pack.identifier.clone(), new_pack);
}
}
pub fn create_nip51_set(note: Note) -> Option<Nip51Set> {
let mut identifier = None;
let mut title = None;
let mut image = None;
let mut description = None;
let mut pks = Vec::new();
for tag in note.tags() {
if tag.count() < 2 {
continue;
}
let Some(first) = tag.get_str(0) else {
continue;
};
match first {
"p" => {
let Some(pk) = tag.get_id(1) else {
continue;
};
pks.push(Pubkey::new(*pk));
}
"d" => {
let Some(id) = tag.get_str(1) else {
continue;
};
identifier = Some(id.to_owned());
}
"image" => {
let Some(cur_img) = tag.get_str(1) else {
continue;
};
image = Some(cur_img.to_owned());
}
"title" => {
let Some(cur_title) = tag.get_str(1) else {
continue;
};
title = Some(cur_title.to_owned());
}
"description" => {
let Some(cur_desc) = tag.get_str(1) else {
continue;
};
description = Some(cur_desc.to_owned());
}
_ => {
continue;
}
};
}
let identifier = identifier?;
Some(Nip51Set {
identifier,
title,
image,
description,
pks,
created_at: note.created_at(),
})
}
/// NIP-51 Set. Read only (do not use for writing)
pub struct Nip51Set {
pub identifier: String, // 'd' tag
pub title: Option<String>,
pub image: Option<String>,
pub description: Option<String>,
pub pks: Vec<Pubkey>,
created_at: u64,
}
impl std::fmt::Debug for Nip51Set {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Nip51Set")
.field("identifier", &self.identifier)
.field("title", &self.title)
.field("image", &self.image)
.field("description", &self.description)
.field("pks", &self.pks.len())
.field("created_at", &self.created_at)
.finish()
}
}

View File

@@ -1,67 +0,0 @@
use super::context::ContextSelection;
use crate::{zaps::NoteZapTargetOwned, MediaAction};
use egui::Vec2;
use enostr::{NoteId, Pubkey};
#[derive(Debug)]
pub struct ScrollInfo {
pub velocity: Vec2,
pub offset: Vec2,
}
#[derive(Debug)]
pub enum NoteAction {
/// User has clicked the quote reply action
Reply(NoteId),
/// User has clicked the quote repost action
Quote(NoteId),
/// User has clicked a hashtag
Hashtag(String),
/// User has clicked a profile
Profile(Pubkey),
/// User has clicked a note link
Note {
note_id: NoteId,
preview: bool,
scroll_offset: f32,
},
/// User has selected some context option
Context(ContextSelection),
/// User has clicked the zap action
Zap(ZapAction),
/// User clicked on media
Media(MediaAction),
/// User scrolled the timeline
Scroll(ScrollInfo),
}
impl NoteAction {
pub fn note(id: NoteId) -> NoteAction {
NoteAction::Note {
note_id: id,
preview: false,
scroll_offset: 0.0,
}
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum ZapAction {
Send(ZapTargetAmount),
CustomizeAmount(NoteZapTargetOwned),
ClearError(NoteZapTargetOwned),
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct ZapTargetAmount {
pub target: NoteZapTargetOwned,
pub specified_msats: Option<u64>, // if None use default amount
}

Some files were not shown because too many files have changed in this diff Show More