update
This commit is contained in:
Generated
+1
@@ -7784,6 +7784,7 @@ dependencies = [
|
|||||||
name = "signed_core"
|
name = "signed_core"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"gpui",
|
||||||
"nostr",
|
"nostr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
gpui.workspace = true
|
||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use gpui::SharedString;
|
||||||
use nostr::prelude::*;
|
use nostr::prelude::*;
|
||||||
|
|
||||||
/// Parsed NIP-34 repository announcement (plain data, ready for the UI).
|
/// Parsed NIP-34 repository announcement (plain data, ready for the UI).
|
||||||
@@ -9,14 +10,14 @@ pub struct Announcement {
|
|||||||
pub created_at: Timestamp,
|
pub created_at: Timestamp,
|
||||||
/// Repository ID (`d` tag).
|
/// Repository ID (`d` tag).
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: Option<String>,
|
pub name: Option<SharedString>,
|
||||||
pub description: Option<String>,
|
pub description: Option<SharedString>,
|
||||||
/// Webpage URLs for browsing.
|
/// Webpage URLs for browsing.
|
||||||
pub web: Vec<String>,
|
pub web: Vec<String>,
|
||||||
/// URLs for `git clone`.
|
/// URLs for `git clone`.
|
||||||
pub clone: Vec<String>,
|
pub clone: Vec<String>,
|
||||||
/// Relays the repository monitors for patches and issues.
|
/// Relays the repository monitors for patches and issues.
|
||||||
pub relays: Vec<String>,
|
pub relays: Vec<RelayUrl>,
|
||||||
/// Earliest unique commit ID (`r` tag with `euc` marker).
|
/// Earliest unique commit ID (`r` tag with `euc` marker).
|
||||||
pub euc: Option<String>,
|
pub euc: Option<String>,
|
||||||
/// Other recognized maintainers.
|
/// Other recognized maintainers.
|
||||||
@@ -33,11 +34,11 @@ impl Announcement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut id: Option<String> = None;
|
let mut id: Option<String> = None;
|
||||||
let mut name: Option<String> = None;
|
let mut name: Option<SharedString> = None;
|
||||||
let mut description: Option<String> = None;
|
let mut description: Option<SharedString> = None;
|
||||||
let mut web: Vec<String> = Vec::new();
|
let mut web: Vec<String> = Vec::new();
|
||||||
let mut clone: Vec<String> = Vec::new();
|
let mut clone: Vec<String> = Vec::new();
|
||||||
let mut relays: Vec<String> = Vec::new();
|
let mut relays: Vec<RelayUrl> = Vec::new();
|
||||||
let mut euc: Option<String> = None;
|
let mut euc: Option<String> = None;
|
||||||
let mut maintainers: Vec<PublicKey> = Vec::new();
|
let mut maintainers: Vec<PublicKey> = Vec::new();
|
||||||
let mut hashtags: Vec<String> = Vec::new();
|
let mut hashtags: Vec<String> = Vec::new();
|
||||||
@@ -56,15 +57,13 @@ impl Announcement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match Nip34Tag::parse(tag.as_slice()) {
|
match Nip34Tag::parse(tag.as_slice()) {
|
||||||
Ok(Nip34Tag::Name(value)) => name = Some(value),
|
Ok(Nip34Tag::Name(value)) => name = Some(value.into()),
|
||||||
Ok(Nip34Tag::Description(value)) => description = Some(value),
|
Ok(Nip34Tag::Description(value)) => description = Some(value.into()),
|
||||||
Ok(Nip34Tag::Web(urls)) => web.extend(urls.into_iter().map(|url| url.to_string())),
|
Ok(Nip34Tag::Web(urls)) => web.extend(urls.into_iter().map(|url| url.to_string())),
|
||||||
Ok(Nip34Tag::Clone(urls)) => {
|
Ok(Nip34Tag::Clone(urls)) => {
|
||||||
clone.extend(urls.into_iter().map(|url| url.to_string()))
|
clone.extend(urls.into_iter().map(|url| url.to_string()))
|
||||||
}
|
}
|
||||||
Ok(Nip34Tag::Relays(urls)) => {
|
Ok(Nip34Tag::Relays(urls)) => relays.extend(urls),
|
||||||
relays.extend(urls.into_iter().map(|url| url.to_string()))
|
|
||||||
}
|
|
||||||
Ok(Nip34Tag::EarliestUniqueCommitId(commit)) => euc = Some(commit.to_string()),
|
Ok(Nip34Tag::EarliestUniqueCommitId(commit)) => euc = Some(commit.to_string()),
|
||||||
Ok(Nip34Tag::Maintainers(keys)) => maintainers.extend(keys),
|
Ok(Nip34Tag::Maintainers(keys)) => maintainers.extend(keys),
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -144,7 +143,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(announcement.web, vec!["https://example.com/repo"]);
|
assert_eq!(announcement.web, vec!["https://example.com/repo"]);
|
||||||
assert_eq!(announcement.clone, vec!["https://example.com/repo.git"]);
|
assert_eq!(announcement.clone, vec!["https://example.com/repo.git"]);
|
||||||
assert_eq!(announcement.relays, vec!["wss://relay.example.com"]);
|
assert_eq!(
|
||||||
|
announcement.relays,
|
||||||
|
vec![RelayUrl::parse("wss://relay.example.com").unwrap()]
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
announcement.euc.as_deref(),
|
announcement.euc.as_deref(),
|
||||||
Some("aa231c4c6a5777dc89b42207b499891a344add5c")
|
Some("aa231c4c6a5777dc89b42207b499891a344add5c")
|
||||||
@@ -185,7 +187,10 @@ mod tests {
|
|||||||
|
|
||||||
// An invalid URL keeps the whole clone tag from being parsed.
|
// An invalid URL keeps the whole clone tag from being parsed.
|
||||||
assert!(announcement.clone.is_empty());
|
assert!(announcement.clone.is_empty());
|
||||||
assert_eq!(announcement.relays, vec!["wss://good.example.com"]);
|
assert_eq!(
|
||||||
|
announcement.relays,
|
||||||
|
vec![RelayUrl::parse("wss://good.example.com").unwrap()]
|
||||||
|
);
|
||||||
assert!(announcement.maintainers.is_empty());
|
assert!(announcement.maintainers.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -243,21 +243,18 @@ impl Backend {
|
|||||||
/// Decrypt the NIP-49 encrypted credential stored in the keyring with
|
/// Decrypt the NIP-49 encrypted credential stored in the keyring with
|
||||||
/// the given passphrase and resume the session.
|
/// the given passphrase and resume the session.
|
||||||
///
|
///
|
||||||
/// The scrypt decryption runs off the UI thread. The returned receiver
|
/// The scrypt decryption runs off the UI thread. The returned task
|
||||||
/// yields the public key on success, or the failure reason (e.g. wrong
|
/// yields the public key on success, or the failure reason (e.g. wrong
|
||||||
/// passphrase), so callers can render inline errors.
|
/// passphrase), so callers can render inline errors.
|
||||||
pub fn restore_with_passphrase(
|
pub fn restore_with_passphrase(
|
||||||
&mut self,
|
&mut self,
|
||||||
password: &str,
|
password: &str,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> flume::Receiver<Result<PublicKey, Error>> {
|
) -> Task<Result<PublicKey, Error>> {
|
||||||
let (tx, rx) = flume::bounded(1);
|
|
||||||
|
|
||||||
let password = password.to_owned();
|
let password = password.to_owned();
|
||||||
let user = cx.read_credentials(USER_KEYRING);
|
let user = cx.read_credentials(USER_KEYRING);
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let result = async {
|
|
||||||
let content = user
|
let content = user
|
||||||
.await?
|
.await?
|
||||||
.map(|(_username, secret)| String::from_utf8(secret))
|
.map(|(_username, secret)| String::from_utf8(secret))
|
||||||
@@ -265,7 +262,7 @@ impl Backend {
|
|||||||
.ok_or_else(|| anyhow!("no stored credential; nothing to unlock"))?;
|
.ok_or_else(|| anyhow!("no stored credential; nothing to unlock"))?;
|
||||||
|
|
||||||
if !content.starts_with("ncryptsec1") {
|
if !content.starts_with("ncryptsec1") {
|
||||||
Err(anyhow!("stored credential is not passphrase-encrypted"))?;
|
return Err(anyhow!("stored credential is not passphrase-encrypted"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let decrypt_task = cx.background_spawn(async move {
|
let decrypt_task = cx.background_spawn(async move {
|
||||||
@@ -279,48 +276,34 @@ impl Backend {
|
|||||||
|
|
||||||
this.update(cx, |this, cx| this.set_signer(keys, cx))?;
|
this.update(cx, |this, cx| this.set_signer(keys, cx))?;
|
||||||
|
|
||||||
Ok::<_, Error>(public_key)
|
Ok(public_key)
|
||||||
}
|
})
|
||||||
.await;
|
|
||||||
|
|
||||||
tx.send_async(result).await.ok();
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
|
|
||||||
rx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new identity: generate keys, encrypt the secret key with the
|
/// Create a new identity: generate keys, encrypt the secret key with the
|
||||||
/// passphrase (NIP-49) and persist it in the keyring, then publish the
|
/// passphrase (NIP-49) and persist it in the keyring, then publish the
|
||||||
/// user's NIP-65 relay list, metadata and grasp list.
|
/// user's NIP-65 relay list, metadata and grasp list.
|
||||||
///
|
///
|
||||||
/// The heavy encryption runs off the UI thread. The returned receiver
|
/// The heavy encryption runs off the UI thread. The returned task yields
|
||||||
/// yields the new public key on success, or the failure reason, so
|
/// the new public key on success, or the failure reason, so callers can
|
||||||
/// callers can render progress and inline errors.
|
/// render progress and inline errors.
|
||||||
pub fn create_identity(
|
pub fn create_identity(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> flume::Receiver<Result<PublicKey, Error>> {
|
) -> Task<Result<PublicKey, Error>> {
|
||||||
let (tx, rx) = flume::bounded(1);
|
|
||||||
|
|
||||||
let name = name.trim().to_owned();
|
let name = name.trim().to_owned();
|
||||||
let password = password.to_owned();
|
let password = password.to_owned();
|
||||||
|
|
||||||
let validation_error = if name.is_empty() || name.len() > 255 {
|
if name.is_empty() || name.len() > 255 {
|
||||||
Some("Name must be 1-255 characters")
|
return Task::ready(Err(anyhow!("Name must be 1-255 characters")));
|
||||||
} else if password.is_empty() {
|
}
|
||||||
Some("Passphrase must not be empty")
|
if password.is_empty() {
|
||||||
} else {
|
return Task::ready(Err(anyhow!("Passphrase must not be empty")));
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(message) = validation_error {
|
|
||||||
tx.try_send(Err(anyhow!(message))).ok();
|
|
||||||
return rx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
let job = cx.background_spawn(async move {
|
let job = cx.background_spawn(async move {
|
||||||
let keys = Keys::generate();
|
let keys = Keys::generate();
|
||||||
let encrypted =
|
let encrypted =
|
||||||
@@ -329,8 +312,6 @@ impl Backend {
|
|||||||
Ok::<_, Error>((keys, ncryptsec))
|
Ok::<_, Error>((keys, ncryptsec))
|
||||||
});
|
});
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
let result = async {
|
|
||||||
let (keys, ncryptsec) = job.await?;
|
let (keys, ncryptsec) = job.await?;
|
||||||
let public_key = keys.public_key();
|
let public_key = keys.public_key();
|
||||||
|
|
||||||
@@ -369,34 +350,28 @@ impl Backend {
|
|||||||
]
|
]
|
||||||
.to_vec();
|
.to_vec();
|
||||||
|
|
||||||
this.send(RelayList::new(relays).into_event_builder(), cx);
|
this.send_fire_and_forget(RelayList::new(relays).into_event_builder(), cx);
|
||||||
|
|
||||||
let metadata = Metadata::new()
|
let metadata = Metadata::new()
|
||||||
.name(&name)
|
.name(&name)
|
||||||
.display_name(&name)
|
.display_name(&name)
|
||||||
.into_event_builder();
|
.into_event_builder();
|
||||||
|
|
||||||
this.send(metadata, cx);
|
this.send_fire_and_forget(metadata, cx);
|
||||||
|
|
||||||
let grasp_servers: Vec<RelayUrl> =
|
let grasp_servers: Vec<RelayUrl> = ["wss://gitnostr.com", "wss://relay.ngit.dev"]
|
||||||
["wss://gitnostr.com", "wss://relay.ngit.dev"]
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|url| RelayUrl::parse(url).expect("valid relay URL"))
|
.map(|url| RelayUrl::parse(url).expect("valid relay URL"))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
this.send(GitUserGraspList { grasp_servers }.into_event_builder(), cx);
|
this.send_fire_and_forget(
|
||||||
|
GitUserGraspList { grasp_servers }.into_event_builder(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(public_key)
|
Ok(public_key)
|
||||||
}
|
})
|
||||||
.await;
|
|
||||||
|
|
||||||
tx.send_async(result).await.ok();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
|
|
||||||
rx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Login with an `nsec1...` key or a `bunker://...` URI, dispatching on
|
/// Login with an `nsec1...` key or a `bunker://...` URI, dispatching on
|
||||||
@@ -786,21 +761,22 @@ impl Backend {
|
|||||||
/// Sign, broadcast and locally store an event. Emits
|
/// Sign, broadcast and locally store an event. Emits
|
||||||
/// [`BackendEvent::Published`] on success so stores can refresh.
|
/// [`BackendEvent::Published`] on success so stores can refresh.
|
||||||
///
|
///
|
||||||
/// The returned receiver yields the outcome of this specific action,
|
/// The returned task yields the outcome of this specific action, so
|
||||||
/// so callers can show inline progress/errors instead of relying on
|
/// callers can show inline progress/errors instead of relying on
|
||||||
/// the global [`BackendEvent::Error`].
|
/// the global [`BackendEvent::Error`]. The task is owned by the caller;
|
||||||
|
/// dropping it cancels the publish.
|
||||||
pub fn send(
|
pub fn send(
|
||||||
&mut self,
|
&mut self,
|
||||||
builder: EventBuilder,
|
builder: EventBuilder,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> flume::Receiver<Result<Event, Error>> {
|
) -> Task<Result<Event, Error>> {
|
||||||
let (tx, rx) = flume::bounded(1);
|
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
let signer = self.signer.clone();
|
let signer = self.signer.clone();
|
||||||
|
|
||||||
let task = cx.background_spawn(async move {
|
cx.spawn(async move |this, cx| {
|
||||||
// Sign with the current signer, broadcast, and save locally so
|
// Sign with the current signer, broadcast, and save locally so
|
||||||
// the event is immediately visible to database queries.
|
// the event is immediately visible to database queries.
|
||||||
|
let work = cx.background_spawn(async move {
|
||||||
let event = builder.finalize_async(&signer).await?;
|
let event = builder.finalize_async(&signer).await?;
|
||||||
let output = client.send_event(&event).await?;
|
let output = client.send_event(&event).await?;
|
||||||
|
|
||||||
@@ -817,28 +793,42 @@ impl Backend {
|
|||||||
Ok(event)
|
Ok(event)
|
||||||
});
|
});
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
let result = work.await;
|
||||||
let result = task.await;
|
|
||||||
|
|
||||||
match &result {
|
match &result {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(BackendEvent::Published(Box::new(event.clone())));
|
cx.emit(BackendEvent::Published(Box::new(event.clone())));
|
||||||
})?;
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(BackendEvent::error(e.to_string()));
|
cx.emit(BackendEvent::error(e.to_string()));
|
||||||
})?;
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.send_async(result)
|
result
|
||||||
.await
|
})
|
||||||
.map_err(|_| anyhow!("action result receiver dropped"))
|
}
|
||||||
|
|
||||||
|
/// Sign, broadcast and store an event without awaiting the result;
|
||||||
|
/// failures surface through [`BackendEvent::Error`]. The spawned task is
|
||||||
|
/// owned by the backend, so it is cancelled when the backend is dropped.
|
||||||
|
fn send_fire_and_forget(&mut self, builder: EventBuilder, cx: &mut Context<Self>) {
|
||||||
|
let task = self.send(builder, cx);
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
if let Err(e) = task.await {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(BackendEvent::error(e.to_string()));
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}));
|
}));
|
||||||
|
|
||||||
rx
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::RwLock;
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
@@ -73,8 +73,8 @@ const BATCH_TIMEOUT: Duration = Duration::from_millis(500);
|
|||||||
/// data; the whole store notifies on change.
|
/// data; the whole store notifies on change.
|
||||||
pub struct ProfileStore {
|
pub struct ProfileStore {
|
||||||
profiles: HashMap<PublicKey, Profile>,
|
profiles: HashMap<PublicKey, Profile>,
|
||||||
/// Public keys we've already requested this session.
|
/// Public keys we've already requested this session (main thread only).
|
||||||
seen: RwLock<HashSet<PublicKey>>,
|
seen: RefCell<HashSet<PublicKey>>,
|
||||||
/// Sender for queuing fetch requests, batched by a background task.
|
/// Sender for queuing fetch requests, batched by a background task.
|
||||||
sender: Sender<PublicKey>,
|
sender: Sender<PublicKey>,
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
@@ -133,7 +133,7 @@ impl ProfileStore {
|
|||||||
|
|
||||||
let mut store = Self {
|
let mut store = Self {
|
||||||
profiles: HashMap::new(),
|
profiles: HashMap::new(),
|
||||||
seen: RwLock::new(HashSet::new()),
|
seen: RefCell::new(HashSet::new()),
|
||||||
sender,
|
sender,
|
||||||
tasks,
|
tasks,
|
||||||
_subscription: subscription,
|
_subscription: subscription,
|
||||||
@@ -152,7 +152,7 @@ impl ProfileStore {
|
|||||||
|
|
||||||
let public_key = *public_key;
|
let public_key = *public_key;
|
||||||
|
|
||||||
if self.seen.write().unwrap().insert(public_key)
|
if self.seen.borrow_mut().insert(public_key)
|
||||||
&& let Err(e) = self.sender.send(public_key)
|
&& let Err(e) = self.sender.send(public_key)
|
||||||
{
|
{
|
||||||
log::warn!("failed to queue profile fetch: {e}");
|
log::warn!("failed to queue profile fetch: {e}");
|
||||||
@@ -232,7 +232,8 @@ impl ProfileStore {
|
|||||||
/// Re-read the latest metadata of every requested author from the local
|
/// Re-read the latest metadata of every requested author from the local
|
||||||
/// database (used after a sync, which produces no NostrUpdate events).
|
/// database (used after a sync, which produces no NostrUpdate events).
|
||||||
fn apply_seen(&mut self, cx: &mut Context<Self>) {
|
fn apply_seen(&mut self, cx: &mut Context<Self>) {
|
||||||
let authors: Vec<PublicKey> = self.seen.read().unwrap().iter().copied().collect();
|
let authors: Vec<PublicKey> = self.seen.borrow().iter().copied().collect();
|
||||||
|
|
||||||
if authors.is_empty() {
|
if authors.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,9 +94,10 @@ impl RepoStore {
|
|||||||
/// Fetch this repository's events from the bootstrap relays (one-shot,
|
/// Fetch this repository's events from the bootstrap relays (one-shot,
|
||||||
/// auto-closing subscription).
|
/// auto-closing subscription).
|
||||||
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
|
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let backend = Backend::global(cx);
|
||||||
let addr = self.addr.clone();
|
let addr = self.addr.clone();
|
||||||
|
|
||||||
Backend::global(cx).update(cx, |backend, cx| {
|
backend.update(cx, |backend, cx| {
|
||||||
let mut repo_filters = vec![
|
let mut repo_filters = vec![
|
||||||
filters::announcement(&addr),
|
filters::announcement(&addr),
|
||||||
filters::state(&addr),
|
filters::state(&addr),
|
||||||
@@ -309,20 +310,18 @@ impl RepoStore {
|
|||||||
fn send(&mut self, builder: EventBuilder, cx: &mut Context<Self>) {
|
fn send(&mut self, builder: EventBuilder, cx: &mut Context<Self>) {
|
||||||
self.last_error = None;
|
self.last_error = None;
|
||||||
|
|
||||||
let rx = Backend::global(cx).update(cx, |backend, cx| backend.send(builder, cx));
|
let backend = Backend::global(cx);
|
||||||
|
let task = backend.update(cx, |backend, cx| backend.send(builder, cx));
|
||||||
|
|
||||||
let task = cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
if let Ok(Err(e)) = rx.recv_async().await {
|
if let Err(e) = task.await {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.last_error = Some(e.to_string());
|
this.last_error = Some(e.to_string());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
}));
|
||||||
|
|
||||||
self.tasks.push(task);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ impl RepoListView {
|
|||||||
let name = announcement
|
let name = announcement
|
||||||
.name
|
.name
|
||||||
.clone()
|
.clone()
|
||||||
.map(|s| SharedString::from(s.trim()))
|
|
||||||
.unwrap_or_else(|| SharedString::from(announcement.id.clone()));
|
.unwrap_or_else(|| SharedString::from(announcement.id.clone()));
|
||||||
|
|
||||||
let description = announcement.description.clone().unwrap_or_default();
|
let description = announcement.description.clone().unwrap_or_default();
|
||||||
|
|||||||
@@ -103,21 +103,20 @@ pub fn open(
|
|||||||
state.error = None;
|
state.error = None;
|
||||||
});
|
});
|
||||||
|
|
||||||
let rx = backend.update(cx, |backend, cx| {
|
let task = backend.update(cx, |backend, cx| {
|
||||||
backend.create_identity(&name, &pass, cx)
|
backend.create_identity(&name, &pass, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
let handle = window.window_handle();
|
let handle = window.window_handle();
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
|
|
||||||
cx.spawn(async move |cx| match rx.recv_async().await {
|
cx.spawn(async move |cx| match task.await {
|
||||||
Ok(Ok(_)) => {
|
Ok(_) => {
|
||||||
cx.update_window(handle, |_, window, cx| {
|
cx.update_window(handle, |_, window, cx| {
|
||||||
window.close_dialog(cx);
|
window.close_dialog(cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Err(e) => {
|
||||||
cx.update_window(handle, |_, _window, cx| {
|
cx.update_window(handle, |_, _window, cx| {
|
||||||
state.update(cx, |state, _| {
|
state.update(cx, |state, _| {
|
||||||
state.busy = false;
|
state.busy = false;
|
||||||
@@ -126,7 +125,6 @@ pub fn open(
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,18 +118,19 @@ fn unlock(
|
|||||||
state.error = None;
|
state.error = None;
|
||||||
});
|
});
|
||||||
|
|
||||||
let rx = backend.update(cx, |backend, cx| backend.restore_with_passphrase(&pass, cx));
|
let task = backend.update(cx, |backend, cx| backend.restore_with_passphrase(&pass, cx));
|
||||||
|
|
||||||
let handle = *handle;
|
let handle = *handle;
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
|
|
||||||
cx.spawn(async move |cx| match rx.recv_async().await {
|
cx.spawn(async move |cx| match task.await {
|
||||||
Ok(Ok(_)) => {
|
Ok(_) => {
|
||||||
cx.update_window(handle, |_, window, cx| window.close_dialog(cx))
|
cx.update_window(handle, |_this, window, cx| {
|
||||||
|
window.close_dialog(cx);
|
||||||
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Err(e) => {
|
||||||
cx.update_window(handle, |_, _window, cx| {
|
cx.update_window(handle, |_this, _window, cx| {
|
||||||
state.update(cx, |state, _| {
|
state.update(cx, |state, _| {
|
||||||
state.busy = false;
|
state.busy = false;
|
||||||
state.error = Some(e.to_string().into());
|
state.error = Some(e.to_string().into());
|
||||||
@@ -137,7 +138,6 @@ fn unlock(
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user