feat: add support for multi languages (#79)
* update backup settings description * add rust-i18n * translate * . * update translations * fix * update translate * .
This commit is contained in:
165
Cargo.lock
generated
165
Cargo.lock
generated
@@ -97,6 +97,12 @@ version = "1.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arc-swap"
|
||||||
|
version = "1.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arg_enum_proc_macro"
|
name = "arg_enum_proc_macro"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -403,9 +409,11 @@ dependencies = [
|
|||||||
"common",
|
"common",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"i18n",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"reqwest 0.12.22",
|
"reqwest 0.12.22",
|
||||||
|
"rust-i18n",
|
||||||
"smol",
|
"smol",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
@@ -477,6 +485,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base62"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10e52a7bcb1d6beebee21fb5053af9e3cbb7a7ed1a4909e534040e676437ab1f"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -928,12 +945,14 @@ dependencies = [
|
|||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"i18n",
|
||||||
"identity",
|
"identity",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
"nostr",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
|
"rust-i18n",
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
@@ -982,8 +1001,10 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"i18n",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
|
"rust-i18n",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1181,6 +1202,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"i18n",
|
||||||
"identity",
|
"identity",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
@@ -1190,6 +1212,7 @@ dependencies = [
|
|||||||
"oneshot",
|
"oneshot",
|
||||||
"reqwest_client",
|
"reqwest_client",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
|
"rust-i18n",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
@@ -2261,6 +2284,17 @@ dependencies = [
|
|||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globwalk"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"ignore",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-timers"
|
name = "gloo-timers"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -2743,6 +2777,13 @@ dependencies = [
|
|||||||
"windows-registry 0.5.3",
|
"windows-registry 0.5.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "i18n"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"rust-i18n",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.63"
|
version = "0.1.63"
|
||||||
@@ -2862,10 +2903,12 @@ dependencies = [
|
|||||||
"common",
|
"common",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"i18n",
|
||||||
"log",
|
"log",
|
||||||
"nostr-connect",
|
"nostr-connect",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
|
"rust-i18n",
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"ui",
|
"ui",
|
||||||
@@ -2892,6 +2935,22 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ignore"
|
||||||
|
version = "0.4.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"globset",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"same-file",
|
||||||
|
"walkdir",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.6"
|
version = "0.25.6"
|
||||||
@@ -3048,6 +3107,15 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -3607,6 +3675,15 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "normpath"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.42.1"
|
version = "0.42.1"
|
||||||
@@ -4964,6 +5041,60 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-i18n"
|
||||||
|
version = "3.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332"
|
||||||
|
dependencies = [
|
||||||
|
"globwalk",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"rust-i18n-macro",
|
||||||
|
"rust-i18n-support",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-i18n-macro"
|
||||||
|
version = "3.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-i18n-support",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-i18n-support"
|
||||||
|
version = "3.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19"
|
||||||
|
dependencies = [
|
||||||
|
"arc-swap",
|
||||||
|
"base62",
|
||||||
|
"globwalk",
|
||||||
|
"itertools 0.11.0",
|
||||||
|
"lazy_static",
|
||||||
|
"normpath",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"siphasher",
|
||||||
|
"toml",
|
||||||
|
"triomphe",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.25"
|
version = "0.1.25"
|
||||||
@@ -5453,6 +5584,19 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.34+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "settings"
|
name = "settings"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -5460,8 +5604,10 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"i18n",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
|
"rust-i18n",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@@ -6358,6 +6504,17 @@ dependencies = [
|
|||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "triomphe"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85"
|
||||||
|
dependencies = [
|
||||||
|
"arc-swap",
|
||||||
|
"serde",
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -6436,6 +6593,7 @@ dependencies = [
|
|||||||
"common",
|
"common",
|
||||||
"emojis",
|
"emojis",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"i18n",
|
||||||
"image",
|
"image",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"linkify",
|
"linkify",
|
||||||
@@ -6443,6 +6601,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"paste",
|
"paste",
|
||||||
"regex",
|
"regex",
|
||||||
|
"rust-i18n",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@@ -6555,6 +6714,12 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|||||||
108
Cargo.toml
108
Cargo.toml
@@ -1,51 +1,57 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
default-members = ["crates/coop"]
|
default-members = ["crates/coop"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.metadata.i18n]
|
||||||
coop = { path = "crates/*" }
|
available-locales = ["en", "zh-CN", "zh-TW", "ru", "vi", "ja", "es", "pt", "ko"]
|
||||||
|
default-locale = "en"
|
||||||
# GPUI
|
load-path = "locales"
|
||||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
|
||||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
[workspace.dependencies]
|
||||||
|
i18n = { path = "crates/i18n" }
|
||||||
# Nostr
|
|
||||||
nostr = { git = "https://github.com/rust-nostr/nostr" }
|
# GPUI
|
||||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
|
gpui = { git = "https://github.com/zed-industries/zed" }
|
||||||
"lmdb",
|
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||||
"nip96",
|
|
||||||
"nip59",
|
# Nostr
|
||||||
"nip49",
|
nostr = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
"nip44",
|
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
] }
|
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
|
||||||
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
"lmdb",
|
||||||
|
"nip96",
|
||||||
# Others
|
"nip59",
|
||||||
reqwest = { version = "0.12", features = ["multipart", "stream", "json"] }
|
"nip49",
|
||||||
emojis = "0.6.4"
|
"nip44",
|
||||||
smol = "2"
|
] }
|
||||||
futures = "0.3"
|
|
||||||
oneshot = "0.1.10"
|
# Others
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
anyhow = "1.0.44"
|
||||||
serde_json = "1.0"
|
chrono = "0.4.38"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
itertools = "0.13.0"
|
emojis = "0.6.4"
|
||||||
chrono = "0.4.38"
|
futures = "0.3"
|
||||||
tracing = "0.1.40"
|
itertools = "0.13.0"
|
||||||
anyhow = "1.0.44"
|
log = "0.4"
|
||||||
smallvec = "1.14.0"
|
oneshot = "0.1.10"
|
||||||
rust-embed = "8.5.0"
|
reqwest = { version = "0.12", features = ["multipart", "stream", "json"] }
|
||||||
log = "0.4"
|
rust-embed = "8.5.0"
|
||||||
|
rust-i18n = "3"
|
||||||
[profile.release]
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
strip = true
|
serde_json = "1.0"
|
||||||
opt-level = "z"
|
smallvec = "1.14.0"
|
||||||
lto = true
|
smol = "2"
|
||||||
codegen-units = 1
|
tracing = "0.1.40"
|
||||||
panic = "abort"
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
|||||||
4
assets/icons/language.svg
Normal file
4
assets/icons/language.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.75 5.816h8.5M8 5.75v-2m4 10.5C7.935 13.198 5.845 10.614 5.25 6"/>
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 14c4.064-1.02 6.154-3.527 6.75-8m3.594 11.125h5.312m1.594 2.125-3.314-8.774c-.326-.862-1.546-.862-1.872 0L12.75 19.25"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 494 B |
@@ -1,18 +1,20 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "auto_update"
|
name = "auto_update"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
|
|
||||||
gpui.workspace = true
|
rust-i18n.workspace = true
|
||||||
nostr-sdk.workspace = true
|
i18n.workspace = true
|
||||||
anyhow.workspace = true
|
gpui.workspace = true
|
||||||
smol.workspace = true
|
nostr-sdk.workspace = true
|
||||||
reqwest.workspace = true
|
anyhow.workspace = true
|
||||||
log.workspace = true
|
smol.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
tempfile = "3.19.1"
|
log.workspace = true
|
||||||
|
|
||||||
|
tempfile = "3.19.1"
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ use smol::io::AsyncWriteExt;
|
|||||||
use smol::process::Command;
|
use smol::process::Command;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
i18n::init!();
|
||||||
|
|
||||||
struct GlobalAutoUpdate(Entity<AutoUpdater>);
|
struct GlobalAutoUpdate(Entity<AutoUpdater>);
|
||||||
|
|
||||||
impl Global for GlobalAutoUpdate {}
|
impl Global for GlobalAutoUpdate {}
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chats"
|
name = "chats"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
identity = { path = "../identity" }
|
identity = { path = "../identity" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
gpui.workspace = true
|
rust-i18n.workspace = true
|
||||||
nostr.workspace = true
|
i18n.workspace = true
|
||||||
nostr-sdk.workspace = true
|
gpui.workspace = true
|
||||||
anyhow.workspace = true
|
nostr.workspace = true
|
||||||
itertools.workspace = true
|
nostr-sdk.workspace = true
|
||||||
chrono.workspace = true
|
anyhow.workspace = true
|
||||||
smallvec.workspace = true
|
itertools.workspace = true
|
||||||
smol.workspace = true
|
chrono.workspace = true
|
||||||
oneshot.workspace = true
|
smallvec.workspace = true
|
||||||
log.workspace = true
|
smol.workspace = true
|
||||||
|
oneshot.workspace = true
|
||||||
fuzzy-matcher = "0.3.7"
|
log.workspace = true
|
||||||
|
|
||||||
|
fuzzy-matcher = "0.3.7"
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ pub mod room;
|
|||||||
|
|
||||||
mod constants;
|
mod constants;
|
||||||
|
|
||||||
|
i18n::init!();
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
ChatRegistry::set_global(cx.new(ChatRegistry::new), cx);
|
ChatRegistry::set_global(cx.new(ChatRegistry::new), cx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "client_keys"
|
name = "client_keys"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
rust-i18n.workspace = true
|
||||||
gpui.workspace = true
|
i18n.workspace = true
|
||||||
anyhow.workspace = true
|
nostr-sdk.workspace = true
|
||||||
log.workspace = true
|
gpui.workspace = true
|
||||||
smallvec.workspace = true
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use gpui::{App, AppContext, Context, Entity, Global, Subscription, Window};
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
|
i18n::init!();
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
ClientKeys::set_global(cx.new(ClientKeys::new), cx);
|
ClientKeys::set_global(cx.new(ClientKeys::new), cx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,43 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "coop"
|
name = "coop"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "coop"
|
name = "coop"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
identity = { path = "../identity" }
|
identity = { path = "../identity" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
chats = { path = "../chats" }
|
chats = { path = "../chats" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
client_keys = { path = "../client_keys" }
|
client_keys = { path = "../client_keys" }
|
||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update" }
|
||||||
|
|
||||||
gpui.workspace = true
|
rust-i18n.workspace = true
|
||||||
reqwest_client.workspace = true
|
i18n.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
nostr-connect.workspace = true
|
reqwest_client.workspace = true
|
||||||
nostr-sdk.workspace = true
|
|
||||||
nostr.workspace = true
|
nostr-connect.workspace = true
|
||||||
|
nostr-sdk.workspace = true
|
||||||
anyhow.workspace = true
|
nostr.workspace = true
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
serde.workspace = true
|
||||||
dirs.workspace = true
|
serde_json.workspace = true
|
||||||
rust-embed.workspace = true
|
itertools.workspace = true
|
||||||
log.workspace = true
|
dirs.workspace = true
|
||||||
smallvec.workspace = true
|
rust-embed.workspace = true
|
||||||
smol.workspace = true
|
log.workspace = true
|
||||||
futures.workspace = true
|
smallvec.workspace = true
|
||||||
oneshot.workspace = true
|
smol.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
oneshot.workspace = true
|
||||||
|
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ use global::shared_state;
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, Action, App, AppContext, Axis, Context, Entity, IntoElement, ParentElement,
|
div, px, relative, Action, App, AppContext, Axis, Context, Entity, IntoElement, ParentElement,
|
||||||
Render, Styled, Subscription, Task, Window,
|
Render, SharedString, Styled, Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -54,6 +55,10 @@ pub enum ModalKind {
|
|||||||
SetupRelay,
|
SetupRelay,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
#[action(namespace = story, no_json)]
|
||||||
|
pub struct SelectLocale(SharedString);
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[action(namespace = modal, no_json)]
|
#[action(namespace = modal, no_json)]
|
||||||
pub struct ToggleModal {
|
pub struct ToggleModal {
|
||||||
@@ -91,17 +96,14 @@ impl ChatSpace {
|
|||||||
|_this: &mut Self, state, window, cx| {
|
|_this: &mut Self, state, window, cx| {
|
||||||
if !state.read(cx).has_keys() {
|
if !state.read(cx).has_keys() {
|
||||||
window.open_modal(cx, |this, _window, cx| {
|
window.open_modal(cx, |this, _window, cx| {
|
||||||
const DESCRIPTION: &str =
|
|
||||||
"Allow Coop to read the client keys stored in Keychain to continue";
|
|
||||||
|
|
||||||
this.overlay_closable(false)
|
this.overlay_closable(false)
|
||||||
.show_close(false)
|
.show_close(false)
|
||||||
.keyboard(false)
|
.keyboard(false)
|
||||||
.confirm()
|
.confirm()
|
||||||
.button_props(
|
.button_props(
|
||||||
ModalButtonProps::default()
|
ModalButtonProps::default()
|
||||||
.cancel_text("Create New Keys")
|
.cancel_text(t!("chatspace.create_new_keys"))
|
||||||
.ok_text("Allow"),
|
.ok_text(t!("common.allow")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -119,9 +121,13 @@ impl ChatSpace {
|
|||||||
div()
|
div()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child("Warning"),
|
.child(SharedString::new(t!("chatspace.warning"))),
|
||||||
)
|
)
|
||||||
.child(div().line_height(relative(1.4)).child(DESCRIPTION)),
|
.child(div().line_height(relative(1.4)).child(
|
||||||
|
SharedString::new(t!(
|
||||||
|
"chatspace.allow_keychain_access"
|
||||||
|
)),
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.on_cancel(|_, _window, cx| {
|
.on_cancel(|_, _window, cx| {
|
||||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
ClientKeys::global(cx).update(cx, |this, cx| {
|
||||||
@@ -182,7 +188,7 @@ impl ChatSpace {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
"Failed to open room. Please try again later.",
|
SharedString::new(t!("chatspace.failed_to_open_room")),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -264,7 +270,7 @@ impl ChatSpace {
|
|||||||
|
|
||||||
window.open_modal(cx, move |modal, _, _| {
|
window.open_modal(cx, move |modal, _, _| {
|
||||||
modal
|
modal
|
||||||
.title("Preferences")
|
.title(SharedString::new(t!("chatspace.preferences_title")))
|
||||||
.width(px(DEFAULT_MODAL_WIDTH))
|
.width(px(DEFAULT_MODAL_WIDTH))
|
||||||
.child(settings.clone())
|
.child(settings.clone())
|
||||||
});
|
});
|
||||||
@@ -349,7 +355,7 @@ impl Render for ChatSpace {
|
|||||||
.px_2()
|
.px_2()
|
||||||
.child(
|
.child(
|
||||||
Button::new("appearance")
|
Button::new("appearance")
|
||||||
.tooltip("Change the app's appearance")
|
.tooltip(t!("chatspace.appearance_tooltip"))
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
@@ -365,7 +371,7 @@ impl Render for ChatSpace {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("preferences")
|
Button::new("preferences")
|
||||||
.tooltip("Open Preferences")
|
.tooltip(t!("chatspace.preferences_tooltip"))
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.icon(IconName::Settings)
|
.icon(IconName::Settings)
|
||||||
@@ -375,7 +381,7 @@ impl Render for ChatSpace {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("logout")
|
Button::new("logout")
|
||||||
.tooltip("Log Out")
|
.tooltip(t!("common.logout"))
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.icon(IconName::Logout)
|
.icon(IconName::Logout)
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ pub(crate) mod asset;
|
|||||||
pub(crate) mod chatspace;
|
pub(crate) mod chatspace;
|
||||||
pub(crate) mod views;
|
pub(crate) mod views;
|
||||||
|
|
||||||
|
i18n::init!();
|
||||||
|
|
||||||
actions!(coop, [Quit]);
|
actions!(coop, [Quit]);
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use gpui::{
|
|||||||
PathPromptOptions, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement,
|
PathPromptOptions, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement,
|
||||||
Styled, StyledImage, Subscription, Window,
|
Styled, StyledImage, Subscription, Window,
|
||||||
};
|
};
|
||||||
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -73,10 +74,7 @@ impl Chat {
|
|||||||
|
|
||||||
let messages = cx.new(|_| {
|
let messages = cx.new(|_| {
|
||||||
let message = Message::builder()
|
let message = Message::builder()
|
||||||
.content(
|
.content(t!("chat.private_conversation_notice").into())
|
||||||
"This conversation is private. Only members can see each other's messages."
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.build_rc()
|
.build_rc()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -85,7 +83,7 @@ impl Chat {
|
|||||||
|
|
||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder("Message...")
|
.placeholder(t!("chat.placeholder"))
|
||||||
.multi_line()
|
.multi_line()
|
||||||
.prevent_new_line_on_enter()
|
.prevent_new_line_on_enter()
|
||||||
.rows(1)
|
.rows(1)
|
||||||
@@ -103,7 +101,10 @@ impl Chat {
|
|||||||
move |this: &mut Self, input, event, window, cx| {
|
move |this: &mut Self, input, event, window, cx| {
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
if input.read(cx).value().trim().is_empty() {
|
if input.read(cx).value().trim().is_empty() {
|
||||||
window.push_notification("Cannot send an empty message", cx);
|
window.push_notification(
|
||||||
|
Notification::new(t!("chat.empty_message_error")),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.send_message(window, cx);
|
this.send_message(window, cx);
|
||||||
}
|
}
|
||||||
@@ -498,7 +499,7 @@ impl Chat {
|
|||||||
.gap_1()
|
.gap_1()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child("Replying to:")
|
.child(SharedString::new(t!("chat.replying_to_label")))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_color(cx.theme().text_accent)
|
.text_color(cx.theme().text_accent)
|
||||||
@@ -687,12 +688,12 @@ impl Chat {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.italic()
|
.italic()
|
||||||
.child(Icon::new(IconName::Info).small())
|
.child(Icon::new(IconName::Info).small())
|
||||||
.child("Failed to send message. Click to see details.")
|
.child(SharedString::new(t!("chat.send_fail")))
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
let errors = errors.clone();
|
let errors = errors.clone();
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, cx| {
|
window.open_modal(cx, move |this, _window, cx| {
|
||||||
this.title("Error Logs")
|
this.title(SharedString::new(t!("chat.logs_title")))
|
||||||
.child(message_errors(errors.clone(), cx))
|
.child(message_errors(errors.clone(), cx))
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
@@ -705,7 +706,7 @@ impl Chat {
|
|||||||
vec![
|
vec![
|
||||||
Button::new("reply")
|
Button::new("reply")
|
||||||
.icon(IconName::Reply)
|
.icon(IconName::Reply)
|
||||||
.tooltip("Reply")
|
.tooltip(t!("chat.reply_button"))
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_click({
|
.on_click({
|
||||||
@@ -716,7 +717,7 @@ impl Chat {
|
|||||||
}),
|
}),
|
||||||
Button::new("copy")
|
Button::new("copy")
|
||||||
.icon(IconName::Copy)
|
.icon(IconName::Copy)
|
||||||
.tooltip("Copy Message")
|
.tooltip(t!("chat.copy_message_button"))
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_click({
|
.on_click({
|
||||||
@@ -779,12 +780,12 @@ impl Panel for Chat {
|
|||||||
|
|
||||||
let button = Button::new("subject")
|
let button = Button::new("subject")
|
||||||
.icon(IconName::EditFill)
|
.icon(IconName::EditFill)
|
||||||
.tooltip("Change Subject")
|
.tooltip(t!("chat.change_subject_button"))
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
let subject = subject::init(id, subject.clone(), window, cx);
|
let subject = subject::init(id, subject.clone(), window, cx);
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
this.title("Change the subject of the conversation")
|
this.title(SharedString::new(t!("chat.change_subject_modal_title")))
|
||||||
.child(subject.clone())
|
.child(subject.clone())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -896,7 +897,7 @@ fn message_errors(errors: Vec<SendError>, cx: &App) -> Div {
|
|||||||
.items_baseline()
|
.items_baseline()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child("Send to:")
|
.child(SharedString::new(t!("chat.send_to_label")))
|
||||||
.child(error.profile.render_name()),
|
.child(error.profile.render_name()),
|
||||||
)
|
)
|
||||||
.child(error.message)
|
.child(error.message)
|
||||||
|
|||||||
@@ -13,16 +13,19 @@ use gpui::{
|
|||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||||
StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Window,
|
StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Window,
|
||||||
};
|
};
|
||||||
|
use i18n::t;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::{
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
button::{Button, ButtonVariants},
|
||||||
use ui::notification::Notification;
|
input::{InputEvent, InputState, TextInput},
|
||||||
use ui::{ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
notification::Notification,
|
||||||
|
ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
||||||
cx.new(|cx| Compose::new(window, cx))
|
cx.new(|cx| Compose::new(window, cx))
|
||||||
@@ -72,10 +75,10 @@ pub struct Compose {
|
|||||||
impl Compose {
|
impl Compose {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<'_, Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<'_, Self>) -> Self {
|
||||||
let user_input =
|
let user_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("npub or nprofile..."));
|
cx.new(|cx| InputState::new(window, cx).placeholder(t!("compose.placeholder_npub")));
|
||||||
|
|
||||||
let title_input =
|
let title_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("Family...(Optional)"));
|
cx.new(|cx| InputState::new(window, cx).placeholder(t!("compose.placeholder_title")));
|
||||||
|
|
||||||
let error_message = cx.new(|_| None);
|
let error_message = cx.new(|_| None);
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
@@ -113,10 +116,7 @@ impl Compose {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
window.push_notification(
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
Notification::error(e.to_string()).title("Contacts"),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ impl Compose {
|
|||||||
let public_keys: Vec<PublicKey> = self.selected(cx);
|
let public_keys: Vec<PublicKey> = self.selected(cx);
|
||||||
|
|
||||||
if public_keys.is_empty() {
|
if public_keys.is_empty() {
|
||||||
self.set_error(Some("You need to add at least 1 receiver".into()), cx);
|
self.set_error(Some(t!("compose.receiver_required").into()), cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,28 +171,30 @@ impl Compose {
|
|||||||
Ok(room)
|
Ok(room)
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| match event.await {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
Ok(room) => {
|
match event.await {
|
||||||
cx.update(|window, cx| {
|
Ok(room) => {
|
||||||
this.update(cx, |this, cx| {
|
cx.update(|window, cx| {
|
||||||
this.set_submitting(false, cx);
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_submitting(false, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||||
|
this.push_room(cx.new(|_| room), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.close_modal(cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
}
|
||||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
Err(e) => {
|
||||||
this.push_room(cx.new(|_| room), cx);
|
this.update(cx, |this, cx| {
|
||||||
});
|
this.set_error(Some(e.to_string().into()), cx);
|
||||||
|
})
|
||||||
window.close_modal(cx);
|
.ok();
|
||||||
})
|
}
|
||||||
.ok();
|
};
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_error(Some(e.to_string().into()), cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
@@ -211,6 +213,11 @@ impl Compose {
|
|||||||
{
|
{
|
||||||
self.contacts.insert(0, cx.new(|_| contact));
|
self.contacts.insert(0, cx.new(|_| contact));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
} else {
|
||||||
|
self.set_error(
|
||||||
|
Some(t!("compose.contact_existed", name = contact.profile.name()).into()),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +266,7 @@ impl Compose {
|
|||||||
|
|
||||||
Ok(contact)
|
Ok(contact)
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("Profile not found"))
|
Err(anyhow!(t!("common.not_found")))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if content.starts_with("nprofile1") {
|
} else if content.starts_with("nprofile1") {
|
||||||
@@ -267,7 +274,7 @@ impl Compose {
|
|||||||
.map(|nip19| nip19.public_key)
|
.map(|nip19| nip19.public_key)
|
||||||
.ok()
|
.ok()
|
||||||
else {
|
else {
|
||||||
self.set_error(Some("Public Key is not valid".into()), cx);
|
self.set_error(Some(t!("common.pubkey_invalid").into()), cx);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -285,7 +292,7 @@ impl Compose {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let Ok(public_key) = PublicKey::parse(&content) else {
|
let Ok(public_key) = PublicKey::parse(&content) else {
|
||||||
self.set_error(Some("Public Key is not valid".into()), cx);
|
self.set_error(Some(t!("common.pubkey_invalid").into()), cx);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -328,7 +335,7 @@ impl Compose {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_error(&mut self, error: Option<SharedString>, cx: &mut Context<Self>) {
|
fn set_error(&mut self, error: impl Into<Option<SharedString>>, cx: &mut Context<Self>) {
|
||||||
if self.adding {
|
if self.adding {
|
||||||
self.set_adding(false, cx);
|
self.set_adding(false, cx);
|
||||||
}
|
}
|
||||||
@@ -340,7 +347,7 @@ impl Compose {
|
|||||||
|
|
||||||
// Update error message
|
// Update error message
|
||||||
self.error_message.update(cx, |this, cx| {
|
self.error_message.update(cx, |this, cx| {
|
||||||
*this = error;
|
*this = error.into();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -418,13 +425,12 @@ impl Compose {
|
|||||||
|
|
||||||
impl Render for Compose {
|
impl Render for Compose {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
const DESCRIPTION: &str =
|
let label = if self.submitting {
|
||||||
"Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
|
t!("compose.creating_dm_button")
|
||||||
|
} else if self.contacts.len() > 1 {
|
||||||
let label: SharedString = if self.contacts.len() > 1 {
|
t!("compose.create_group_dm_button")
|
||||||
"Create Group DM".into()
|
|
||||||
} else {
|
} else {
|
||||||
"Create DM".into()
|
t!("compose.create_dm_button")
|
||||||
};
|
};
|
||||||
|
|
||||||
div()
|
div()
|
||||||
@@ -436,7 +442,7 @@ impl Render for Compose {
|
|||||||
.px_3()
|
.px_3()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(DESCRIPTION),
|
.child(SharedString::new(t!("compose.description"))),
|
||||||
)
|
)
|
||||||
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
|
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
|
||||||
this.child(div().px_3().text_xs().text_color(red()).child(msg.clone()))
|
this.child(div().px_3().text_xs().text_color(red()).child(msg.clone()))
|
||||||
@@ -450,7 +456,12 @@ impl Render for Compose {
|
|||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(div().text_sm().font_semibold().child("Subject:"))
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.font_semibold()
|
||||||
|
.child(SharedString::new(t!("compose.subject_label"))),
|
||||||
|
)
|
||||||
.child(TextInput::new(&self.title_input).small().appearance(false)),
|
.child(TextInput::new(&self.title_input).small().appearance(false)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -466,7 +477,12 @@ impl Render for Compose {
|
|||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(div().text_sm().font_semibold().child("To:"))
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.font_semibold()
|
||||||
|
.child(SharedString::new(t!("compose.to_label"))),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -505,13 +521,16 @@ impl Render for Compose {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.line_height(relative(1.2))
|
.line_height(relative(1.2))
|
||||||
.child("No contacts"),
|
.child(SharedString::new(t!(
|
||||||
|
"compose.no_contacts_message"
|
||||||
|
))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div().text_xs().text_color(cx.theme().text_muted).child(
|
||||||
.text_xs()
|
SharedString::new(t!(
|
||||||
.text_color(cx.theme().text_muted)
|
"compose.no_contacts_description"
|
||||||
.child("Your recently contacts will appear here."),
|
)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ use gpui::{
|
|||||||
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
|
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
|
||||||
Styled, Window,
|
Styled, Window,
|
||||||
};
|
};
|
||||||
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
@@ -40,17 +41,20 @@ impl NewAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
|
let name_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
let avatar_input =
|
.placeholder(SharedString::new(t!("profile.placeholder_name")))
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
|
});
|
||||||
|
|
||||||
let bio_input = cx.new(|cx| {
|
let bio_input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line()
|
||||||
.placeholder("A short introduce about you.")
|
.placeholder(SharedString::new(t!("profile.placeholder_bio")))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let avatar_input =
|
||||||
|
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.png"));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name_input,
|
name_input,
|
||||||
avatar_input,
|
avatar_input,
|
||||||
@@ -93,7 +97,7 @@ impl NewAccount {
|
|||||||
.on_cancel(move |_, window, cx| {
|
.on_cancel(move |_, window, cx| {
|
||||||
view_cancel
|
view_cancel
|
||||||
.update(cx, |_this, cx| {
|
.update(cx, |_this, cx| {
|
||||||
window.push_notification("Password is invalid", cx)
|
window.push_notification(t!("new_account.password_invalid"), cx)
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
true
|
true
|
||||||
@@ -121,7 +125,7 @@ impl NewAccount {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child("Set password to encrypt your key *")
|
.child(SharedString::new(t!("new_account.set_password_prompt")))
|
||||||
.child(TextInput::new(&pwd_input).small()),
|
.child(TextInput::new(&pwd_input).small()),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -258,7 +262,7 @@ impl Render for NewAccount {
|
|||||||
.text_lg()
|
.text_lg()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.line_height(relative(1.3))
|
.line_height(relative(1.3))
|
||||||
.child("Create New Account"),
|
.child(SharedString::new(t!("new_account.title"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -294,7 +298,7 @@ impl Render for NewAccount {
|
|||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
Button::new("upload")
|
Button::new("upload")
|
||||||
.label("Set Profile Picture")
|
.label(t!("profile.set_profile_picture"))
|
||||||
.icon(Icon::new(IconName::Plus))
|
.icon(Icon::new(IconName::Plus))
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
@@ -311,7 +315,7 @@ impl Render for NewAccount {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child("Name *:")
|
.child(SharedString::new(t!("profile.label_name")))
|
||||||
.child(TextInput::new(&self.name_input).small()),
|
.child(TextInput::new(&self.name_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -320,7 +324,7 @@ impl Render for NewAccount {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child("Bio:")
|
.child(SharedString::new(t!("profile.label_bio")))
|
||||||
.child(TextInput::new(&self.bio_input).small()),
|
.child(TextInput::new(&self.bio_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -332,7 +336,7 @@ impl Render for NewAccount {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("submit")
|
Button::new("submit")
|
||||||
.label("Continue")
|
.label(SharedString::new(t!("common.continue")))
|
||||||
.primary()
|
.primary()
|
||||||
.loading(self.is_submitting)
|
.loading(self.is_submitting)
|
||||||
.disabled(self.is_submitting || self.is_uploading)
|
.disabled(self.is_submitting || self.is_uploading)
|
||||||
|
|||||||
@@ -1,290 +1,294 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use common::profile::RenderProfile;
|
use common::profile::RenderProfile;
|
||||||
use global::constants::ACCOUNT_D;
|
use global::constants::ACCOUNT_D;
|
||||||
use global::shared_state;
|
use global::shared_state;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||||
StatefulInteractiveElement, Styled, Window,
|
StatefulInteractiveElement, Styled, Window,
|
||||||
};
|
};
|
||||||
use identity::Identity;
|
use i18n::t;
|
||||||
use itertools::Itertools;
|
use identity::Identity;
|
||||||
use nostr_sdk::prelude::*;
|
use itertools::Itertools;
|
||||||
use settings::AppSettings;
|
use nostr_sdk::prelude::*;
|
||||||
use theme::ActiveTheme;
|
use settings::AppSettings;
|
||||||
use ui::avatar::Avatar;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::avatar::Avatar;
|
||||||
use ui::checkbox::Checkbox;
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::checkbox::Checkbox;
|
||||||
use ui::indicator::Indicator;
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::popup_menu::PopupMenu;
|
use ui::indicator::Indicator;
|
||||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt};
|
use ui::popup_menu::PopupMenu;
|
||||||
|
use ui::{Disableable, Icon, IconName, Sizable, StyledExt};
|
||||||
use crate::chatspace;
|
|
||||||
|
use crate::chatspace;
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
|
||||||
Onboarding::new(window, cx)
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||||
}
|
Onboarding::new(window, cx)
|
||||||
|
}
|
||||||
pub struct Onboarding {
|
|
||||||
name: SharedString,
|
pub struct Onboarding {
|
||||||
local_account: Entity<Option<Profile>>,
|
name: SharedString,
|
||||||
loading: bool,
|
local_account: Entity<Option<Profile>>,
|
||||||
closable: bool,
|
loading: bool,
|
||||||
zoomable: bool,
|
closable: bool,
|
||||||
focus_handle: FocusHandle,
|
zoomable: bool,
|
||||||
}
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
impl Onboarding {
|
|
||||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
impl Onboarding {
|
||||||
cx.new(|cx| Self::view(window, cx))
|
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
}
|
cx.new(|cx| Self::view(window, cx))
|
||||||
|
}
|
||||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let local_account = cx.new(|_| None);
|
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let local_account = cx.new(|_| None);
|
||||||
let task = cx.background_spawn(async move {
|
|
||||||
let database = shared_state().client().database();
|
let task = cx.background_spawn(async move {
|
||||||
|
let database = shared_state().client().database();
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::ApplicationSpecificData)
|
let filter = Filter::new()
|
||||||
.identifier(ACCOUNT_D)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.limit(1);
|
.identifier(ACCOUNT_D)
|
||||||
|
.limit(1);
|
||||||
if let Some(event) = database.query(filter).await?.first_owned() {
|
|
||||||
let public_key = event
|
if let Some(event) = database.query(filter).await?.first_owned() {
|
||||||
.tags
|
let public_key = event
|
||||||
.public_keys()
|
.tags
|
||||||
.copied()
|
.public_keys()
|
||||||
.collect_vec()
|
.copied()
|
||||||
.first()
|
.collect_vec()
|
||||||
.cloned()
|
.first()
|
||||||
.unwrap();
|
.cloned()
|
||||||
let metadata = database.metadata(public_key).await?.unwrap_or_default();
|
.unwrap();
|
||||||
let profile = Profile::new(public_key, metadata);
|
let metadata = database.metadata(public_key).await?.unwrap_or_default();
|
||||||
|
let profile = Profile::new(public_key, metadata);
|
||||||
Ok(profile)
|
|
||||||
} else {
|
Ok(profile)
|
||||||
Err(anyhow!("Not found"))
|
} else {
|
||||||
}
|
Err(anyhow!("Not found"))
|
||||||
});
|
}
|
||||||
|
});
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
if let Ok(profile) = task.await {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
this.update(cx, |this, cx| {
|
if let Ok(profile) = task.await {
|
||||||
this.local_account.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
*this = Some(profile);
|
this.local_account.update(cx, |this, cx| {
|
||||||
cx.notify();
|
*this = Some(profile);
|
||||||
});
|
cx.notify();
|
||||||
})
|
});
|
||||||
.ok();
|
})
|
||||||
}
|
.ok();
|
||||||
})
|
}
|
||||||
.detach();
|
})
|
||||||
|
.detach();
|
||||||
Self {
|
|
||||||
local_account,
|
Self {
|
||||||
name: "Onboarding".into(),
|
local_account,
|
||||||
loading: false,
|
name: "Onboarding".into(),
|
||||||
closable: true,
|
loading: false,
|
||||||
zoomable: true,
|
closable: true,
|
||||||
focus_handle: cx.focus_handle(),
|
zoomable: true,
|
||||||
}
|
focus_handle: cx.focus_handle(),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.loading = status;
|
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
cx.notify();
|
self.loading = status;
|
||||||
}
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl Panel for Onboarding {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
impl Panel for Onboarding {
|
||||||
self.name.clone()
|
fn panel_id(&self) -> SharedString {
|
||||||
}
|
self.name.clone()
|
||||||
|
}
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
fn title(&self, _cx: &App) -> AnyElement {
|
||||||
}
|
self.name.clone().into_any_element()
|
||||||
|
}
|
||||||
fn closable(&self, _cx: &App) -> bool {
|
|
||||||
self.closable
|
fn closable(&self, _cx: &App) -> bool {
|
||||||
}
|
self.closable
|
||||||
|
}
|
||||||
fn zoomable(&self, _cx: &App) -> bool {
|
|
||||||
self.zoomable
|
fn zoomable(&self, _cx: &App) -> bool {
|
||||||
}
|
self.zoomable
|
||||||
|
}
|
||||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
|
||||||
menu.track_focus(&self.focus_handle)
|
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||||
}
|
menu.track_focus(&self.focus_handle)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl EventEmitter<PanelEvent> for Onboarding {}
|
|
||||||
|
impl EventEmitter<PanelEvent> for Onboarding {}
|
||||||
impl Focusable for Onboarding {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
impl Focusable for Onboarding {
|
||||||
self.focus_handle.clone()
|
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||||
}
|
self.focus_handle.clone()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl Render for Onboarding {
|
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
impl Render for Onboarding {
|
||||||
const TITLE: &str = "Welcome to Coop!";
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
const SUBTITLE: &str = "Secure Communication on Nostr.";
|
let auto_login = AppSettings::get_global(cx).settings.auto_login;
|
||||||
|
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||||
let auto_login = AppSettings::get_global(cx).settings.auto_login;
|
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
div()
|
||||||
|
.py_4()
|
||||||
div()
|
.size_full()
|
||||||
.py_4()
|
.flex()
|
||||||
.size_full()
|
.flex_col()
|
||||||
.flex()
|
.items_center()
|
||||||
.flex_col()
|
.justify_center()
|
||||||
.items_center()
|
.child(
|
||||||
.justify_center()
|
div()
|
||||||
.child(
|
.mb_10()
|
||||||
div()
|
.flex()
|
||||||
.mb_10()
|
.flex_col()
|
||||||
.flex()
|
.items_center()
|
||||||
.flex_col()
|
.gap_4()
|
||||||
.items_center()
|
.child(
|
||||||
.gap_4()
|
svg()
|
||||||
.child(
|
.path("brand/coop.svg")
|
||||||
svg()
|
.size_16()
|
||||||
.path("brand/coop.svg")
|
.text_color(cx.theme().elevated_surface_background),
|
||||||
.size_16()
|
)
|
||||||
.text_color(cx.theme().elevated_surface_background),
|
.child(
|
||||||
)
|
div()
|
||||||
.child(
|
.text_center()
|
||||||
div()
|
.child(
|
||||||
.text_center()
|
div()
|
||||||
.child(
|
.text_xl()
|
||||||
div()
|
.font_semibold()
|
||||||
.text_xl()
|
.line_height(relative(1.3))
|
||||||
.font_semibold()
|
.child(SharedString::new(t!("welcome.title"))),
|
||||||
.line_height(relative(1.3))
|
)
|
||||||
.child(TITLE),
|
.child(
|
||||||
)
|
div()
|
||||||
.child(div().text_color(cx.theme().text_muted).child(SUBTITLE)),
|
.text_color(cx.theme().text_muted)
|
||||||
),
|
.child(SharedString::new(t!("welcome.subtitle"))),
|
||||||
)
|
),
|
||||||
.map(|this| {
|
),
|
||||||
if let Some(profile) = self.local_account.read(cx).as_ref() {
|
)
|
||||||
this.relative()
|
.map(|this| {
|
||||||
.child(
|
if let Some(profile) = self.local_account.read(cx).as_ref() {
|
||||||
div()
|
this.relative()
|
||||||
.id("account")
|
.child(
|
||||||
.mb_3()
|
div()
|
||||||
.h_10()
|
.id("account")
|
||||||
.w_72()
|
.mb_3()
|
||||||
.bg(cx.theme().element_background)
|
.h_10()
|
||||||
.text_color(cx.theme().element_foreground)
|
.w_72()
|
||||||
.rounded_lg()
|
.bg(cx.theme().element_background)
|
||||||
.text_sm()
|
.text_color(cx.theme().element_foreground)
|
||||||
.map(|this| {
|
.rounded_lg()
|
||||||
if self.loading {
|
.text_sm()
|
||||||
this.child(
|
.map(|this| {
|
||||||
div()
|
if self.loading {
|
||||||
.size_full()
|
this.child(
|
||||||
.flex()
|
div()
|
||||||
.items_center()
|
.size_full()
|
||||||
.justify_center()
|
.flex()
|
||||||
.child(Indicator::new().small()),
|
.items_center()
|
||||||
)
|
.justify_center()
|
||||||
} else {
|
.child(Indicator::new().small()),
|
||||||
this.child(
|
)
|
||||||
div()
|
} else {
|
||||||
.h_full()
|
this.child(
|
||||||
.flex()
|
div()
|
||||||
.items_center()
|
.h_full()
|
||||||
.justify_center()
|
.flex()
|
||||||
.gap_2()
|
.items_center()
|
||||||
.child("Continue as")
|
.justify_center()
|
||||||
.child(
|
.gap_2()
|
||||||
div()
|
.child(SharedString::new(t!(
|
||||||
.flex()
|
"onboarding.choose_account"
|
||||||
.items_center()
|
)))
|
||||||
.gap_1()
|
.child(
|
||||||
.font_semibold()
|
div()
|
||||||
.child(
|
.flex()
|
||||||
Avatar::new(
|
.items_center()
|
||||||
profile.render_avatar(proxy),
|
.gap_1()
|
||||||
)
|
.font_semibold()
|
||||||
.size(rems(1.5)),
|
.child(
|
||||||
)
|
Avatar::new(
|
||||||
.child(
|
profile.render_avatar(proxy),
|
||||||
div()
|
)
|
||||||
.pb_px()
|
.size(rems(1.5)),
|
||||||
.child(profile.render_name()),
|
)
|
||||||
),
|
.child(
|
||||||
),
|
div()
|
||||||
)
|
.pb_px()
|
||||||
}
|
.child(profile.render_name()),
|
||||||
})
|
),
|
||||||
.hover(|this| this.bg(cx.theme().element_hover))
|
),
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
)
|
||||||
this.set_loading(true, cx);
|
}
|
||||||
Identity::global(cx).update(cx, |this, cx| {
|
})
|
||||||
this.load(window, cx);
|
.hover(|this| this.bg(cx.theme().element_hover))
|
||||||
});
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
})),
|
this.set_loading(true, cx);
|
||||||
)
|
Identity::global(cx).update(cx, |this, cx| {
|
||||||
.child(
|
this.load(window, cx);
|
||||||
Checkbox::new("auto_login")
|
});
|
||||||
.label("Automatically log in next time")
|
})),
|
||||||
.checked(auto_login)
|
)
|
||||||
.on_click(|_, _window, cx| {
|
.child(
|
||||||
AppSettings::global(cx).update(cx, |this, cx| {
|
Checkbox::new("auto_login")
|
||||||
this.settings.auto_login = !this.settings.auto_login;
|
.label(SharedString::new(t!("onboarding.auto_login")))
|
||||||
cx.notify();
|
.checked(auto_login)
|
||||||
})
|
.on_click(|_, _window, cx| {
|
||||||
}),
|
AppSettings::global(cx).update(cx, |this, cx| {
|
||||||
)
|
this.settings.auto_login = !this.settings.auto_login;
|
||||||
.child(
|
cx.notify();
|
||||||
div().w_24().absolute().bottom_4().right_4().child(
|
})
|
||||||
Button::new("unload")
|
}),
|
||||||
.icon(IconName::Logout)
|
)
|
||||||
.label("Logout")
|
.child(
|
||||||
.ghost()
|
div().w_24().absolute().bottom_4().right_4().child(
|
||||||
.small()
|
Button::new("unload")
|
||||||
.disabled(self.loading)
|
.icon(IconName::Logout)
|
||||||
.on_click(|_, window, cx| {
|
.label(SharedString::new(t!("common.logout")))
|
||||||
Identity::global(cx).update(cx, |this, cx| {
|
.ghost()
|
||||||
this.unload(window, cx);
|
.small()
|
||||||
});
|
.disabled(self.loading)
|
||||||
}),
|
.on_click(|_, window, cx| {
|
||||||
),
|
Identity::global(cx).update(cx, |this, cx| {
|
||||||
)
|
this.unload(window, cx);
|
||||||
} else {
|
});
|
||||||
this.child(
|
}),
|
||||||
div()
|
),
|
||||||
.w_72()
|
)
|
||||||
.flex()
|
} else {
|
||||||
.flex_col()
|
this.child(
|
||||||
.gap_2()
|
div()
|
||||||
.child(
|
.w_72()
|
||||||
Button::new("continue_btn")
|
.flex()
|
||||||
.icon(Icon::new(IconName::ArrowRight))
|
.flex_col()
|
||||||
.label("Start Messaging")
|
.gap_2()
|
||||||
.primary()
|
.child(
|
||||||
.reverse()
|
Button::new("continue_btn")
|
||||||
.on_click(cx.listener(move |_, _, window, cx| {
|
.icon(Icon::new(IconName::ArrowRight))
|
||||||
chatspace::new_account(window, cx);
|
.label(SharedString::new(t!("onboarding.start_messaging")))
|
||||||
})),
|
.primary()
|
||||||
)
|
.reverse()
|
||||||
.child(
|
.on_click(cx.listener(move |_, _, window, cx| {
|
||||||
Button::new("login_btn")
|
chatspace::new_account(window, cx);
|
||||||
.label("Already have an account? Log in.")
|
})),
|
||||||
.ghost()
|
)
|
||||||
.underline()
|
.child(
|
||||||
.on_click(cx.listener(move |_, _, window, cx| {
|
Button::new("login_btn")
|
||||||
chatspace::login(window, cx);
|
.label(SharedString::new(t!("onboarding.already_have_account")))
|
||||||
})),
|
.ghost()
|
||||||
),
|
.underline()
|
||||||
)
|
.on_click(cx.listener(move |_, _, window, cx| {
|
||||||
}
|
chatspace::login(window, cx);
|
||||||
})
|
})),
|
||||||
}
|
),
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ use gpui::http_client::Url;
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
div, px, relative, rems, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
||||||
IntoElement, ParentElement, Render, StatefulInteractiveElement, Styled, Window,
|
IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||||
};
|
};
|
||||||
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
@@ -50,9 +51,10 @@ impl Preferences {
|
|||||||
fn open_profile(&self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_profile(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let profile = profile::init(window, cx);
|
let profile = profile::init(window, cx);
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, _, _| {
|
window.open_modal(cx, move |modal, _window, _cx| {
|
||||||
|
let title = SharedString::new(t!("preferences.modal_profile_title"));
|
||||||
modal
|
modal
|
||||||
.title("Profile")
|
.title(title)
|
||||||
.width(px(DEFAULT_MODAL_WIDTH))
|
.width(px(DEFAULT_MODAL_WIDTH))
|
||||||
.child(profile.clone())
|
.child(profile.clone())
|
||||||
});
|
});
|
||||||
@@ -61,9 +63,10 @@ impl Preferences {
|
|||||||
fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let relays = relays::init(window, cx);
|
let relays = relays::init(window, cx);
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _, _| {
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
|
let title = SharedString::new(t!("preferences.modal_relays_title"));
|
||||||
this.width(px(DEFAULT_MODAL_WIDTH))
|
this.width(px(DEFAULT_MODAL_WIDTH))
|
||||||
.title("Edit your Messaging Relays")
|
.title(title)
|
||||||
.child(relays.clone())
|
.child(relays.clone())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -71,15 +74,6 @@ impl Preferences {
|
|||||||
|
|
||||||
impl Render for Preferences {
|
impl Render for Preferences {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
const MEDIA_DESCRIPTION: &str = "Coop only supports NIP-96 media servers for now. If you're not sure about it, please keep the default value.";
|
|
||||||
const BACKUP_DESCRIPTION: &str =
|
|
||||||
"When a user sends a message, Coop won't back it up to the user's messaging relays";
|
|
||||||
const TRUSTED_DESCRIPTION: &str = "Show trusted requests by default";
|
|
||||||
const HIDE_AVATAR_DESCRIPTION: &str =
|
|
||||||
"Unload all avatar pictures to improve performance and reduce memory usage";
|
|
||||||
const PROXY_DESCRIPTION: &str =
|
|
||||||
"Use wsrv.nl to resize and downscale avatar pictures (saves ~50MB of data)";
|
|
||||||
|
|
||||||
let input_state = self.media_input.downgrade();
|
let input_state = self.media_input.downgrade();
|
||||||
let settings = AppSettings::get_global(cx).settings.as_ref();
|
let settings = AppSettings::get_global(cx).settings.as_ref();
|
||||||
|
|
||||||
@@ -101,7 +95,7 @@ impl Render for Preferences {
|
|||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_placeholder)
|
.text_color(cx.theme().text_placeholder)
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child("Account"),
|
.child(SharedString::new(t!("preferences.account_header"))),
|
||||||
)
|
)
|
||||||
.when_some(Identity::get_global(cx).profile(), |this, profile| {
|
.when_some(Identity::get_global(cx).profile(), |this, profile| {
|
||||||
this.child(
|
this.child(
|
||||||
@@ -137,7 +131,9 @@ impl Render for Preferences {
|
|||||||
.line_height(relative(1.3))
|
.line_height(relative(1.3))
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child("See your profile"),
|
.child(SharedString::new(t!(
|
||||||
|
"preferences.see_your_profile"
|
||||||
|
))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
@@ -169,7 +165,7 @@ impl Render for Preferences {
|
|||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_placeholder)
|
.text_color(cx.theme().text_placeholder)
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child("Media Server"),
|
.child(SharedString::new(t!("preferences.media_server_header"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -186,7 +182,10 @@ impl Render for Preferences {
|
|||||||
if let Some(input) = input_state.upgrade() {
|
if let Some(input) = input_state.upgrade() {
|
||||||
let value = input.read(cx).value();
|
let value = input.read(cx).value();
|
||||||
let Ok(url) = Url::parse(value) else {
|
let Ok(url) = Url::parse(value) else {
|
||||||
window.push_notification("URL is not valid", cx);
|
window.push_notification(
|
||||||
|
t!("preferences.url_not_valid"),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,7 +201,7 @@ impl Render for Preferences {
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(MEDIA_DESCRIPTION),
|
.child(SharedString::new(t!("preferences.media_description"))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -218,39 +217,22 @@ impl Render for Preferences {
|
|||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_placeholder)
|
.text_color(cx.theme().text_placeholder)
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child("Messages"),
|
.child(SharedString::new(t!("preferences.messages_header"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div().flex().flex_col().gap_2().child(
|
||||||
.flex()
|
Switch::new("backup_messages")
|
||||||
.flex_col()
|
.label(t!("preferences.backup_messages_label"))
|
||||||
.gap_2()
|
.description(t!("preferences.backup_description"))
|
||||||
.child(
|
.checked(settings.backup_messages)
|
||||||
Switch::new("backup_messages")
|
.on_click(|_, _window, cx| {
|
||||||
.label("Backup messages")
|
AppSettings::global(cx).update(cx, |this, cx| {
|
||||||
.description(BACKUP_DESCRIPTION)
|
this.settings.backup_messages =
|
||||||
.checked(settings.backup_messages)
|
!this.settings.backup_messages;
|
||||||
.on_click(|_, _window, cx| {
|
cx.notify();
|
||||||
AppSettings::global(cx).update(cx, |this, cx| {
|
})
|
||||||
this.settings.backup_messages =
|
}),
|
||||||
!this.settings.backup_messages;
|
),
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Switch::new("only_show_trusted")
|
|
||||||
.label("Only trusted")
|
|
||||||
.description(TRUSTED_DESCRIPTION)
|
|
||||||
.checked(settings.only_show_trusted)
|
|
||||||
.on_click(|_, _window, cx| {
|
|
||||||
AppSettings::global(cx).update(cx, |this, cx| {
|
|
||||||
this.settings.only_show_trusted =
|
|
||||||
!this.settings.only_show_trusted;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -266,7 +248,7 @@ impl Render for Preferences {
|
|||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_placeholder)
|
.text_color(cx.theme().text_placeholder)
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child("Display"),
|
.child(SharedString::new(t!("preferences.display_header"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -275,8 +257,8 @@ impl Render for Preferences {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
Switch::new("hide_user_avatars")
|
Switch::new("hide_user_avatars")
|
||||||
.label("Hide user avatars")
|
.label(t!("preferences.hide_avatars_label"))
|
||||||
.description(HIDE_AVATAR_DESCRIPTION)
|
.description(t!("preferences.hide_avatar_description"))
|
||||||
.checked(settings.hide_user_avatars)
|
.checked(settings.hide_user_avatars)
|
||||||
.on_click(|_, _window, cx| {
|
.on_click(|_, _window, cx| {
|
||||||
AppSettings::global(cx).update(cx, |this, cx| {
|
AppSettings::global(cx).update(cx, |this, cx| {
|
||||||
@@ -288,8 +270,8 @@ impl Render for Preferences {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Switch::new("proxy_user_avatars")
|
Switch::new("proxy_user_avatars")
|
||||||
.label("Proxy user avatars")
|
.label(t!("preferences.proxy_avatars_label"))
|
||||||
.description(PROXY_DESCRIPTION)
|
.description(t!("preferences.proxy_description"))
|
||||||
.checked(settings.proxy_user_avatars)
|
.checked(settings.proxy_user_avatars)
|
||||||
.on_click(|_, _window, cx| {
|
.on_click(|_, _window, cx| {
|
||||||
AppSettings::global(cx).update(cx, |this, cx| {
|
AppSettings::global(cx).update(cx, |this, cx| {
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ use global::shared_state;
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
||||||
PathPromptOptions, Render, Styled, Task, Window,
|
PathPromptOptions, Render, SharedString, Styled, Task, Window,
|
||||||
};
|
};
|
||||||
|
use i18n::t;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
@@ -32,7 +33,8 @@ pub struct Profile {
|
|||||||
|
|
||||||
impl Profile {
|
impl Profile {
|
||||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
|
let name_input =
|
||||||
|
cx.new(|cx| InputState::new(window, cx).placeholder(t!("profile.placeholder_name")));
|
||||||
let avatar_input =
|
let avatar_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
|
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
|
||||||
let website_input =
|
let website_input =
|
||||||
@@ -40,7 +42,7 @@ impl Profile {
|
|||||||
let bio_input = cx.new(|cx| {
|
let bio_input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line()
|
||||||
.placeholder("A short introduce about you.")
|
.placeholder(t!("profile.placeholder_bio"))
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
@@ -124,10 +126,8 @@ impl Profile {
|
|||||||
let (tx, rx) = oneshot::channel::<Url>();
|
let (tx, rx) = oneshot::channel::<Url>();
|
||||||
|
|
||||||
nostr_sdk::async_utility::task::spawn(async move {
|
nostr_sdk::async_utility::task::spawn(async move {
|
||||||
if let Ok(url) =
|
let client = shared_state().client();
|
||||||
nip96_upload(shared_state().client(), &nip96_server, file_data)
|
if let Ok(url) = nip96_upload(client, &nip96_server, file_data).await {
|
||||||
.await
|
|
||||||
{
|
|
||||||
_ = tx.send(url);
|
_ = tx.send(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -197,17 +197,23 @@ impl Profile {
|
|||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| match task.await {
|
||||||
if task.await.is_ok() {
|
Ok(_) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
|
window.push_notification(t!("profile.updated_successfully"), cx);
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_submitting(false, cx);
|
this.set_submitting(false, cx);
|
||||||
window.push_notification("Your profile has been updated successfully", cx);
|
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
@@ -263,7 +269,7 @@ impl Render for Profile {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("upload")
|
Button::new("upload")
|
||||||
.icon(IconName::Upload)
|
.icon(IconName::Upload)
|
||||||
.label("Change")
|
.label(t!("common.change"))
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.disabled(self.is_loading || self.is_submitting)
|
.disabled(self.is_loading || self.is_submitting)
|
||||||
@@ -279,7 +285,7 @@ impl Render for Profile {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child("Name:")
|
.child(SharedString::new(t!("profile.label_name")))
|
||||||
.child(TextInput::new(&self.name_input).small()),
|
.child(TextInput::new(&self.name_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -288,7 +294,7 @@ impl Render for Profile {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child("Website:")
|
.child(SharedString::new(t!("profile.label_website")))
|
||||||
.child(TextInput::new(&self.website_input).small()),
|
.child(TextInput::new(&self.website_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -297,13 +303,13 @@ impl Render for Profile {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child("Bio:")
|
.child(SharedString::new(t!("profile.label_bio")))
|
||||||
.child(TextInput::new(&self.bio_input).small()),
|
.child(TextInput::new(&self.bio_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().py_3().child(
|
div().py_3().child(
|
||||||
Button::new("submit")
|
Button::new("submit")
|
||||||
.label("Update")
|
.label(SharedString::new(t!("common.update")))
|
||||||
.primary()
|
.primary()
|
||||||
.disabled(self.is_loading || self.is_submitting)
|
.disabled(self.is_loading || self.is_submitting)
|
||||||
.loading(self.is_submitting)
|
.loading(self.is_submitting)
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use global::shared_state;
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
||||||
IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign, UniformList, Window,
|
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, TextAlign,
|
||||||
|
UniformList, Window,
|
||||||
};
|
};
|
||||||
|
use i18n::t;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
@@ -14,8 +16,6 @@ use ui::input::{InputEvent, InputState, TextInput};
|
|||||||
use ui::{ContextModal, Disableable, IconName, Sizable};
|
use ui::{ContextModal, Disableable, IconName, Sizable};
|
||||||
|
|
||||||
const MIN_HEIGHT: f32 = 200.0;
|
const MIN_HEIGHT: f32 = 200.0;
|
||||||
const MESSAGE: &str = "In order to receive messages from others, you need to setup at least one Messaging Relay. You can use the recommend relays or add more.";
|
|
||||||
const HELP_TEXT: &str = "Please add some relays.";
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Relays> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Relays> {
|
||||||
Relays::new(window, cx)
|
Relays::new(window, cx)
|
||||||
@@ -270,7 +270,7 @@ impl Relays {
|
|||||||
.justify_center()
|
.justify_center()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_align(TextAlign::Center)
|
.text_align(TextAlign::Center)
|
||||||
.child(HELP_TEXT)
|
.child(SharedString::new(t!("relays.add_some_relays")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@ impl Render for Relays {
|
|||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(MESSAGE),
|
.child(SharedString::new(t!("relays.description"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -312,7 +312,7 @@ impl Render for Relays {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("add_relay_btn")
|
Button::new("add_relay_btn")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
.label("Add")
|
.label(t!("common.add"))
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
@@ -334,7 +334,7 @@ impl Render for Relays {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("submti")
|
Button::new("submti")
|
||||||
.label("Update")
|
.label(t!("common.update"))
|
||||||
.primary()
|
.primary()
|
||||||
.w_full()
|
.w_full()
|
||||||
.loading(self.is_loading)
|
.loading(self.is_loading)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,131 +1,131 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||||
};
|
};
|
||||||
use identity::Identity;
|
use i18n::t;
|
||||||
use theme::ActiveTheme;
|
use identity::Identity;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use theme::ActiveTheme;
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::indicator::Indicator;
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::popup_menu::PopupMenu;
|
use ui::indicator::Indicator;
|
||||||
use ui::{Sizable, StyledExt};
|
use ui::popup_menu::PopupMenu;
|
||||||
|
use ui::{Sizable, StyledExt};
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Startup> {
|
|
||||||
Startup::new(window, cx)
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Startup> {
|
||||||
}
|
Startup::new(window, cx)
|
||||||
|
}
|
||||||
pub struct Startup {
|
|
||||||
name: SharedString,
|
pub struct Startup {
|
||||||
focus_handle: FocusHandle,
|
name: SharedString,
|
||||||
}
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
impl Startup {
|
|
||||||
fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
|
impl Startup {
|
||||||
cx.new(|cx| Self {
|
fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
name: "Welcome".into(),
|
cx.new(|cx| Self {
|
||||||
focus_handle: cx.focus_handle(),
|
name: "Startup".into(),
|
||||||
})
|
focus_handle: cx.focus_handle(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl Panel for Startup {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
impl Panel for Startup {
|
||||||
self.name.clone()
|
fn panel_id(&self) -> SharedString {
|
||||||
}
|
self.name.clone()
|
||||||
|
}
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
"Startup".into_any_element()
|
fn title(&self, _cx: &App) -> AnyElement {
|
||||||
}
|
self.name.clone().into_any_element()
|
||||||
|
}
|
||||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
|
||||||
menu.track_focus(&self.focus_handle)
|
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||||
}
|
menu.track_focus(&self.focus_handle)
|
||||||
|
}
|
||||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
|
||||||
vec![]
|
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||||
}
|
vec![]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl EventEmitter<PanelEvent> for Startup {}
|
|
||||||
|
impl EventEmitter<PanelEvent> for Startup {}
|
||||||
impl Focusable for Startup {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
impl Focusable for Startup {
|
||||||
self.focus_handle.clone()
|
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||||
}
|
self.focus_handle.clone()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl Render for Startup {
|
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
impl Render for Startup {
|
||||||
let identity = Identity::global(cx);
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let logging_in = identity.read(cx).logging_in();
|
let identity = Identity::global(cx);
|
||||||
|
let logging_in = identity.read(cx).logging_in();
|
||||||
div()
|
|
||||||
.relative()
|
div()
|
||||||
.size_full()
|
.relative()
|
||||||
.flex()
|
.size_full()
|
||||||
.items_center()
|
.flex()
|
||||||
.justify_center()
|
.items_center()
|
||||||
.child(
|
.justify_center()
|
||||||
div()
|
.child(
|
||||||
.flex()
|
div()
|
||||||
.flex_col()
|
.flex()
|
||||||
.items_center()
|
.flex_col()
|
||||||
.justify_center()
|
.items_center()
|
||||||
.text_center()
|
.justify_center()
|
||||||
.gap_6()
|
.text_center()
|
||||||
.child(
|
.gap_6()
|
||||||
svg()
|
.child(
|
||||||
.path("brand/coop.svg")
|
svg()
|
||||||
.size_12()
|
.path("brand/coop.svg")
|
||||||
.text_color(cx.theme().elevated_surface_background),
|
.size_12()
|
||||||
)
|
.text_color(cx.theme().elevated_surface_background),
|
||||||
.child(
|
)
|
||||||
div()
|
.child(
|
||||||
.w_24()
|
div()
|
||||||
.flex()
|
.w_24()
|
||||||
.items_center()
|
.flex()
|
||||||
.justify_center()
|
.items_center()
|
||||||
.gap_2()
|
.justify_center()
|
||||||
.when(logging_in, |this| {
|
.gap_2()
|
||||||
this.child(
|
.when(logging_in, |this| {
|
||||||
div()
|
this.child(
|
||||||
.text_sm()
|
div().text_sm().text_color(cx.theme().text).child(
|
||||||
.text_color(cx.theme().text)
|
SharedString::new(t!("startup.auto_login_in_progress")),
|
||||||
.child("Auto login in progress"),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(Indicator::new().small()),
|
.child(Indicator::new().small()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().absolute().bottom_3().right_3().child(
|
div().absolute().bottom_3().right_3().child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child("Stuck?"),
|
.child(SharedString::new(t!("startup.stuck"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("reset")
|
Button::new("reset")
|
||||||
.label("Reset")
|
.label(SharedString::new(t!("startup.reset")))
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
Identity::global(cx).update(cx, |this, cx| {
|
Identity::global(cx).update(cx, |this, cx| {
|
||||||
this.unload(window, cx);
|
this.unload(window, cx);
|
||||||
// Restart application
|
// Restart application
|
||||||
cx.restart(None);
|
cx.restart(None);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,103 @@
|
|||||||
use chats::ChatRegistry;
|
use chats::ChatRegistry;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
div, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
||||||
ParentElement, Render, Styled, Window,
|
ParentElement, Render, SharedString, Styled, Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use i18n::t;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use theme::ActiveTheme;
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::{ContextModal, Sizable};
|
use ui::input::{InputState, TextInput};
|
||||||
|
use ui::{ContextModal, Sizable};
|
||||||
pub fn init(
|
|
||||||
id: u64,
|
pub fn init(
|
||||||
subject: Option<String>,
|
id: u64,
|
||||||
window: &mut Window,
|
subject: Option<String>,
|
||||||
cx: &mut App,
|
window: &mut Window,
|
||||||
) -> Entity<Subject> {
|
cx: &mut App,
|
||||||
Subject::new(id, subject, window, cx)
|
) -> Entity<Subject> {
|
||||||
}
|
Subject::new(id, subject, window, cx)
|
||||||
|
}
|
||||||
pub struct Subject {
|
|
||||||
id: u64,
|
pub struct Subject {
|
||||||
input: Entity<InputState>,
|
id: u64,
|
||||||
focus_handle: FocusHandle,
|
input: Entity<InputState>,
|
||||||
}
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
impl Subject {
|
|
||||||
pub fn new(
|
impl Subject {
|
||||||
id: u64,
|
pub fn new(
|
||||||
subject: Option<String>,
|
id: u64,
|
||||||
window: &mut Window,
|
subject: Option<String>,
|
||||||
cx: &mut App,
|
window: &mut Window,
|
||||||
) -> Entity<Self> {
|
cx: &mut App,
|
||||||
let input = cx.new(|cx| {
|
) -> Entity<Self> {
|
||||||
let mut this = InputState::new(window, cx).placeholder("Exciting Project...");
|
let input = cx.new(|cx| {
|
||||||
if let Some(text) = subject.clone() {
|
let mut this = InputState::new(window, cx).placeholder(t!("subject.placeholder"));
|
||||||
this.set_value(text, window, cx);
|
if let Some(text) = subject.clone() {
|
||||||
}
|
this.set_value(text, window, cx);
|
||||||
this
|
}
|
||||||
});
|
this
|
||||||
|
});
|
||||||
cx.new(|cx| Self {
|
|
||||||
id,
|
cx.new(|cx| Self {
|
||||||
input,
|
id,
|
||||||
focus_handle: cx.focus_handle(),
|
input,
|
||||||
})
|
focus_handle: cx.focus_handle(),
|
||||||
}
|
})
|
||||||
|
}
|
||||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let registry = ChatRegistry::global(cx).read(cx);
|
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let subject = self.input.read(cx).value().clone();
|
let registry = ChatRegistry::global(cx).read(cx);
|
||||||
|
let subject = self.input.read(cx).value().clone();
|
||||||
if let Some(room) = registry.room(&self.id, cx) {
|
|
||||||
room.update(cx, |this, cx| {
|
if let Some(room) = registry.room(&self.id, cx) {
|
||||||
this.subject = Some(subject);
|
room.update(cx, |this, cx| {
|
||||||
cx.notify();
|
this.subject = Some(subject);
|
||||||
});
|
cx.notify();
|
||||||
window.close_modal(cx);
|
});
|
||||||
} else {
|
window.close_modal(cx);
|
||||||
window.push_notification("Room not found", cx);
|
} else {
|
||||||
}
|
window.push_notification(SharedString::new(t!("subject.room_not_found")), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl Render for Subject {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
impl Render for Subject {
|
||||||
const HELP_TEXT: &str = "Subject will be updated when you send a message.";
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
div()
|
.track_focus(&self.focus_handle)
|
||||||
.track_focus(&self.focus_handle)
|
.size_full()
|
||||||
.size_full()
|
.flex()
|
||||||
.flex()
|
.flex_col()
|
||||||
.flex_col()
|
.gap_3()
|
||||||
.gap_3()
|
.px_3()
|
||||||
.px_3()
|
.pb_3()
|
||||||
.pb_3()
|
.child(
|
||||||
.child(
|
div()
|
||||||
div()
|
.flex()
|
||||||
.flex()
|
.flex_col()
|
||||||
.flex_col()
|
.gap_1()
|
||||||
.gap_1()
|
.child(
|
||||||
.child(
|
div()
|
||||||
div()
|
.text_sm()
|
||||||
.text_sm()
|
.text_color(cx.theme().text_muted)
|
||||||
.text_color(cx.theme().text_muted)
|
.child(SharedString::new(t!("subject.title"))),
|
||||||
.child("Subject:"),
|
)
|
||||||
)
|
.child(TextInput::new(&self.input).small())
|
||||||
.child(TextInput::new(&self.input).small())
|
.child(
|
||||||
.child(
|
div()
|
||||||
div()
|
.text_xs()
|
||||||
.text_xs()
|
.italic()
|
||||||
.italic()
|
.text_color(cx.theme().text_placeholder)
|
||||||
.text_color(cx.theme().text_placeholder)
|
.child(SharedString::new(t!("subject.help_text"))),
|
||||||
.child(HELP_TEXT),
|
),
|
||||||
),
|
)
|
||||||
)
|
.child(
|
||||||
.child(
|
Button::new("submit")
|
||||||
Button::new("submit")
|
.label(t!("common.change"))
|
||||||
.label("Change")
|
.primary()
|
||||||
.primary()
|
.w_full()
|
||||||
.w_full()
|
.on_click(cx.listener(|this, _, window, cx| this.update(window, cx))),
|
||||||
.on_click(cx.listener(|this, _, window, cx| this.update(window, cx))),
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ impl Render for Welcome {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.child("coop on nostr.")
|
.child("coop on nostr")
|
||||||
.text_color(cx.theme().text_placeholder)
|
.text_color(cx.theme().text_placeholder)
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_sm(),
|
.text_sm(),
|
||||||
|
|||||||
8
crates/i18n/Cargo.toml
Normal file
8
crates/i18n/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "i18n"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rust-i18n.workspace = true
|
||||||
30
crates/i18n/src/lib.rs
Normal file
30
crates/i18n/src/lib.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use rust_i18n::Backend;
|
||||||
|
|
||||||
|
rust_i18n::i18n!("../../locales");
|
||||||
|
|
||||||
|
pub struct I18nBackend;
|
||||||
|
|
||||||
|
impl Backend for I18nBackend {
|
||||||
|
fn available_locales(&self) -> Vec<&str> {
|
||||||
|
_RUST_I18N_BACKEND.available_locales()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn translate(&self, locale: &str, key: &str) -> Option<&str> {
|
||||||
|
let val = _RUST_I18N_BACKEND.translate(locale, key);
|
||||||
|
if val.is_none() {
|
||||||
|
_RUST_I18N_BACKEND.translate("en", key)
|
||||||
|
} else {
|
||||||
|
val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! init {
|
||||||
|
() => {
|
||||||
|
rust_i18n::i18n!(backend = i18n::I18nBackend);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use rust_i18n::set_locale;
|
||||||
|
pub use rust_i18n::t;
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "identity"
|
name = "identity"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
client_keys = { path = "../client_keys" }
|
client_keys = { path = "../client_keys" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
rust-i18n.workspace = true
|
||||||
nostr-connect.workspace = true
|
i18n.workspace = true
|
||||||
oneshot.workspace = true
|
nostr-sdk.workspace = true
|
||||||
gpui.workspace = true
|
nostr-connect.workspace = true
|
||||||
anyhow.workspace = true
|
oneshot.workspace = true
|
||||||
log.workspace = true
|
gpui.workspace = true
|
||||||
smallvec.workspace = true
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ use ui::input::{InputState, TextInput};
|
|||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{ContextModal, Sizable};
|
use ui::{ContextModal, Sizable};
|
||||||
|
|
||||||
|
i18n::init!();
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) {
|
pub fn init(window: &mut Window, cx: &mut App) {
|
||||||
Identity::set_global(cx.new(|cx| Identity::new(window, cx)), cx);
|
Identity::set_global(cx.new(|cx| Identity::new(window, cx)), cx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "settings"
|
name = "settings"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
rust-i18n.workspace = true
|
||||||
gpui.workspace = true
|
i18n.workspace = true
|
||||||
anyhow.workspace = true
|
nostr-sdk.workspace = true
|
||||||
log.workspace = true
|
gpui.workspace = true
|
||||||
smallvec.workspace = true
|
anyhow.workspace = true
|
||||||
serde.workspace = true
|
log.workspace = true
|
||||||
serde_json.workspace = true
|
smallvec.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use nostr_sdk::prelude::*;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
|
i18n::init!();
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
let state = cx.new(AppSettings::new);
|
let state = cx.new(AppSettings::new);
|
||||||
|
|
||||||
@@ -25,7 +27,6 @@ pub struct Settings {
|
|||||||
pub media_server: Url,
|
pub media_server: Url,
|
||||||
pub proxy_user_avatars: bool,
|
pub proxy_user_avatars: bool,
|
||||||
pub hide_user_avatars: bool,
|
pub hide_user_avatars: bool,
|
||||||
pub only_show_trusted: bool,
|
|
||||||
pub backup_messages: bool,
|
pub backup_messages: bool,
|
||||||
pub auto_login: bool,
|
pub auto_login: bool,
|
||||||
}
|
}
|
||||||
@@ -67,7 +68,6 @@ impl AppSettings {
|
|||||||
media_server: Url::parse("https://nostrmedia.com").expect("it's fine"),
|
media_server: Url::parse("https://nostrmedia.com").expect("it's fine"),
|
||||||
proxy_user_avatars: true,
|
proxy_user_avatars: true,
|
||||||
hide_user_avatars: false,
|
hide_user_avatars: false,
|
||||||
only_show_trusted: false,
|
|
||||||
backup_messages: true,
|
backup_messages: true,
|
||||||
auto_login: false,
|
auto_login: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ui"
|
name = "ui"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
rust-i18n.workspace = true
|
||||||
gpui.workspace = true
|
i18n.workspace = true
|
||||||
smol.workspace = true
|
nostr-sdk.workspace = true
|
||||||
serde.workspace = true
|
gpui.workspace = true
|
||||||
serde_json.workspace = true
|
smol.workspace = true
|
||||||
smallvec.workspace = true
|
serde.workspace = true
|
||||||
anyhow.workspace = true
|
serde_json.workspace = true
|
||||||
itertools.workspace = true
|
smallvec.workspace = true
|
||||||
chrono.workspace = true
|
anyhow.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
paste = "1"
|
chrono.workspace = true
|
||||||
regex = "1"
|
|
||||||
unicode-segmentation = "1.12.0"
|
paste = "1"
|
||||||
uuid = "1.10"
|
regex = "1"
|
||||||
once_cell = "1.19.0"
|
unicode-segmentation = "1.12.0"
|
||||||
image = "0.25.1"
|
uuid = "1.10"
|
||||||
linkify = "0.10.0"
|
once_cell = "1.19.0"
|
||||||
emojis.workspace = true
|
image = "0.25.1"
|
||||||
|
linkify = "0.10.0"
|
||||||
|
emojis.workspace = true
|
||||||
|
|||||||
@@ -1,312 +1,314 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
||||||
SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::{Sizable, Size};
|
use crate::{Sizable, Size};
|
||||||
|
|
||||||
#[derive(IntoElement, Clone)]
|
#[derive(IntoElement, Clone)]
|
||||||
pub enum IconName {
|
pub enum IconName {
|
||||||
AddressBook,
|
AddressBook,
|
||||||
ArrowIn,
|
ArrowIn,
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
ArrowUp,
|
ArrowUp,
|
||||||
ArrowUpCircle,
|
ArrowUpCircle,
|
||||||
Bell,
|
Bell,
|
||||||
CaretUp,
|
CaretUp,
|
||||||
CaretDown,
|
CaretDown,
|
||||||
CaretDownFill,
|
CaretDownFill,
|
||||||
CaretRight,
|
CaretRight,
|
||||||
Check,
|
Check,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
CheckCircleFill,
|
CheckCircleFill,
|
||||||
Close,
|
Close,
|
||||||
CloseCircle,
|
CloseCircle,
|
||||||
CloseCircleFill,
|
CloseCircleFill,
|
||||||
Copy,
|
Copy,
|
||||||
EditFill,
|
EditFill,
|
||||||
Ellipsis,
|
Ellipsis,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
EmojiFill,
|
EmojiFill,
|
||||||
Folder,
|
Folder,
|
||||||
FolderFill,
|
FolderFill,
|
||||||
Filter,
|
Filter,
|
||||||
FilterFill,
|
FilterFill,
|
||||||
Inbox,
|
Inbox,
|
||||||
Info,
|
Info,
|
||||||
Loader,
|
Language,
|
||||||
Logout,
|
Loader,
|
||||||
Moon,
|
Logout,
|
||||||
PanelBottom,
|
Moon,
|
||||||
PanelBottomOpen,
|
PanelBottom,
|
||||||
PanelLeft,
|
PanelBottomOpen,
|
||||||
PanelLeftClose,
|
PanelLeft,
|
||||||
PanelLeftOpen,
|
PanelLeftClose,
|
||||||
PanelRight,
|
PanelLeftOpen,
|
||||||
PanelRightClose,
|
PanelRight,
|
||||||
PanelRightOpen,
|
PanelRightClose,
|
||||||
Plus,
|
PanelRightOpen,
|
||||||
PlusFill,
|
Plus,
|
||||||
PlusCircleFill,
|
PlusFill,
|
||||||
Relays,
|
PlusCircleFill,
|
||||||
ResizeCorner,
|
Relays,
|
||||||
Reply,
|
ResizeCorner,
|
||||||
Forward,
|
Reply,
|
||||||
Search,
|
Forward,
|
||||||
SearchFill,
|
Search,
|
||||||
Settings,
|
SearchFill,
|
||||||
SortAscending,
|
Settings,
|
||||||
SortDescending,
|
SortAscending,
|
||||||
Sun,
|
SortDescending,
|
||||||
Toggle,
|
Sun,
|
||||||
ToggleFill,
|
Toggle,
|
||||||
ThumbsDown,
|
ToggleFill,
|
||||||
ThumbsUp,
|
ThumbsDown,
|
||||||
Upload,
|
ThumbsUp,
|
||||||
UsersThreeFill,
|
Upload,
|
||||||
WindowClose,
|
UsersThreeFill,
|
||||||
WindowMaximize,
|
WindowClose,
|
||||||
WindowMinimize,
|
WindowMaximize,
|
||||||
WindowRestore,
|
WindowMinimize,
|
||||||
}
|
WindowRestore,
|
||||||
|
}
|
||||||
impl IconName {
|
|
||||||
pub fn path(self) -> SharedString {
|
impl IconName {
|
||||||
match self {
|
pub fn path(self) -> SharedString {
|
||||||
Self::AddressBook => "icons/address-book.svg",
|
match self {
|
||||||
Self::ArrowIn => "icons/arrows-in.svg",
|
Self::AddressBook => "icons/address-book.svg",
|
||||||
Self::ArrowDown => "icons/arrow-down.svg",
|
Self::ArrowIn => "icons/arrows-in.svg",
|
||||||
Self::ArrowLeft => "icons/arrow-left.svg",
|
Self::ArrowDown => "icons/arrow-down.svg",
|
||||||
Self::ArrowRight => "icons/arrow-right.svg",
|
Self::ArrowLeft => "icons/arrow-left.svg",
|
||||||
Self::ArrowUp => "icons/arrow-up.svg",
|
Self::ArrowRight => "icons/arrow-right.svg",
|
||||||
Self::ArrowUpCircle => "icons/arrow-up-circle.svg",
|
Self::ArrowUp => "icons/arrow-up.svg",
|
||||||
Self::Bell => "icons/bell.svg",
|
Self::ArrowUpCircle => "icons/arrow-up-circle.svg",
|
||||||
Self::CaretRight => "icons/caret-right.svg",
|
Self::Bell => "icons/bell.svg",
|
||||||
Self::CaretUp => "icons/caret-up.svg",
|
Self::CaretRight => "icons/caret-right.svg",
|
||||||
Self::CaretDown => "icons/caret-down.svg",
|
Self::CaretUp => "icons/caret-up.svg",
|
||||||
Self::CaretDownFill => "icons/caret-down-fill.svg",
|
Self::CaretDown => "icons/caret-down.svg",
|
||||||
Self::Check => "icons/check.svg",
|
Self::CaretDownFill => "icons/caret-down-fill.svg",
|
||||||
Self::CheckCircle => "icons/check-circle.svg",
|
Self::Check => "icons/check.svg",
|
||||||
Self::CheckCircleFill => "icons/check-circle-fill.svg",
|
Self::CheckCircle => "icons/check-circle.svg",
|
||||||
Self::Close => "icons/close.svg",
|
Self::CheckCircleFill => "icons/check-circle-fill.svg",
|
||||||
Self::CloseCircle => "icons/close-circle.svg",
|
Self::Close => "icons/close.svg",
|
||||||
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
Self::CloseCircle => "icons/close-circle.svg",
|
||||||
Self::Copy => "icons/copy.svg",
|
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
||||||
Self::EditFill => "icons/edit-fill.svg",
|
Self::Copy => "icons/copy.svg",
|
||||||
Self::Ellipsis => "icons/ellipsis.svg",
|
Self::EditFill => "icons/edit-fill.svg",
|
||||||
Self::Eye => "icons/eye.svg",
|
Self::Ellipsis => "icons/ellipsis.svg",
|
||||||
Self::EmojiFill => "icons/emoji-fill.svg",
|
Self::Eye => "icons/eye.svg",
|
||||||
Self::EyeOff => "icons/eye-off.svg",
|
Self::EmojiFill => "icons/emoji-fill.svg",
|
||||||
Self::Folder => "icons/folder.svg",
|
Self::EyeOff => "icons/eye-off.svg",
|
||||||
Self::FolderFill => "icons/folder-fill.svg",
|
Self::Folder => "icons/folder.svg",
|
||||||
Self::Filter => "icons/filter.svg",
|
Self::FolderFill => "icons/folder-fill.svg",
|
||||||
Self::FilterFill => "icons/filter-fill.svg",
|
Self::Filter => "icons/filter.svg",
|
||||||
Self::Inbox => "icons/inbox.svg",
|
Self::FilterFill => "icons/filter-fill.svg",
|
||||||
Self::Info => "icons/info.svg",
|
Self::Inbox => "icons/inbox.svg",
|
||||||
Self::Loader => "icons/loader.svg",
|
Self::Info => "icons/info.svg",
|
||||||
Self::Logout => "icons/logout.svg",
|
Self::Language => "icons/language.svg",
|
||||||
Self::Moon => "icons/moon.svg",
|
Self::Loader => "icons/loader.svg",
|
||||||
Self::PanelBottom => "icons/panel-bottom.svg",
|
Self::Logout => "icons/logout.svg",
|
||||||
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
Self::Moon => "icons/moon.svg",
|
||||||
Self::PanelLeft => "icons/panel-left.svg",
|
Self::PanelBottom => "icons/panel-bottom.svg",
|
||||||
Self::PanelLeftClose => "icons/panel-left-close.svg",
|
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
||||||
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
Self::PanelLeft => "icons/panel-left.svg",
|
||||||
Self::PanelRight => "icons/panel-right.svg",
|
Self::PanelLeftClose => "icons/panel-left-close.svg",
|
||||||
Self::PanelRightClose => "icons/panel-right-close.svg",
|
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
||||||
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
Self::PanelRight => "icons/panel-right.svg",
|
||||||
Self::Plus => "icons/plus.svg",
|
Self::PanelRightClose => "icons/panel-right-close.svg",
|
||||||
Self::PlusFill => "icons/plus-fill.svg",
|
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
||||||
Self::PlusCircleFill => "icons/plus-circle-fill.svg",
|
Self::Plus => "icons/plus.svg",
|
||||||
Self::Relays => "icons/relays.svg",
|
Self::PlusFill => "icons/plus-fill.svg",
|
||||||
Self::ResizeCorner => "icons/resize-corner.svg",
|
Self::PlusCircleFill => "icons/plus-circle-fill.svg",
|
||||||
Self::Reply => "icons/reply.svg",
|
Self::Relays => "icons/relays.svg",
|
||||||
Self::Forward => "icons/forward.svg",
|
Self::ResizeCorner => "icons/resize-corner.svg",
|
||||||
Self::Search => "icons/search.svg",
|
Self::Reply => "icons/reply.svg",
|
||||||
Self::SearchFill => "icons/search-fill.svg",
|
Self::Forward => "icons/forward.svg",
|
||||||
Self::Settings => "icons/settings.svg",
|
Self::Search => "icons/search.svg",
|
||||||
Self::SortAscending => "icons/sort-ascending.svg",
|
Self::SearchFill => "icons/search-fill.svg",
|
||||||
Self::SortDescending => "icons/sort-descending.svg",
|
Self::Settings => "icons/settings.svg",
|
||||||
Self::Sun => "icons/sun.svg",
|
Self::SortAscending => "icons/sort-ascending.svg",
|
||||||
Self::Toggle => "icons/toggle.svg",
|
Self::SortDescending => "icons/sort-descending.svg",
|
||||||
Self::ToggleFill => "icons/toggle-fill.svg",
|
Self::Sun => "icons/sun.svg",
|
||||||
Self::ThumbsDown => "icons/thumbs-down.svg",
|
Self::Toggle => "icons/toggle.svg",
|
||||||
Self::ThumbsUp => "icons/thumbs-up.svg",
|
Self::ToggleFill => "icons/toggle-fill.svg",
|
||||||
Self::Upload => "icons/upload.svg",
|
Self::ThumbsDown => "icons/thumbs-down.svg",
|
||||||
Self::UsersThreeFill => "icons/users-three-fill.svg",
|
Self::ThumbsUp => "icons/thumbs-up.svg",
|
||||||
Self::WindowClose => "icons/window-close.svg",
|
Self::Upload => "icons/upload.svg",
|
||||||
Self::WindowMaximize => "icons/window-maximize.svg",
|
Self::UsersThreeFill => "icons/users-three-fill.svg",
|
||||||
Self::WindowMinimize => "icons/window-minimize.svg",
|
Self::WindowClose => "icons/window-close.svg",
|
||||||
Self::WindowRestore => "icons/window-restore.svg",
|
Self::WindowMaximize => "icons/window-maximize.svg",
|
||||||
}
|
Self::WindowMinimize => "icons/window-minimize.svg",
|
||||||
.into()
|
Self::WindowRestore => "icons/window-restore.svg",
|
||||||
}
|
}
|
||||||
|
.into()
|
||||||
/// Return the icon as a Entity<Icon>
|
}
|
||||||
pub fn view(self, window: &mut Window, cx: &mut App) -> Entity<Icon> {
|
|
||||||
Icon::build(self).view(window, cx)
|
/// Return the icon as a Entity<Icon>
|
||||||
}
|
pub fn view(self, window: &mut Window, cx: &mut App) -> Entity<Icon> {
|
||||||
}
|
Icon::build(self).view(window, cx)
|
||||||
|
}
|
||||||
impl From<IconName> for Icon {
|
}
|
||||||
fn from(val: IconName) -> Self {
|
|
||||||
Icon::build(val)
|
impl From<IconName> for Icon {
|
||||||
}
|
fn from(val: IconName) -> Self {
|
||||||
}
|
Icon::build(val)
|
||||||
|
}
|
||||||
impl From<IconName> for AnyElement {
|
}
|
||||||
fn from(val: IconName) -> Self {
|
|
||||||
Icon::build(val).into_any_element()
|
impl From<IconName> for AnyElement {
|
||||||
}
|
fn from(val: IconName) -> Self {
|
||||||
}
|
Icon::build(val).into_any_element()
|
||||||
|
}
|
||||||
impl RenderOnce for IconName {
|
}
|
||||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
|
||||||
Icon::build(self)
|
impl RenderOnce for IconName {
|
||||||
}
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
}
|
Icon::build(self)
|
||||||
|
}
|
||||||
#[derive(IntoElement)]
|
}
|
||||||
pub struct Icon {
|
|
||||||
base: Svg,
|
#[derive(IntoElement)]
|
||||||
path: SharedString,
|
pub struct Icon {
|
||||||
text_color: Option<Hsla>,
|
base: Svg,
|
||||||
size: Option<Size>,
|
path: SharedString,
|
||||||
rotation: Option<Radians>,
|
text_color: Option<Hsla>,
|
||||||
}
|
size: Option<Size>,
|
||||||
|
rotation: Option<Radians>,
|
||||||
impl Default for Icon {
|
}
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
impl Default for Icon {
|
||||||
base: svg().flex_none().size_4(),
|
fn default() -> Self {
|
||||||
path: "".into(),
|
Self {
|
||||||
text_color: None,
|
base: svg().flex_none().size_4(),
|
||||||
size: None,
|
path: "".into(),
|
||||||
rotation: None,
|
text_color: None,
|
||||||
}
|
size: None,
|
||||||
}
|
rotation: None,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl Clone for Icon {
|
}
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let mut this = Self::default().path(self.path.clone());
|
impl Clone for Icon {
|
||||||
if let Some(size) = self.size {
|
fn clone(&self) -> Self {
|
||||||
this = this.with_size(size);
|
let mut this = Self::default().path(self.path.clone());
|
||||||
}
|
if let Some(size) = self.size {
|
||||||
this
|
this = this.with_size(size);
|
||||||
}
|
}
|
||||||
}
|
this
|
||||||
|
}
|
||||||
pub trait IconNamed {
|
}
|
||||||
fn path(&self) -> SharedString;
|
|
||||||
}
|
pub trait IconNamed {
|
||||||
|
fn path(&self) -> SharedString;
|
||||||
impl Icon {
|
}
|
||||||
pub fn new(icon: impl Into<Icon>) -> Self {
|
|
||||||
icon.into()
|
impl Icon {
|
||||||
}
|
pub fn new(icon: impl Into<Icon>) -> Self {
|
||||||
|
icon.into()
|
||||||
fn build(name: IconName) -> Self {
|
}
|
||||||
Self::default().path(name.path())
|
|
||||||
}
|
fn build(name: IconName) -> Self {
|
||||||
|
Self::default().path(name.path())
|
||||||
/// Set the icon path of the Assets bundle
|
}
|
||||||
///
|
|
||||||
/// For example: `icons/foo.svg`
|
/// Set the icon path of the Assets bundle
|
||||||
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
///
|
||||||
self.path = path.into();
|
/// For example: `icons/foo.svg`
|
||||||
self
|
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||||
}
|
self.path = path.into();
|
||||||
|
self
|
||||||
/// Create a new view for the icon
|
}
|
||||||
pub fn view(self, _window: &mut Window, cx: &mut App) -> Entity<Icon> {
|
|
||||||
cx.new(|_| self)
|
/// Create a new view for the icon
|
||||||
}
|
pub fn view(self, _window: &mut Window, cx: &mut App) -> Entity<Icon> {
|
||||||
|
cx.new(|_| self)
|
||||||
pub fn transform(mut self, transformation: gpui::Transformation) -> Self {
|
}
|
||||||
self.base = self.base.with_transformation(transformation);
|
|
||||||
self
|
pub fn transform(mut self, transformation: gpui::Transformation) -> Self {
|
||||||
}
|
self.base = self.base.with_transformation(transformation);
|
||||||
|
self
|
||||||
pub fn empty() -> Self {
|
}
|
||||||
Self::default()
|
|
||||||
}
|
pub fn empty() -> Self {
|
||||||
|
Self::default()
|
||||||
/// Rotate the icon by the given angle
|
}
|
||||||
pub fn rotate(mut self, radians: impl Into<Radians>) -> Self {
|
|
||||||
self.base = self
|
/// Rotate the icon by the given angle
|
||||||
.base
|
pub fn rotate(mut self, radians: impl Into<Radians>) -> Self {
|
||||||
.with_transformation(Transformation::rotate(radians));
|
self.base = self
|
||||||
self
|
.base
|
||||||
}
|
.with_transformation(Transformation::rotate(radians));
|
||||||
}
|
self
|
||||||
|
}
|
||||||
impl Styled for Icon {
|
}
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
|
||||||
self.base.style()
|
impl Styled for Icon {
|
||||||
}
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
self.base.style()
|
||||||
fn text_color(mut self, color: impl Into<Hsla>) -> Self {
|
}
|
||||||
self.text_color = Some(color.into());
|
|
||||||
self
|
fn text_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||||
}
|
self.text_color = Some(color.into());
|
||||||
}
|
self
|
||||||
|
}
|
||||||
impl Sizable for Icon {
|
}
|
||||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
|
||||||
self.size = Some(size.into());
|
impl Sizable for Icon {
|
||||||
self
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||||
}
|
self.size = Some(size.into());
|
||||||
}
|
self
|
||||||
|
}
|
||||||
impl RenderOnce for Icon {
|
}
|
||||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
|
||||||
let text_color = self.text_color.unwrap_or_else(|| window.text_style().color);
|
impl RenderOnce for Icon {
|
||||||
|
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
self.base
|
let text_color = self.text_color.unwrap_or_else(|| window.text_style().color);
|
||||||
.text_color(text_color)
|
|
||||||
.when_some(self.size, |this, size| match size {
|
self.base
|
||||||
Size::Size(px) => this.size(px),
|
.text_color(text_color)
|
||||||
Size::XSmall => this.size_3(),
|
.when_some(self.size, |this, size| match size {
|
||||||
Size::Small => this.size_4(),
|
Size::Size(px) => this.size(px),
|
||||||
Size::Medium => this.size_5(),
|
Size::XSmall => this.size_3(),
|
||||||
Size::Large => this.size_6(),
|
Size::Small => this.size_4(),
|
||||||
})
|
Size::Medium => this.size_5(),
|
||||||
.path(self.path)
|
Size::Large => this.size_6(),
|
||||||
}
|
})
|
||||||
}
|
.path(self.path)
|
||||||
|
}
|
||||||
impl From<Icon> for AnyElement {
|
}
|
||||||
fn from(val: Icon) -> Self {
|
|
||||||
val.into_any_element()
|
impl From<Icon> for AnyElement {
|
||||||
}
|
fn from(val: Icon) -> Self {
|
||||||
}
|
val.into_any_element()
|
||||||
|
}
|
||||||
impl Render for Icon {
|
}
|
||||||
fn render(
|
|
||||||
&mut self,
|
impl Render for Icon {
|
||||||
_window: &mut gpui::Window,
|
fn render(
|
||||||
cx: &mut gpui::Context<Self>,
|
&mut self,
|
||||||
) -> impl IntoElement {
|
_window: &mut gpui::Window,
|
||||||
let text_color = self.text_color.unwrap_or_else(|| cx.theme().icon);
|
cx: &mut gpui::Context<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
svg()
|
let text_color = self.text_color.unwrap_or_else(|| cx.theme().icon);
|
||||||
.flex_none()
|
|
||||||
.text_color(text_color)
|
svg()
|
||||||
.when_some(self.size, |this, size| match size {
|
.flex_none()
|
||||||
Size::Size(px) => this.size(px),
|
.text_color(text_color)
|
||||||
Size::XSmall => this.size_3(),
|
.when_some(self.size, |this, size| match size {
|
||||||
Size::Small => this.size_4(),
|
Size::Size(px) => this.size(px),
|
||||||
Size::Medium => this.size_5(),
|
Size::XSmall => this.size_3(),
|
||||||
Size::Large => this.size_6(),
|
Size::Small => this.size_4(),
|
||||||
})
|
Size::Medium => this.size_5(),
|
||||||
.path(self.path.clone())
|
Size::Large => this.size_6(),
|
||||||
.when_some(self.rotation, |this, rotation| {
|
})
|
||||||
this.with_transformation(Transformation::rotate(rotation))
|
.when(!self.path.is_empty(), |this| this.path(self.path.clone()))
|
||||||
})
|
.when_some(self.rotation, |this, rotation| {
|
||||||
}
|
this.with_transformation(Transformation::rotate(rotation))
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ mod styled;
|
|||||||
mod title_bar;
|
mod title_bar;
|
||||||
mod window_border;
|
mod window_border;
|
||||||
|
|
||||||
|
i18n::init!();
|
||||||
|
|
||||||
/// Initialize the UI module.
|
/// Initialize the UI module.
|
||||||
///
|
///
|
||||||
/// This must be called before using any of the UI components.
|
/// This must be called before using any of the UI components.
|
||||||
|
|||||||
@@ -1,382 +1,389 @@
|
|||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::borrow::Cow;
|
||||||
use std::sync::Arc;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::time::Duration;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
use gpui::prelude::FluentBuilder;
|
||||||
blue, div, green, px, red, yellow, Animation, AnimationExt, App, AppContext, ClickEvent,
|
use gpui::{
|
||||||
Context, DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
blue, div, green, px, red, yellow, Animation, AnimationExt, App, AppContext, ClickEvent,
|
||||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
Context, DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
||||||
Window,
|
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
||||||
};
|
Window,
|
||||||
use smol::Timer;
|
};
|
||||||
use theme::ActiveTheme;
|
use smol::Timer;
|
||||||
|
use theme::ActiveTheme;
|
||||||
use crate::animation::cubic_bezier;
|
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::animation::cubic_bezier;
|
||||||
use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
|
use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt};
|
||||||
pub enum NotificationType {
|
|
||||||
Info,
|
pub enum NotificationType {
|
||||||
Success,
|
Info,
|
||||||
Warning,
|
Success,
|
||||||
Error,
|
Warning,
|
||||||
}
|
Error,
|
||||||
|
}
|
||||||
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
|
||||||
pub(crate) enum NotificationId {
|
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
||||||
Id(TypeId),
|
pub(crate) enum NotificationId {
|
||||||
IdAndElementId(TypeId, ElementId),
|
Id(TypeId),
|
||||||
}
|
IdAndElementId(TypeId, ElementId),
|
||||||
|
}
|
||||||
impl From<TypeId> for NotificationId {
|
|
||||||
fn from(type_id: TypeId) -> Self {
|
impl From<TypeId> for NotificationId {
|
||||||
Self::Id(type_id)
|
fn from(type_id: TypeId) -> Self {
|
||||||
}
|
Self::Id(type_id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl From<(TypeId, ElementId)> for NotificationId {
|
|
||||||
fn from((type_id, id): (TypeId, ElementId)) -> Self {
|
impl From<(TypeId, ElementId)> for NotificationId {
|
||||||
Self::IdAndElementId(type_id, id)
|
fn from((type_id, id): (TypeId, ElementId)) -> Self {
|
||||||
}
|
Self::IdAndElementId(type_id, id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
type OnClick = Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>;
|
|
||||||
|
type OnClick = Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>;
|
||||||
/// A notification element.
|
|
||||||
pub struct Notification {
|
/// A notification element.
|
||||||
/// The id is used make the notification unique.
|
pub struct Notification {
|
||||||
/// Then you push a notification with the same id, the previous notification will be replaced.
|
/// The id is used make the notification unique.
|
||||||
///
|
/// Then you push a notification with the same id, the previous notification will be replaced.
|
||||||
/// None means the notification will be added to the end of the list.
|
///
|
||||||
id: NotificationId,
|
/// None means the notification will be added to the end of the list.
|
||||||
kind: NotificationType,
|
id: NotificationId,
|
||||||
title: Option<SharedString>,
|
kind: NotificationType,
|
||||||
message: SharedString,
|
title: Option<SharedString>,
|
||||||
icon: Option<Icon>,
|
message: SharedString,
|
||||||
autohide: bool,
|
icon: Option<Icon>,
|
||||||
on_click: OnClick,
|
autohide: bool,
|
||||||
closing: bool,
|
on_click: OnClick,
|
||||||
}
|
closing: bool,
|
||||||
|
}
|
||||||
impl From<String> for Notification {
|
|
||||||
fn from(s: String) -> Self {
|
impl From<String> for Notification {
|
||||||
Self::new(s)
|
fn from(s: String) -> Self {
|
||||||
}
|
Self::new(s)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl From<SharedString> for Notification {
|
|
||||||
fn from(s: SharedString) -> Self {
|
impl From<Cow<'static, str>> for Notification {
|
||||||
Self::new(s)
|
fn from(s: Cow<'static, str>) -> Self {
|
||||||
}
|
Self::new(s)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl From<&'static str> for Notification {
|
|
||||||
fn from(s: &'static str) -> Self {
|
impl From<SharedString> for Notification {
|
||||||
Self::new(s)
|
fn from(s: SharedString) -> Self {
|
||||||
}
|
Self::new(s)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl From<(NotificationType, &'static str)> for Notification {
|
|
||||||
fn from((type_, content): (NotificationType, &'static str)) -> Self {
|
impl From<&'static str> for Notification {
|
||||||
Self::new(content).with_type(type_)
|
fn from(s: &'static str) -> Self {
|
||||||
}
|
Self::new(s)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl From<(NotificationType, SharedString)> for Notification {
|
|
||||||
fn from((type_, content): (NotificationType, SharedString)) -> Self {
|
impl From<(NotificationType, &'static str)> for Notification {
|
||||||
Self::new(content).with_type(type_)
|
fn from((type_, content): (NotificationType, &'static str)) -> Self {
|
||||||
}
|
Self::new(content).with_type(type_)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
struct DefaultIdType;
|
|
||||||
|
impl From<(NotificationType, SharedString)> for Notification {
|
||||||
impl Notification {
|
fn from((type_, content): (NotificationType, SharedString)) -> Self {
|
||||||
/// Create a new notification with the given content.
|
Self::new(content).with_type(type_)
|
||||||
///
|
}
|
||||||
/// default width is 320px.
|
}
|
||||||
pub fn new(message: impl Into<SharedString>) -> Self {
|
|
||||||
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
|
struct DefaultIdType;
|
||||||
let id = (TypeId::of::<DefaultIdType>(), id.into());
|
|
||||||
|
impl Notification {
|
||||||
Self {
|
/// Create a new notification with the given content.
|
||||||
id: id.into(),
|
///
|
||||||
title: None,
|
/// default width is 320px.
|
||||||
message: message.into(),
|
pub fn new(message: impl Into<SharedString>) -> Self {
|
||||||
kind: NotificationType::Info,
|
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
|
||||||
icon: None,
|
let id = (TypeId::of::<DefaultIdType>(), id.into());
|
||||||
autohide: true,
|
|
||||||
on_click: None,
|
Self {
|
||||||
closing: false,
|
id: id.into(),
|
||||||
}
|
title: None,
|
||||||
}
|
message: message.into(),
|
||||||
|
kind: NotificationType::Info,
|
||||||
pub fn info(message: impl Into<SharedString>) -> Self {
|
icon: None,
|
||||||
Self::new(message).with_type(NotificationType::Info)
|
autohide: true,
|
||||||
}
|
on_click: None,
|
||||||
|
closing: false,
|
||||||
pub fn success(message: impl Into<SharedString>) -> Self {
|
}
|
||||||
Self::new(message).with_type(NotificationType::Success)
|
}
|
||||||
}
|
|
||||||
|
pub fn info(message: impl Into<SharedString>) -> Self {
|
||||||
pub fn warning(message: impl Into<SharedString>) -> Self {
|
Self::new(message).with_type(NotificationType::Info)
|
||||||
Self::new(message).with_type(NotificationType::Warning)
|
}
|
||||||
}
|
|
||||||
|
pub fn success(message: impl Into<SharedString>) -> Self {
|
||||||
pub fn error(message: impl Into<SharedString>) -> Self {
|
Self::new(message).with_type(NotificationType::Success)
|
||||||
Self::new(message).with_type(NotificationType::Error)
|
}
|
||||||
}
|
|
||||||
|
pub fn warning(message: impl Into<SharedString>) -> Self {
|
||||||
/// Set the type for unique identification of the notification.
|
Self::new(message).with_type(NotificationType::Warning)
|
||||||
///
|
}
|
||||||
/// ```rs
|
|
||||||
/// struct MyNotificationKind;
|
pub fn error(message: impl Into<SharedString>) -> Self {
|
||||||
/// let notification = Notification::new("Hello").id::<MyNotificationKind>();
|
Self::new(message).with_type(NotificationType::Error)
|
||||||
/// ```
|
}
|
||||||
pub fn id<T: Sized + 'static>(mut self) -> Self {
|
|
||||||
self.id = TypeId::of::<T>().into();
|
/// Set the type for unique identification of the notification.
|
||||||
self
|
///
|
||||||
}
|
/// ```rs
|
||||||
|
/// struct MyNotificationKind;
|
||||||
/// Set the type and id of the notification, used to uniquely identify the notification.
|
/// let notification = Notification::new("Hello").id::<MyNotificationKind>();
|
||||||
pub fn id1<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
|
/// ```
|
||||||
self.id = (TypeId::of::<T>(), key.into()).into();
|
pub fn id<T: Sized + 'static>(mut self) -> Self {
|
||||||
self
|
self.id = TypeId::of::<T>().into();
|
||||||
}
|
self
|
||||||
|
}
|
||||||
/// Set the title of the notification, default is None.
|
|
||||||
///
|
/// Set the type and id of the notification, used to uniquely identify the notification.
|
||||||
/// If title is None, the notification will not have a title.
|
pub fn id1<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
|
||||||
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
|
self.id = (TypeId::of::<T>(), key.into()).into();
|
||||||
self.title = Some(title.into());
|
self
|
||||||
self
|
}
|
||||||
}
|
|
||||||
|
/// Set the title of the notification, default is None.
|
||||||
/// Set the icon of the notification.
|
///
|
||||||
///
|
/// If title is None, the notification will not have a title.
|
||||||
/// If icon is None, the notification will use the default icon of the type.
|
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
|
||||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
self.title = Some(title.into());
|
||||||
self.icon = Some(icon.into());
|
self
|
||||||
self
|
}
|
||||||
}
|
|
||||||
|
/// Set the icon of the notification.
|
||||||
/// Set the type of the notification, default is NotificationType::Info.
|
///
|
||||||
pub fn with_type(mut self, type_: NotificationType) -> Self {
|
/// If icon is None, the notification will use the default icon of the type.
|
||||||
self.kind = type_;
|
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||||
self
|
self.icon = Some(icon.into());
|
||||||
}
|
self
|
||||||
|
}
|
||||||
/// Set the auto hide of the notification, default is true.
|
|
||||||
pub fn autohide(mut self, autohide: bool) -> Self {
|
/// Set the type of the notification, default is NotificationType::Info.
|
||||||
self.autohide = autohide;
|
pub fn with_type(mut self, type_: NotificationType) -> Self {
|
||||||
self
|
self.kind = type_;
|
||||||
}
|
self
|
||||||
|
}
|
||||||
/// Set the click callback of the notification.
|
|
||||||
pub fn on_click(
|
/// Set the auto hide of the notification, default is true.
|
||||||
mut self,
|
pub fn autohide(mut self, autohide: bool) -> Self {
|
||||||
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
self.autohide = autohide;
|
||||||
) -> Self {
|
self
|
||||||
self.on_click = Some(Arc::new(on_click));
|
}
|
||||||
self
|
|
||||||
}
|
/// Set the click callback of the notification.
|
||||||
|
pub fn on_click(
|
||||||
fn dismiss(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
mut self,
|
||||||
self.closing = true;
|
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
cx.notify();
|
) -> Self {
|
||||||
|
self.on_click = Some(Arc::new(on_click));
|
||||||
// Dismiss the notification after 0.15s to show the animation.
|
self
|
||||||
cx.spawn(async move |view, cx| {
|
}
|
||||||
Timer::after(Duration::from_secs_f32(0.15)).await;
|
|
||||||
cx.update(|cx| {
|
fn dismiss(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(view) = view.upgrade() {
|
self.closing = true;
|
||||||
view.update(cx, |view, cx| {
|
cx.notify();
|
||||||
view.closing = false;
|
|
||||||
cx.emit(DismissEvent);
|
// Dismiss the notification after 0.15s to show the animation.
|
||||||
});
|
cx.spawn(async move |view, cx| {
|
||||||
}
|
Timer::after(Duration::from_secs_f32(0.15)).await;
|
||||||
})
|
cx.update(|cx| {
|
||||||
})
|
if let Some(view) = view.upgrade() {
|
||||||
.detach()
|
view.update(cx, |view, cx| {
|
||||||
}
|
view.closing = false;
|
||||||
}
|
cx.emit(DismissEvent);
|
||||||
|
});
|
||||||
impl EventEmitter<DismissEvent> for Notification {}
|
}
|
||||||
|
})
|
||||||
impl FluentBuilder for Notification {}
|
})
|
||||||
|
.detach()
|
||||||
impl Render for Notification {
|
}
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
}
|
||||||
let closing = self.closing;
|
|
||||||
let icon = match self.icon.clone() {
|
impl EventEmitter<DismissEvent> for Notification {}
|
||||||
Some(icon) => icon,
|
|
||||||
None => match self.kind {
|
impl FluentBuilder for Notification {}
|
||||||
NotificationType::Info => Icon::new(IconName::Info).text_color(blue()),
|
|
||||||
NotificationType::Warning => Icon::new(IconName::Info).text_color(yellow()),
|
impl Render for Notification {
|
||||||
NotificationType::Error => Icon::new(IconName::CloseCircle).text_color(red()),
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
NotificationType::Success => Icon::new(IconName::CheckCircle).text_color(green()),
|
let closing = self.closing;
|
||||||
},
|
let icon = match self.icon.clone() {
|
||||||
};
|
Some(icon) => icon,
|
||||||
|
None => match self.kind {
|
||||||
div()
|
NotificationType::Info => Icon::new(IconName::Info).text_color(blue()),
|
||||||
.id("notification")
|
NotificationType::Warning => Icon::new(IconName::Info).text_color(yellow()),
|
||||||
.group("")
|
NotificationType::Error => Icon::new(IconName::CloseCircle).text_color(red()),
|
||||||
.occlude()
|
NotificationType::Success => Icon::new(IconName::CheckCircle).text_color(green()),
|
||||||
.relative()
|
},
|
||||||
.w_72()
|
};
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border)
|
div()
|
||||||
.bg(cx.theme().surface_background)
|
.id("notification")
|
||||||
.rounded(cx.theme().radius)
|
.group("")
|
||||||
.shadow_md()
|
.occlude()
|
||||||
.p_2()
|
.relative()
|
||||||
.gap_3()
|
.w_72()
|
||||||
.child(div().absolute().top_2p5().left_2().child(icon))
|
.border_1()
|
||||||
.child(
|
.border_color(cx.theme().border)
|
||||||
v_flex()
|
.bg(cx.theme().surface_background)
|
||||||
.pl_6()
|
.rounded(cx.theme().radius)
|
||||||
.gap_1()
|
.shadow_md()
|
||||||
.when_some(self.title.clone(), |this, title| {
|
.p_2()
|
||||||
this.child(div().text_xs().font_semibold().child(title))
|
.gap_3()
|
||||||
})
|
.child(div().absolute().top_2p5().left_2().child(icon))
|
||||||
.overflow_hidden()
|
.child(
|
||||||
.child(div().text_xs().child(self.message.clone())),
|
v_flex()
|
||||||
)
|
.pl_6()
|
||||||
.when_some(self.on_click.clone(), |this, on_click| {
|
.gap_1()
|
||||||
this.cursor_pointer()
|
.when_some(self.title.clone(), |this, title| {
|
||||||
.on_click(cx.listener(move |view, event, window, cx| {
|
this.child(div().text_xs().font_semibold().child(title))
|
||||||
view.dismiss(event, window, cx);
|
})
|
||||||
on_click(event, window, cx);
|
.overflow_hidden()
|
||||||
}))
|
.child(div().text_xs().child(self.message.clone())),
|
||||||
})
|
)
|
||||||
.when(!self.autohide, |this| {
|
.when_some(self.on_click.clone(), |this, on_click| {
|
||||||
this.child(
|
this.cursor_pointer()
|
||||||
h_flex()
|
.on_click(cx.listener(move |view, event, window, cx| {
|
||||||
.absolute()
|
view.dismiss(event, window, cx);
|
||||||
.top_1()
|
on_click(event, window, cx);
|
||||||
.right_1()
|
}))
|
||||||
.invisible()
|
})
|
||||||
.group_hover("", |this| this.visible())
|
.when(!self.autohide, |this| {
|
||||||
.child(
|
this.child(
|
||||||
Button::new("close")
|
h_flex()
|
||||||
.icon(IconName::Close)
|
.absolute()
|
||||||
.ghost()
|
.top_1()
|
||||||
.xsmall()
|
.right_1()
|
||||||
.on_click(cx.listener(Self::dismiss)),
|
.invisible()
|
||||||
),
|
.group_hover("", |this| this.visible())
|
||||||
)
|
.child(
|
||||||
})
|
Button::new("close")
|
||||||
.with_animation(
|
.icon(IconName::Close)
|
||||||
ElementId::NamedInteger("slide-down".into(), closing as u64),
|
.ghost()
|
||||||
Animation::new(Duration::from_secs_f64(0.15))
|
.xsmall()
|
||||||
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
.on_click(cx.listener(Self::dismiss)),
|
||||||
move |this, delta| {
|
),
|
||||||
if closing {
|
)
|
||||||
let x_offset = px(0.) + delta * px(45.);
|
})
|
||||||
this.left(px(0.) + x_offset).opacity(1. - delta)
|
.with_animation(
|
||||||
} else {
|
ElementId::NamedInteger("slide-down".into(), closing as u64),
|
||||||
let y_offset = px(-45.) + delta * px(45.);
|
Animation::new(Duration::from_secs_f64(0.15))
|
||||||
this.top(px(0.) + y_offset).opacity(delta)
|
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
||||||
}
|
move |this, delta| {
|
||||||
},
|
if closing {
|
||||||
)
|
let x_offset = px(0.) + delta * px(45.);
|
||||||
}
|
this.left(px(0.) + x_offset).opacity(1. - delta)
|
||||||
}
|
} else {
|
||||||
|
let y_offset = px(-45.) + delta * px(45.);
|
||||||
/// A list of notifications.
|
this.top(px(0.) + y_offset).opacity(delta)
|
||||||
pub struct NotificationList {
|
}
|
||||||
/// Notifications that will be auto hidden.
|
},
|
||||||
pub(crate) notifications: VecDeque<Entity<Notification>>,
|
)
|
||||||
expanded: bool,
|
}
|
||||||
subscriptions: HashMap<NotificationId, Subscription>,
|
}
|
||||||
}
|
|
||||||
|
/// A list of notifications.
|
||||||
impl NotificationList {
|
pub struct NotificationList {
|
||||||
pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
/// Notifications that will be auto hidden.
|
||||||
Self {
|
pub(crate) notifications: VecDeque<Entity<Notification>>,
|
||||||
notifications: VecDeque::new(),
|
expanded: bool,
|
||||||
expanded: false,
|
subscriptions: HashMap<NotificationId, Subscription>,
|
||||||
subscriptions: HashMap::new(),
|
}
|
||||||
}
|
|
||||||
}
|
impl NotificationList {
|
||||||
|
pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
||||||
pub fn push(
|
Self {
|
||||||
&mut self,
|
notifications: VecDeque::new(),
|
||||||
notification: impl Into<Notification>,
|
expanded: false,
|
||||||
window: &mut Window,
|
subscriptions: HashMap::new(),
|
||||||
cx: &mut Context<Self>,
|
}
|
||||||
) {
|
}
|
||||||
let notification = notification.into();
|
|
||||||
let id = notification.id.clone();
|
pub fn push(
|
||||||
let autohide = notification.autohide;
|
&mut self,
|
||||||
|
notification: impl Into<Notification>,
|
||||||
// Remove the notification by id, for keep unique.
|
window: &mut Window,
|
||||||
self.notifications.retain(|note| note.read(cx).id != id);
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
let notification = cx.new(|_| notification);
|
let notification = notification.into();
|
||||||
|
let id = notification.id.clone();
|
||||||
self.subscriptions.insert(
|
let autohide = notification.autohide;
|
||||||
id.clone(),
|
|
||||||
cx.subscribe(¬ification, move |view, _, _: &DismissEvent, cx| {
|
// Remove the notification by id, for keep unique.
|
||||||
view.notifications.retain(|note| id != note.read(cx).id);
|
self.notifications.retain(|note| note.read(cx).id != id);
|
||||||
view.subscriptions.remove(&id);
|
|
||||||
}),
|
let notification = cx.new(|_| notification);
|
||||||
);
|
|
||||||
|
self.subscriptions.insert(
|
||||||
self.notifications.push_back(notification.clone());
|
id.clone(),
|
||||||
if autohide {
|
cx.subscribe(¬ification, move |view, _, _: &DismissEvent, cx| {
|
||||||
// Sleep for 3 seconds to autohide the notification
|
view.notifications.retain(|note| id != note.read(cx).id);
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
view.subscriptions.remove(&id);
|
||||||
Timer::after(Duration::from_secs(3)).await;
|
}),
|
||||||
_ = notification.update_in(cx, |note, window, cx| {
|
);
|
||||||
note.dismiss(&ClickEvent::default(), window, cx)
|
|
||||||
});
|
self.notifications.push_back(notification.clone());
|
||||||
})
|
if autohide {
|
||||||
.detach();
|
// Sleep for 3 seconds to autohide the notification
|
||||||
}
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
cx.notify();
|
Timer::after(Duration::from_secs(3)).await;
|
||||||
}
|
_ = notification.update_in(cx, |note, window, cx| {
|
||||||
|
note.dismiss(&ClickEvent::default(), window, cx)
|
||||||
pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
});
|
||||||
self.notifications.clear();
|
})
|
||||||
cx.notify();
|
.detach();
|
||||||
}
|
}
|
||||||
|
cx.notify();
|
||||||
pub fn notifications(&self) -> Vec<Entity<Notification>> {
|
}
|
||||||
self.notifications.iter().cloned().collect()
|
|
||||||
}
|
pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
}
|
self.notifications.clear();
|
||||||
|
cx.notify();
|
||||||
impl Render for NotificationList {
|
}
|
||||||
fn render(
|
|
||||||
&mut self,
|
pub fn notifications(&self) -> Vec<Entity<Notification>> {
|
||||||
window: &mut gpui::Window,
|
self.notifications.iter().cloned().collect()
|
||||||
cx: &mut gpui::Context<Self>,
|
}
|
||||||
) -> impl IntoElement {
|
}
|
||||||
let size = window.viewport_size();
|
|
||||||
let items = self.notifications.iter().rev().take(10).rev().cloned();
|
impl Render for NotificationList {
|
||||||
|
fn render(
|
||||||
div()
|
&mut self,
|
||||||
.absolute()
|
window: &mut gpui::Window,
|
||||||
.flex()
|
cx: &mut gpui::Context<Self>,
|
||||||
.top_4()
|
) -> impl IntoElement {
|
||||||
.bottom_4()
|
let size = window.viewport_size();
|
||||||
.right_4()
|
let items = self.notifications.iter().rev().take(10).rev().cloned();
|
||||||
.justify_end()
|
|
||||||
.child(
|
div()
|
||||||
v_flex()
|
.absolute()
|
||||||
.id("notification-list")
|
.flex()
|
||||||
.gap_3()
|
.top_4()
|
||||||
.absolute()
|
.bottom_4()
|
||||||
.relative()
|
.right_4()
|
||||||
.right_0()
|
.justify_end()
|
||||||
.h(size.height - px(8.))
|
.child(
|
||||||
.children(items)
|
v_flex()
|
||||||
.on_hover(cx.listener(|view, hovered, _window, cx| {
|
.id("notification-list")
|
||||||
view.expanded = *hovered;
|
.gap_3()
|
||||||
cx.notify();
|
.absolute()
|
||||||
})),
|
.relative()
|
||||||
)
|
.right_0()
|
||||||
}
|
.h(size.height - px(8.))
|
||||||
}
|
.children(items)
|
||||||
|
.on_hover(cx.listener(|view, hovered, _window, cx| {
|
||||||
|
view.expanded = *hovered;
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
0
locales/.keep
Normal file
0
locales/.keep
Normal file
1176
locales/app.yml
Normal file
1176
locales/app.yml
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user