Compare commits
14 Commits
0.1.0-alph
...
0.1.2-alph
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e1d76bbcd | |||
| 61fb90bd34 | |||
| 50242981a5 | |||
| 85c485a4e4 | |||
| 48af00950a | |||
| 31e94c53c6 | |||
| ae01a2d67a | |||
| 2a5a3b5c0a | |||
| 0c45695edb | |||
| ea5009933c | |||
| 0feb69b72e | |||
| ab7664c872 | |||
| cd6a9f0550 | |||
| ce9193c187 |
754
Cargo.lock
generated
754
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@@ -8,7 +8,6 @@ coop = { path = "crates/*" }
|
|||||||
|
|
||||||
# UI
|
# UI
|
||||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
gpui = { git = "https://github.com/zed-industries/zed" }
|
||||||
gpui_tokio = { git = "https://github.com/zed-industries/zed" }
|
|
||||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
|
||||||
# Nostr
|
# Nostr
|
||||||
@@ -16,11 +15,15 @@ nostr-relay-builder = { git = "https://github.com/rust-nostr/nostr" }
|
|||||||
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
|
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
|
||||||
"lmdb",
|
"lmdb",
|
||||||
"all-nips",
|
"nip96",
|
||||||
|
"nip59",
|
||||||
|
"nip49",
|
||||||
|
"nip44",
|
||||||
|
"nip05",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
smol = "2"
|
smol = "2"
|
||||||
tokio = { version = "1", features = ["full"] }
|
oneshot = { git = "https://github.com/faern/oneshot" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
@@ -31,6 +34,7 @@ tracing = "0.1.40"
|
|||||||
anyhow = "1.0.44"
|
anyhow = "1.0.44"
|
||||||
smallvec = "1.13.2"
|
smallvec = "1.13.2"
|
||||||
rust-embed = "8.5.0"
|
rust-embed = "8.5.0"
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name = "coop"
|
|||||||
description = "Coop is a cross-platform Nostr client designed for secure communication focus on simplicity and customizability."
|
description = "Coop is a cross-platform Nostr client designed for secure communication focus on simplicity and customizability."
|
||||||
product-name = "Coop"
|
product-name = "Coop"
|
||||||
identifier = "su.reya.coop"
|
identifier = "su.reya.coop"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
resources = ["assets/*/*", "Cargo.toml", "./LICENSE", "./README.md"]
|
resources = ["assets/*/*", "Cargo.toml", "./LICENSE", "./README.md"]
|
||||||
icons = [
|
icons = [
|
||||||
"assets/brand/32x32.png",
|
"assets/brand/32x32.png",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "coop"
|
name = "coop"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
@@ -15,10 +15,8 @@ state = { path = "../state" }
|
|||||||
chats = { path = "../chats" }
|
chats = { path = "../chats" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_tokio.workspace = true
|
|
||||||
reqwest_client.workspace = true
|
reqwest_client.workspace = true
|
||||||
|
|
||||||
tokio.workspace = true
|
|
||||||
nostr-connect.workspace = true
|
nostr-connect.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
@@ -27,8 +25,10 @@ serde_json.workspace = true
|
|||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
dirs.workspace = true
|
dirs.workspace = true
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
|
log.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
oneshot.workspace = true
|
||||||
|
|
||||||
cargo-packager-updater = "0.2.2"
|
rustls = "0.23.23"
|
||||||
|
futures= "0.3"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||||
log = "0.4"
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
use asset::Assets;
|
use asset::Assets;
|
||||||
use async_utility::task::spawn;
|
|
||||||
use chats::registry::ChatRegistry;
|
use chats::registry::ChatRegistry;
|
||||||
use common::{
|
use common::{
|
||||||
constants::{
|
constants::{ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, KEYRING_SERVICE, NEW_MESSAGE_SUB_ID},
|
||||||
ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, FAKE_SIG, KEYRING_SERVICE, NEW_MESSAGE_SUB_ID,
|
|
||||||
},
|
|
||||||
profile::NostrProfile,
|
profile::NostrProfile,
|
||||||
};
|
};
|
||||||
|
use futures::{select, FutureExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, px, size, App, AppContext, Application, AsyncApp, Bounds, KeyBinding, Menu, MenuItem,
|
actions, px, size, App, AppContext, Application, AsyncApp, Bounds, KeyBinding, Menu, MenuItem,
|
||||||
WindowBounds, WindowKind, WindowOptions,
|
WindowBounds, WindowKind, WindowOptions,
|
||||||
@@ -16,20 +14,24 @@ use gpui::{point, SharedString, TitlebarOptions};
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::{
|
||||||
|
pool::prelude::ReqExitPolicy, Client, Event, Filter, Keys, Kind, Metadata, PublicKey,
|
||||||
|
RelayMessage, RelayPoolNotification, SubscribeAutoCloseOptions,
|
||||||
|
};
|
||||||
|
use nostr_sdk::{prelude::NostrEventsDatabaseExt, FromBech32, SubscriptionId};
|
||||||
|
use smol::Timer;
|
||||||
use state::{get_client, initialize_client};
|
use state::{get_client, initialize_client};
|
||||||
use std::{borrow::Cow, collections::HashSet, str::FromStr, sync::Arc, time::Duration};
|
use std::{collections::HashSet, mem, sync::Arc, time::Duration};
|
||||||
use tokio::sync::{mpsc, oneshot};
|
|
||||||
use ui::{theme::Theme, Root};
|
use ui::{theme::Theme, Root};
|
||||||
use views::{app, onboarding, startup};
|
use views::{app, onboarding, startup};
|
||||||
|
|
||||||
mod asset;
|
mod asset;
|
||||||
mod views;
|
mod views;
|
||||||
|
|
||||||
actions!(main_menu, [Quit]);
|
actions!(coop, [Quit]);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum Signal {
|
enum Signal {
|
||||||
/// Receive event
|
/// Receive event
|
||||||
Event(Event),
|
Event(Event),
|
||||||
/// Receive EOSE
|
/// Receive EOSE
|
||||||
@@ -37,209 +39,166 @@ pub enum Signal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Initialize Nostr client
|
// Fix crash on startup
|
||||||
initialize_client();
|
// TODO: why this is needed?
|
||||||
|
_ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
// Enable logging
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
// Get client
|
let (event_tx, event_rx) = smol::channel::bounded::<Signal>(2048);
|
||||||
let client = get_client();
|
let (batch_tx, batch_rx) = smol::channel::bounded::<Vec<PublicKey>>(100);
|
||||||
let (signal_tx, mut signal_rx) = tokio::sync::mpsc::channel::<Signal>(2048);
|
|
||||||
|
|
||||||
spawn(async move {
|
// Initialize nostr client
|
||||||
// Add some bootstrap relays
|
let client = initialize_client();
|
||||||
_ = client.add_relay("wss://relay.damus.io/").await;
|
|
||||||
_ = client.add_relay("wss://relay.primal.net/").await;
|
|
||||||
_ = client.add_relay("wss://user.kindpag.es/").await;
|
|
||||||
_ = client.add_relay("wss://directory.yabu.me/").await;
|
|
||||||
_ = client.add_discovery_relay("wss://relaydiscovery.com").await;
|
|
||||||
|
|
||||||
// Connect to all relays
|
|
||||||
_ = client.connect().await
|
|
||||||
});
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
let (batch_tx, mut batch_rx) = mpsc::channel::<Cow<Event>>(20);
|
|
||||||
|
|
||||||
async fn sync_metadata(client: &Client, buffer: &HashSet<PublicKey>) {
|
|
||||||
let filter = Filter::new()
|
|
||||||
.authors(buffer.iter().copied())
|
|
||||||
.kind(Kind::Metadata)
|
|
||||||
.limit(buffer.len());
|
|
||||||
|
|
||||||
if let Err(e) = client.sync(filter, &SyncOptions::default()).await {
|
|
||||||
error!("NEG error: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn process_batch(client: &Client, events: &[Cow<'_, Event>]) {
|
|
||||||
let sig = Signature::from_str(FAKE_SIG).unwrap();
|
|
||||||
let mut buffer: HashSet<PublicKey> = HashSet::with_capacity(20);
|
|
||||||
|
|
||||||
for event in events.iter() {
|
|
||||||
if let Ok(UnwrappedGift { mut rumor, sender }) =
|
|
||||||
client.unwrap_gift_wrap(event).await
|
|
||||||
{
|
|
||||||
let pubkeys: HashSet<PublicKey> = event.tags.public_keys().copied().collect();
|
|
||||||
buffer.extend(pubkeys);
|
|
||||||
buffer.insert(sender);
|
|
||||||
|
|
||||||
// Create event's ID is not exist
|
|
||||||
rumor.ensure_id();
|
|
||||||
|
|
||||||
// Save event to database
|
|
||||||
if let Some(id) = rumor.id {
|
|
||||||
let ev = Event::new(
|
|
||||||
id,
|
|
||||||
rumor.pubkey,
|
|
||||||
rumor.created_at,
|
|
||||||
rumor.kind,
|
|
||||||
rumor.tags,
|
|
||||||
rumor.content,
|
|
||||||
sig,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Err(e) = client.database().save_event(&ev).await {
|
|
||||||
error!("Save error: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sync_metadata(client, &buffer).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn a thread to handle batch process
|
|
||||||
spawn(async move {
|
|
||||||
const BATCH_SIZE: usize = 20;
|
|
||||||
const BATCH_TIMEOUT: Duration = Duration::from_millis(200);
|
|
||||||
|
|
||||||
let mut batch = Vec::with_capacity(20);
|
|
||||||
let mut timeout = Box::pin(tokio::time::sleep(BATCH_TIMEOUT));
|
|
||||||
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
event = batch_rx.recv() => {
|
|
||||||
if let Some(event) = event {
|
|
||||||
batch.push(event);
|
|
||||||
|
|
||||||
if batch.len() == BATCH_SIZE {
|
|
||||||
process_batch(client, &batch).await;
|
|
||||||
batch.clear();
|
|
||||||
timeout = Box::pin(tokio::time::sleep(BATCH_TIMEOUT));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = &mut timeout => {
|
|
||||||
if !batch.is_empty() {
|
|
||||||
process_batch(client, &batch).await;
|
|
||||||
batch.clear();
|
|
||||||
}
|
|
||||||
timeout = Box::pin(tokio::time::sleep(BATCH_TIMEOUT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let all_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
|
||||||
let new_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
|
||||||
let sig = Signature::from_str(FAKE_SIG).unwrap();
|
|
||||||
let mut notifications = client.notifications();
|
|
||||||
|
|
||||||
while let Ok(notification) = notifications.recv().await {
|
|
||||||
if let RelayPoolNotification::Message { message, .. } = notification {
|
|
||||||
match message {
|
|
||||||
RelayMessage::Event {
|
|
||||||
event,
|
|
||||||
subscription_id,
|
|
||||||
} => match event.kind {
|
|
||||||
Kind::GiftWrap => {
|
|
||||||
if new_id == *subscription_id {
|
|
||||||
if let Ok(UnwrappedGift { mut rumor, .. }) =
|
|
||||||
client.unwrap_gift_wrap(&event).await
|
|
||||||
{
|
|
||||||
// Compute event id if not exist
|
|
||||||
rumor.ensure_id();
|
|
||||||
|
|
||||||
if let Some(id) = rumor.id {
|
|
||||||
let ev = Event::new(
|
|
||||||
id,
|
|
||||||
rumor.pubkey,
|
|
||||||
rumor.created_at,
|
|
||||||
rumor.kind,
|
|
||||||
rumor.tags,
|
|
||||||
rumor.content,
|
|
||||||
sig,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save rumor to database to further query
|
|
||||||
if let Err(e) = client.database().save_event(&ev).await {
|
|
||||||
error!("Save error: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send new event to GPUI
|
|
||||||
if let Err(e) = signal_tx.send(Signal::Event(ev)).await {
|
|
||||||
error!("Send error: {}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = batch_tx.send(event).await {
|
|
||||||
error!("Failed to add to batch: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Kind::ContactList => {
|
|
||||||
let public_keys: HashSet<_> =
|
|
||||||
event.tags.public_keys().copied().collect();
|
|
||||||
|
|
||||||
sync_metadata(client, &public_keys).await;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
RelayMessage::EndOfStoredEvents(subscription_id) => {
|
|
||||||
if all_id == *subscription_id {
|
|
||||||
if let Err(e) = signal_tx.send(Signal::Eose).await {
|
|
||||||
error!("Failed to send eose: {}", e)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Initialize application
|
||||||
let app = Application::new()
|
let app = Application::new()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
|
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
|
||||||
|
|
||||||
|
// Connect to default relays
|
||||||
|
app.background_executor()
|
||||||
|
.spawn(async {
|
||||||
|
_ = client.add_relay("wss://relay.damus.io/").await;
|
||||||
|
_ = client.add_relay("wss://relay.primal.net/").await;
|
||||||
|
_ = client.add_relay("wss://user.kindpag.es/").await;
|
||||||
|
_ = client.add_relay("wss://purplepag.es/").await;
|
||||||
|
_ = client.add_discovery_relay("wss://relaydiscovery.com").await;
|
||||||
|
_ = client.connect().await
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
// Handle batch metadata
|
||||||
|
app.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
const BATCH_SIZE: usize = 20;
|
||||||
|
const BATCH_TIMEOUT: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
|
let mut batch: HashSet<PublicKey> = HashSet::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut timeout = Box::pin(Timer::after(BATCH_TIMEOUT).fuse());
|
||||||
|
|
||||||
|
select! {
|
||||||
|
pubkeys = batch_rx.recv().fuse() => {
|
||||||
|
match pubkeys {
|
||||||
|
Ok(keys) => {
|
||||||
|
batch.extend(keys);
|
||||||
|
if batch.len() >= BATCH_SIZE {
|
||||||
|
sync_metadata(client, mem::take(&mut batch)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = timeout => {
|
||||||
|
if !batch.is_empty() {
|
||||||
|
sync_metadata(client, mem::take(&mut batch)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
// Handle notifications
|
||||||
|
app.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let rng_keys = Keys::generate();
|
||||||
|
let all_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
||||||
|
let new_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||||
|
let mut notifications = client.notifications();
|
||||||
|
|
||||||
|
while let Ok(notification) = notifications.recv().await {
|
||||||
|
if let RelayPoolNotification::Message { message, .. } = notification {
|
||||||
|
match message {
|
||||||
|
RelayMessage::Event {
|
||||||
|
event,
|
||||||
|
subscription_id,
|
||||||
|
} => {
|
||||||
|
match event.kind {
|
||||||
|
Kind::GiftWrap => {
|
||||||
|
if let Ok(gift) = client.unwrap_gift_wrap(&event).await {
|
||||||
|
let mut pubkeys = vec![];
|
||||||
|
|
||||||
|
// Sign the rumor with the generated keys,
|
||||||
|
// this event will be used for internal only,
|
||||||
|
// and NEVER send to relays.
|
||||||
|
if let Ok(event) = gift.rumor.sign_with_keys(&rng_keys) {
|
||||||
|
pubkeys.extend(event.tags.public_keys());
|
||||||
|
pubkeys.push(event.pubkey);
|
||||||
|
|
||||||
|
// Save the event to the database, use for query directly.
|
||||||
|
if let Err(e) =
|
||||||
|
client.database().save_event(&event).await
|
||||||
|
{
|
||||||
|
error!("Failed to save event: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all pubkeys to the batch
|
||||||
|
if let Err(e) = batch_tx.send(pubkeys).await {
|
||||||
|
error!("Failed to send pubkeys to batch: {}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send this event to the GPUI
|
||||||
|
if new_id == *subscription_id {
|
||||||
|
if let Err(e) =
|
||||||
|
event_tx.send(Signal::Event(event)).await
|
||||||
|
{
|
||||||
|
error!("Failed to send event to GPUI: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kind::ContactList => {
|
||||||
|
let pubkeys =
|
||||||
|
event.tags.public_keys().copied().collect::<HashSet<_>>();
|
||||||
|
sync_metadata(client, pubkeys).await;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RelayMessage::EndOfStoredEvents(subscription_id) => {
|
||||||
|
if all_id == *subscription_id {
|
||||||
|
if let Err(e) = event_tx.send(Signal::Eose).await {
|
||||||
|
error!("Failed to send eose: {}", e)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
// Handle re-open window
|
||||||
app.on_reopen(move |cx| {
|
app.on_reopen(move |cx| {
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
let (tx, rx) = oneshot::channel::<Option<NostrProfile>>();
|
let (tx, rx) = oneshot::channel::<Option<NostrProfile>>();
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
cx.background_spawn(async move {
|
||||||
cx.background_spawn(async move {
|
if let Ok(signer) = client.signer().await {
|
||||||
if let Ok(signer) = client.signer().await {
|
if let Ok(public_key) = signer.get_public_key().await {
|
||||||
if let Ok(public_key) = signer.get_public_key().await {
|
let metadata =
|
||||||
let metadata = if let Ok(Some(metadata)) =
|
if let Ok(Some(metadata)) = client.database().metadata(public_key).await {
|
||||||
client.database().metadata(public_key).await
|
|
||||||
{
|
|
||||||
metadata
|
metadata
|
||||||
} else {
|
} else {
|
||||||
Metadata::new()
|
Metadata::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
_ = tx.send(Some(NostrProfile::new(public_key, metadata)));
|
_ = tx.send(Some(NostrProfile::new(public_key, metadata)));
|
||||||
} else {
|
|
||||||
_ = tx.send(None);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
_ = tx.send(None);
|
_ = tx.send(None);
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
.detach();
|
_ = tx.send(None);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
if let Ok(result) = rx.await {
|
if let Ok(result) = rx.await {
|
||||||
_ = restore_window(result, &mut cx).await;
|
_ = restore_window(result, &mut cx).await;
|
||||||
}
|
}
|
||||||
@@ -264,116 +223,131 @@ fn main() {
|
|||||||
items: vec![MenuItem::action("Quit", Quit)],
|
items: vec![MenuItem::action("Quit", Quit)],
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
let opts = WindowOptions {
|
// Open window with default options
|
||||||
#[cfg(not(target_os = "linux"))]
|
cx.open_window(
|
||||||
titlebar: Some(TitlebarOptions {
|
WindowOptions {
|
||||||
title: Some(SharedString::new_static(APP_NAME)),
|
#[cfg(not(target_os = "linux"))]
|
||||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
titlebar: Some(TitlebarOptions {
|
||||||
appears_transparent: true,
|
title: Some(SharedString::new_static(APP_NAME)),
|
||||||
}),
|
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
appears_transparent: true,
|
||||||
None,
|
}),
|
||||||
size(px(900.0), px(680.0)),
|
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||||
cx,
|
None,
|
||||||
))),
|
size(px(900.0), px(680.0)),
|
||||||
#[cfg(target_os = "linux")]
|
cx,
|
||||||
window_background: WindowBackgroundAppearance::Transparent,
|
))),
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
window_decorations: Some(WindowDecorations::Client),
|
window_background: WindowBackgroundAppearance::Transparent,
|
||||||
kind: WindowKind::Normal,
|
#[cfg(target_os = "linux")]
|
||||||
..Default::default()
|
window_decorations: Some(WindowDecorations::Client),
|
||||||
};
|
kind: WindowKind::Normal,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|window, cx| {
|
||||||
|
window.set_window_title(APP_NAME);
|
||||||
|
window.set_app_id(APP_ID);
|
||||||
|
|
||||||
cx.open_window(opts, |window, cx| {
|
#[cfg(not(target_os = "linux"))]
|
||||||
window.set_window_title(APP_NAME);
|
window
|
||||||
window.set_app_id(APP_ID);
|
.observe_window_appearance(|window, cx| {
|
||||||
window
|
Theme::sync_system_appearance(Some(window), cx);
|
||||||
.observe_window_appearance(|window, cx| {
|
})
|
||||||
Theme::sync_system_appearance(Some(window), cx);
|
.detach();
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
let handle = window.window_handle();
|
let handle = window.window_handle();
|
||||||
let root = cx.new(|cx| Root::new(startup::init(window, cx).into(), window, cx));
|
let root = cx.new(|cx| Root::new(startup::init(window, cx).into(), window, cx));
|
||||||
|
|
||||||
let task = cx.read_credentials(KEYRING_SERVICE);
|
let task = cx.read_credentials(KEYRING_SERVICE);
|
||||||
let (tx, rx) = oneshot::channel::<Option<NostrProfile>>();
|
let (tx, rx) = oneshot::channel::<Option<NostrProfile>>();
|
||||||
|
|
||||||
// Read credential in OS Keyring
|
// Read credential in OS Keyring
|
||||||
cx.background_spawn(async {
|
cx.background_spawn(async {
|
||||||
let profile = if let Ok(Some((npub, secret))) = task.await {
|
let profile = if let Ok(Some((npub, secret))) = task.await {
|
||||||
let public_key = PublicKey::from_bech32(&npub).unwrap();
|
let public_key = PublicKey::from_bech32(&npub).unwrap();
|
||||||
let secret_hex = String::from_utf8(secret).unwrap();
|
let secret_hex = String::from_utf8(secret).unwrap();
|
||||||
let keys = Keys::parse(&secret_hex).unwrap();
|
let keys = Keys::parse(&secret_hex).unwrap();
|
||||||
|
|
||||||
// Update nostr signer
|
// Update nostr signer
|
||||||
_ = client.set_signer(keys).await;
|
_ = client.set_signer(keys).await;
|
||||||
|
|
||||||
// Get user's metadata
|
// Get user's metadata
|
||||||
let metadata =
|
let metadata = if let Ok(Some(metadata)) =
|
||||||
if let Ok(Some(metadata)) = client.database().metadata(public_key).await {
|
client.database().metadata(public_key).await
|
||||||
|
{
|
||||||
metadata
|
metadata
|
||||||
} else {
|
} else {
|
||||||
Metadata::new()
|
Metadata::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(NostrProfile::new(public_key, metadata))
|
Some(NostrProfile::new(public_key, metadata))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
_ = tx.send(profile)
|
_ = tx.send(profile)
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
// Set root view based on credential status
|
// Set root view based on credential status
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
if let Ok(Some(profile)) = rx.await {
|
if let Ok(Some(profile)) = rx.await {
|
||||||
_ = cx.update_window(handle, |_, window, cx| {
|
_ = cx.update_window(handle, |_, window, cx| {
|
||||||
window.replace_root(cx, |window, cx| {
|
window.replace_root(cx, |window, cx| {
|
||||||
Root::new(app::init(profile, window, cx).into(), window, cx)
|
Root::new(app::init(profile, window, cx).into(), window, cx)
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_ = cx.update_window(handle, |_, window, cx| {
|
|
||||||
window.replace_root(cx, |window, cx| {
|
|
||||||
Root::new(onboarding::init(window, cx).into(), window, cx)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
// Listen for messages from the Nostr thread
|
|
||||||
cx.spawn(|cx| async move {
|
|
||||||
while let Some(signal) = signal_rx.recv().await {
|
|
||||||
match signal {
|
|
||||||
Signal::Eose => {
|
|
||||||
_ = cx.update(|cx| {
|
|
||||||
if let Some(chats) = ChatRegistry::global(cx) {
|
|
||||||
chats.update(cx, |this, cx| this.load_chat_rooms(cx))
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
Signal::Event(event) => {
|
} else {
|
||||||
_ = cx.update(|cx| {
|
_ = cx.update_window(handle, |_, window, cx| {
|
||||||
if let Some(chats) = ChatRegistry::global(cx) {
|
window.replace_root(cx, |window, cx| {
|
||||||
chats.update(cx, |this, cx| this.push_message(event, cx))
|
Root::new(onboarding::init(window, cx).into(), window, cx)
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
.detach();
|
||||||
.detach();
|
|
||||||
|
|
||||||
root
|
cx.spawn(|cx| async move {
|
||||||
})
|
while let Ok(signal) = event_rx.recv().await {
|
||||||
|
cx.update(|cx| {
|
||||||
|
match signal {
|
||||||
|
Signal::Eose => {
|
||||||
|
if let Some(chats) = ChatRegistry::global(cx) {
|
||||||
|
chats.update(cx, |this, cx| this.load_chat_rooms(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Signal::Event(event) => {
|
||||||
|
if let Some(chats) = ChatRegistry::global(cx) {
|
||||||
|
chats.update(cx, |this, cx| this.push_message(event, cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
root
|
||||||
|
},
|
||||||
|
)
|
||||||
.expect("System error. Please re-open the app.");
|
.expect("System error. Please re-open the app.");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restore_window(profile: Option<NostrProfile>, cx: &mut AsyncApp) -> Result<()> {
|
async fn sync_metadata(client: &Client, buffer: HashSet<PublicKey>) {
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
let filter = Filter::new()
|
||||||
|
.authors(buffer.iter().cloned())
|
||||||
|
.kind(Kind::Metadata)
|
||||||
|
.limit(buffer.len());
|
||||||
|
|
||||||
|
if let Err(e) = client.subscribe(filter, Some(opts)).await {
|
||||||
|
error!("Subscribe error: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restore_window(profile: Option<NostrProfile>, cx: &mut AsyncApp) -> anyhow::Result<()> {
|
||||||
let opts = cx
|
let opts = cx
|
||||||
.update(|cx| WindowOptions {
|
.update(|cx| WindowOptions {
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
@@ -400,6 +374,8 @@ async fn restore_window(profile: Option<NostrProfile>, cx: &mut AsyncApp) -> Res
|
|||||||
_ = cx.open_window(opts, |window, cx| {
|
_ = cx.open_window(opts, |window, cx| {
|
||||||
window.set_window_title(APP_NAME);
|
window.set_window_title(APP_NAME);
|
||||||
window.set_app_id(APP_ID);
|
window.set_app_id(APP_ID);
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
window
|
window
|
||||||
.observe_window_appearance(|window, cx| {
|
.observe_window_appearance(|window, cx| {
|
||||||
Theme::sync_system_appearance(Some(window), cx);
|
Theme::sync_system_appearance(Some(window), cx);
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
use cargo_packager_updater::{check_update, semver::Version, url::Url};
|
use common::profile::NostrProfile;
|
||||||
use common::{
|
|
||||||
constants::{UPDATER_PUBKEY, UPDATER_URL},
|
|
||||||
profile::NostrProfile,
|
|
||||||
};
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis,
|
actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis,
|
||||||
Context, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled,
|
Context, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled,
|
||||||
StyledImage, Window,
|
StyledImage, Window,
|
||||||
};
|
};
|
||||||
use log::info;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonRounded, ButtonVariants},
|
button::{Button, ButtonRounded, ButtonVariants},
|
||||||
dock_area::{dock::DockPlacement, DockArea, DockItem},
|
dock_area::{dock::DockPlacement, DockArea, DockItem},
|
||||||
@@ -88,26 +82,6 @@ impl AppView {
|
|||||||
view.set_center(center_panel, window, cx);
|
view.set_center(center_panel, window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check and auto update to the latest version
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
// Set auto updater config
|
|
||||||
let config = cargo_packager_updater::Config {
|
|
||||||
endpoints: vec![Url::parse(UPDATER_URL).expect("Failed to parse UPDATER URL")],
|
|
||||||
pubkey: String::from(UPDATER_PUBKEY),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run auto updater
|
|
||||||
if let Ok(current_version) = Version::parse(env!("CARGO_PKG_VERSION")) {
|
|
||||||
if let Ok(Some(update)) = check_update(current_version, config) {
|
|
||||||
if update.download_and_install().is_ok() {
|
|
||||||
info!("Update installed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let public_key = account.public_key();
|
let public_key = account.public_key();
|
||||||
let relays = cx.new(|_| None);
|
let relays = cx.new(|_| None);
|
||||||
@@ -188,7 +162,7 @@ impl AppView {
|
|||||||
this.keyboard(false)
|
this.keyboard(false)
|
||||||
.closable(false)
|
.closable(false)
|
||||||
.width(px(420.))
|
.width(px(420.))
|
||||||
.title("Your Messaging Relays is not configured")
|
.title("Your Messaging Relays are not configured")
|
||||||
.child(relays.clone())
|
.child(relays.clone())
|
||||||
.footer(
|
.footer(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ use nostr_sdk::prelude::*;
|
|||||||
use smol::fs;
|
use smol::fs;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonRounded, ButtonVariants},
|
button::{Button, ButtonRounded, ButtonVariants},
|
||||||
dock_area::panel::{Panel, PanelEvent},
|
dock_area::panel::{Panel, PanelEvent},
|
||||||
@@ -50,22 +49,39 @@ pub fn init(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
struct ChatItem {
|
struct ParsedMessage {
|
||||||
profile: NostrProfile,
|
avatar: SharedString,
|
||||||
|
display_name: SharedString,
|
||||||
|
created_at: SharedString,
|
||||||
content: SharedString,
|
content: SharedString,
|
||||||
ago: SharedString,
|
}
|
||||||
|
|
||||||
|
impl ParsedMessage {
|
||||||
|
pub fn new(profile: &NostrProfile, content: &str, created_at: Timestamp) -> Self {
|
||||||
|
let avatar = profile.avatar().into();
|
||||||
|
let display_name = profile.name().into();
|
||||||
|
let content = SharedString::new(content);
|
||||||
|
let created_at = LastSeen(created_at).human_readable();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
avatar,
|
||||||
|
display_name,
|
||||||
|
created_at,
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
enum Message {
|
enum Message {
|
||||||
Item(Box<ChatItem>),
|
User(Box<ParsedMessage>),
|
||||||
System(SharedString),
|
System(SharedString),
|
||||||
Placeholder,
|
Placeholder,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
pub fn new(chat_message: ChatItem) -> Self {
|
pub fn new(message: ParsedMessage) -> Self {
|
||||||
Self::Item(Box::new(chat_message))
|
Self::User(Box::new(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn system(content: SharedString) -> Self {
|
pub fn system(content: SharedString) -> Self {
|
||||||
@@ -122,7 +138,9 @@ impl Chat {
|
|||||||
},
|
},
|
||||||
)];
|
)];
|
||||||
|
|
||||||
let list_state = ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
// Initialize list state
|
||||||
|
// [item_count] always equal to 1 at the beginning
|
||||||
|
let list_state = ListState::new(1, ListAlignment::Bottom, px(1024.), {
|
||||||
let this = cx.entity().downgrade();
|
let this = cx.entity().downgrade();
|
||||||
move |ix, window, cx| {
|
move |ix, window, cx| {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
@@ -285,12 +303,7 @@ impl Chat {
|
|||||||
|
|
||||||
let old_len = self.messages.read(cx).len();
|
let old_len = self.messages.read(cx).len();
|
||||||
let room = model.read(cx);
|
let room = model.read(cx);
|
||||||
let ago = LastSeen(Timestamp::now()).human_readable();
|
let message = Message::new(ParsedMessage::new(&room.owner, &content, Timestamp::now()));
|
||||||
let message = Message::new(ChatItem {
|
|
||||||
profile: room.owner.clone(),
|
|
||||||
content: content.into(),
|
|
||||||
ago,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update message list
|
// Update message list
|
||||||
cx.update_entity(&self.messages, |this, cx| {
|
cx.update_entity(&self.messages, |this, cx| {
|
||||||
@@ -335,11 +348,10 @@ impl Chat {
|
|||||||
room.owner.to_owned()
|
room.owner.to_owned()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(Message::new(ChatItem {
|
let message =
|
||||||
profile: member,
|
Message::new(ParsedMessage::new(&member, &ev.content, ev.created_at));
|
||||||
content: ev.content.into(),
|
|
||||||
ago: LastSeen(ev.created_at).human_readable(),
|
Some(message)
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -377,11 +389,11 @@ impl Chat {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|event| {
|
.filter_map(|event| {
|
||||||
if let Some(profile) = room.member(&event.pubkey) {
|
if let Some(profile) = room.member(&event.pubkey) {
|
||||||
let message = Message::new(ChatItem {
|
let message = Message::new(ParsedMessage::new(
|
||||||
profile,
|
&profile,
|
||||||
content: event.content.clone().into(),
|
&event.content,
|
||||||
ago: LastSeen(event.created_at).human_readable(),
|
event.created_at,
|
||||||
});
|
));
|
||||||
|
|
||||||
if !old_messages.iter().any(|old| old == &message) {
|
if !old_messages.iter().any(|old| old == &message) {
|
||||||
Some(message)
|
Some(message)
|
||||||
@@ -424,7 +436,7 @@ impl Chat {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get message
|
// Get message
|
||||||
let mut content = self.input.read(cx).text().to_string();
|
let mut content = self.input.read(cx).text();
|
||||||
|
|
||||||
// Get all attaches and merge its with message
|
// Get all attaches and merge its with message
|
||||||
if let Some(attaches) = self.attaches.read(cx).as_ref() {
|
if let Some(attaches) = self.attaches.read(cx).as_ref() {
|
||||||
@@ -434,7 +446,7 @@ impl Chat {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
content = format!("{}\n{}", content, merged)
|
content = format!("{}\n{}", content, merged).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
if content.is_empty() {
|
if content.is_empty() {
|
||||||
@@ -454,7 +466,7 @@ impl Chat {
|
|||||||
|
|
||||||
let room = model.read(cx);
|
let room = model.read(cx);
|
||||||
let pubkeys = room.pubkeys();
|
let pubkeys = room.pubkeys();
|
||||||
let async_content = content.clone();
|
let async_content = content.clone().to_string();
|
||||||
let tags: Vec<Tag> = room
|
let tags: Vec<Tag> = room
|
||||||
.pubkeys()
|
.pubkeys()
|
||||||
.iter()
|
.iter()
|
||||||
@@ -487,7 +499,7 @@ impl Chat {
|
|||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||||
_ = this.update(cx, |this, cx| {
|
_ = this.update(cx, |this, cx| {
|
||||||
this.push_message(content.clone(), window, cx);
|
this.push_message(content.to_string(), window, cx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -597,7 +609,7 @@ impl Chat {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.p_2()
|
.p_2()
|
||||||
.map(|this| match message {
|
.map(|this| match message {
|
||||||
Message::Item(item) => this
|
Message::User(item) => this
|
||||||
.hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE)))
|
.hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE)))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -612,7 +624,7 @@ impl Chat {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
img(item.profile.avatar())
|
img(item.avatar.clone())
|
||||||
.size_8()
|
.size_8()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.flex_shrink_0(),
|
.flex_shrink_0(),
|
||||||
@@ -629,8 +641,10 @@ impl Chat {
|
|||||||
.items_baseline()
|
.items_baseline()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.child(div().font_semibold().child(item.profile.name()))
|
.child(
|
||||||
.child(div().child(item.ago.clone()).text_color(
|
div().font_semibold().child(item.display_name.clone()),
|
||||||
|
)
|
||||||
|
.child(div().child(item.created_at.clone()).text_color(
|
||||||
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
@@ -667,7 +681,7 @@ impl Chat {
|
|||||||
.text_center()
|
.text_center()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||||
.line_height(relative(1.))
|
.line_height(relative(1.2))
|
||||||
.child(
|
.child(
|
||||||
svg()
|
svg()
|
||||||
.path("brand/coop.svg")
|
.path("brand/coop.svg")
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
button::Button,
|
button::Button,
|
||||||
dock_area::panel::{Panel, PanelEvent},
|
dock_area::panel::{Panel, PanelEvent},
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use gpui::{
|
|||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use std::{path::PathBuf, time::Duration};
|
use std::{path::PathBuf, time::Duration};
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonCustomVariant, ButtonVariants},
|
button::{Button, ButtonCustomVariant, ButtonVariants},
|
||||||
input::{InputEvent, TextInput},
|
input::{InputEvent, TextInput},
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use nostr_sdk::prelude::*;
|
|||||||
use smol::fs;
|
use smol::fs;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonVariants},
|
button::{Button, ButtonVariants},
|
||||||
dock_area::panel::{Panel, PanelEvent},
|
dock_area::panel::{Panel, PanelEvent},
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonVariants},
|
button::{Button, ButtonVariants},
|
||||||
input::{InputEvent, TextInput},
|
input::{InputEvent, TextInput},
|
||||||
@@ -66,41 +65,54 @@ impl Relays {
|
|||||||
|
|
||||||
self.set_loading(true, cx);
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
|
let client = get_client();
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let signer = client.signer().await.expect("Signer is required");
|
||||||
|
let public_key = signer
|
||||||
|
.get_public_key()
|
||||||
|
.await
|
||||||
|
.expect("Cannot get public key");
|
||||||
|
|
||||||
|
// If user didn't have any NIP-65 relays, add default ones
|
||||||
|
// TODO: Is this really necessary?
|
||||||
|
if let Ok(relay_list) = client.database().relay_list(public_key).await {
|
||||||
|
if relay_list.is_empty() {
|
||||||
|
let builder = EventBuilder::relay_list(vec![
|
||||||
|
(RelayUrl::parse("wss://relay.damus.io/").unwrap(), None),
|
||||||
|
(RelayUrl::parse("wss://relay.primal.net/").unwrap(), None),
|
||||||
|
(RelayUrl::parse("wss://nos.lol/").unwrap(), None),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if let Err(e) = client.send_event_builder(builder).await {
|
||||||
|
log::error!("Failed to send relay list event: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tags: Vec<Tag> = relays
|
||||||
|
.into_iter()
|
||||||
|
.map(|relay| Tag::custom(TagKind::Relay, vec![relay.to_string()]))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
|
||||||
|
|
||||||
|
if let Ok(output) = client.send_event_builder(builder).await {
|
||||||
|
_ = tx.send(output.val);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let client = get_client();
|
|
||||||
let signer = client.signer().await.unwrap();
|
|
||||||
let public_key = signer.get_public_key().await.unwrap();
|
|
||||||
|
|
||||||
let tags: Vec<Tag> = relays
|
|
||||||
.into_iter()
|
|
||||||
.map(|relay| Tag::custom(TagKind::Relay, vec![relay.to_string()]))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let event = EventBuilder::new(Kind::InboxRelays, "")
|
|
||||||
.tags(tags)
|
|
||||||
.build(public_key)
|
|
||||||
.sign(&signer)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if let Ok(output) = client.send_event(&event).await {
|
|
||||||
_ = tx.send(output.val);
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
if rx.await.is_ok() {
|
if rx.await.is_ok() {
|
||||||
cx.update_window(window_handle, |_, window, cx| {
|
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||||
window.close_modal(cx);
|
_ = this.update(cx, |this, cx| {
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_loading(false, cx);
|
this.set_loading(false, cx);
|
||||||
})
|
});
|
||||||
.unwrap();
|
|
||||||
})
|
window.close_modal(cx);
|
||||||
.unwrap();
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use async_utility::task::spawn;
|
||||||
use chats::{registry::ChatRegistry, room::Room};
|
use chats::{registry::ChatRegistry, room::Room};
|
||||||
use common::{
|
use common::{
|
||||||
profile::NostrProfile,
|
profile::NostrProfile,
|
||||||
@@ -13,7 +14,6 @@ use serde::Deserialize;
|
|||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use std::{collections::HashSet, time::Duration};
|
use std::{collections::HashSet, time::Duration};
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonRounded},
|
button::{Button, ButtonRounded},
|
||||||
input::{InputEvent, TextInput},
|
input::{InputEvent, TextInput},
|
||||||
@@ -71,9 +71,13 @@ impl Compose {
|
|||||||
subscriptions.push(cx.subscribe_in(
|
subscriptions.push(cx.subscribe_in(
|
||||||
&user_input,
|
&user_input,
|
||||||
window,
|
window,
|
||||||
move |this, _, input_event, window, cx| {
|
move |this, input, input_event, window, cx| {
|
||||||
if let InputEvent::PressEnter = input_event {
|
if let InputEvent::PressEnter = input_event {
|
||||||
this.add(window, cx);
|
if input.read(cx).text().contains("@") {
|
||||||
|
this.add_nip05(window, cx);
|
||||||
|
} else {
|
||||||
|
this.add_npub(window, cx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
@@ -145,7 +149,7 @@ impl Compose {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let tags = Tags::new(tag_list);
|
let tags = Tags::from_list(tag_list);
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
let window_handle = window.window_handle();
|
let window_handle = window.window_handle();
|
||||||
let (tx, rx) = oneshot::channel::<Event>();
|
let (tx, rx) = oneshot::channel::<Event>();
|
||||||
@@ -206,68 +210,133 @@ impl Compose {
|
|||||||
self.is_submitting
|
self.is_submitting
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn add_npub(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let window_handle = window.window_handle();
|
let window_handle = window.window_handle();
|
||||||
let content = self.user_input.read(cx).text().to_string();
|
let content = self.user_input.read(cx).text().to_string();
|
||||||
|
|
||||||
// Show loading spinner
|
// Show loading spinner
|
||||||
self.set_loading(true, cx);
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
if let Ok(public_key) = PublicKey::parse(&content) {
|
let Ok(public_key) = PublicKey::parse(&content) else {
|
||||||
if self
|
self.set_loading(false, cx);
|
||||||
.contacts
|
self.set_error(Some("Public Key is not valid".into()), cx);
|
||||||
.read(cx)
|
return;
|
||||||
.iter()
|
};
|
||||||
.any(|c| c.public_key() == public_key)
|
|
||||||
{
|
|
||||||
self.set_loading(false, cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
if self
|
||||||
let (tx, rx) = oneshot::channel::<Metadata>();
|
.contacts
|
||||||
|
.read(cx)
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.public_key() == public_key)
|
||||||
|
{
|
||||||
|
self.set_loading(false, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
let client = get_client();
|
||||||
let client = get_client();
|
let (tx, rx) = oneshot::channel::<Metadata>();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let metadata = (client
|
||||||
|
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||||
|
.await)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
_ = tx.send(metadata);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
if let Ok(metadata) = rx.await {
|
||||||
|
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||||
|
_ = this.update(cx, |this, cx| {
|
||||||
|
this.contacts.update(cx, |this, cx| {
|
||||||
|
this.insert(0, NostrProfile::new(public_key, metadata));
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selected.update(cx, |this, cx| {
|
||||||
|
this.insert(public_key);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop loading indicator
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
|
||||||
|
// Clear input
|
||||||
|
this.user_input.update(cx, |this, cx| {
|
||||||
|
this.set_text("", window, cx);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_nip05(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let window_handle = window.window_handle();
|
||||||
|
let content = self.user_input.read(cx).text().to_string();
|
||||||
|
|
||||||
|
// Show loading spinner
|
||||||
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
|
let client = get_client();
|
||||||
|
let (tx, rx) = oneshot::channel::<Option<NostrProfile>>();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
spawn(async move {
|
||||||
|
if let Ok(profile) = nip05::profile(&content, None).await {
|
||||||
let metadata = (client
|
let metadata = (client
|
||||||
.fetch_metadata(public_key, Duration::from_secs(3))
|
.fetch_metadata(profile.public_key, Duration::from_secs(2))
|
||||||
.await)
|
.await)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
_ = tx.send(metadata);
|
_ = tx.send(Some(NostrProfile::new(profile.public_key, metadata)));
|
||||||
})
|
} else {
|
||||||
.detach();
|
_ = tx.send(None);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
if let Ok(metadata) = rx.await {
|
cx.spawn(|this, mut cx| async move {
|
||||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
if let Ok(Some(profile)) = rx.await {
|
||||||
_ = this.update(cx, |this, cx| {
|
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||||
this.contacts.update(cx, |this, cx| {
|
_ = this.update(cx, |this, cx| {
|
||||||
this.insert(0, NostrProfile::new(public_key, metadata));
|
let public_key = profile.public_key();
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.selected.update(cx, |this, cx| {
|
this.contacts.update(cx, |this, cx| {
|
||||||
this.insert(public_key);
|
this.insert(0, profile);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stop loading indicator
|
this.selected.update(cx, |this, cx| {
|
||||||
this.set_loading(false, cx);
|
this.insert(public_key);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
// Clear input
|
// Stop loading indicator
|
||||||
this.user_input.update(cx, |this, cx| {
|
this.set_loading(false, cx);
|
||||||
this.set_text("", window, cx);
|
|
||||||
cx.notify();
|
// Clear input
|
||||||
});
|
this.user_input.update(cx, |this, cx| {
|
||||||
|
this.set_text("", window, cx);
|
||||||
|
cx.notify();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
})
|
} else {
|
||||||
.detach();
|
_ = cx.update_window(window_handle, |_, _, cx| {
|
||||||
} else {
|
_ = this.update(cx, |this, cx| {
|
||||||
self.set_loading(false, cx);
|
this.set_loading(false, cx);
|
||||||
self.set_error(Some("Public Key is not valid".into()), cx);
|
this.set_error(Some("NIP-05 Address is not valid".into()), cx);
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_error(&mut self, error: Option<SharedString>, cx: &mut Context<Self>) {
|
fn set_error(&mut self, error: Option<SharedString>, cx: &mut Context<Self>) {
|
||||||
@@ -372,9 +441,13 @@ impl Render for Compose {
|
|||||||
.small()
|
.small()
|
||||||
.rounded(ButtonRounded::Size(px(9999.)))
|
.rounded(ButtonRounded::Size(px(9999.)))
|
||||||
.loading(self.is_loading)
|
.loading(self.is_loading)
|
||||||
.on_click(
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
cx.listener(|this, _, window, cx| this.add(window, cx)),
|
if this.user_input.read(cx).text().contains("@") {
|
||||||
),
|
this.add_nip05(window, cx);
|
||||||
|
} else {
|
||||||
|
this.add_npub(window, cx);
|
||||||
|
}
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.child(self.user_input.clone()),
|
.child(self.user_input.clone()),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chats"
|
name = "chats"
|
||||||
version = "0.1.0"
|
version = "0.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
@@ -12,5 +12,6 @@ gpui.workspace = true
|
|||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
oneshot.workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use async_utility::tokio::sync::oneshot;
|
|
||||||
use common::utils::{compare, room_hash, signer_public_key};
|
use common::utils::{compare, room_hash, signer_public_key};
|
||||||
use gpui::{App, AppContext, Context, Entity, Global};
|
use gpui::{App, AppContext, Context, Entity, Global};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@@ -31,8 +30,10 @@ impl ChatRegistry {
|
|||||||
pub fn register(cx: &mut App) -> Entity<Self> {
|
pub fn register(cx: &mut App) -> Entity<Self> {
|
||||||
Self::global(cx).unwrap_or_else(|| {
|
Self::global(cx).unwrap_or_else(|| {
|
||||||
let entity = cx.new(Self::new);
|
let entity = cx.new(Self::new);
|
||||||
|
|
||||||
// Set global state
|
// Set global state
|
||||||
cx.set_global(GlobalChatRegistry(entity.clone()));
|
cx.set_global(GlobalChatRegistry(entity.clone()));
|
||||||
|
|
||||||
// Observe and load metadata for any new rooms
|
// Observe and load metadata for any new rooms
|
||||||
cx.observe_new::<Room>(|this, _window, cx| {
|
cx.observe_new::<Room>(|this, _window, cx| {
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
@@ -74,7 +75,7 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
fn new(_cx: &mut Context<Self>) -> Self {
|
fn new(_cx: &mut Context<Self>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rooms: Vec::with_capacity(5),
|
rooms: vec![],
|
||||||
is_loading: true,
|
is_loading: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,6 +186,12 @@ impl ChatRegistry {
|
|||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Re sort rooms by last seen
|
||||||
|
self.rooms
|
||||||
|
.sort_by_key(|room| Reverse(room.read(cx).last_seen()));
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
} else {
|
} else {
|
||||||
let room = cx.new(|cx| Room::parse(&event, cx));
|
let room = cx.new(|cx| Room::parse(&event, cx));
|
||||||
self.rooms.insert(0, room);
|
self.rooms.insert(0, room);
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn last_seen(&self) -> &LastSeen {
|
||||||
|
&self.last_seen
|
||||||
|
}
|
||||||
|
|
||||||
/// Get all public keys from current room
|
/// Get all public keys from current room
|
||||||
pub fn pubkeys(&self) -> Vec<PublicKey> {
|
pub fn pubkeys(&self) -> Vec<PublicKey> {
|
||||||
let mut pubkeys: Vec<_> = self.members.iter().map(|m| m.public_key()).collect();
|
let mut pubkeys: Vec<_> = self.members.iter().map(|m| m.public_key()).collect();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "common"
|
name = "common"
|
||||||
version = "0.1.0"
|
version = "0.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ pub const KEYRING_SERVICE: &str = "Coop Safe Storage";
|
|||||||
pub const APP_NAME: &str = "Coop";
|
pub const APP_NAME: &str = "Coop";
|
||||||
pub const APP_ID: &str = "su.reya.coop";
|
pub const APP_ID: &str = "su.reya.coop";
|
||||||
|
|
||||||
pub const FAKE_SIG: &str = "f9e79d141c004977192d05a86f81ec7c585179c371f7350a5412d33575a2a356433f58e405c2296ed273e2fe0aafa25b641e39cc4e1f3f261ebf55bce0cbac83";
|
|
||||||
|
|
||||||
/// Subscriptions
|
/// Subscriptions
|
||||||
pub const NEW_MESSAGE_SUB_ID: &str = "listen_new_giftwraps";
|
pub const NEW_MESSAGE_SUB_ID: &str = "listen_new_giftwraps";
|
||||||
pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps";
|
pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use chrono::{Datelike, Local, TimeZone};
|
|||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct LastSeen(pub Timestamp);
|
pub struct LastSeen(pub Timestamp);
|
||||||
|
|
||||||
impl LastSeen {
|
impl LastSeen {
|
||||||
|
|||||||
@@ -16,13 +16,14 @@ pub async fn signer_public_key(client: &Client) -> anyhow::Result<PublicKey, any
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn preload(client: &Client, public_key: PublicKey) -> anyhow::Result<(), anyhow::Error> {
|
pub async fn preload(client: &Client, public_key: PublicKey) -> anyhow::Result<(), anyhow::Error> {
|
||||||
|
let sync_opts = SyncOptions::default();
|
||||||
let subscription = Filter::new()
|
let subscription = Filter::new()
|
||||||
.kind(Kind::ContactList)
|
.kind(Kind::ContactList)
|
||||||
.author(public_key)
|
.author(public_key)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
// Get contact list
|
// Get contact list
|
||||||
_ = client.sync(subscription, &SyncOptions::default()).await;
|
_ = client.sync(subscription, &sync_opts).await;
|
||||||
|
|
||||||
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
||||||
let new_message_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
let new_message_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "state"
|
name = "state"
|
||||||
version = "0.1.0"
|
version = "0.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
tokio.workspace = true
|
|
||||||
dirs.workspace = true
|
dirs.workspace = true
|
||||||
|
|||||||
@@ -4,23 +4,29 @@ use std::{fs, sync::OnceLock, time::Duration};
|
|||||||
|
|
||||||
static CLIENT: OnceLock<Client> = OnceLock::new();
|
static CLIENT: OnceLock<Client> = OnceLock::new();
|
||||||
|
|
||||||
pub fn initialize_client() {
|
pub fn initialize_client() -> &'static Client {
|
||||||
// Setup app data folder
|
// Setup app data folder
|
||||||
let config_dir = config_dir().expect("Config directory not found");
|
let config_dir = config_dir().expect("Config directory not found");
|
||||||
let _ = fs::create_dir_all(config_dir.join("Coop/"));
|
let app_dir = config_dir.join("Coop/");
|
||||||
|
|
||||||
|
// Create app directory if it doesn't exist
|
||||||
|
_ = fs::create_dir_all(&app_dir);
|
||||||
|
|
||||||
// Setup database
|
// Setup database
|
||||||
let lmdb = NostrLMDB::open(config_dir.join("Coop/nostr")).expect("Database is NOT initialized");
|
let lmdb = NostrLMDB::open(app_dir.join("nostr")).expect("Database is NOT initialized");
|
||||||
|
|
||||||
// Client options
|
// Client options
|
||||||
let opts = Options::new()
|
let opts = Options::new()
|
||||||
|
// NIP-65
|
||||||
.gossip(true)
|
.gossip(true)
|
||||||
.max_avg_latency(Duration::from_secs(2));
|
// Skip all very slow relays
|
||||||
|
.max_avg_latency(Duration::from_millis(800));
|
||||||
|
|
||||||
// Setup Nostr Client
|
// Setup Nostr Client
|
||||||
let client = ClientBuilder::default().database(lmdb).opts(opts).build();
|
let client = ClientBuilder::default().database(lmdb).opts(opts).build();
|
||||||
|
|
||||||
CLIENT.set(client).expect("Client is already initialized!");
|
CLIENT.set(client).expect("Client is already initialized!");
|
||||||
|
CLIENT.get().expect("Client is NOT initialized!")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_client() -> &'static Client {
|
pub fn get_client() -> &'static Client {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ui"
|
name = "ui"
|
||||||
version = "0.1.0"
|
version = "0.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::FluentBuilder as _, px, AnyView, App, AppContext, Axis, Context, Element, Entity,
|
div, prelude::FluentBuilder as _, px, App, AppContext, Axis, Context, Element, Entity,
|
||||||
InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels,
|
InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels,
|
||||||
Point, Render, StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window,
|
Point, Render, StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
@@ -374,9 +374,7 @@ impl Render for Dock {
|
|||||||
})
|
})
|
||||||
.map(|this| match &self.panel {
|
.map(|this| match &self.panel {
|
||||||
DockItem::Split { view, .. } => this.child(view.clone()),
|
DockItem::Split { view, .. } => this.child(view.clone()),
|
||||||
DockItem::Tabs { view, .. } => {
|
DockItem::Tabs { view, .. } => this.child(view.clone()),
|
||||||
this.child(AnyView::from(view.clone()).cached(cache_style))
|
|
||||||
}
|
|
||||||
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
|
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
|
||||||
})
|
})
|
||||||
.child(self.render_resize_handle(window, cx))
|
.child(self.render_resize_handle(window, cx))
|
||||||
@@ -432,14 +430,20 @@ impl Element for DockElement {
|
|||||||
_: &mut Self::RequestLayoutState,
|
_: &mut Self::RequestLayoutState,
|
||||||
_: &mut Self::PrepaintState,
|
_: &mut Self::PrepaintState,
|
||||||
window: &mut gpui::Window,
|
window: &mut gpui::Window,
|
||||||
_: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let view = self.view.clone();
|
let view = self.view.clone();
|
||||||
|
let is_resizing = view.read(cx).is_resizing;
|
||||||
move |e: &MouseMoveEvent, phase, window, cx| {
|
move |e: &MouseMoveEvent, phase, window, cx| {
|
||||||
if phase.bubble() {
|
if !is_resizing {
|
||||||
view.update(cx, |view, cx| view.resize(e.position, window, cx))
|
return;
|
||||||
}
|
}
|
||||||
|
if !phase.bubble() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.update(cx, |view, cx| view.resize(e.position, window, cx))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ impl RenderOnce for Modal {
|
|||||||
.with_easing(cubic_bezier(0.32, 0.72, 0., 1.)),
|
.with_easing(cubic_bezier(0.32, 0.72, 0., 1.)),
|
||||||
move |this, delta| {
|
move |this, delta| {
|
||||||
let y_offset = px(0.) + delta * px(30.);
|
let y_offset = px(0.) + delta * px(30.);
|
||||||
this.top(y + y_offset).opacity(delta)
|
this.top(y + y_offset)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ impl PopupMenu {
|
|||||||
this.dismiss(&Dismiss, window, cx)
|
this.dismiss(&Dismiss, window, cx)
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
let menu = Self {
|
let menu = Self {
|
||||||
focus_handle,
|
focus_handle,
|
||||||
action_focus_handle: None,
|
action_focus_handle: None,
|
||||||
@@ -150,7 +149,7 @@ impl PopupMenu {
|
|||||||
scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
|
scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
|
||||||
subscriptions,
|
subscriptions,
|
||||||
};
|
};
|
||||||
window.refresh();
|
|
||||||
f(menu, window, cx)
|
f(menu, window, cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -510,41 +510,38 @@ impl Element for ResizePanelGroupElement {
|
|||||||
let axis = self.axis;
|
let axis = self.axis;
|
||||||
let current_ix = view.read(cx).resizing_panel_ix;
|
let current_ix = view.read(cx).resizing_panel_ix;
|
||||||
move |e: &MouseMoveEvent, phase, window, cx| {
|
move |e: &MouseMoveEvent, phase, window, cx| {
|
||||||
if phase.bubble() {
|
if !phase.bubble() {
|
||||||
if let Some(ix) = current_ix {
|
return;
|
||||||
view.update(cx, |view, cx| {
|
|
||||||
let panel = view
|
|
||||||
.panels
|
|
||||||
.get(ix)
|
|
||||||
.expect("BUG: invalid panel index")
|
|
||||||
.read(cx);
|
|
||||||
|
|
||||||
match axis {
|
|
||||||
Axis::Horizontal => view.resize_panels(
|
|
||||||
ix,
|
|
||||||
e.position.x - panel.bounds.left(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
),
|
|
||||||
Axis::Vertical => {
|
|
||||||
view.resize_panels(
|
|
||||||
ix,
|
|
||||||
e.position.y - panel.bounds.top(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let Some(ix) = current_ix else { return };
|
||||||
|
|
||||||
|
view.update(cx, |view, cx| {
|
||||||
|
let panel = view
|
||||||
|
.panels
|
||||||
|
.get(ix)
|
||||||
|
.expect("BUG: invalid panel index")
|
||||||
|
.read(cx);
|
||||||
|
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
view.resize_panels(ix, e.position.x - panel.bounds.left(), window, cx)
|
||||||
|
}
|
||||||
|
Axis::Vertical => {
|
||||||
|
view.resize_panels(ix, e.position.y - panel.bounds.top(), window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// When any mouse up, stop dragging
|
// When any mouse up, stop dragging
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let view = self.view.clone();
|
let view = self.view.clone();
|
||||||
|
let current_ix = view.read(cx).resizing_panel_ix;
|
||||||
move |_: &MouseUpEvent, phase, window, cx| {
|
move |_: &MouseUpEvent, phase, window, cx| {
|
||||||
|
if current_ix.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if phase.bubble() {
|
if phase.bubble() {
|
||||||
view.update(cx, |view, cx| view.done_resizing(window, cx));
|
view.update(cx, |view, cx| view.done_resizing(window, cx));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
use gpui::*;
|
use gpui::{
|
||||||
|
fill, point, px, relative, App, Bounds, ContentMask, CursorStyle, Edges, Element, EntityId,
|
||||||
|
Hitbox, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels,
|
||||||
|
Point, Position, ScrollHandle, ScrollWheelEvent, UniformListScrollHandle, Window,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{cell::Cell, rc::Rc, time::Instant};
|
use std::{
|
||||||
|
cell::Cell,
|
||||||
|
rc::Rc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
|
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
|
||||||
|
|
||||||
@@ -10,18 +18,24 @@ pub enum ScrollbarShow {
|
|||||||
#[default]
|
#[default]
|
||||||
Scrolling,
|
Scrolling,
|
||||||
Hover,
|
Hover,
|
||||||
|
Always,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollbarShow {
|
impl ScrollbarShow {
|
||||||
fn is_hover(&self) -> bool {
|
fn is_hover(&self) -> bool {
|
||||||
matches!(self, Self::Hover)
|
matches!(self, Self::Hover)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_always(&self) -> bool {
|
||||||
|
matches!(self, Self::Always)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BORDER_WIDTH: Pixels = px(0.);
|
const BORDER_WIDTH: Pixels = px(0.);
|
||||||
|
pub(crate) const WIDTH: Pixels = px(12.);
|
||||||
const MIN_THUMB_SIZE: f32 = 80.;
|
const MIN_THUMB_SIZE: f32 = 80.;
|
||||||
const THUMB_RADIUS: Pixels = Pixels(3.0);
|
const THUMB_RADIUS: Pixels = Pixels(4.0);
|
||||||
const THUMB_INSET: Pixels = Pixels(4.);
|
const THUMB_INSET: Pixels = Pixels(3.);
|
||||||
const FADE_OUT_DURATION: f32 = 3.0;
|
const FADE_OUT_DURATION: f32 = 3.0;
|
||||||
const FADE_OUT_DELAY: f32 = 2.0;
|
const FADE_OUT_DELAY: f32 = 2.0;
|
||||||
|
|
||||||
@@ -65,6 +79,8 @@ pub struct ScrollbarState {
|
|||||||
drag_pos: Point<Pixels>,
|
drag_pos: Point<Pixels>,
|
||||||
last_scroll_offset: Point<Pixels>,
|
last_scroll_offset: Point<Pixels>,
|
||||||
last_scroll_time: Option<Instant>,
|
last_scroll_time: Option<Instant>,
|
||||||
|
// Last update offset
|
||||||
|
last_update: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ScrollbarState {
|
impl Default for ScrollbarState {
|
||||||
@@ -76,6 +92,7 @@ impl Default for ScrollbarState {
|
|||||||
drag_pos: point(px(0.), px(0.)),
|
drag_pos: point(px(0.), px(0.)),
|
||||||
last_scroll_offset: point(px(0.), px(0.)),
|
last_scroll_offset: point(px(0.), px(0.)),
|
||||||
last_scroll_time: None,
|
last_scroll_time: None,
|
||||||
|
last_update: Instant::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,8 +123,8 @@ impl ScrollbarState {
|
|||||||
fn with_hovered(&self, axis: Option<ScrollbarAxis>) -> Self {
|
fn with_hovered(&self, axis: Option<ScrollbarAxis>) -> Self {
|
||||||
let mut state = *self;
|
let mut state = *self;
|
||||||
state.hovered_axis = axis;
|
state.hovered_axis = axis;
|
||||||
if self.is_scrollbar_visible() {
|
if axis.is_some() {
|
||||||
state.last_scroll_time = Some(Instant::now());
|
state.last_scroll_time = Some(std::time::Instant::now());
|
||||||
}
|
}
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@@ -115,6 +132,9 @@ impl ScrollbarState {
|
|||||||
fn with_hovered_on_thumb(&self, axis: Option<ScrollbarAxis>) -> Self {
|
fn with_hovered_on_thumb(&self, axis: Option<ScrollbarAxis>) -> Self {
|
||||||
let mut state = *self;
|
let mut state = *self;
|
||||||
state.hovered_on_thumb = axis;
|
state.hovered_on_thumb = axis;
|
||||||
|
if axis.is_some() {
|
||||||
|
state.last_scroll_time = Some(std::time::Instant::now());
|
||||||
|
}
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +155,18 @@ impl ScrollbarState {
|
|||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn with_last_update(&self, t: Instant) -> Self {
|
||||||
|
let mut state = *self;
|
||||||
|
state.last_update = t;
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
fn is_scrollbar_visible(&self) -> bool {
|
fn is_scrollbar_visible(&self) -> bool {
|
||||||
|
// On drag
|
||||||
|
if self.dragged_axis.is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(last_time) = self.last_scroll_time {
|
if let Some(last_time) = self.last_scroll_time {
|
||||||
let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
|
let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
|
||||||
elapsed < FADE_OUT_DURATION
|
elapsed < FADE_OUT_DURATION
|
||||||
@@ -178,9 +209,9 @@ impl ScrollbarAxis {
|
|||||||
match self {
|
match self {
|
||||||
Self::Vertical => vec![Self::Vertical],
|
Self::Vertical => vec![Self::Vertical],
|
||||||
Self::Horizontal => vec![Self::Horizontal],
|
Self::Horizontal => vec![Self::Horizontal],
|
||||||
// This should keep vertical first, vertical is the primary axis
|
// This should keep Horizontal first, Vertical is the primary axis
|
||||||
// if vertical not need display, then horizontal will not keep right margin.
|
// if Vertical not need display, then Horizontal will not keep right margin.
|
||||||
Self::Both => vec![Self::Vertical, Self::Horizontal],
|
Self::Both => vec![Self::Horizontal, Self::Vertical],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,11 +220,14 @@ impl ScrollbarAxis {
|
|||||||
pub struct Scrollbar {
|
pub struct Scrollbar {
|
||||||
view_id: EntityId,
|
view_id: EntityId,
|
||||||
axis: ScrollbarAxis,
|
axis: ScrollbarAxis,
|
||||||
/// When is vertical, this is the height of the scrollbar.
|
|
||||||
width: Pixels,
|
|
||||||
scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>,
|
scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>,
|
||||||
scroll_size: gpui::Size<Pixels>,
|
scroll_size: gpui::Size<Pixels>,
|
||||||
state: Rc<Cell<ScrollbarState>>,
|
state: Rc<Cell<ScrollbarState>>,
|
||||||
|
/// Maximum frames per second for scrolling by drag. Default is 120 FPS.
|
||||||
|
///
|
||||||
|
/// This is used to limit the update rate of the scrollbar when it is
|
||||||
|
/// being dragged for some complex interactions for reducing CPU usage.
|
||||||
|
max_fps: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scrollbar {
|
impl Scrollbar {
|
||||||
@@ -209,8 +243,8 @@ impl Scrollbar {
|
|||||||
state,
|
state,
|
||||||
axis,
|
axis,
|
||||||
scroll_size,
|
scroll_size,
|
||||||
width: px(12.),
|
|
||||||
scroll_handle: Rc::new(Box::new(scroll_handle)),
|
scroll_handle: Rc::new(Box::new(scroll_handle)),
|
||||||
|
max_fps: 120,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,11 +324,21 @@ impl Scrollbar {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set maximum frames per second for scrolling by drag. Default is 120 FPS.
|
||||||
|
///
|
||||||
|
/// If you have very high CPU usage, consider reducing this value to improve performance.
|
||||||
|
///
|
||||||
|
/// Available values: 30..120
|
||||||
|
pub fn max_fps(mut self, max_fps: usize) -> Self {
|
||||||
|
self.max_fps = max_fps.clamp(30, 120);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
|
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
|
||||||
(
|
(
|
||||||
cx.theme().scrollbar_thumb_hover,
|
cx.theme().scrollbar_thumb_hover,
|
||||||
cx.theme().scrollbar,
|
cx.theme().scrollbar,
|
||||||
cx.theme().base.step(cx, ColorScaleStep::THREE),
|
cx.theme().base.step(cx, ColorScaleStep::SEVEN),
|
||||||
THUMB_INSET - px(1.),
|
THUMB_INSET - px(1.),
|
||||||
THUMB_RADIUS,
|
THUMB_RADIUS,
|
||||||
)
|
)
|
||||||
@@ -304,7 +348,7 @@ impl Scrollbar {
|
|||||||
(
|
(
|
||||||
cx.theme().scrollbar_thumb_hover,
|
cx.theme().scrollbar_thumb_hover,
|
||||||
cx.theme().scrollbar,
|
cx.theme().scrollbar,
|
||||||
cx.theme().base.step(cx, ColorScaleStep::THREE),
|
cx.theme().base.step(cx, ColorScaleStep::SIX),
|
||||||
THUMB_INSET - px(1.),
|
THUMB_INSET - px(1.),
|
||||||
THUMB_RADIUS,
|
THUMB_RADIUS,
|
||||||
)
|
)
|
||||||
@@ -382,11 +426,11 @@ impl Element for Scrollbar {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||||
let style = Style {
|
let style = gpui::Style {
|
||||||
position: Position::Absolute,
|
position: Position::Absolute,
|
||||||
flex_grow: 1.0,
|
flex_grow: 1.0,
|
||||||
flex_shrink: 1.0,
|
flex_shrink: 1.0,
|
||||||
size: Size {
|
size: gpui::Size {
|
||||||
width: relative(1.).into(),
|
width: relative(1.).into(),
|
||||||
height: relative(1.).into(),
|
height: relative(1.).into(),
|
||||||
},
|
},
|
||||||
@@ -409,7 +453,6 @@ impl Element for Scrollbar {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut states = vec![];
|
let mut states = vec![];
|
||||||
|
|
||||||
let mut has_both = self.axis.is_both();
|
let mut has_both = self.axis.is_both();
|
||||||
|
|
||||||
for axis in self.axis.all().into_iter() {
|
for axis in self.axis.all().into_iter() {
|
||||||
@@ -430,7 +473,7 @@ impl Element for Scrollbar {
|
|||||||
|
|
||||||
// The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
|
// The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
|
||||||
let margin_end = if has_both && !is_vertical {
|
let margin_end = if has_both && !is_vertical {
|
||||||
self.width
|
WIDTH
|
||||||
} else {
|
} else {
|
||||||
px(0.)
|
px(0.)
|
||||||
};
|
};
|
||||||
@@ -449,31 +492,29 @@ impl Element for Scrollbar {
|
|||||||
|
|
||||||
let bounds = Bounds {
|
let bounds = Bounds {
|
||||||
origin: if is_vertical {
|
origin: if is_vertical {
|
||||||
point(
|
point(hitbox.origin.x + hitbox.size.width - WIDTH, hitbox.origin.y)
|
||||||
hitbox.origin.x + hitbox.size.width - self.width,
|
|
||||||
hitbox.origin.y,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
point(
|
point(
|
||||||
hitbox.origin.x,
|
hitbox.origin.x,
|
||||||
hitbox.origin.y + hitbox.size.height - self.width,
|
hitbox.origin.y + hitbox.size.height - WIDTH,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
size: gpui::Size {
|
size: gpui::Size {
|
||||||
width: if is_vertical {
|
width: if is_vertical {
|
||||||
self.width
|
WIDTH
|
||||||
} else {
|
} else {
|
||||||
hitbox.size.width
|
hitbox.size.width
|
||||||
},
|
},
|
||||||
height: if is_vertical {
|
height: if is_vertical {
|
||||||
hitbox.size.height
|
hitbox.size.height
|
||||||
} else {
|
} else {
|
||||||
self.width
|
WIDTH
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
|
let is_always_to_show = cx.theme().scrollbar_show.is_always();
|
||||||
let is_hover_to_show = cx.theme().scrollbar_show.is_hover();
|
let is_hover_to_show = cx.theme().scrollbar_show.is_hover();
|
||||||
let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
|
let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
|
||||||
let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis);
|
let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis);
|
||||||
@@ -481,7 +522,9 @@ impl Element for Scrollbar {
|
|||||||
let (thumb_bg, bar_bg, bar_border, inset, radius) =
|
let (thumb_bg, bar_bg, bar_border, inset, radius) =
|
||||||
if state.get().dragged_axis == Some(axis) {
|
if state.get().dragged_axis == Some(axis) {
|
||||||
Self::style_for_active(cx)
|
Self::style_for_active(cx)
|
||||||
} else if is_hover_to_show && is_hovered_on_bar {
|
} else if (is_hover_to_show || is_always_to_show)
|
||||||
|
&& (is_hovered_on_bar || is_hovered_on_thumb)
|
||||||
|
{
|
||||||
if is_hovered_on_thumb {
|
if is_hovered_on_thumb {
|
||||||
Self::style_for_hovered_thumb(cx)
|
Self::style_for_hovered_thumb(cx)
|
||||||
} else {
|
} else {
|
||||||
@@ -520,12 +563,12 @@ impl Element for Scrollbar {
|
|||||||
let thumb_bounds = if is_vertical {
|
let thumb_bounds = if is_vertical {
|
||||||
Bounds::from_corners(
|
Bounds::from_corners(
|
||||||
point(bounds.origin.x, bounds.origin.y + thumb_start),
|
point(bounds.origin.x, bounds.origin.y + thumb_start),
|
||||||
point(bounds.origin.x + self.width, bounds.origin.y + thumb_end),
|
point(bounds.origin.x + WIDTH, bounds.origin.y + thumb_end),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Bounds::from_corners(
|
Bounds::from_corners(
|
||||||
point(bounds.origin.x + thumb_start, bounds.origin.y),
|
point(bounds.origin.x + thumb_start, bounds.origin.y),
|
||||||
point(bounds.origin.x + thumb_end, bounds.origin.y + self.width),
|
point(bounds.origin.x + thumb_end, bounds.origin.y + WIDTH),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let thumb_fill_bounds = if is_vertical {
|
let thumb_fill_bounds = if is_vertical {
|
||||||
@@ -535,7 +578,7 @@ impl Element for Scrollbar {
|
|||||||
bounds.origin.y + thumb_start + inset,
|
bounds.origin.y + thumb_start + inset,
|
||||||
),
|
),
|
||||||
point(
|
point(
|
||||||
bounds.origin.x + self.width - inset,
|
bounds.origin.x + WIDTH - inset,
|
||||||
bounds.origin.y + thumb_end - inset,
|
bounds.origin.y + thumb_end - inset,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -547,7 +590,7 @@ impl Element for Scrollbar {
|
|||||||
),
|
),
|
||||||
point(
|
point(
|
||||||
bounds.origin.x + thumb_end - inset,
|
bounds.origin.x + thumb_end - inset,
|
||||||
bounds.origin.y + self.width - inset,
|
bounds.origin.y + WIDTH - inset,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -589,6 +632,15 @@ impl Element for Scrollbar {
|
|||||||
let is_visible = self.state.get().is_scrollbar_visible();
|
let is_visible = self.state.get().is_scrollbar_visible();
|
||||||
let is_hover_to_show = cx.theme().scrollbar_show.is_hover();
|
let is_hover_to_show = cx.theme().scrollbar_show.is_hover();
|
||||||
|
|
||||||
|
// Update last_scroll_time when offset is changed.
|
||||||
|
if self.scroll_handle.offset() != self.state.get().last_scroll_offset {
|
||||||
|
self.state.set(
|
||||||
|
self.state
|
||||||
|
.get()
|
||||||
|
.with_last_scroll(self.scroll_handle.offset(), Some(Instant::now())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
window.with_content_mask(
|
window.with_content_mask(
|
||||||
Some(ContentMask {
|
Some(ContentMask {
|
||||||
bounds: hitbox_bounds,
|
bounds: hitbox_bounds,
|
||||||
@@ -711,30 +763,36 @@ impl Element for Scrollbar {
|
|||||||
let scroll_handle = self.scroll_handle.clone();
|
let scroll_handle = self.scroll_handle.clone();
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
let view_id = self.view_id;
|
let view_id = self.view_id;
|
||||||
|
let max_fps_duration = Duration::from_millis((1000 / self.max_fps) as u64);
|
||||||
|
|
||||||
move |event: &MouseMoveEvent, _, _, cx| {
|
move |event: &MouseMoveEvent, _, _, cx| {
|
||||||
|
let mut notify = false;
|
||||||
|
// When is hover to show mode or it was visible,
|
||||||
|
// we need to update the hovered state and increase the last_scroll_time.
|
||||||
|
let need_hover_to_update = is_hover_to_show || is_visible;
|
||||||
// Update hovered state for scrollbar
|
// Update hovered state for scrollbar
|
||||||
if bounds.contains(&event.position) {
|
if bounds.contains(&event.position) && need_hover_to_update {
|
||||||
|
state.set(state.get().with_hovered(Some(axis)));
|
||||||
|
|
||||||
if state.get().hovered_axis != Some(axis) {
|
if state.get().hovered_axis != Some(axis) {
|
||||||
state.set(state.get().with_hovered(Some(axis)));
|
notify = true;
|
||||||
cx.notify(view_id);
|
|
||||||
}
|
}
|
||||||
} else if state.get().hovered_axis == Some(axis)
|
} else if state.get().hovered_axis == Some(axis)
|
||||||
&& state.get().hovered_axis.is_some()
|
&& state.get().hovered_axis.is_some()
|
||||||
{
|
{
|
||||||
state.set(state.get().with_hovered(None));
|
state.set(state.get().with_hovered(None));
|
||||||
cx.notify(view_id);
|
notify = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update hovered state for scrollbar thumb
|
// Update hovered state for scrollbar thumb
|
||||||
if thumb_bounds.contains(&event.position) {
|
if thumb_bounds.contains(&event.position) {
|
||||||
if state.get().hovered_on_thumb != Some(axis) {
|
if state.get().hovered_on_thumb != Some(axis) {
|
||||||
state.set(state.get().with_hovered_on_thumb(Some(axis)));
|
state.set(state.get().with_hovered_on_thumb(Some(axis)));
|
||||||
cx.notify(view_id);
|
notify = true;
|
||||||
}
|
}
|
||||||
} else if state.get().hovered_on_thumb == Some(axis) {
|
} else if state.get().hovered_on_thumb == Some(axis) {
|
||||||
state.set(state.get().with_hovered_on_thumb(None));
|
state.set(state.get().with_hovered_on_thumb(None));
|
||||||
cx.notify(view_id);
|
notify = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move thumb position on dragging
|
// Move thumb position on dragging
|
||||||
@@ -769,10 +827,18 @@ impl Element for Scrollbar {
|
|||||||
if (scroll_handle.offset().y - offset.y).abs() > px(1.)
|
if (scroll_handle.offset().y - offset.y).abs() > px(1.)
|
||||||
|| (scroll_handle.offset().x - offset.x).abs() > px(1.)
|
|| (scroll_handle.offset().x - offset.x).abs() > px(1.)
|
||||||
{
|
{
|
||||||
scroll_handle.set_offset(offset);
|
// Limit update rate
|
||||||
cx.notify(view_id);
|
if state.get().last_update.elapsed() > max_fps_duration {
|
||||||
|
scroll_handle.set_offset(offset);
|
||||||
|
state.set(state.get().with_last_update(Instant::now()));
|
||||||
|
notify = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if notify {
|
||||||
|
cx.notify(view_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ impl RenderOnce for WindowBorder {
|
|||||||
.when(!tiling.bottom, |div| div.pb(SHADOW_SIZE))
|
.when(!tiling.bottom, |div| div.pb(SHADOW_SIZE))
|
||||||
.when(!tiling.left, |div| div.pl(SHADOW_SIZE))
|
.when(!tiling.left, |div| div.pl(SHADOW_SIZE))
|
||||||
.when(!tiling.right, |div| div.pr(SHADOW_SIZE))
|
.when(!tiling.right, |div| div.pr(SHADOW_SIZE))
|
||||||
.on_mouse_move(|_e, window, _cx| window.refresh())
|
|
||||||
.on_mouse_down(MouseButton::Left, move |_, window, _cx| {
|
.on_mouse_down(MouseButton::Left, move |_, window, _cx| {
|
||||||
let size = window.window_bounds().get_bounds().size;
|
let size = window.window_bounds().get_bounds().size;
|
||||||
let pos = window.mouse_position();
|
let pos = window.mouse_position();
|
||||||
|
|||||||
Reference in New Issue
Block a user