chore: Improve Request Screening (#101)
* open chat while screening * close panel on ignore * bypass screening * . * improve settings * refine modal * . * . * . * . * .
This commit is contained in:
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -1083,7 +1083,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1484,7 +1484,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_refineable"
|
name = "derive_refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2336,7 +2336,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2429,7 +2429,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_macros"
|
name = "gpui_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2441,7 +2441,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_tokio"
|
name = "gpui_tokio"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2663,7 +2663,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client"
|
name = "http_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2681,7 +2681,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client_tls"
|
name = "http_client_tls"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3459,7 +3459,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -4440,9 +4440,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.35"
|
version = "0.2.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
|
checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
@@ -4811,7 +4811,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
@@ -4863,6 +4863,7 @@ dependencies = [
|
|||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
]
|
]
|
||||||
@@ -4962,7 +4963,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest_client"
|
name = "reqwest_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5488,7 +5489,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "semantic_version"
|
name = "semantic_version"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5630,6 +5631,7 @@ dependencies = [
|
|||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
|
"paste",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@@ -5882,7 +5884,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sum_tree"
|
name = "sum_tree"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -6855,7 +6857,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util"
|
name = "util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b93e1c736b33615e0b80a8e7fb3a294f40c70862"
|
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
|
|||||||
3
assets/icons/report.svg
Normal file
3
assets/icons/report.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m20.001 16-2 2m0 0-2 2m2-2-2-2m2 2 2 2m-8.147-6.749c-3.319.058-5.832 2.055-6.87 4.862-.41 1.105.535 2.137 1.713 2.137h5.554m-.397-6.999L12 13.25c.52 0 1.021.047 1.5.138m-1.647-.137A7.89 7.89 0 0 0 10 13.5m5.75-7a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 462 B |
@@ -1,6 +1,10 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error};
|
||||||
use global::constants::IMAGE_RESIZE_SERVICE;
|
use global::constants::IMAGE_RESIZE_SERVICE;
|
||||||
use gpui::SharedString;
|
use gpui::{Image, ImageFormat, SharedString};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use qrcode_generator::QrCodeEcc;
|
||||||
|
|
||||||
const FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png";
|
const FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png";
|
||||||
|
|
||||||
@@ -45,6 +49,53 @@ impl DisplayProfile for Profile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait TextUtils {
|
||||||
|
fn to_public_key(&self) -> Result<PublicKey, Error>;
|
||||||
|
fn to_qr(&self) -> Option<Arc<Image>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextUtils for String {
|
||||||
|
fn to_public_key(&self) -> Result<PublicKey, Error> {
|
||||||
|
if self.starts_with("nprofile1") {
|
||||||
|
Ok(Nip19Profile::from_bech32(self)?.public_key)
|
||||||
|
} else if self.starts_with("npub1") {
|
||||||
|
Ok(PublicKey::parse(self)?)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Invalid public key"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_qr(&self) -> Option<Arc<Image>> {
|
||||||
|
let Ok(bytes) = qrcode_generator::to_png_to_vec_from_str(self, QrCodeEcc::Medium, 256)
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Arc::new(Image::from_bytes(ImageFormat::Png, bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextUtils for &str {
|
||||||
|
fn to_public_key(&self) -> Result<PublicKey, Error> {
|
||||||
|
if self.starts_with("nprofile1") {
|
||||||
|
Ok(Nip19Profile::from_bech32(self)?.public_key)
|
||||||
|
} else if self.starts_with("npub1") {
|
||||||
|
Ok(PublicKey::parse(self)?)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Invalid public key"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_qr(&self) -> Option<Arc<Image>> {
|
||||||
|
let Ok(bytes) = qrcode_generator::to_png_to_vec_from_str(self, QrCodeEcc::Medium, 256)
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Arc::new(Image::from_bytes(ImageFormat::Png, bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> SharedString {
|
pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> SharedString {
|
||||||
let Ok(pubkey) = public_key.to_bech32();
|
let Ok(pubkey) = public_key.to_bech32();
|
||||||
|
|
||||||
|
|||||||
47
crates/common/src/event.rs
Normal file
47
crates/common/src/event.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
pub trait EventUtils {
|
||||||
|
fn uniq_id(&self) -> u64;
|
||||||
|
fn all_pubkeys(&self) -> Vec<PublicKey>;
|
||||||
|
fn compare_pubkeys(&self, other: &[PublicKey]) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventUtils for Event {
|
||||||
|
fn uniq_id(&self) -> u64 {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
let mut pubkeys: Vec<PublicKey> = vec![];
|
||||||
|
|
||||||
|
// Add all public keys from event
|
||||||
|
pubkeys.push(self.pubkey);
|
||||||
|
pubkeys.extend(self.tags.public_keys().collect::<Vec<_>>());
|
||||||
|
|
||||||
|
// Generate unique hash
|
||||||
|
pubkeys
|
||||||
|
.into_iter()
|
||||||
|
.unique()
|
||||||
|
.sorted()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.hash(&mut hasher);
|
||||||
|
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_pubkeys(&self) -> Vec<PublicKey> {
|
||||||
|
let mut public_keys: Vec<PublicKey> = self.tags.public_keys().copied().collect();
|
||||||
|
public_keys.push(self.pubkey);
|
||||||
|
|
||||||
|
public_keys
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_pubkeys(&self, other: &[PublicKey]) -> bool {
|
||||||
|
let pubkeys = self.all_pubkeys();
|
||||||
|
let a: HashSet<_> = pubkeys.iter().collect();
|
||||||
|
let b: HashSet<_> = other.iter().collect();
|
||||||
|
|
||||||
|
a == b
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,62 +1,6 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use gpui::{Image, ImageFormat};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use qrcode_generator::QrCodeEcc;
|
|
||||||
|
|
||||||
pub mod debounced_delay;
|
pub mod debounced_delay;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
|
pub mod event;
|
||||||
pub mod handle_auth;
|
pub mod handle_auth;
|
||||||
pub mod nip05;
|
pub mod nip05;
|
||||||
pub mod nip96;
|
pub mod nip96;
|
||||||
|
|
||||||
pub fn room_hash(event: &Event) -> u64 {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
let mut pubkeys: Vec<PublicKey> = vec![];
|
|
||||||
|
|
||||||
// Add all public keys from event
|
|
||||||
pubkeys.push(event.pubkey);
|
|
||||||
pubkeys.extend(event.tags.public_keys().collect::<Vec<_>>());
|
|
||||||
|
|
||||||
// Generate unique hash
|
|
||||||
pubkeys
|
|
||||||
.into_iter()
|
|
||||||
.unique()
|
|
||||||
.sorted()
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.hash(&mut hasher);
|
|
||||||
|
|
||||||
hasher.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_pubkey_from_str(content: &str) -> Result<PublicKey, anyhow::Error> {
|
|
||||||
if content.starts_with("nprofile1") {
|
|
||||||
Ok(Nip19Profile::from_bech32(content)?.public_key)
|
|
||||||
} else if content.starts_with("npub1") {
|
|
||||||
Ok(PublicKey::parse(content)?)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Invalid public key"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn string_to_qr(data: &str) -> Option<Arc<Image>> {
|
|
||||||
let Ok(bytes) = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256) else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(Arc::new(Image::from_bytes(ImageFormat::Png, bytes)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compare<T>(a: &[T], b: &[T]) -> bool
|
|
||||||
where
|
|
||||||
T: Eq + Hash,
|
|
||||||
{
|
|
||||||
let a: HashSet<_> = a.iter().collect();
|
|
||||||
let b: HashSet<_> = b.iter().collect();
|
|
||||||
|
|
||||||
a == b
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,15 +20,14 @@ use ui::actions::OpenProfile;
|
|||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::dock::DockPlacement;
|
use ui::dock_area::dock::DockPlacement;
|
||||||
use ui::dock_area::panel::PanelView;
|
use ui::dock_area::panel::PanelView;
|
||||||
use ui::dock_area::{DockArea, DockItem};
|
use ui::dock_area::{ClosePanel, DockArea, DockItem};
|
||||||
use ui::modal::ModalButtonProps;
|
use ui::modal::ModalButtonProps;
|
||||||
use ui::{ContextModal, IconName, Root, Sizable, StyledExt, TitleBar};
|
use ui::{ContextModal, IconName, Root, Sizable, StyledExt, TitleBar};
|
||||||
|
|
||||||
use crate::views::chat::{self, Chat};
|
|
||||||
use crate::views::screening::Screening;
|
use crate::views::screening::Screening;
|
||||||
use crate::views::user_profile::UserProfile;
|
use crate::views::user_profile::UserProfile;
|
||||||
use crate::views::{
|
use crate::views::{
|
||||||
login, new_account, onboarding, preferences, sidebar, startup, user_profile, welcome,
|
chat, login, new_account, onboarding, preferences, sidebar, startup, user_profile, welcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
||||||
@@ -74,7 +73,7 @@ pub struct ChatSpace {
|
|||||||
dock: Entity<DockArea>,
|
dock: Entity<DockArea>,
|
||||||
toolbar: bool,
|
toolbar: bool,
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
subscriptions: SmallVec<[Subscription; 6]>,
|
subscriptions: SmallVec<[Subscription; 5]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatSpace {
|
impl ChatSpace {
|
||||||
@@ -112,7 +111,6 @@ impl ChatSpace {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.px_10()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.h_40()
|
.h_40()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -166,13 +164,6 @@ impl ChatSpace {
|
|||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
// Automatically load messages when chat panel opens
|
|
||||||
subscriptions.push(cx.observe_new::<Chat>(|this, window, cx| {
|
|
||||||
if let Some(window) = window {
|
|
||||||
this.load_messages(window, cx);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Automatically run on_load function when UserProfile is created
|
// Automatically run on_load function when UserProfile is created
|
||||||
subscriptions.push(cx.observe_new::<UserProfile>(|this, window, cx| {
|
subscriptions.push(cx.observe_new::<UserProfile>(|this, window, cx| {
|
||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
@@ -183,7 +174,7 @@ impl ChatSpace {
|
|||||||
// Automatically run on_load function when Screening is created
|
// Automatically run on_load function when Screening is created
|
||||||
subscriptions.push(cx.observe_new::<Screening>(|this, window, cx| {
|
subscriptions.push(cx.observe_new::<Screening>(|this, window, cx| {
|
||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
this.on_load(window, cx);
|
this.load(window, cx);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -192,18 +183,34 @@ impl ChatSpace {
|
|||||||
®istry,
|
®istry,
|
||||||
window,
|
window,
|
||||||
|this: &mut Self, _state, event, window, cx| {
|
|this: &mut Self, _state, event, window, cx| {
|
||||||
if let RoomEmitter::Open(room) = event {
|
match event {
|
||||||
|
RoomEmitter::Open(room) => {
|
||||||
if let Some(room) = room.upgrade() {
|
if let Some(room) = room.upgrade() {
|
||||||
this.dock.update(cx, |this, cx| {
|
this.dock.update(cx, |this, cx| {
|
||||||
let panel = chat::init(room, window, cx);
|
let panel = chat::init(room, window, cx);
|
||||||
let placement = DockPlacement::Center;
|
// Load messages on panel creation
|
||||||
|
panel.update(cx, |this, cx| {
|
||||||
|
this.load_messages(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
this.add_panel(panel, placement, window, cx);
|
this.add_panel(panel, DockPlacement::Center, window, cx);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
window.push_notification(t!("chatspace.failed_to_open_room"), cx);
|
window.push_notification(t!("chatspace.failed_to_open_room"), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
RoomEmitter::Close(..) => {
|
||||||
|
this.dock.update(cx, |this, cx| {
|
||||||
|
this.focus_tab_panel(window, cx);
|
||||||
|
|
||||||
|
cx.defer_in(window, |_, window, cx| {
|
||||||
|
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||||
|
window.close_all_modals(cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -283,10 +290,11 @@ impl ChatSpace {
|
|||||||
|
|
||||||
pub fn open_settings(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn open_settings(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let settings = preferences::init(window, cx);
|
let settings = preferences::init(window, cx);
|
||||||
|
let title = SharedString::new(t!("chatspace.preferences_title"));
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, _, _| {
|
window.open_modal(cx, move |modal, _, _| {
|
||||||
modal
|
modal
|
||||||
.title(SharedString::new(t!("chatspace.preferences_title")))
|
.title(title.clone())
|
||||||
.width(px(DEFAULT_MODAL_WIDTH))
|
.width(px(DEFAULT_MODAL_WIDTH))
|
||||||
.child(settings.clone())
|
.child(settings.clone())
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ impl Chat {
|
|||||||
subscriptions.push(cx.subscribe_in(
|
subscriptions.push(cx.subscribe_in(
|
||||||
&input,
|
&input,
|
||||||
window,
|
window,
|
||||||
move |this: &mut Self, input, event, window, cx| match event {
|
move |this: &mut Self, input, event, window, cx| {
|
||||||
|
match event {
|
||||||
InputEvent::PressEnter { .. } => {
|
InputEvent::PressEnter { .. } => {
|
||||||
this.send_message(window, cx);
|
this.send_message(window, cx);
|
||||||
}
|
}
|
||||||
@@ -102,11 +103,14 @@ impl Chat {
|
|||||||
this.mention_popup(text, input, cx);
|
this.mention_popup(text, input, cx);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(cx.subscribe_in(
|
||||||
cx.subscribe_in(&room, window, move |this, _, incoming, _w, cx| {
|
&room,
|
||||||
|
window,
|
||||||
|
move |this, _, incoming, _window, cx| {
|
||||||
// Check if the incoming message is the same as the new message created by optimistic update
|
// Check if the incoming message is the same as the new message created by optimistic update
|
||||||
if this.prevent_duplicate_message(&incoming.0, cx) {
|
if this.prevent_duplicate_message(&incoming.0, cx) {
|
||||||
return;
|
return;
|
||||||
@@ -121,8 +125,8 @@ impl Chat {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.list_state.splice(old_len..old_len, 1);
|
this.list_state.splice(old_len..old_len, 1);
|
||||||
}),
|
},
|
||||||
);
|
));
|
||||||
|
|
||||||
// Initialize list state
|
// Initialize list state
|
||||||
// [item_count] always equal to 1 at the beginning
|
// [item_count] always equal to 1 at the beginning
|
||||||
@@ -251,7 +255,7 @@ impl Chat {
|
|||||||
// Get the message which includes all attachments
|
// Get the message which includes all attachments
|
||||||
let content = self.input_content(cx);
|
let content = self.input_content(cx);
|
||||||
// Get the backup setting
|
// Get the backup setting
|
||||||
let backup = AppSettings::get_global(cx).settings.backup_messages;
|
let backup = AppSettings::get_backup_messages(cx);
|
||||||
|
|
||||||
// Return if message is empty
|
// Return if message is empty
|
||||||
if content.trim().is_empty() {
|
if content.trim().is_empty() {
|
||||||
@@ -397,7 +401,7 @@ impl Chat {
|
|||||||
self.uploading(true, cx);
|
self.uploading(true, cx);
|
||||||
|
|
||||||
// Get the user's configured NIP96 server
|
// Get the user's configured NIP96 server
|
||||||
let nip96_server = AppSettings::get_global(cx).settings.media_server.clone();
|
let nip96_server = AppSettings::get_media_server(cx);
|
||||||
|
|
||||||
// Open native file dialog
|
// Open native file dialog
|
||||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||||
@@ -575,8 +579,8 @@ impl Chat {
|
|||||||
return div().id(ix);
|
return div().id(ix);
|
||||||
};
|
};
|
||||||
|
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
let hide_avatar = AppSettings::get_global(cx).settings.hide_user_avatars;
|
let hide_avatar = AppSettings::get_hide_user_avatars(cx);
|
||||||
let registry = Registry::read_global(cx);
|
let registry = Registry::read_global(cx);
|
||||||
let author = registry.get_person(&message.author, cx);
|
let author = registry.get_person(&message.author, cx);
|
||||||
|
|
||||||
@@ -715,8 +719,6 @@ impl Chat {
|
|||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.px_3()
|
|
||||||
.pb_3()
|
|
||||||
.children(errors.iter().map(|error| {
|
.children(errors.iter().map(|error| {
|
||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
@@ -792,7 +794,7 @@ impl Panel for Chat {
|
|||||||
|
|
||||||
fn title(&self, cx: &App) -> AnyElement {
|
fn title(&self, cx: &App) -> AnyElement {
|
||||||
self.room.read_with(cx, |this, cx| {
|
self.room.read_with(cx, |this, cx| {
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
let label = this.display_name(cx);
|
let label = this.display_name(cx);
|
||||||
let url = this.display_image(proxy, cx);
|
let url = this.display_image(proxy, cx);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
div, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
div, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
|
||||||
ParentElement, Render, SharedString, Styled, Window,
|
Styled, Window,
|
||||||
};
|
};
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
use registry::Registry;
|
use registry::Registry;
|
||||||
@@ -21,7 +21,6 @@ pub fn init(
|
|||||||
pub struct Subject {
|
pub struct Subject {
|
||||||
id: u64,
|
id: u64,
|
||||||
input: Entity<InputState>,
|
input: Entity<InputState>,
|
||||||
focus_handle: FocusHandle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Subject {
|
impl Subject {
|
||||||
@@ -39,11 +38,7 @@ impl Subject {
|
|||||||
this
|
this
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.new(|cx| Self {
|
cx.new(|_| Self { id, input })
|
||||||
id,
|
|
||||||
input,
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
@@ -65,13 +60,9 @@ impl Subject {
|
|||||||
impl Render for Subject {
|
impl Render for Subject {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div()
|
div()
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.size_full()
|
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.px_3()
|
|
||||||
.pb_3()
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::ops::Range;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use common::display::DisplayProfile;
|
use common::display::{DisplayProfile, TextUtils};
|
||||||
use common::nip05::nip05_profile;
|
use common::nip05::nip05_profile;
|
||||||
use global::constants::BOOTSTRAP_RELAYS;
|
use global::constants::BOOTSTRAP_RELAYS;
|
||||||
use global::nostr_client;
|
use global::nostr_client;
|
||||||
@@ -24,7 +24,7 @@ use theme::ActiveTheme;
|
|||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
use ui::{v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
||||||
cx.new(|cx| Compose::new(window, cx))
|
cx.new(|cx| Compose::new(window, cx))
|
||||||
@@ -278,7 +278,7 @@ impl Compose {
|
|||||||
Err(anyhow!(t!("common.not_found")))
|
Err(anyhow!(t!("common.not_found")))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if let Ok(public_key) = common::parse_pubkey_from_str(&content) {
|
} else if let Ok(public_key) = content.to_public_key() {
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
let contact = Contact::new(public_key).select();
|
let contact = Contact::new(public_key).select();
|
||||||
@@ -357,7 +357,7 @@ impl Compose {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
let registry = Registry::read_global(cx);
|
let registry = Registry::read_global(cx);
|
||||||
let mut items = Vec::with_capacity(self.contacts.len());
|
let mut items = Vec::with_capacity(self.contacts.len());
|
||||||
|
|
||||||
@@ -420,22 +420,19 @@ impl Render for Compose {
|
|||||||
t!("compose.create_dm_button")
|
t!("compose.create_dm_button")
|
||||||
};
|
};
|
||||||
|
|
||||||
div()
|
v_flex()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.px_3()
|
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::new(t!("compose.description"))),
|
.child(SharedString::new(t!("compose.description"))),
|
||||||
)
|
)
|
||||||
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
|
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
|
||||||
this.child(div().px_3().text_xs().text_color(red()).child(msg.clone()))
|
this.child(div().text_xs().text_color(red()).child(msg.clone()))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div().px_3().flex().flex_col().child(
|
div().flex().flex_col().child(
|
||||||
div()
|
div()
|
||||||
.h_10()
|
.h_10()
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
@@ -460,7 +457,6 @@ impl Render for Compose {
|
|||||||
.mt_1()
|
.mt_1()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.px_3()
|
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
@@ -535,7 +531,6 @@ impl Render for Compose {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().p_3().child(
|
|
||||||
Button::new("create_dm_btn")
|
Button::new("create_dm_btn")
|
||||||
.label(label)
|
.label(label)
|
||||||
.primary()
|
.primary()
|
||||||
@@ -545,7 +540,6 @@ impl Render for Compose {
|
|||||||
.on_click(cx.listener(move |this, _event, window, cx| {
|
.on_click(cx.listener(move |this, _event, window, cx| {
|
||||||
this.compose(window, cx);
|
this.compose(window, cx);
|
||||||
})),
|
})),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ impl EditProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let nip96 = AppSettings::get_global(cx).settings.media_server.clone();
|
let nip96 = AppSettings::get_media_server(cx);
|
||||||
let avatar_input = self.avatar_input.downgrade();
|
let avatar_input = self.avatar_input.downgrade();
|
||||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||||
files: true,
|
files: true,
|
||||||
@@ -233,7 +233,6 @@ impl EditProfile {
|
|||||||
impl Render for EditProfile {
|
impl Render for EditProfile {
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div()
|
div()
|
||||||
.size_full()
|
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use client_keys::ClientKeys;
|
use client_keys::ClientKeys;
|
||||||
|
use common::display::TextUtils;
|
||||||
use common::handle_auth::CoopAuthUrlHandler;
|
use common::handle_auth::CoopAuthUrlHandler;
|
||||||
use common::string_to_qr;
|
|
||||||
use global::constants::{APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
use global::constants::{APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -99,7 +99,7 @@ impl Login {
|
|||||||
|
|
||||||
// Update the QR Image with the new connection string
|
// Update the QR Image with the new connection string
|
||||||
this.qr_image.update(cx, |this, cx| {
|
this.qr_image.update(cx, |this, cx| {
|
||||||
*this = string_to_qr(&connection_string.to_string());
|
*this = connection_string.to_string().to_qr();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -234,8 +234,6 @@ impl Login {
|
|||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.pt_4()
|
|
||||||
.px_4()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
|
|||||||
@@ -118,8 +118,6 @@ impl NewAccount {
|
|||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.pt_4()
|
|
||||||
.px_4()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
@@ -132,7 +130,7 @@ impl NewAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let nip96 = AppSettings::get_global(cx).settings.media_server.clone();
|
let nip96 = AppSettings::get_media_server(cx);
|
||||||
let avatar_input = self.avatar_input.downgrade();
|
let avatar_input = self.avatar_input.downgrade();
|
||||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||||
files: true,
|
files: true,
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ impl Focusable for Onboarding {
|
|||||||
|
|
||||||
impl Render for Onboarding {
|
impl Render for Onboarding {
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let auto_login = AppSettings::get_global(cx).settings.auto_login;
|
let auto_login = AppSettings::get_auto_login(cx);
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.py_4()
|
.py_4()
|
||||||
@@ -239,11 +239,8 @@ impl Render for Onboarding {
|
|||||||
Checkbox::new("auto_login")
|
Checkbox::new("auto_login")
|
||||||
.label(SharedString::new(t!("onboarding.auto_login")))
|
.label(SharedString::new(t!("onboarding.auto_login")))
|
||||||
.checked(auto_login)
|
.checked(auto_login)
|
||||||
.on_click(|_, _window, cx| {
|
.on_click(move |_, _window, cx| {
|
||||||
AppSettings::global(cx).update(cx, |this, cx| {
|
AppSettings::update_auto_login(!auto_login, cx);
|
||||||
this.settings.auto_login = !this.settings.auto_login;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use global::constants::{DEFAULT_MODAL_WIDTH, NIP96_SERVER};
|
|||||||
use gpui::http_client::Url;
|
use gpui::http_client::Url;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
||||||
IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window,
|
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||||
};
|
};
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
@@ -15,7 +15,7 @@ use ui::avatar::Avatar;
|
|||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{InputState, TextInput};
|
||||||
use ui::switch::Switch;
|
use ui::switch::Switch;
|
||||||
use ui::{ContextModal, IconName, Sizable, Size, StyledExt};
|
use ui::{v_flex, ContextModal, IconName, Sizable, Size, StyledExt};
|
||||||
|
|
||||||
use crate::views::{edit_profile, relays};
|
use crate::views::{edit_profile, relays};
|
||||||
|
|
||||||
@@ -25,27 +25,19 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
|||||||
|
|
||||||
pub struct Preferences {
|
pub struct Preferences {
|
||||||
media_input: Entity<InputState>,
|
media_input: Entity<InputState>,
|
||||||
focus_handle: FocusHandle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Preferences {
|
impl Preferences {
|
||||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let media_server = AppSettings::get_global(cx)
|
let media_server = AppSettings::get_media_server(cx).to_string();
|
||||||
.settings
|
|
||||||
.media_server
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let media_input = cx.new(|cx| {
|
let media_input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.default_value(media_server)
|
.default_value(media_server)
|
||||||
.placeholder(NIP96_SERVER)
|
.placeholder(NIP96_SERVER)
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self { media_input }
|
||||||
media_input,
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,22 +67,18 @@ impl Preferences {
|
|||||||
|
|
||||||
impl Render for Preferences {
|
impl Render for Preferences {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let registry = Registry::read_global(cx);
|
let input_state = self.media_input.downgrade();
|
||||||
let settings = AppSettings::get_global(cx).settings.as_ref();
|
|
||||||
|
|
||||||
let profile = Identity::read_global(cx)
|
let profile = Identity::read_global(cx)
|
||||||
.public_key()
|
.public_key()
|
||||||
.map(|pk| registry.get_person(&pk, cx));
|
.map(|pk| Registry::read_global(cx).get_person(&pk, cx));
|
||||||
|
|
||||||
let input_state = self.media_input.downgrade();
|
let backup_messages = AppSettings::get_backup_messages(cx);
|
||||||
|
let screening = AppSettings::get_screening(cx);
|
||||||
|
let contact_bypass = AppSettings::get_contact_bypass(cx);
|
||||||
|
let proxy_avatar = AppSettings::get_proxy_user_avatars(cx);
|
||||||
|
let hide_avatar = AppSettings::get_hide_user_avatars(cx);
|
||||||
|
|
||||||
div()
|
v_flex()
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.size_full()
|
|
||||||
.px_3()
|
|
||||||
.pb_3()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.py_2()
|
.py_2()
|
||||||
@@ -118,9 +106,7 @@ impl Render for Preferences {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
Avatar::new(
|
Avatar::new(profile.avatar_url(proxy_avatar))
|
||||||
profile.avatar_url(settings.proxy_user_avatars),
|
|
||||||
)
|
|
||||||
.size(rems(2.4)),
|
.size(rems(2.4)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -187,19 +173,14 @@ impl Render for Preferences {
|
|||||||
.with_size(Size::Size(px(26.)))
|
.with_size(Size::Size(px(26.)))
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
if let Some(input) = input_state.upgrade() {
|
if let Some(input) = input_state.upgrade() {
|
||||||
let value = input.read(cx).value();
|
let Ok(url) = Url::parse(input.read(cx).value()) else {
|
||||||
let Ok(url) = Url::parse(value) else {
|
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
t!("preferences.url_not_valid"),
|
t!("preferences.url_not_valid"),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
AppSettings::update_media_server(url, cx);
|
||||||
AppSettings::global(cx).update(cx, |this, cx| {
|
|
||||||
this.settings.media_server = url;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -227,17 +208,35 @@ impl Render for Preferences {
|
|||||||
.child(SharedString::new(t!("preferences.messages_header"))),
|
.child(SharedString::new(t!("preferences.messages_header"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().flex().flex_col().gap_2().child(
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Switch::new("screening")
|
||||||
|
.label(t!("preferences.screening_label"))
|
||||||
|
.description(t!("preferences.screening_description"))
|
||||||
|
.checked(screening)
|
||||||
|
.on_click(move |_, _window, cx| {
|
||||||
|
AppSettings::update_screening(!screening, cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Switch::new("bypass")
|
||||||
|
.label(t!("preferences.bypass_label"))
|
||||||
|
.description(t!("preferences.bypass_description"))
|
||||||
|
.checked(contact_bypass)
|
||||||
|
.on_click(move |_, _window, cx| {
|
||||||
|
AppSettings::update_contact_bypass(!contact_bypass, cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
Switch::new("backup_messages")
|
Switch::new("backup_messages")
|
||||||
.label(t!("preferences.backup_messages_label"))
|
.label(t!("preferences.backup_messages_label"))
|
||||||
.description(t!("preferences.backup_description"))
|
.description(t!("preferences.backup_description"))
|
||||||
.checked(settings.backup_messages)
|
.checked(backup_messages)
|
||||||
.on_click(|_, _window, cx| {
|
.on_click(move |_, _window, cx| {
|
||||||
AppSettings::global(cx).update(cx, |this, cx| {
|
AppSettings::update_backup_messages(!backup_messages, cx);
|
||||||
this.settings.backup_messages =
|
|
||||||
!this.settings.backup_messages;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -266,26 +265,18 @@ impl Render for Preferences {
|
|||||||
Switch::new("hide_user_avatars")
|
Switch::new("hide_user_avatars")
|
||||||
.label(t!("preferences.hide_avatars_label"))
|
.label(t!("preferences.hide_avatars_label"))
|
||||||
.description(t!("preferences.hide_avatar_description"))
|
.description(t!("preferences.hide_avatar_description"))
|
||||||
.checked(settings.hide_user_avatars)
|
.checked(hide_avatar)
|
||||||
.on_click(|_, _window, cx| {
|
.on_click(move |_, _window, cx| {
|
||||||
AppSettings::global(cx).update(cx, |this, cx| {
|
AppSettings::update_hide_user_avatars(!hide_avatar, cx);
|
||||||
this.settings.hide_user_avatars =
|
|
||||||
!this.settings.hide_user_avatars;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Switch::new("proxy_user_avatars")
|
Switch::new("proxy_user_avatars")
|
||||||
.label(t!("preferences.proxy_avatars_label"))
|
.label(t!("preferences.proxy_avatars_label"))
|
||||||
.description(t!("preferences.proxy_description"))
|
.description(t!("preferences.proxy_description"))
|
||||||
.checked(settings.proxy_user_avatars)
|
.checked(proxy_avatar)
|
||||||
.on_click(|_, _window, cx| {
|
.on_click(move |_, _window, cx| {
|
||||||
AppSettings::global(cx).update(cx, |this, cx| {
|
AppSettings::update_proxy_user_avatars(!proxy_avatar, cx);
|
||||||
this.settings.proxy_user_avatars =
|
|
||||||
!this.settings.proxy_user_avatars;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
use common::display::{shorten_pubkey, DisplayProfile};
|
use common::display::{shorten_pubkey, DisplayProfile};
|
||||||
use common::nip05::nip05_verify;
|
use common::nip05::nip05_verify;
|
||||||
use global::nostr_client;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, App, AppContext, Context, Entity, IntoElement, ParentElement, Render,
|
div, relative, rems, App, AppContext, Context, Div, Entity, IntoElement, ParentElement, Render,
|
||||||
SharedString, Styled, Task, Window,
|
SharedString, Styled, Task, Window,
|
||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use i18n::t;
|
use i18n::{shared_t, t};
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use registry::Registry;
|
use registry::Registry;
|
||||||
@@ -23,30 +22,31 @@ pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<
|
|||||||
|
|
||||||
pub struct Screening {
|
pub struct Screening {
|
||||||
public_key: PublicKey,
|
public_key: PublicKey,
|
||||||
followed: bool,
|
|
||||||
connections: usize,
|
|
||||||
verified: bool,
|
verified: bool,
|
||||||
|
followed: bool,
|
||||||
|
dm_relays: bool,
|
||||||
|
mutual_contacts: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Screening {
|
impl Screening {
|
||||||
pub fn new(public_key: PublicKey, _window: &mut Window, cx: &mut App) -> Entity<Self> {
|
pub fn new(public_key: PublicKey, _window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
cx.new(|_| Self {
|
cx.new(|_| Self {
|
||||||
public_key,
|
public_key,
|
||||||
followed: false,
|
|
||||||
connections: 0,
|
|
||||||
verified: false,
|
verified: false,
|
||||||
|
followed: false,
|
||||||
|
dm_relays: false,
|
||||||
|
mutual_contacts: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
// Skip if user isn't logged in
|
// Skip if user isn't logged in
|
||||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let public_key = self.public_key;
|
let public_key = self.public_key;
|
||||||
|
|
||||||
let check_trust_score: Task<(bool, usize)> = cx.background_spawn(async move {
|
let check_trust_score: Task<(bool, usize, bool)> = cx.background_spawn(async move {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
|
|
||||||
let follow = Filter::new()
|
let follow = Filter::new()
|
||||||
@@ -55,15 +55,21 @@ impl Screening {
|
|||||||
.pubkey(public_key)
|
.pubkey(public_key)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
let connection = Filter::new()
|
let contacts = Filter::new()
|
||||||
.kind(Kind::ContactList)
|
.kind(Kind::ContactList)
|
||||||
.pubkey(public_key)
|
.pubkey(public_key)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
let is_follow = client.database().count(follow).await.unwrap_or(0) >= 1;
|
let relays = Filter::new()
|
||||||
let connects = client.database().count(connection).await.unwrap_or(0);
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
(is_follow, connects)
|
let is_follow = client.database().count(follow).await.unwrap_or(0) >= 1;
|
||||||
|
let mutual_contacts = client.database().count(contacts).await.unwrap_or(0);
|
||||||
|
let dm_relays = client.database().count(relays).await.unwrap_or(0) >= 1;
|
||||||
|
|
||||||
|
(is_follow, mutual_contacts, dm_relays)
|
||||||
});
|
});
|
||||||
|
|
||||||
let verify_nip05 = if let Some(address) = self.address(cx) {
|
let verify_nip05 = if let Some(address) = self.address(cx) {
|
||||||
@@ -75,11 +81,12 @@ impl Screening {
|
|||||||
};
|
};
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let (followed, connections) = check_trust_score.await;
|
let (followed, mutual_contacts, dm_relays) = check_trust_score.await;
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.followed = followed;
|
this.followed = followed;
|
||||||
this.connections = connections;
|
this.mutual_contacts = mutual_contacts;
|
||||||
|
this.dm_relays = dm_relays;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
@@ -140,15 +147,11 @@ impl Screening {
|
|||||||
|
|
||||||
impl Render for Screening {
|
impl Render for Screening {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
let profile = self.profile(cx);
|
let profile = self.profile(cx);
|
||||||
let shorten_pubkey = shorten_pubkey(profile.public_key(), 8);
|
let shorten_pubkey = shorten_pubkey(profile.public_key(), 8);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.w_full()
|
|
||||||
.px_4()
|
|
||||||
.pt_8()
|
|
||||||
.pb_4()
|
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -166,7 +169,7 @@ impl Render for Screening {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_3()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.p_1()
|
.p_1()
|
||||||
@@ -176,7 +179,7 @@ impl Render for Screening {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.bg(cx.theme().elevated_surface_background)
|
.bg(cx.theme().surface_background)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.truncate()
|
.truncate()
|
||||||
.text_ellipsis()
|
.text_ellipsis()
|
||||||
@@ -184,6 +187,9 @@ impl Render for Screening {
|
|||||||
.line_height(relative(1.))
|
.line_height(relative(1.))
|
||||||
.child(shorten_pubkey),
|
.child(shorten_pubkey),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
Button::new("njump")
|
Button::new("njump")
|
||||||
.label(t!("profile.njump"))
|
.label(t!("profile.njump"))
|
||||||
@@ -197,92 +203,123 @@ impl Render for Screening {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("report")
|
Button::new("report")
|
||||||
.tooltip(t!("screening.report"))
|
.tooltip(t!("screening.report"))
|
||||||
.icon(IconName::Info)
|
.icon(IconName::Report)
|
||||||
.danger_alt()
|
.danger()
|
||||||
.small()
|
|
||||||
.rounded(ButtonRounded::Full)
|
.rounded(ButtonRounded::Full)
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
this.report(window, cx);
|
this.report(window, cx);
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_3()
|
||||||
.when_some(self.address(cx), |this, address| {
|
|
||||||
this.child(h_flex().gap_2().map(|this| {
|
|
||||||
if self.verified {
|
|
||||||
this.text_sm()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::CheckCircleFill)
|
|
||||||
.small()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.text_color(cx.theme().icon_accent),
|
|
||||||
)
|
|
||||||
.child(div().flex_1().child(SharedString::new(t!(
|
|
||||||
"screening.verified",
|
|
||||||
address = address
|
|
||||||
))))
|
|
||||||
} else {
|
|
||||||
this.text_sm()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::CheckCircleFill)
|
|
||||||
.small()
|
|
||||||
.text_color(cx.theme().icon_muted),
|
|
||||||
)
|
|
||||||
.child(div().flex_1().child(SharedString::new(t!(
|
|
||||||
"screening.not_verified",
|
|
||||||
address = address
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
.child(h_flex().gap_2().map(|this| {
|
|
||||||
if !self.followed {
|
|
||||||
this.text_sm()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::CheckCircleFill)
|
|
||||||
.small()
|
|
||||||
.text_color(cx.theme().icon_muted),
|
|
||||||
)
|
|
||||||
.child(SharedString::new(t!("screening.not_contact")))
|
|
||||||
} else {
|
|
||||||
this.text_sm()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::CheckCircleFill)
|
|
||||||
.small()
|
|
||||||
.text_color(cx.theme().icon_accent),
|
|
||||||
)
|
|
||||||
.child(SharedString::new(t!("screening.contact")))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.items_start()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
|
.child(status_badge(self.followed, cx))
|
||||||
.child(
|
.child(
|
||||||
Icon::new(IconName::CheckCircleFill)
|
v_flex()
|
||||||
.small()
|
.text_sm()
|
||||||
.flex_shrink_0()
|
.child(shared_t!("screening.contact_label"))
|
||||||
.text_color({
|
.child(div().text_color(cx.theme().text_muted).child({
|
||||||
if self.connections > 0 {
|
if self.followed {
|
||||||
cx.theme().icon_accent
|
shared_t!("screening.contact")
|
||||||
} else {
|
} else {
|
||||||
cx.theme().icon_muted
|
shared_t!("screening.not_contact")
|
||||||
}
|
}
|
||||||
}),
|
})),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.map(|this| {
|
.child(
|
||||||
if self.connections > 0 {
|
h_flex()
|
||||||
this.child(SharedString::new(t!(
|
.items_start()
|
||||||
"screening.total_connections",
|
.gap_2()
|
||||||
u = self.connections
|
.child(status_badge(self.verified, cx))
|
||||||
)))
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.text_sm()
|
||||||
|
.child({
|
||||||
|
if let Some(addr) = self.address(cx) {
|
||||||
|
shared_t!("screening.nip05_addr", addr = addr)
|
||||||
} else {
|
} else {
|
||||||
this.child(SharedString::new(t!("screening.no_connections")))
|
shared_t!("screening.nip05_label")
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
|
.child(div().text_color(cx.theme().text_muted).child({
|
||||||
|
if self.address(cx).is_some() {
|
||||||
|
if self.verified {
|
||||||
|
shared_t!("screening.nip05_ok")
|
||||||
|
} else {
|
||||||
|
shared_t!("screening.nip05_failed")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shared_t!("screening.nip05_empty")
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.items_start()
|
||||||
|
.gap_2()
|
||||||
|
.child(status_badge(self.mutual_contacts > 0, cx))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.text_sm()
|
||||||
|
.child(shared_t!("screening.mutual_label"))
|
||||||
|
.child(div().text_color(cx.theme().text_muted).child({
|
||||||
|
if self.mutual_contacts > 0 {
|
||||||
|
shared_t!("screening.mutual", u = self.mutual_contacts)
|
||||||
|
} else {
|
||||||
|
shared_t!("screening.no_mutual")
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.items_start()
|
||||||
|
.gap_2()
|
||||||
|
.child(status_badge(self.dm_relays, cx))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.text_sm()
|
||||||
|
.child({
|
||||||
|
if self.dm_relays {
|
||||||
|
shared_t!("screening.relay_found")
|
||||||
|
} else {
|
||||||
|
shared_t!("screening.relay_empty")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(div().w_full().text_color(cx.theme().text_muted).child(
|
||||||
|
{
|
||||||
|
if self.dm_relays {
|
||||||
|
shared_t!("screening.relay_found_desc")
|
||||||
|
} else {
|
||||||
|
shared_t!("screening.relay_empty_desc")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn status_badge(status: bool, cx: &App) -> Div {
|
||||||
|
div()
|
||||||
|
.pt_1()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.child(Icon::new(IconName::CheckCircleFill).small().text_color({
|
||||||
|
if status {
|
||||||
|
cx.theme().icon_accent
|
||||||
|
} else {
|
||||||
|
cx.theme().icon_muted
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, rems, App, ClickEvent, Div, InteractiveElement, IntoElement,
|
div, rems, App, ClickEvent, Div, InteractiveElement, IntoElement, ParentElement as _,
|
||||||
ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||||
};
|
};
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use registry::room::RoomKind;
|
use registry::room::RoomKind;
|
||||||
|
use registry::Registry;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::actions::OpenProfile;
|
use ui::actions::OpenProfile;
|
||||||
@@ -22,49 +23,39 @@ use crate::views::screening;
|
|||||||
pub struct RoomListItem {
|
pub struct RoomListItem {
|
||||||
ix: usize,
|
ix: usize,
|
||||||
base: Div,
|
base: Div,
|
||||||
|
room_id: u64,
|
||||||
public_key: PublicKey,
|
public_key: PublicKey,
|
||||||
name: Option<SharedString>,
|
name: SharedString,
|
||||||
avatar: Option<SharedString>,
|
avatar: SharedString,
|
||||||
created_at: Option<SharedString>,
|
created_at: SharedString,
|
||||||
kind: Option<RoomKind>,
|
kind: RoomKind,
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
handler: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
|
handler: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RoomListItem {
|
impl RoomListItem {
|
||||||
pub fn new(ix: usize, public_key: PublicKey) -> Self {
|
pub fn new(
|
||||||
|
ix: usize,
|
||||||
|
room_id: u64,
|
||||||
|
public_key: PublicKey,
|
||||||
|
name: SharedString,
|
||||||
|
avatar: SharedString,
|
||||||
|
created_at: SharedString,
|
||||||
|
kind: RoomKind,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ix,
|
ix,
|
||||||
public_key,
|
public_key,
|
||||||
|
room_id,
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
created_at,
|
||||||
|
kind,
|
||||||
base: h_flex().h_9().w_full().px_1p5(),
|
base: h_flex().h_9().w_full().px_1p5(),
|
||||||
name: None,
|
|
||||||
avatar: None,
|
|
||||||
created_at: None,
|
|
||||||
kind: None,
|
|
||||||
handler: Rc::new(|_, _, _| {}),
|
handler: Rc::new(|_, _, _| {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(mut self, name: impl Into<SharedString>) -> Self {
|
|
||||||
self.name = Some(name.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn created_at(mut self, created_at: impl Into<SharedString>) -> Self {
|
|
||||||
self.created_at = Some(created_at.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn avatar(mut self, avatar: impl Into<SharedString>) -> Self {
|
|
||||||
self.avatar = Some(avatar.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn kind(mut self, kind: RoomKind) -> Self {
|
|
||||||
self.kind = Some(kind);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_click(
|
pub fn on_click(
|
||||||
mut self,
|
mut self,
|
||||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
@@ -77,10 +68,11 @@ impl RoomListItem {
|
|||||||
impl RenderOnce for RoomListItem {
|
impl RenderOnce for RoomListItem {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let public_key = self.public_key;
|
let public_key = self.public_key;
|
||||||
|
let room_id = self.room_id;
|
||||||
let kind = self.kind;
|
let kind = self.kind;
|
||||||
let handler = self.handler.clone();
|
let handler = self.handler.clone();
|
||||||
let hide_avatar = AppSettings::get_global(cx).settings.hide_user_avatars;
|
let hide_avatar = AppSettings::get_hide_user_avatars(cx);
|
||||||
let screening = AppSettings::get_global(cx).settings.screening;
|
let require_screening = AppSettings::get_screening(cx);
|
||||||
|
|
||||||
self.base
|
self.base
|
||||||
.id(self.ix)
|
.id(self.ix)
|
||||||
@@ -94,18 +86,7 @@ impl RenderOnce for RoomListItem {
|
|||||||
.size_6()
|
.size_6()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.map(|this| {
|
.child(Avatar::new(self.avatar).size(rems(1.5))),
|
||||||
if let Some(img) = self.avatar {
|
|
||||||
this.child(Avatar::new(img).size(rems(1.5)))
|
|
||||||
} else {
|
|
||||||
this.child(
|
|
||||||
img("brand/avatar.png")
|
|
||||||
.rounded_full()
|
|
||||||
.size_6()
|
|
||||||
.into_any_element(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
@@ -114,26 +95,22 @@ impl RenderOnce for RoomListItem {
|
|||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.when_some(self.name, |this, name| {
|
.child(
|
||||||
this.child(
|
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.line_clamp(1)
|
.line_clamp(1)
|
||||||
.text_ellipsis()
|
.text_ellipsis()
|
||||||
.truncate()
|
.truncate()
|
||||||
.font_medium()
|
.font_medium()
|
||||||
.child(name),
|
.child(self.name),
|
||||||
)
|
)
|
||||||
})
|
.child(
|
||||||
.when_some(self.created_at, |this, ago| {
|
|
||||||
this.child(
|
|
||||||
div()
|
div()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_placeholder)
|
.text_color(cx.theme().text_placeholder)
|
||||||
.child(ago),
|
.child(self.created_at),
|
||||||
)
|
),
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.context_menu(move |this, _window, _cx| {
|
.context_menu(move |this, _window, _cx| {
|
||||||
// TODO: add share chat room
|
// TODO: add share chat room
|
||||||
@@ -141,15 +118,12 @@ impl RenderOnce for RoomListItem {
|
|||||||
})
|
})
|
||||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||||
.on_click(move |event, window, cx| {
|
.on_click(move |event, window, cx| {
|
||||||
let handler = handler.clone();
|
handler(event, window, cx);
|
||||||
|
|
||||||
if let Some(kind) = kind {
|
if kind != RoomKind::Ongoing && require_screening {
|
||||||
if kind != RoomKind::Ongoing && screening {
|
|
||||||
let screening = screening::init(public_key, window, cx);
|
let screening = screening::init(public_key, window, cx);
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
let handler_clone = handler.clone();
|
|
||||||
|
|
||||||
this.confirm()
|
this.confirm()
|
||||||
.child(screening.clone())
|
.child(screening.clone())
|
||||||
.button_props(
|
.button_props(
|
||||||
@@ -157,17 +131,15 @@ impl RenderOnce for RoomListItem {
|
|||||||
.cancel_text(t!("screening.ignore"))
|
.cancel_text(t!("screening.ignore"))
|
||||||
.ok_text(t!("screening.response")),
|
.ok_text(t!("screening.response")),
|
||||||
)
|
)
|
||||||
.on_ok(move |event, window, cx| {
|
.on_cancel(move |_event, _window, cx| {
|
||||||
handler_clone(event, window, cx);
|
Registry::global(cx).update(cx, |this, cx| {
|
||||||
// true to close the modal
|
this.close_room(room_id, cx);
|
||||||
true
|
});
|
||||||
|
// false to prevent closing the modal
|
||||||
|
// modal will be closed after closing panel
|
||||||
|
false
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
handler(event, window, cx)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handler(event, window, cx)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use common::debounced_delay::DebouncedDelay;
|
use common::debounced_delay::DebouncedDelay;
|
||||||
use common::display::DisplayProfile;
|
use common::display::{DisplayProfile, TextUtils};
|
||||||
use global::constants::{BOOTSTRAP_RELAYS, DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
use global::constants::{BOOTSTRAP_RELAYS, DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
||||||
use global::nostr_client;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
@@ -32,7 +32,7 @@ use ui::indicator::Indicator;
|
|||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
use ui::popup_menu::PopupMenu;
|
use ui::popup_menu::PopupMenu;
|
||||||
use ui::skeleton::Skeleton;
|
use ui::skeleton::Skeleton;
|
||||||
use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
|
use ui::{v_flex, ContextModal, IconName, Selectable, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::views::compose;
|
use crate::views::compose;
|
||||||
|
|
||||||
@@ -146,6 +146,8 @@ impl Sidebar {
|
|||||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
log::info!("Subscribe to get metadata for: {public_key}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +336,7 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn search_by_pubkey(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
fn search_by_pubkey(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Ok(public_key) = common::parse_pubkey_from_str(query) else {
|
let Ok(public_key) = query.to_public_key() else {
|
||||||
window.push_notification(t!("common.pubkey_invalid"), cx);
|
window.push_notification(t!("common.pubkey_invalid"), cx);
|
||||||
self.set_finding(false, window, cx);
|
self.set_finding(false, window, cx);
|
||||||
return;
|
return;
|
||||||
@@ -568,15 +570,13 @@ impl Sidebar {
|
|||||||
let desc = SharedString::new(t!("sidebar.loading_modal_description"));
|
let desc = SharedString::new(t!("sidebar.loading_modal_description"));
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, cx| {
|
window.open_modal(cx, move |this, _window, cx| {
|
||||||
this.child(
|
this.show_close(true)
|
||||||
div()
|
.keyboard(true)
|
||||||
.pt_8()
|
.title(title.clone())
|
||||||
.px_4()
|
.child(
|
||||||
|
v_flex()
|
||||||
.pb_4()
|
.pb_4()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(div().font_semibold().child(title.clone()))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -597,7 +597,7 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn account(&self, profile: &Profile, cx: &Context<Self>) -> impl IntoElement {
|
fn account(&self, profile: &Profile, cx: &Context<Self>) -> impl IntoElement {
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.px_3()
|
.px_3()
|
||||||
@@ -666,25 +666,29 @@ impl Sidebar {
|
|||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> Vec<impl IntoElement> {
|
) -> Vec<impl IntoElement> {
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
let mut items = Vec::with_capacity(range.end - range.start);
|
let mut items = Vec::with_capacity(range.end - range.start);
|
||||||
|
|
||||||
for ix in range {
|
for ix in range {
|
||||||
if let Some(room) = rooms.get(ix) {
|
if let Some(room) = rooms.get(ix) {
|
||||||
let this = room.read(cx);
|
let this = room.read(cx);
|
||||||
|
let room_id = this.id;
|
||||||
let handler = cx.listener({
|
let handler = cx.listener({
|
||||||
let id = this.id;
|
|
||||||
move |this, _, window, cx| {
|
move |this, _, window, cx| {
|
||||||
this.open_room(id, window, cx);
|
this.open_room(room_id, window, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
RoomListItem::new(ix, this.members[0])
|
RoomListItem::new(
|
||||||
.avatar(this.display_image(proxy, cx))
|
ix,
|
||||||
.name(this.display_name(cx))
|
room_id,
|
||||||
.created_at(this.ago())
|
this.members[0],
|
||||||
.kind(this.kind)
|
this.display_name(cx),
|
||||||
|
this.display_image(proxy, cx),
|
||||||
|
this.ago(),
|
||||||
|
this.kind,
|
||||||
|
)
|
||||||
.on_click(handler),
|
.on_click(handler),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,16 +136,13 @@ impl UserProfile {
|
|||||||
|
|
||||||
impl Render for UserProfile {
|
impl Render for UserProfile {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
let profile = self.profile(cx);
|
let profile = self.profile(cx);
|
||||||
|
|
||||||
let Ok(bech32) = profile.public_key().to_bech32();
|
let Ok(bech32) = profile.public_key().to_bech32();
|
||||||
let shared_bech32 = SharedString::new(bech32);
|
let shared_bech32 = SharedString::new(bech32);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.px_4()
|
|
||||||
.pt_8()
|
|
||||||
.pb_4()
|
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|||||||
@@ -26,5 +26,14 @@ macro_rules! init {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use rust_i18n::set_locale;
|
#[macro_export]
|
||||||
pub use rust_i18n::t;
|
macro_rules! shared_t {
|
||||||
|
($key:expr) => {
|
||||||
|
SharedString::new(t!($key))
|
||||||
|
};
|
||||||
|
($key:expr, $($param:ident = $value:expr),+) => {
|
||||||
|
SharedString::new(t!($key, $($param = $value),+))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use rust_i18n::{set_locale, t};
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ impl Identity {
|
|||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
cx.observe_in(&client_keys, window, |this, state, window, cx| {
|
cx.observe_in(&client_keys, window, |this, state, window, cx| {
|
||||||
let auto_login = AppSettings::get_global(cx).settings.auto_login;
|
let auto_login = AppSettings::get_auto_login(cx);
|
||||||
let has_client_keys = state.read(cx).has_keys();
|
let has_client_keys = state.read(cx).has_keys();
|
||||||
|
|
||||||
// Skip auto login if the user hasn't enabled auto login
|
// Skip auto login if the user hasn't enabled auto login
|
||||||
@@ -264,8 +264,6 @@ impl Identity {
|
|||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.pt_4()
|
|
||||||
.px_4()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
rust-i18n.workspace = true
|
rust-i18n.workspace = true
|
||||||
i18n.workspace = true
|
i18n.workspace = true
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::cmp::Reverse;
|
|||||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use common::room_hash;
|
use common::event::EventUtils;
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
use global::nostr_client;
|
use global::nostr_client;
|
||||||
@@ -12,6 +12,7 @@ use gpui::{
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use room::RoomKind;
|
use room::RoomKind;
|
||||||
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
use crate::room::Room;
|
use crate::room::Room;
|
||||||
@@ -32,6 +33,7 @@ impl Global for GlobalRegistry {}
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum RoomEmitter {
|
pub enum RoomEmitter {
|
||||||
Open(WeakEntity<Room>),
|
Open(WeakEntity<Room>),
|
||||||
|
Close(u64),
|
||||||
Request(RoomKind),
|
Request(RoomKind),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +204,13 @@ impl Registry {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Close a room.
|
||||||
|
pub fn close_room(&mut self, id: u64, cx: &mut Context<Self>) {
|
||||||
|
if self.rooms.iter().any(|r| r.read(cx).id == id) {
|
||||||
|
cx.emit(RoomEmitter::Close(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sort rooms by their created at.
|
/// Sort rooms by their created at.
|
||||||
pub fn sort(&mut self, cx: &mut Context<Self>) {
|
pub fn sort(&mut self, cx: &mut Context<Self>) {
|
||||||
self.rooms.sort_by_key(|ev| Reverse(ev.read(cx).created_at));
|
self.rooms.sort_by_key(|ev| Reverse(ev.read(cx).created_at));
|
||||||
@@ -238,15 +247,12 @@ impl Registry {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load all rooms from the lmdb.
|
/// Load all rooms from the database.
|
||||||
///
|
|
||||||
/// This method:
|
|
||||||
/// 1. Fetches all private direct messages from the lmdb
|
|
||||||
/// 2. Groups them by ID
|
|
||||||
/// 3. Determines each room's type based on message frequency and trust status
|
|
||||||
/// 4. Creates Room entities for each unique room
|
|
||||||
pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
log::info!("Starting to load rooms from database...");
|
log::info!("Starting to load chat rooms...");
|
||||||
|
|
||||||
|
// Get the contact bypass setting
|
||||||
|
let contact_bypass = AppSettings::get_contact_bypass(cx);
|
||||||
|
|
||||||
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
@@ -275,14 +281,22 @@ impl Registry {
|
|||||||
.sorted_by_key(|event| Reverse(event.created_at))
|
.sorted_by_key(|event| Reverse(event.created_at))
|
||||||
.filter(|ev| ev.tags.public_keys().peekable().peek().is_some())
|
.filter(|ev| ev.tags.public_keys().peekable().peek().is_some())
|
||||||
{
|
{
|
||||||
let hash = room_hash(&event);
|
if rooms.iter().any(|room| room.id == event.uniq_id()) {
|
||||||
|
|
||||||
if rooms.iter().any(|room| room.id == hash) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut public_keys = event.tags.public_keys().copied().collect_vec();
|
// Get all public keys from the event
|
||||||
public_keys.push(event.pubkey);
|
let public_keys = event.all_pubkeys();
|
||||||
|
|
||||||
|
// Bypass screening flag
|
||||||
|
let mut bypass = false;
|
||||||
|
|
||||||
|
// If user enabled bypass screening for contacts
|
||||||
|
// Check if room's members are in contact with current user
|
||||||
|
if contact_bypass {
|
||||||
|
let contacts = client.database().contacts_public_keys(public_key).await?;
|
||||||
|
bypass = public_keys.iter().any(|k| contacts.contains(k));
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the current user has sent at least one message to this room
|
// Check if the current user has sent at least one message to this room
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
@@ -296,7 +310,7 @@ impl Registry {
|
|||||||
// Create a new room
|
// Create a new room
|
||||||
let room = Room::new(&event).rearrange_by(public_key);
|
let room = Room::new(&event).rearrange_by(public_key);
|
||||||
|
|
||||||
if is_ongoing {
|
if is_ongoing || bypass {
|
||||||
rooms.insert(room.kind(RoomKind::Ongoing));
|
rooms.insert(room.kind(RoomKind::Ongoing));
|
||||||
} else {
|
} else {
|
||||||
rooms.insert(room);
|
rooms.insert(room);
|
||||||
@@ -375,7 +389,7 @@ impl Registry {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let id = room_hash(&event);
|
let id = event.uniq_id();
|
||||||
let author = event.pubkey;
|
let author = event.pubkey;
|
||||||
|
|
||||||
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
|
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::cmp::Ordering;
|
|||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, TimeZone};
|
||||||
use common::display::DisplayProfile;
|
use common::display::DisplayProfile;
|
||||||
|
use common::event::EventUtils;
|
||||||
use global::nostr_client;
|
use global::nostr_client;
|
||||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
|
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@@ -72,16 +73,12 @@ impl EventEmitter<Incoming> for Room {}
|
|||||||
|
|
||||||
impl Room {
|
impl Room {
|
||||||
pub fn new(event: &Event) -> Self {
|
pub fn new(event: &Event) -> Self {
|
||||||
let id = common::room_hash(event);
|
let id = event.uniq_id();
|
||||||
let created_at = event.created_at;
|
let created_at = event.created_at;
|
||||||
|
let public_keys = event.all_pubkeys();
|
||||||
// Get all pubkeys from the event's tags
|
|
||||||
let mut pubkeys: Vec<PublicKey> = event.tags.public_keys().cloned().collect();
|
|
||||||
// The author is always put at the end of the vector
|
|
||||||
pubkeys.push(event.pubkey);
|
|
||||||
|
|
||||||
// Convert pubkeys into members
|
// Convert pubkeys into members
|
||||||
let members = pubkeys.into_iter().unique().sorted().collect();
|
let members = public_keys.into_iter().unique().sorted().collect();
|
||||||
|
|
||||||
// Get the subject from the event's tags
|
// Get the subject from the event's tags
|
||||||
let subject = if let Some(tag) = event.tags.find(TagKind::Subject) {
|
let subject = if let Some(tag) = event.tags.find(TagKind::Subject) {
|
||||||
@@ -363,12 +360,7 @@ impl Room {
|
|||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_by_key(|ev| ev.created_at)
|
.sorted_by_key(|ev| ev.created_at)
|
||||||
.filter(|ev| {
|
.filter(|ev| ev.compare_pubkeys(&pubkeys))
|
||||||
let mut other_pubkeys = ev.tags.public_keys().copied().collect::<Vec<_>>();
|
|
||||||
other_pubkeys.push(ev.pubkey);
|
|
||||||
// Check if the event is belong to a member of the current room
|
|
||||||
common::compare(&other_pubkeys, &pubkeys)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for event in events.into_iter() {
|
for event in events.into_iter() {
|
||||||
|
|||||||
@@ -14,3 +14,5 @@ log.workspace = true
|
|||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
paste = "1.0.15"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use global::{constants::SETTINGS_D, nostr_client};
|
use global::constants::SETTINGS_D;
|
||||||
|
use global::nostr_client;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -19,6 +20,37 @@ pub fn init(cx: &mut App) {
|
|||||||
AppSettings::set_global(state, cx);
|
AppSettings::set_global(state, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! setting_accessors {
|
||||||
|
($(pub $field:ident: $type:ty),* $(,)?) => {
|
||||||
|
impl AppSettings {
|
||||||
|
$(
|
||||||
|
paste::paste! {
|
||||||
|
pub fn [<get_ $field>](cx: &App) -> $type {
|
||||||
|
Self::read_global(cx).setting_values.$field.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn [<update_ $field>](value: $type, cx: &mut App) {
|
||||||
|
Self::global(cx).update(cx, |this, cx| {
|
||||||
|
this.setting_values.$field = value;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setting_accessors! {
|
||||||
|
pub media_server: Url,
|
||||||
|
pub proxy_user_avatars: bool,
|
||||||
|
pub hide_user_avatars: bool,
|
||||||
|
pub backup_messages: bool,
|
||||||
|
pub screening: bool,
|
||||||
|
pub contact_bypass: bool,
|
||||||
|
pub auto_login: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub media_server: Url,
|
pub media_server: Url,
|
||||||
@@ -26,9 +58,24 @@ pub struct Settings {
|
|||||||
pub hide_user_avatars: bool,
|
pub hide_user_avatars: bool,
|
||||||
pub backup_messages: bool,
|
pub backup_messages: bool,
|
||||||
pub screening: bool,
|
pub screening: bool,
|
||||||
|
pub contact_bypass: bool,
|
||||||
pub auto_login: bool,
|
pub auto_login: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Settings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
media_server: Url::parse("https://nostrmedia.com").unwrap(),
|
||||||
|
proxy_user_avatars: true,
|
||||||
|
hide_user_avatars: false,
|
||||||
|
backup_messages: true,
|
||||||
|
screening: true,
|
||||||
|
contact_bypass: true,
|
||||||
|
auto_login: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AsRef<Settings> for Settings {
|
impl AsRef<Settings> for Settings {
|
||||||
fn as_ref(&self) -> &Settings {
|
fn as_ref(&self) -> &Settings {
|
||||||
self
|
self
|
||||||
@@ -40,7 +87,7 @@ struct GlobalAppSettings(Entity<AppSettings>);
|
|||||||
impl Global for GlobalAppSettings {}
|
impl Global for GlobalAppSettings {}
|
||||||
|
|
||||||
pub struct AppSettings {
|
pub struct AppSettings {
|
||||||
pub settings: Settings,
|
setting_values: Settings,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
subscriptions: SmallVec<[Subscription; 1]>,
|
subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
}
|
}
|
||||||
@@ -52,7 +99,7 @@ impl AppSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the Settings instance
|
/// Retrieve the Settings instance
|
||||||
pub fn get_global(cx: &App) -> &Self {
|
pub fn read_global(cx: &App) -> &Self {
|
||||||
cx.global::<GlobalAppSettings>().0.read(cx)
|
cx.global::<GlobalAppSettings>().0.read(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,23 +109,15 @@ impl AppSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn new(cx: &mut Context<Self>) -> Self {
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
let settings = Settings {
|
let setting_values = Settings::default();
|
||||||
media_server: Url::parse("https://nostrmedia.com").unwrap(),
|
|
||||||
proxy_user_avatars: true,
|
|
||||||
hide_user_avatars: false,
|
|
||||||
backup_messages: true,
|
|
||||||
screening: true,
|
|
||||||
auto_login: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(cx.observe_new::<Self>(|this, _window, cx| {
|
subscriptions.push(cx.observe_new::<Self>(move |this, _window, cx| {
|
||||||
this.get_settings_from_db(cx);
|
this.get_settings_from_db(cx);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
settings,
|
setting_values,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +131,7 @@ impl AppSettings {
|
|||||||
|
|
||||||
if let Some(event) = nostr_client().database().query(filter).await?.first_owned() {
|
if let Some(event) = nostr_client().database().query(filter).await?.first_owned() {
|
||||||
log::info!("Successfully loaded settings from database");
|
log::info!("Successfully loaded settings from database");
|
||||||
Ok(serde_json::from_str(&event.content)?)
|
Ok(serde_json::from_str(&event.content).unwrap_or(Settings::default()))
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("Not found"))
|
Err(anyhow!("Not found"))
|
||||||
}
|
}
|
||||||
@@ -101,7 +140,7 @@ impl AppSettings {
|
|||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
if let Ok(settings) = task.await {
|
if let Ok(settings) = task.await {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.settings = settings;
|
this.setting_values = settings;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
@@ -111,13 +150,11 @@ impl AppSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_settings(&self, cx: &mut Context<Self>) {
|
pub(crate) fn set_settings(&self, cx: &mut Context<Self>) {
|
||||||
if let Ok(content) = serde_json::to_string(&self.settings) {
|
if let Ok(content) = serde_json::to_string(&self.setting_values) {
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let keys = Keys::generate();
|
|
||||||
|
|
||||||
if let Ok(event) = EventBuilder::new(Kind::ApplicationSpecificData, content)
|
if let Ok(event) = EventBuilder::new(Kind::ApplicationSpecificData, content)
|
||||||
.tags(vec![Tag::identifier(SETTINGS_D)])
|
.tags(vec![Tag::identifier(SETTINGS_D)])
|
||||||
.sign(&keys)
|
.sign(&Keys::generate())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if let Err(e) = nostr_client().database().save_event(&event).await {
|
if let Err(e) = nostr_client().database().save_event(&event).await {
|
||||||
|
|||||||
@@ -143,18 +143,18 @@ impl ThemeColor {
|
|||||||
element_selected: brand().light().step_11(),
|
element_selected: brand().light().step_11(),
|
||||||
element_disabled: brand().light_alpha().step_3(),
|
element_disabled: brand().light_alpha().step_3(),
|
||||||
|
|
||||||
secondary_foreground: brand().light().step_12(),
|
secondary_foreground: brand().light().step_11(),
|
||||||
secondary_background: brand().light().step_3(),
|
secondary_background: brand().light().step_3(),
|
||||||
secondary_hover: brand().light_alpha().step_4(),
|
secondary_hover: brand().light_alpha().step_4(),
|
||||||
secondary_active: brand().light().step_5(),
|
secondary_active: brand().light().step_5(),
|
||||||
secondary_selected: brand().light().step_5(),
|
secondary_selected: brand().light().step_5(),
|
||||||
secondary_disabled: brand().light_alpha().step_3(),
|
secondary_disabled: brand().light_alpha().step_3(),
|
||||||
|
|
||||||
danger_foreground: danger().light().step_1(),
|
danger_foreground: danger().light().step_12(),
|
||||||
danger_background: danger().light().step_9(),
|
danger_background: danger().light().step_3(),
|
||||||
danger_hover: danger().light_alpha().step_10(),
|
danger_hover: danger().light_alpha().step_4(),
|
||||||
danger_active: danger().light().step_10(),
|
danger_active: danger().light().step_5(),
|
||||||
danger_selected: danger().light().step_11(),
|
danger_selected: danger().light().step_5(),
|
||||||
danger_disabled: danger().light_alpha().step_3(),
|
danger_disabled: danger().light_alpha().step_3(),
|
||||||
|
|
||||||
warning_foreground: warning().light().step_12(),
|
warning_foreground: warning().light().step_12(),
|
||||||
@@ -231,11 +231,11 @@ impl ThemeColor {
|
|||||||
secondary_selected: brand().dark().step_5(),
|
secondary_selected: brand().dark().step_5(),
|
||||||
secondary_disabled: brand().dark_alpha().step_3(),
|
secondary_disabled: brand().dark_alpha().step_3(),
|
||||||
|
|
||||||
danger_foreground: danger().dark().step_1(),
|
danger_foreground: danger().dark().step_12(),
|
||||||
danger_background: danger().dark().step_9(),
|
danger_background: danger().dark().step_3(),
|
||||||
danger_hover: danger().dark_alpha().step_10(),
|
danger_hover: danger().dark_alpha().step_4(),
|
||||||
danger_active: danger().dark().step_10(),
|
danger_active: danger().dark().step_5(),
|
||||||
danger_selected: danger().dark().step_11(),
|
danger_selected: danger().dark().step_5(),
|
||||||
danger_disabled: danger().dark_alpha().step_3(),
|
danger_disabled: danger().dark_alpha().step_3(),
|
||||||
|
|
||||||
warning_foreground: warning().dark().step_12(),
|
warning_foreground: warning().dark().step_12(),
|
||||||
@@ -309,6 +309,24 @@ impl From<WindowAppearance> for ThemeMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub enum ScrollBarMode {
|
||||||
|
#[default]
|
||||||
|
Scrolling,
|
||||||
|
Hover,
|
||||||
|
Always,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrollBarMode {
|
||||||
|
pub fn is_hover(&self) -> bool {
|
||||||
|
matches!(self, Self::Hover)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_always(&self) -> bool {
|
||||||
|
matches!(self, Self::Always)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub colors: ThemeColor,
|
pub colors: ThemeColor,
|
||||||
@@ -316,6 +334,7 @@ pub struct Theme {
|
|||||||
pub font_family: SharedString,
|
pub font_family: SharedString,
|
||||||
pub font_size: Pixels,
|
pub font_size: Pixels,
|
||||||
pub radius: Pixels,
|
pub radius: Pixels,
|
||||||
|
pub scrollbar_mode: ScrollBarMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for Theme {
|
impl Deref for Theme {
|
||||||
@@ -392,6 +411,7 @@ impl From<ThemeColor> for Theme {
|
|||||||
font_size: px(15.),
|
font_size: px(15.),
|
||||||
font_family: ".SystemUIFont".into(),
|
font_family: ".SystemUIFont".into(),
|
||||||
radius: px(5.),
|
radius: px(5.),
|
||||||
|
scrollbar_mode: ScrollBarMode::default(),
|
||||||
mode,
|
mode,
|
||||||
colors,
|
colors,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,11 +41,6 @@ pub trait ButtonVariants: Sized {
|
|||||||
self.with_variant(ButtonVariant::Danger)
|
self.with_variant(ButtonVariant::Danger)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// With the danger alternate style for the Button.
|
|
||||||
fn danger_alt(self) -> Self {
|
|
||||||
self.with_variant(ButtonVariant::DangerAlt)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// With the warning style for the Button.
|
/// With the warning style for the Button.
|
||||||
fn warning(self) -> Self {
|
fn warning(self) -> Self {
|
||||||
self.with_variant(ButtonVariant::Warning)
|
self.with_variant(ButtonVariant::Warning)
|
||||||
@@ -104,7 +99,6 @@ pub enum ButtonVariant {
|
|||||||
Primary,
|
Primary,
|
||||||
Secondary,
|
Secondary,
|
||||||
Danger,
|
Danger,
|
||||||
DangerAlt,
|
|
||||||
Warning,
|
Warning,
|
||||||
Ghost,
|
Ghost,
|
||||||
Transparent,
|
Transparent,
|
||||||
@@ -286,6 +280,7 @@ impl RenderOnce for Button {
|
|||||||
let normal_style = style.normal(window, cx);
|
let normal_style = style.normal(window, cx);
|
||||||
let icon_size = match self.size {
|
let icon_size = match self.size {
|
||||||
Size::Size(v) => Size::Size(v * 0.75),
|
Size::Size(v) => Size::Size(v * 0.75),
|
||||||
|
Size::Medium => Size::Small,
|
||||||
_ => self.size,
|
_ => self.size,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -307,6 +302,7 @@ impl RenderOnce for Button {
|
|||||||
Size::Size(px) => this.size(px),
|
Size::Size(px) => this.size(px),
|
||||||
Size::XSmall => this.size_5(),
|
Size::XSmall => this.size_5(),
|
||||||
Size::Small => this.size_6(),
|
Size::Small => this.size_6(),
|
||||||
|
Size::Medium => this.size_7(),
|
||||||
_ => this.size_9(),
|
_ => this.size_9(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -321,8 +317,8 @@ impl RenderOnce for Button {
|
|||||||
this.h_7().px_3()
|
this.h_7().px_3()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Size::Medium => this.h_8().px_3(),
|
||||||
Size::Large => this.h_10().px_4(),
|
Size::Large => this.h_10().px_4(),
|
||||||
_ => this.h_9().px_2(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -342,21 +338,6 @@ impl RenderOnce for Button {
|
|||||||
this.bg(active_style.bg).text_color(active_style.fg)
|
this.bg(active_style.bg).text_color(active_style.fg)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.when_some(
|
|
||||||
self.on_click.filter(|_| !self.disabled && !self.loading),
|
|
||||||
|this, on_click| {
|
|
||||||
let stop_propagation = self.stop_propagation;
|
|
||||||
this.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
|
||||||
window.prevent_default();
|
|
||||||
if stop_propagation {
|
|
||||||
cx.stop_propagation();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_click(move |event, window, cx| {
|
|
||||||
(on_click)(event, window, cx);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.when(self.disabled, |this| {
|
.when(self.disabled, |this| {
|
||||||
let disabled_style = style.disabled(window, cx);
|
let disabled_style = style.disabled(window, cx);
|
||||||
this.cursor_not_allowed()
|
this.cursor_not_allowed()
|
||||||
@@ -403,6 +384,21 @@ impl RenderOnce for Button {
|
|||||||
.when_some(self.tooltip.clone(), |this, tooltip| {
|
.when_some(self.tooltip.clone(), |this, tooltip| {
|
||||||
this.tooltip(move |window, cx| Tooltip::new(tooltip.clone(), window, cx).into())
|
this.tooltip(move |window, cx| Tooltip::new(tooltip.clone(), window, cx).into())
|
||||||
})
|
})
|
||||||
|
.when_some(
|
||||||
|
self.on_click.filter(|_| !self.disabled && !self.loading),
|
||||||
|
|this, on_click| {
|
||||||
|
let stop_propagation = self.stop_propagation;
|
||||||
|
this.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
||||||
|
window.prevent_default();
|
||||||
|
if stop_propagation {
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_click(move |event, window, cx| {
|
||||||
|
(on_click)(event, window, cx);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,7 +431,6 @@ impl ButtonVariant {
|
|||||||
ButtonVariant::Primary => cx.theme().element_foreground,
|
ButtonVariant::Primary => cx.theme().element_foreground,
|
||||||
ButtonVariant::Secondary => cx.theme().text_muted,
|
ButtonVariant::Secondary => cx.theme().text_muted,
|
||||||
ButtonVariant::Danger => cx.theme().danger_foreground,
|
ButtonVariant::Danger => cx.theme().danger_foreground,
|
||||||
ButtonVariant::DangerAlt => cx.theme().danger_background,
|
|
||||||
ButtonVariant::Warning => cx.theme().warning_foreground,
|
ButtonVariant::Warning => cx.theme().warning_foreground,
|
||||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||||
ButtonVariant::Ghost => cx.theme().text_muted,
|
ButtonVariant::Ghost => cx.theme().text_muted,
|
||||||
@@ -448,7 +443,6 @@ impl ButtonVariant {
|
|||||||
ButtonVariant::Primary => cx.theme().element_hover,
|
ButtonVariant::Primary => cx.theme().element_hover,
|
||||||
ButtonVariant::Secondary => cx.theme().secondary_hover,
|
ButtonVariant::Secondary => cx.theme().secondary_hover,
|
||||||
ButtonVariant::Danger => cx.theme().danger_hover,
|
ButtonVariant::Danger => cx.theme().danger_hover,
|
||||||
ButtonVariant::DangerAlt => gpui::transparent_black(),
|
|
||||||
ButtonVariant::Warning => cx.theme().warning_hover,
|
ButtonVariant::Warning => cx.theme().warning_hover,
|
||||||
ButtonVariant::Ghost => cx.theme().ghost_element_hover,
|
ButtonVariant::Ghost => cx.theme().ghost_element_hover,
|
||||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||||
@@ -470,7 +464,6 @@ impl ButtonVariant {
|
|||||||
ButtonVariant::Primary => cx.theme().element_active,
|
ButtonVariant::Primary => cx.theme().element_active,
|
||||||
ButtonVariant::Secondary => cx.theme().secondary_active,
|
ButtonVariant::Secondary => cx.theme().secondary_active,
|
||||||
ButtonVariant::Danger => cx.theme().danger_active,
|
ButtonVariant::Danger => cx.theme().danger_active,
|
||||||
ButtonVariant::DangerAlt => gpui::transparent_black(),
|
|
||||||
ButtonVariant::Warning => cx.theme().warning_active,
|
ButtonVariant::Warning => cx.theme().warning_active,
|
||||||
ButtonVariant::Ghost => cx.theme().ghost_element_active,
|
ButtonVariant::Ghost => cx.theme().ghost_element_active,
|
||||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||||
@@ -491,7 +484,6 @@ impl ButtonVariant {
|
|||||||
ButtonVariant::Primary => cx.theme().element_selected,
|
ButtonVariant::Primary => cx.theme().element_selected,
|
||||||
ButtonVariant::Secondary => cx.theme().secondary_selected,
|
ButtonVariant::Secondary => cx.theme().secondary_selected,
|
||||||
ButtonVariant::Danger => cx.theme().danger_selected,
|
ButtonVariant::Danger => cx.theme().danger_selected,
|
||||||
ButtonVariant::DangerAlt => gpui::transparent_black(),
|
|
||||||
ButtonVariant::Warning => cx.theme().warning_selected,
|
ButtonVariant::Warning => cx.theme().warning_selected,
|
||||||
ButtonVariant::Ghost => cx.theme().ghost_element_selected,
|
ButtonVariant::Ghost => cx.theme().ghost_element_selected,
|
||||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use std::sync::Arc;
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, canvas, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges,
|
actions, canvas, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges,
|
||||||
Entity, EntityId, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _,
|
Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement,
|
||||||
Pixels, Render, Styled, Subscription, WeakEntity, Window,
|
ParentElement as _, Pixels, Render, Styled, Subscription, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::dock_area::dock::{Dock, DockPlacement};
|
use crate::dock_area::dock::{Dock, DockPlacement};
|
||||||
@@ -39,7 +39,7 @@ pub enum DockEvent {
|
|||||||
pub struct DockArea {
|
pub struct DockArea {
|
||||||
pub(crate) bounds: Bounds<Pixels>,
|
pub(crate) bounds: Bounds<Pixels>,
|
||||||
/// The center view of the dockarea.
|
/// The center view of the dockarea.
|
||||||
items: DockItem,
|
pub items: DockItem,
|
||||||
/// The entity_id of the [`TabPanel`](TabPanel) where each toggle button should be displayed,
|
/// The entity_id of the [`TabPanel`](TabPanel) where each toggle button should be displayed,
|
||||||
toggle_button_panels: Edges<Option<EntityId>>,
|
toggle_button_panels: Edges<Option<EntityId>>,
|
||||||
/// The left dock of the dock_area.
|
/// The left dock of the dock_area.
|
||||||
@@ -73,7 +73,7 @@ pub enum DockItem {
|
|||||||
active_ix: usize,
|
active_ix: usize,
|
||||||
view: Entity<TabPanel>,
|
view: Entity<TabPanel>,
|
||||||
},
|
},
|
||||||
/// Panel layout
|
/// Single panel layout
|
||||||
Panel { view: Arc<dyn PanelView> },
|
Panel { view: Arc<dyn PanelView> },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +286,12 @@ impl DockItem {
|
|||||||
DockItem::Panel { .. } => None,
|
DockItem::Panel { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn focus_tab_panel(&self, window: &mut Window, cx: &mut App) {
|
||||||
|
if let DockItem::Tabs { view, .. } = self {
|
||||||
|
window.focus(&view.read(cx).focus_handle(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DockArea {
|
impl DockArea {
|
||||||
@@ -572,12 +578,8 @@ impl DockArea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DockPlacement::Center => {
|
DockPlacement::Center => {
|
||||||
let focus_handle = panel.focus_handle(cx);
|
|
||||||
// Add panel
|
|
||||||
self.items
|
self.items
|
||||||
.add_panel(panel, &cx.entity().downgrade(), window, cx);
|
.add_panel(panel, &cx.entity().downgrade(), window, cx);
|
||||||
// Focus to the newly added panel
|
|
||||||
window.focus(&focus_handle);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -707,6 +709,10 @@ impl DockArea {
|
|||||||
.and_then(|dock| dock.read(cx).panel.left_top_tab_panel(cx))
|
.and_then(|dock| dock.read(cx).panel.left_top_tab_panel(cx))
|
||||||
.map(|view| view.entity_id());
|
.map(|view| view.entity_id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn focus_tab_panel(&mut self, window: &mut Window, cx: &mut App) {
|
||||||
|
self.items.focus_tab_panel(window, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DockEvent> for DockArea {}
|
impl EventEmitter<DockEvent> for DockArea {}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use super::panel::PanelView;
|
|||||||
use super::stack_panel::StackPanel;
|
use super::stack_panel::StackPanel;
|
||||||
use super::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
use super::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
use crate::dock_area::dock::DockPlacement;
|
|
||||||
use crate::dock_area::panel::Panel;
|
use crate::dock_area::panel::Panel;
|
||||||
use crate::popup_menu::{PopupMenu, PopupMenuExt};
|
use crate::popup_menu::{PopupMenu, PopupMenuExt};
|
||||||
use crate::tab::tab_bar::TabBar;
|
use crate::tab::tab_bar::TabBar;
|
||||||
@@ -454,89 +453,6 @@ impl TabPanel {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _render_dock_toggle_button(
|
|
||||||
&self,
|
|
||||||
placement: DockPlacement,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Option<impl IntoElement> {
|
|
||||||
if self.is_zoomed {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dock_area = self.dock_area.upgrade()?.read(cx);
|
|
||||||
|
|
||||||
if !dock_area.is_dock_collapsible(placement, cx) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let view_entity_id = cx.entity().entity_id();
|
|
||||||
let toggle_button_panels = dock_area.toggle_button_panels;
|
|
||||||
|
|
||||||
// Check if current TabPanel's entity_id matches the one stored in DockArea for this placement
|
|
||||||
if !match placement {
|
|
||||||
DockPlacement::Left => {
|
|
||||||
dock_area.left_dock.is_some() && toggle_button_panels.left == Some(view_entity_id)
|
|
||||||
}
|
|
||||||
DockPlacement::Right => {
|
|
||||||
dock_area.right_dock.is_some() && toggle_button_panels.right == Some(view_entity_id)
|
|
||||||
}
|
|
||||||
DockPlacement::Bottom => {
|
|
||||||
dock_area.bottom_dock.is_some()
|
|
||||||
&& toggle_button_panels.bottom == Some(view_entity_id)
|
|
||||||
}
|
|
||||||
DockPlacement::Center => unreachable!(),
|
|
||||||
} {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_open = dock_area.is_dock_open(placement, cx);
|
|
||||||
|
|
||||||
let icon = match placement {
|
|
||||||
DockPlacement::Left => {
|
|
||||||
if is_open {
|
|
||||||
IconName::PanelLeft
|
|
||||||
} else {
|
|
||||||
IconName::PanelLeftOpen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DockPlacement::Right => {
|
|
||||||
if is_open {
|
|
||||||
IconName::PanelRight
|
|
||||||
} else {
|
|
||||||
IconName::PanelRightOpen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DockPlacement::Bottom => {
|
|
||||||
if is_open {
|
|
||||||
IconName::PanelBottom
|
|
||||||
} else {
|
|
||||||
IconName::PanelBottomOpen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DockPlacement::Center => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(
|
|
||||||
Button::new(SharedString::from(format!("toggle-dock:{placement:?}")))
|
|
||||||
.icon(icon)
|
|
||||||
.small()
|
|
||||||
.ghost()
|
|
||||||
.tooltip(match is_open {
|
|
||||||
true => "Collapse",
|
|
||||||
false => "Expand",
|
|
||||||
})
|
|
||||||
.on_click(cx.listener({
|
|
||||||
let dock_area = self.dock_area.clone();
|
|
||||||
move |_, _, window, cx| {
|
|
||||||
_ = dock_area.update(cx, |dock_area, cx| {
|
|
||||||
dock_area.toggle_dock(placement, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_title_bar(
|
fn render_title_bar(
|
||||||
&self,
|
&self,
|
||||||
state: &TabState,
|
state: &TabState,
|
||||||
@@ -1038,6 +954,7 @@ impl Render for TabPanel {
|
|||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
|
||||||
let focus_handle = self.focus_handle(cx);
|
let focus_handle = self.focus_handle(cx);
|
||||||
let active_panel = self.active_panel(cx);
|
let active_panel = self.active_panel(cx);
|
||||||
|
|
||||||
let mut state = TabState {
|
let mut state = TabState {
|
||||||
closable: self.closable(cx),
|
closable: self.closable(cx),
|
||||||
draggable: self.draggable(cx),
|
draggable: self.draggable(cx),
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ pub enum IconName {
|
|||||||
Relays,
|
Relays,
|
||||||
ResizeCorner,
|
ResizeCorner,
|
||||||
Reply,
|
Reply,
|
||||||
|
Report,
|
||||||
Forward,
|
Forward,
|
||||||
Search,
|
Search,
|
||||||
SearchFill,
|
SearchFill,
|
||||||
@@ -128,6 +129,7 @@ impl IconName {
|
|||||||
Self::Relays => "icons/relays.svg",
|
Self::Relays => "icons/relays.svg",
|
||||||
Self::ResizeCorner => "icons/resize-corner.svg",
|
Self::ResizeCorner => "icons/resize-corner.svg",
|
||||||
Self::Reply => "icons/reply.svg",
|
Self::Reply => "icons/reply.svg",
|
||||||
|
Self::Report => "icons/report.svg",
|
||||||
Self::Forward => "icons/forward.svg",
|
Self::Forward => "icons/forward.svg",
|
||||||
Self::Search => "icons/search.svg",
|
Self::Search => "icons/search.svg",
|
||||||
Self::SearchFill => "icons/search-fill.svg",
|
Self::SearchFill => "icons/search-fill.svg",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::cell::Cell;
|
|
||||||
use std::ops::{Deref, Range};
|
use std::ops::{Deref, Range};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -358,7 +357,7 @@ pub struct InputState {
|
|||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub(super) validate: Option<Box<dyn Fn(&str) -> bool + 'static>>,
|
pub(super) validate: Option<Box<dyn Fn(&str) -> bool + 'static>>,
|
||||||
pub(crate) scroll_handle: ScrollHandle,
|
pub(crate) scroll_handle: ScrollHandle,
|
||||||
pub(super) scrollbar_state: Rc<Cell<ScrollbarState>>,
|
pub(super) scrollbar_state: ScrollbarState,
|
||||||
/// The size of the scrollable content.
|
/// The size of the scrollable content.
|
||||||
pub(crate) scroll_size: gpui::Size<Pixels>,
|
pub(crate) scroll_size: gpui::Size<Pixels>,
|
||||||
pub(crate) line_number_width: Pixels,
|
pub(crate) line_number_width: Pixels,
|
||||||
@@ -434,7 +433,7 @@ impl InputState {
|
|||||||
last_selected_range: None,
|
last_selected_range: None,
|
||||||
last_cursor_offset: None,
|
last_cursor_offset: None,
|
||||||
scroll_handle: ScrollHandle::new(),
|
scroll_handle: ScrollHandle::new(),
|
||||||
scrollbar_state: Rc::new(Cell::new(ScrollbarState::default())),
|
scrollbar_state: ScrollbarState::default(),
|
||||||
scroll_size: gpui::size(px(0.), px(0.)),
|
scroll_size: gpui::size(px(0.), px(0.)),
|
||||||
line_number_width: px(0.),
|
line_number_width: px(0.),
|
||||||
preferred_x_offset: None,
|
preferred_x_offset: None,
|
||||||
|
|||||||
@@ -276,8 +276,6 @@ impl RenderOnce for TextInput {
|
|||||||
.children(suffix),
|
.children(suffix),
|
||||||
)
|
)
|
||||||
.when(state.is_multi_line(), |this| {
|
.when(state.is_multi_line(), |this| {
|
||||||
let entity_id = self.state.entity_id();
|
|
||||||
|
|
||||||
if state.last_layout.is_some() {
|
if state.last_layout.is_some() {
|
||||||
this.relative().child(
|
this.relative().child(
|
||||||
div()
|
div()
|
||||||
@@ -287,12 +285,7 @@ impl RenderOnce for TextInput {
|
|||||||
.right(px(1.))
|
.right(px(1.))
|
||||||
.bottom_0()
|
.bottom_0()
|
||||||
.child(
|
.child(
|
||||||
Scrollbar::vertical(
|
Scrollbar::vertical(&state.scrollbar_state, &state.scroll_handle)
|
||||||
entity_id,
|
|
||||||
state.scrollbar_state.clone(),
|
|
||||||
state.scroll_handle.clone(),
|
|
||||||
state.scroll_size,
|
|
||||||
)
|
|
||||||
.axis(ScrollbarAxis::Vertical),
|
.axis(ScrollbarAxis::Vertical),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use std::cell::Cell;
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::rc::Rc;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
@@ -154,7 +152,7 @@ pub struct List<D: ListDelegate> {
|
|||||||
querying: bool,
|
querying: bool,
|
||||||
scrollbar_visible: bool,
|
scrollbar_visible: bool,
|
||||||
vertical_scroll_handle: UniformListScrollHandle,
|
vertical_scroll_handle: UniformListScrollHandle,
|
||||||
scrollbar_state: Rc<Cell<ScrollbarState>>,
|
scrollbar_state: ScrollbarState,
|
||||||
pub(crate) size: Size,
|
pub(crate) size: Size,
|
||||||
selected_index: Option<usize>,
|
selected_index: Option<usize>,
|
||||||
right_clicked_index: Option<usize>,
|
right_clicked_index: Option<usize>,
|
||||||
@@ -181,7 +179,7 @@ where
|
|||||||
selected_index: None,
|
selected_index: None,
|
||||||
right_clicked_index: None,
|
right_clicked_index: None,
|
||||||
vertical_scroll_handle: UniformListScrollHandle::new(),
|
vertical_scroll_handle: UniformListScrollHandle::new(),
|
||||||
scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())),
|
scrollbar_state: ScrollbarState::default(),
|
||||||
max_height: None,
|
max_height: None,
|
||||||
scrollbar_visible: true,
|
scrollbar_visible: true,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
@@ -265,15 +263,18 @@ where
|
|||||||
self.selected_index
|
self.selected_index
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_scrollbar(&self, _: &mut Window, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
fn render_scrollbar(
|
||||||
|
&self,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Self>,
|
||||||
|
) -> Option<impl IntoElement> {
|
||||||
if !self.scrollbar_visible {
|
if !self.scrollbar_visible {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(Scrollbar::uniform_scroll(
|
Some(Scrollbar::uniform_scroll(
|
||||||
cx.entity().entity_id(),
|
&self.scrollbar_state,
|
||||||
self.scrollbar_state.clone(),
|
&self.vertical_scroll_handle,
|
||||||
self.vertical_scroll_handle.clone(),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, div, hsla, point, px, relative, Animation, AnimationExt as _, AnyElement, App,
|
anchored, div, hsla, point, px, Animation, AnimationExt as _, AnyElement, App, Axis, Bounds,
|
||||||
Bounds, BoxShadow, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
BoxShadow, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
||||||
MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, Styled, Window,
|
MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, StyleRefinement, Styled,
|
||||||
|
Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ impl ModalButtonProps {
|
|||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Modal {
|
pub struct Modal {
|
||||||
base: Div,
|
style: StyleRefinement,
|
||||||
title: Option<AnyElement>,
|
title: Option<AnyElement>,
|
||||||
footer: Option<FooterFn>,
|
footer: Option<FooterFn>,
|
||||||
content: Div,
|
content: Div,
|
||||||
@@ -88,11 +89,12 @@ pub struct Modal {
|
|||||||
on_close: OnClose,
|
on_close: OnClose,
|
||||||
on_ok: OnOk,
|
on_ok: OnOk,
|
||||||
on_cancel: OnCancel,
|
on_cancel: OnCancel,
|
||||||
button_props: ModalButtonProps,
|
|
||||||
show_close: bool,
|
|
||||||
overlay: bool,
|
overlay: bool,
|
||||||
overlay_closable: bool,
|
overlay_closable: bool,
|
||||||
keyboard: bool,
|
keyboard: bool,
|
||||||
|
show_close: bool,
|
||||||
|
button_props: ModalButtonProps,
|
||||||
|
|
||||||
/// This will be change when open the modal, the focus handle is create when open the modal.
|
/// This will be change when open the modal, the focus handle is create when open the modal.
|
||||||
pub(crate) focus_handle: FocusHandle,
|
pub(crate) focus_handle: FocusHandle,
|
||||||
@@ -102,18 +104,8 @@ pub struct Modal {
|
|||||||
|
|
||||||
impl Modal {
|
impl Modal {
|
||||||
pub fn new(_window: &mut Window, cx: &mut App) -> Self {
|
pub fn new(_window: &mut Window, cx: &mut App) -> Self {
|
||||||
let radius = (cx.theme().radius * 2.).min(px(20.));
|
|
||||||
|
|
||||||
let base = v_flex()
|
|
||||||
.bg(cx.theme().background)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.rounded(radius)
|
|
||||||
.shadow_xl()
|
|
||||||
.min_h_24();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
base,
|
style: StyleRefinement::default(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
title: None,
|
title: None,
|
||||||
footer: None,
|
footer: None,
|
||||||
@@ -276,7 +268,7 @@ impl ParentElement for Modal {
|
|||||||
|
|
||||||
impl Styled for Modal {
|
impl Styled for Modal {
|
||||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||||
self.base.style()
|
&mut self.style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,6 +342,7 @@ impl RenderOnce for Modal {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let window_paddings = crate::window_border::window_paddings(window, cx);
|
let window_paddings = crate::window_border::window_paddings(window, cx);
|
||||||
|
let radius = (cx.theme().radius * 2.).min(px(20.));
|
||||||
|
|
||||||
let view_size = window.viewport_size()
|
let view_size = window.viewport_size()
|
||||||
- gpui::size(
|
- gpui::size(
|
||||||
@@ -366,6 +359,17 @@ impl RenderOnce for Modal {
|
|||||||
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
||||||
let x = bounds.center().x - self.width / 2.;
|
let x = bounds.center().x - self.width / 2.;
|
||||||
|
|
||||||
|
let mut padding_right = px(16.);
|
||||||
|
let mut padding_left = px(16.);
|
||||||
|
|
||||||
|
if let Some(pl) = self.style.padding.left {
|
||||||
|
padding_left = pl.to_pixels(self.width.into(), window.rem_size());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pr) = self.style.padding.right {
|
||||||
|
padding_right = pr.to_pixels(self.width.into(), window.rem_size());
|
||||||
|
}
|
||||||
|
|
||||||
let animation = Animation::new(Duration::from_secs_f64(0.25))
|
let animation = Animation::new(Duration::from_secs_f64(0.25))
|
||||||
.with_easing(cubic_bezier(0.32, 0.72, 0., 1.));
|
.with_easing(cubic_bezier(0.32, 0.72, 0., 1.));
|
||||||
|
|
||||||
@@ -374,6 +378,7 @@ impl RenderOnce for Modal {
|
|||||||
.snap_to_window()
|
.snap_to_window()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.id("modal")
|
||||||
.w(view_size.width)
|
.w(view_size.width)
|
||||||
.h(view_size.height)
|
.h(view_size.height)
|
||||||
.when(self.overlay_visible, |this| {
|
.when(self.overlay_visible, |this| {
|
||||||
@@ -396,10 +401,17 @@ impl RenderOnce for Modal {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
self.base
|
v_flex()
|
||||||
.id(SharedString::from(format!("modal-{layer_ix}")))
|
.id(layer_ix)
|
||||||
|
.bg(cx.theme().background)
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().border.alpha(0.4))
|
||||||
|
.rounded(radius)
|
||||||
|
.shadow_xl()
|
||||||
|
.min_h_24()
|
||||||
.key_context(CONTEXT)
|
.key_context(CONTEXT)
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
|
.refine_style(&self.style)
|
||||||
.when(self.keyboard, |this| {
|
.when(self.keyboard, |this| {
|
||||||
this.on_action({
|
this.on_action({
|
||||||
let on_cancel = on_cancel.clone();
|
let on_cancel = on_cancel.clone();
|
||||||
@@ -430,6 +442,7 @@ impl RenderOnce for Modal {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
// There style is high priority, can't be overridden.
|
||||||
.absolute()
|
.absolute()
|
||||||
.occlude()
|
.occlude()
|
||||||
.relative()
|
.relative()
|
||||||
@@ -437,26 +450,15 @@ impl RenderOnce for Modal {
|
|||||||
.top(y)
|
.top(y)
|
||||||
.w(self.width)
|
.w(self.width)
|
||||||
.when_some(self.max_width, |this, w| this.max_w(w))
|
.when_some(self.max_width, |this, w| this.max_w(w))
|
||||||
.when_some(self.title, |this, title| {
|
.child(h_flex().h_4().px_3().justify_center().when_some(
|
||||||
this.child(
|
self.title,
|
||||||
div()
|
|this, title| {
|
||||||
.h_12()
|
this.h_12().font_semibold().text_center().child(title)
|
||||||
.px_3()
|
},
|
||||||
.mb_2()
|
))
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.font_semibold()
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.line_height(relative(1.))
|
|
||||||
.child(title),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(self.show_close, |this| {
|
.when(self.show_close, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new(SharedString::from(format!(
|
Button::new("close")
|
||||||
"modal-close-{layer_ix}"
|
|
||||||
)))
|
|
||||||
.icon(IconName::CloseCircleFill)
|
.icon(IconName::CloseCircleFill)
|
||||||
.absolute()
|
.absolute()
|
||||||
.top_1p5()
|
.top_1p5()
|
||||||
@@ -468,26 +470,39 @@ impl RenderOnce for Modal {
|
|||||||
.hover(cx.theme().ghost_element_background)
|
.hover(cx.theme().ghost_element_background)
|
||||||
.active(cx.theme().ghost_element_background),
|
.active(cx.theme().ghost_element_background),
|
||||||
)
|
)
|
||||||
.on_click(
|
.on_click(move |_, window, cx| {
|
||||||
move |_, window, cx| {
|
|
||||||
on_cancel(&ClickEvent::default(), window, cx);
|
on_cancel(&ClickEvent::default(), window, cx);
|
||||||
on_close(&ClickEvent::default(), window, cx);
|
on_close(&ClickEvent::default(), window, cx);
|
||||||
window.close_modal(cx);
|
window.close_modal(cx);
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(div().relative().w_full().flex_1().child(self.content))
|
.child(
|
||||||
.when(self.footer.is_some(), |this| {
|
div()
|
||||||
let footer = self.footer.unwrap();
|
.pt_px()
|
||||||
|
.w_full()
|
||||||
|
.h_auto()
|
||||||
|
.flex_1()
|
||||||
|
.relative()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.pr(padding_right)
|
||||||
|
.pl(padding_left)
|
||||||
|
.scrollable(Axis::Vertical)
|
||||||
|
.child(self.content),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(self.footer, |this, footer| {
|
||||||
this.child(
|
this.child(
|
||||||
h_flex().p_4().gap_1p5().justify_center().children(footer(
|
h_flex()
|
||||||
render_ok,
|
.gap_2()
|
||||||
render_cancel,
|
.pt(padding_left)
|
||||||
window,
|
.pr(padding_right)
|
||||||
cx,
|
.pb(padding_left)
|
||||||
)),
|
.pl(padding_right)
|
||||||
|
.justify_end()
|
||||||
|
.children(footer(render_ok, render_cancel, window, cx)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.with_animation("slide-down", animation.clone(), move |this, delta| {
|
.with_animation("slide-down", animation.clone(), move |this, delta| {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::cell::Cell;
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
@@ -125,7 +124,7 @@ pub struct PopupMenu {
|
|||||||
|
|
||||||
scrollable: bool,
|
scrollable: bool,
|
||||||
scroll_handle: ScrollHandle,
|
scroll_handle: ScrollHandle,
|
||||||
scroll_state: Rc<Cell<ScrollbarState>>,
|
scroll_state: ScrollbarState,
|
||||||
|
|
||||||
action_focus_handle: Option<FocusHandle>,
|
action_focus_handle: Option<FocusHandle>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -159,7 +158,7 @@ impl PopupMenu {
|
|||||||
bounds: Bounds::default(),
|
bounds: Bounds::default(),
|
||||||
scrollable: false,
|
scrollable: false,
|
||||||
scroll_handle: ScrollHandle::default(),
|
scroll_handle: ScrollHandle::default(),
|
||||||
scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
|
scroll_state: ScrollbarState::default(),
|
||||||
subscriptions,
|
subscriptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -714,12 +713,7 @@ impl Render for PopupMenu {
|
|||||||
.left_0()
|
.left_0()
|
||||||
.right_0p5()
|
.right_0p5()
|
||||||
.bottom_0()
|
.bottom_0()
|
||||||
.child(Scrollbar::vertical(
|
.child(Scrollbar::vertical(&self.scroll_state, &self.scroll_handle)),
|
||||||
cx.entity_id(),
|
|
||||||
self.scroll_state.clone(),
|
|
||||||
self.scroll_handle.clone(),
|
|
||||||
self.bounds.size,
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
use std::cell::Cell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
canvas, div, relative, AnyElement, App, Div, Element, ElementId, EntityId, GlobalElementId,
|
div, relative, AnyElement, App, Bounds, Div, Element, ElementId, GlobalElementId,
|
||||||
InteractiveElement, IntoElement, ParentElement, Pixels, Position, ScrollHandle, SharedString,
|
InspectorElementId, InteractiveElement, Interactivity, IntoElement, LayoutId, ParentElement,
|
||||||
Size, Stateful, StatefulInteractiveElement, Style, StyleRefinement, Styled, Window,
|
Pixels, Position, ScrollHandle, SharedString, Size, Stateful, StatefulInteractiveElement,
|
||||||
|
Style, StyleRefinement, Styled, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Scrollbar, ScrollbarAxis, ScrollbarState};
|
use super::{Scrollbar, ScrollbarAxis, ScrollbarState};
|
||||||
@@ -13,7 +11,6 @@ use super::{Scrollbar, ScrollbarAxis, ScrollbarState};
|
|||||||
pub struct Scrollable<E> {
|
pub struct Scrollable<E> {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
element: Option<E>,
|
element: Option<E>,
|
||||||
view_id: EntityId,
|
|
||||||
axis: ScrollbarAxis,
|
axis: ScrollbarAxis,
|
||||||
/// This is a fake element to handle Styled, InteractiveElement, not used.
|
/// This is a fake element to handle Styled, InteractiveElement, not used.
|
||||||
_element: Stateful<Div>,
|
_element: Stateful<Div>,
|
||||||
@@ -23,19 +20,16 @@ impl<E> Scrollable<E>
|
|||||||
where
|
where
|
||||||
E: Element,
|
E: Element,
|
||||||
{
|
{
|
||||||
pub(crate) fn new(view_id: EntityId, element: E, axis: ScrollbarAxis) -> Self {
|
pub(crate) fn new(axis: impl Into<ScrollbarAxis>, element: E) -> Self {
|
||||||
let id = ElementId::Name(SharedString::from(format!(
|
let id = ElementId::Name(SharedString::from(
|
||||||
"ScrollView:{}-{:?}",
|
format!("scrollable-{:?}", element.id(),),
|
||||||
view_id,
|
));
|
||||||
element.id(),
|
|
||||||
)));
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
element: Some(element),
|
element: Some(element),
|
||||||
_element: div().id("fake"),
|
_element: div().id("fake"),
|
||||||
id,
|
id,
|
||||||
view_id,
|
axis: axis.into(),
|
||||||
axis,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +47,8 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the axis of the scroll view.
|
/// Set the axis of the scroll view.
|
||||||
pub fn set_axis(&mut self, axis: ScrollbarAxis) {
|
pub fn set_axis(&mut self, axis: impl Into<ScrollbarAxis>) {
|
||||||
self.axis = axis;
|
self.axis = axis.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_element_state<R>(
|
fn with_element_state<R>(
|
||||||
@@ -76,8 +70,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct ScrollViewState {
|
pub struct ScrollViewState {
|
||||||
scroll_size: Rc<Cell<Size<Pixels>>>,
|
state: ScrollbarState,
|
||||||
state: Rc<Cell<ScrollbarState>>,
|
|
||||||
handle: ScrollHandle,
|
handle: ScrollHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +78,7 @@ impl Default for ScrollViewState {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
handle: ScrollHandle::new(),
|
handle: ScrollHandle::new(),
|
||||||
scroll_size: Rc::new(Cell::new(Size::default())),
|
state: ScrollbarState::default(),
|
||||||
state: Rc::new(Cell::new(ScrollbarState::default())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,7 +111,7 @@ impl<E> InteractiveElement for Scrollable<E>
|
|||||||
where
|
where
|
||||||
E: Element + InteractiveElement,
|
E: Element + InteractiveElement,
|
||||||
{
|
{
|
||||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
fn interactivity(&mut self) -> &mut Interactivity {
|
||||||
if let Some(element) = &mut self.element {
|
if let Some(element) = &mut self.element {
|
||||||
element.interactivity()
|
element.interactivity()
|
||||||
} else {
|
} else {
|
||||||
@@ -127,7 +119,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E> StatefulInteractiveElement for Scrollable<E> where E: Element + StatefulInteractiveElement {}
|
impl<E> StatefulInteractiveElement for Scrollable<E> where E: Element + StatefulInteractiveElement {}
|
||||||
|
|
||||||
impl<E> IntoElement for Scrollable<E>
|
impl<E> IntoElement for Scrollable<E>
|
||||||
@@ -148,7 +139,7 @@ where
|
|||||||
type PrepaintState = ScrollViewState;
|
type PrepaintState = ScrollViewState;
|
||||||
type RequestLayoutState = AnyElement;
|
type RequestLayoutState = AnyElement;
|
||||||
|
|
||||||
fn id(&self) -> Option<gpui::ElementId> {
|
fn id(&self) -> Option<ElementId> {
|
||||||
Some(self.id.clone())
|
Some(self.id.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,11 +149,11 @@ where
|
|||||||
|
|
||||||
fn request_layout(
|
fn request_layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: Option<&gpui::GlobalElementId>,
|
id: Option<&GlobalElementId>,
|
||||||
_: Option<&gpui::InspectorElementId>,
|
_: Option<&InspectorElementId>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
let style = Style {
|
let style = Style {
|
||||||
position: Position::Relative,
|
position: Position::Relative,
|
||||||
flex_grow: 1.0,
|
flex_grow: 1.0,
|
||||||
@@ -175,16 +166,10 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let axis = self.axis;
|
let axis = self.axis;
|
||||||
let view_id = self.view_id;
|
|
||||||
|
|
||||||
let scroll_id = self.id.clone();
|
let scroll_id = self.id.clone();
|
||||||
let content = self.element.take().map(|c| c.into_any_element());
|
let content = self.element.take().map(|c| c.into_any_element());
|
||||||
|
|
||||||
self.with_element_state(id.unwrap(), window, cx, |_, element_state, window, cx| {
|
self.with_element_state(id.unwrap(), window, cx, |_, element_state, window, cx| {
|
||||||
let handle = element_state.handle.clone();
|
|
||||||
let state = element_state.state.clone();
|
|
||||||
let scroll_size = element_state.scroll_size.clone();
|
|
||||||
|
|
||||||
let mut element = div()
|
let mut element = div()
|
||||||
.relative()
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -192,16 +177,11 @@ where
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.id(scroll_id)
|
.id(scroll_id)
|
||||||
.track_scroll(&handle)
|
.track_scroll(&element_state.handle)
|
||||||
.overflow_scroll()
|
.overflow_scroll()
|
||||||
.relative()
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(div().children(content).child({
|
.child(div().children(content)),
|
||||||
let scroll_size = element_state.scroll_size.clone();
|
|
||||||
canvas(move |b, _, _| scroll_size.set(b.size), |_, _, _, _| {})
|
|
||||||
.absolute()
|
|
||||||
.size_full()
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -211,8 +191,7 @@ where
|
|||||||
.right_0()
|
.right_0()
|
||||||
.bottom_0()
|
.bottom_0()
|
||||||
.child(
|
.child(
|
||||||
Scrollbar::both(view_id, state, handle.clone(), scroll_size.get())
|
Scrollbar::both(&element_state.state, &element_state.handle).axis(axis),
|
||||||
.axis(axis),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.into_any_element();
|
.into_any_element();
|
||||||
@@ -226,9 +205,9 @@ where
|
|||||||
|
|
||||||
fn prepaint(
|
fn prepaint(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Option<&gpui::GlobalElementId>,
|
_: Option<&GlobalElementId>,
|
||||||
_: Option<&gpui::InspectorElementId>,
|
_: Option<&InspectorElementId>,
|
||||||
_: gpui::Bounds<Pixels>,
|
_: Bounds<Pixels>,
|
||||||
element: &mut Self::RequestLayoutState,
|
element: &mut Self::RequestLayoutState,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
@@ -240,9 +219,9 @@ where
|
|||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Option<&gpui::GlobalElementId>,
|
_: Option<&GlobalElementId>,
|
||||||
_: Option<&gpui::InspectorElementId>,
|
_: Option<&InspectorElementId>,
|
||||||
_: gpui::Bounds<Pixels>,
|
_: Bounds<Pixels>,
|
||||||
element: &mut Self::RequestLayoutState,
|
element: &mut Self::RequestLayoutState,
|
||||||
_: &mut Self::PrepaintState,
|
_: &mut Self::PrepaintState,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId,
|
px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId,
|
||||||
EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, IntoElement, IsZero as _, LayoutId,
|
EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels,
|
||||||
PaintQuad, Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window,
|
Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::AxisExt;
|
use crate::AxisExt;
|
||||||
@@ -96,7 +96,7 @@ impl Element for ScrollableMask {
|
|||||||
size: bounds.size,
|
size: bounds.size,
|
||||||
};
|
};
|
||||||
|
|
||||||
window.insert_hitbox(cover_bounds, HitboxBehavior::Normal)
|
window.insert_hitbox(cover_bounds, gpui::HitboxBehavior::Normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
@@ -118,9 +118,9 @@ impl Element for ScrollableMask {
|
|||||||
bounds,
|
bounds,
|
||||||
border_widths: Edges::all(px(1.0)),
|
border_widths: Edges::all(px(1.0)),
|
||||||
border_color: color,
|
border_color: color,
|
||||||
border_style: BorderStyle::Solid,
|
|
||||||
background: gpui::transparent_white().into(),
|
background: gpui::transparent_white().into(),
|
||||||
corner_radii: Corners::all(px(0.)),
|
corner_radii: Corners::all(px(0.)),
|
||||||
|
border_style: BorderStyle::default(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
use std::ops::Deref;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
fill, point, px, relative, App, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element,
|
fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
|
||||||
EntityId, Hitbox, HitboxBehavior, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent,
|
CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId,
|
||||||
MouseUpEvent, PaintQuad, Pixels, Point, Position, ScrollHandle, ScrollWheelEvent,
|
IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||||
UniformListScrollHandle, Window,
|
Position, ScrollHandle, ScrollWheelEvent, Size, UniformListScrollHandle, Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::{ActiveTheme, ScrollBarMode};
|
||||||
|
|
||||||
const WIDTH: Pixels = px(12.);
|
use crate::AxisExt;
|
||||||
const BORDER_WIDTH: Pixels = px(0.);
|
|
||||||
const MIN_THUMB_SIZE: f32 = 80.;
|
const WIDTH: Pixels = px(2. * 2. + 8.);
|
||||||
const THUMB_RADIUS: Pixels = Pixels(4.0);
|
const MIN_THUMB_SIZE: f32 = 48.;
|
||||||
const THUMB_INSET: Pixels = Pixels(3.);
|
|
||||||
const FADE_OUT_DURATION: f32 = 2.0;
|
const THUMB_WIDTH: Pixels = px(6.);
|
||||||
const FADE_OUT_DELAY: f32 = 1.2;
|
const THUMB_RADIUS: Pixels = Pixels(6. / 2.);
|
||||||
|
const THUMB_INSET: Pixels = Pixels(2.);
|
||||||
|
|
||||||
|
const THUMB_ACTIVE_WIDTH: Pixels = px(8.);
|
||||||
|
const THUMB_ACTIVE_RADIUS: Pixels = Pixels(8. / 2.);
|
||||||
|
const THUMB_ACTIVE_INSET: Pixels = Pixels(2.);
|
||||||
|
|
||||||
|
const FADE_OUT_DURATION: f32 = 3.0;
|
||||||
|
const FADE_OUT_DELAY: f32 = 2.0;
|
||||||
|
|
||||||
pub trait ScrollHandleOffsetable {
|
pub trait ScrollHandleOffsetable {
|
||||||
fn offset(&self) -> Point<Pixels>;
|
fn offset(&self) -> Point<Pixels>;
|
||||||
@@ -24,6 +33,8 @@ pub trait ScrollHandleOffsetable {
|
|||||||
fn is_uniform_list(&self) -> bool {
|
fn is_uniform_list(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
/// The full size of the content, including padding.
|
||||||
|
fn content_size(&self) -> Size<Pixels>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollHandleOffsetable for ScrollHandle {
|
impl ScrollHandleOffsetable for ScrollHandle {
|
||||||
@@ -34,6 +45,10 @@ impl ScrollHandleOffsetable for ScrollHandle {
|
|||||||
fn set_offset(&self, offset: Point<Pixels>) {
|
fn set_offset(&self, offset: Point<Pixels>) {
|
||||||
self.set_offset(offset);
|
self.set_offset(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn content_size(&self) -> Size<Pixels> {
|
||||||
|
self.max_offset() + self.bounds().size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollHandleOffsetable for UniformListScrollHandle {
|
impl ScrollHandleOffsetable for UniformListScrollHandle {
|
||||||
@@ -48,13 +63,21 @@ impl ScrollHandleOffsetable for UniformListScrollHandle {
|
|||||||
fn is_uniform_list(&self) -> bool {
|
fn is_uniform_list(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn content_size(&self) -> Size<Pixels> {
|
||||||
|
let base_handle = &self.0.borrow().base_handle;
|
||||||
|
base_handle.max_offset() + base_handle.bounds().size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct ScrollbarState {
|
pub struct ScrollbarStateInner {
|
||||||
hovered_axis: Option<ScrollbarAxis>,
|
hovered_axis: Option<Axis>,
|
||||||
hovered_on_thumb: Option<ScrollbarAxis>,
|
hovered_on_thumb: Option<Axis>,
|
||||||
dragged_axis: Option<ScrollbarAxis>,
|
dragged_axis: Option<Axis>,
|
||||||
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>,
|
||||||
@@ -64,7 +87,7 @@ pub struct ScrollbarState {
|
|||||||
|
|
||||||
impl Default for ScrollbarState {
|
impl Default for ScrollbarState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self(Rc::new(Cell::new(ScrollbarStateInner {
|
||||||
hovered_axis: None,
|
hovered_axis: None,
|
||||||
hovered_on_thumb: None,
|
hovered_on_thumb: None,
|
||||||
dragged_axis: None,
|
dragged_axis: None,
|
||||||
@@ -72,16 +95,20 @@ impl Default for ScrollbarState {
|
|||||||
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(),
|
last_update: Instant::now(),
|
||||||
}
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollbarState {
|
impl Deref for ScrollbarState {
|
||||||
pub fn new() -> Self {
|
type Target = Rc<Cell<ScrollbarStateInner>>;
|
||||||
Self::default()
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_drag_pos(&self, axis: ScrollbarAxis, pos: Point<Pixels>) -> Self {
|
impl ScrollbarStateInner {
|
||||||
|
fn with_drag_pos(&self, axis: Axis, pos: Point<Pixels>) -> Self {
|
||||||
let mut state = *self;
|
let mut state = *self;
|
||||||
if axis.is_vertical() {
|
if axis.is_vertical() {
|
||||||
state.drag_pos.y = pos.y;
|
state.drag_pos.y = pos.y;
|
||||||
@@ -99,7 +126,7 @@ impl ScrollbarState {
|
|||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_hovered(&self, axis: Option<ScrollbarAxis>) -> Self {
|
fn with_hovered(&self, axis: Option<Axis>) -> Self {
|
||||||
let mut state = *self;
|
let mut state = *self;
|
||||||
state.hovered_axis = axis;
|
state.hovered_axis = axis;
|
||||||
if axis.is_some() {
|
if axis.is_some() {
|
||||||
@@ -108,10 +135,10 @@ impl ScrollbarState {
|
|||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_hovered_on_thumb(&self, axis: Option<ScrollbarAxis>) -> Self {
|
fn with_hovered_on_thumb(&self, axis: Option<Axis>) -> Self {
|
||||||
let mut state = *self;
|
let mut state = *self;
|
||||||
state.hovered_on_thumb = axis;
|
state.hovered_on_thumb = axis;
|
||||||
if axis.is_some() {
|
if self.is_scrollbar_visible() && axis.is_some() {
|
||||||
state.last_scroll_time = Some(std::time::Instant::now());
|
state.last_scroll_time = Some(std::time::Instant::now());
|
||||||
}
|
}
|
||||||
state
|
state
|
||||||
@@ -162,14 +189,28 @@ pub enum ScrollbarAxis {
|
|||||||
Both,
|
Both,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Axis> for ScrollbarAxis {
|
||||||
|
fn from(axis: Axis) -> Self {
|
||||||
|
match axis {
|
||||||
|
Axis::Vertical => Self::Vertical,
|
||||||
|
Axis::Horizontal => Self::Horizontal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ScrollbarAxis {
|
impl ScrollbarAxis {
|
||||||
#[inline]
|
/// Return true if the scrollbar axis is vertical.
|
||||||
fn is_vertical(&self) -> bool {
|
pub fn is_vertical(&self) -> bool {
|
||||||
matches!(self, Self::Vertical)
|
matches!(self, Self::Vertical)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
/// Return true if the scrollbar axis is horizontal.
|
||||||
fn is_both(&self) -> bool {
|
pub fn is_horizontal(&self) -> bool {
|
||||||
|
matches!(self, Self::Horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return true if the scrollbar axis is both vertical and horizontal.
|
||||||
|
pub fn is_both(&self) -> bool {
|
||||||
matches!(self, Self::Both)
|
matches!(self, Self::Both)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,24 +225,23 @@ impl ScrollbarAxis {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn all(&self) -> Vec<ScrollbarAxis> {
|
fn all(&self) -> Vec<Axis> {
|
||||||
match self {
|
match self {
|
||||||
Self::Vertical => vec![Self::Vertical],
|
Self::Vertical => vec![Axis::Vertical],
|
||||||
Self::Horizontal => vec![Self::Horizontal],
|
Self::Horizontal => vec![Axis::Horizontal],
|
||||||
// This should keep Horizontal 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::Horizontal, Self::Vertical],
|
Self::Both => vec![Axis::Horizontal, Axis::Vertical],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrollbar control for scroll-area or a uniform-list.
|
/// Scrollbar control for scroll-area or a uniform-list.
|
||||||
pub struct Scrollbar {
|
pub struct Scrollbar {
|
||||||
view_id: EntityId,
|
|
||||||
axis: ScrollbarAxis,
|
axis: ScrollbarAxis,
|
||||||
scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>,
|
scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>,
|
||||||
scroll_size: gpui::Size<Pixels>,
|
state: ScrollbarState,
|
||||||
state: Rc<Cell<ScrollbarState>>,
|
scroll_size: Option<Size<Pixels>>,
|
||||||
/// Maximum frames per second for scrolling by drag. Default is 120 FPS.
|
/// 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
|
/// This is used to limit the update rate of the scrollbar when it is
|
||||||
@@ -211,95 +251,62 @@ pub struct Scrollbar {
|
|||||||
|
|
||||||
impl Scrollbar {
|
impl Scrollbar {
|
||||||
fn new(
|
fn new(
|
||||||
view_id: EntityId,
|
axis: impl Into<ScrollbarAxis>,
|
||||||
state: Rc<Cell<ScrollbarState>>,
|
state: &ScrollbarState,
|
||||||
axis: ScrollbarAxis,
|
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
||||||
scroll_handle: impl ScrollHandleOffsetable + 'static,
|
|
||||||
scroll_size: gpui::Size<Pixels>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
view_id,
|
state: state.clone(),
|
||||||
state,
|
axis: axis.into(),
|
||||||
axis,
|
scroll_handle: Rc::new(Box::new(scroll_handle.clone())),
|
||||||
scroll_size,
|
|
||||||
scroll_handle: Rc::new(Box::new(scroll_handle)),
|
|
||||||
max_fps: 120,
|
max_fps: 120,
|
||||||
|
scroll_size: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create with vertical and horizontal scrollbar.
|
/// Create with vertical and horizontal scrollbar.
|
||||||
pub fn both(
|
pub fn both(
|
||||||
view_id: EntityId,
|
state: &ScrollbarState,
|
||||||
state: Rc<Cell<ScrollbarState>>,
|
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
||||||
scroll_handle: impl ScrollHandleOffsetable + 'static,
|
|
||||||
scroll_size: gpui::Size<Pixels>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::new(
|
Self::new(ScrollbarAxis::Both, state, scroll_handle)
|
||||||
view_id,
|
|
||||||
state,
|
|
||||||
ScrollbarAxis::Both,
|
|
||||||
scroll_handle,
|
|
||||||
scroll_size,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create with horizontal scrollbar.
|
/// Create with horizontal scrollbar.
|
||||||
pub fn horizontal(
|
pub fn horizontal(
|
||||||
view_id: EntityId,
|
state: &ScrollbarState,
|
||||||
state: Rc<Cell<ScrollbarState>>,
|
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
||||||
scroll_handle: impl ScrollHandleOffsetable + 'static,
|
|
||||||
scroll_size: gpui::Size<Pixels>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::new(
|
Self::new(ScrollbarAxis::Horizontal, state, scroll_handle)
|
||||||
view_id,
|
|
||||||
state,
|
|
||||||
ScrollbarAxis::Horizontal,
|
|
||||||
scroll_handle,
|
|
||||||
scroll_size,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create with vertical scrollbar.
|
/// Create with vertical scrollbar.
|
||||||
pub fn vertical(
|
pub fn vertical(
|
||||||
view_id: EntityId,
|
state: &ScrollbarState,
|
||||||
state: Rc<Cell<ScrollbarState>>,
|
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
||||||
scroll_handle: impl ScrollHandleOffsetable + 'static,
|
|
||||||
scroll_size: gpui::Size<Pixels>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::new(
|
Self::new(ScrollbarAxis::Vertical, state, scroll_handle)
|
||||||
view_id,
|
|
||||||
state,
|
|
||||||
ScrollbarAxis::Vertical,
|
|
||||||
scroll_handle,
|
|
||||||
scroll_size,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create vertical scrollbar for uniform list.
|
/// Create vertical scrollbar for uniform list.
|
||||||
pub fn uniform_scroll(
|
pub fn uniform_scroll(
|
||||||
view_id: EntityId,
|
state: &ScrollbarState,
|
||||||
state: Rc<Cell<ScrollbarState>>,
|
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
||||||
scroll_handle: UniformListScrollHandle,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let scroll_size = scroll_handle
|
Self::new(ScrollbarAxis::Vertical, state, scroll_handle)
|
||||||
.0
|
}
|
||||||
.borrow()
|
|
||||||
.last_item_size
|
|
||||||
.map(|size| size.contents)
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
Self::new(
|
/// Set a special scroll size of the content area, default is None.
|
||||||
view_id,
|
///
|
||||||
state,
|
/// Default will sync the `content_size` from `scroll_handle`.
|
||||||
ScrollbarAxis::Vertical,
|
pub fn scroll_size(mut self, scroll_size: Size<Pixels>) -> Self {
|
||||||
scroll_handle,
|
self.scroll_size = Some(scroll_size);
|
||||||
scroll_size,
|
self
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set scrollbar axis.
|
/// Set scrollbar axis.
|
||||||
pub fn axis(mut self, axis: ScrollbarAxis) -> Self {
|
pub fn axis(mut self, axis: impl Into<ScrollbarAxis>) -> Self {
|
||||||
self.axis = axis;
|
self.axis = axis.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,47 +320,54 @@ impl Scrollbar {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
|
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||||
(
|
(
|
||||||
cx.theme().scrollbar_thumb_hover_background,
|
cx.theme().scrollbar_thumb_hover_background,
|
||||||
cx.theme().scrollbar_thumb_background,
|
cx.theme().scrollbar_thumb_background,
|
||||||
cx.theme().scrollbar_thumb_border,
|
cx.theme().scrollbar_thumb_border,
|
||||||
THUMB_INSET - px(1.),
|
THUMB_ACTIVE_WIDTH,
|
||||||
THUMB_RADIUS,
|
THUMB_ACTIVE_INSET,
|
||||||
|
THUMB_ACTIVE_RADIUS,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn style_for_hovered_thumb(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
|
fn style_for_hovered_thumb(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||||
(
|
(
|
||||||
cx.theme().scrollbar_thumb_hover_background,
|
cx.theme().scrollbar_thumb_hover_background,
|
||||||
cx.theme().scrollbar_thumb_background,
|
cx.theme().scrollbar_thumb_background,
|
||||||
cx.theme().scrollbar_thumb_border,
|
cx.theme().scrollbar_thumb_border,
|
||||||
THUMB_INSET - px(1.),
|
THUMB_ACTIVE_WIDTH,
|
||||||
THUMB_RADIUS,
|
THUMB_ACTIVE_INSET,
|
||||||
|
THUMB_ACTIVE_RADIUS,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn style_for_hovered_bar(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
|
fn style_for_hovered_bar(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||||
let (inset, radius) = (THUMB_INSET - px(1.), THUMB_RADIUS);
|
|
||||||
|
|
||||||
(
|
(
|
||||||
cx.theme().scrollbar_thumb_background,
|
cx.theme().scrollbar_thumb_background,
|
||||||
cx.theme().scrollbar_thumb_border,
|
cx.theme().scrollbar_thumb_border,
|
||||||
gpui::transparent_black(),
|
gpui::transparent_black(),
|
||||||
|
THUMB_ACTIVE_WIDTH,
|
||||||
|
THUMB_ACTIVE_INSET,
|
||||||
|
THUMB_ACTIVE_RADIUS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style_for_idle(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||||
|
let (width, inset, radius) = match cx.theme().scrollbar_mode {
|
||||||
|
ScrollBarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
|
||||||
|
_ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
gpui::transparent_black(),
|
||||||
|
gpui::transparent_black(),
|
||||||
|
gpui::transparent_black(),
|
||||||
|
width,
|
||||||
inset,
|
inset,
|
||||||
radius,
|
radius,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn style_for_idle(_: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
|
|
||||||
(
|
|
||||||
gpui::transparent_black(),
|
|
||||||
gpui::transparent_black(),
|
|
||||||
gpui::transparent_black(),
|
|
||||||
THUMB_INSET,
|
|
||||||
THUMB_RADIUS - px(1.),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoElement for Scrollbar {
|
impl IntoElement for Scrollbar {
|
||||||
@@ -370,7 +384,7 @@ pub struct PrepaintState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct AxisPrepaintState {
|
pub struct AxisPrepaintState {
|
||||||
axis: ScrollbarAxis,
|
axis: Axis,
|
||||||
bar_hitbox: Hitbox,
|
bar_hitbox: Hitbox,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
radius: Pixels,
|
radius: Pixels,
|
||||||
@@ -400,11 +414,11 @@ impl Element for Scrollbar {
|
|||||||
|
|
||||||
fn request_layout(
|
fn request_layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Option<&gpui::GlobalElementId>,
|
_: Option<&GlobalElementId>,
|
||||||
_: Option<&gpui::InspectorElementId>,
|
_: Option<&InspectorElementId>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
let style = gpui::Style {
|
let style = gpui::Style {
|
||||||
position: Position::Absolute,
|
position: Position::Absolute,
|
||||||
flex_grow: 1.0,
|
flex_grow: 1.0,
|
||||||
@@ -421,8 +435,8 @@ impl Element for Scrollbar {
|
|||||||
|
|
||||||
fn prepaint(
|
fn prepaint(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Option<&gpui::GlobalElementId>,
|
_: Option<&GlobalElementId>,
|
||||||
_: Option<&gpui::InspectorElementId>,
|
_: Option<&InspectorElementId>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
_: &mut Self::RequestLayoutState,
|
_: &mut Self::RequestLayoutState,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
@@ -434,26 +448,30 @@ 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();
|
||||||
|
let scroll_size = self
|
||||||
|
.scroll_size
|
||||||
|
.unwrap_or(self.scroll_handle.content_size());
|
||||||
|
|
||||||
for axis in self.axis.all().into_iter() {
|
for axis in self.axis.all().into_iter() {
|
||||||
let is_vertical = axis.is_vertical();
|
let is_vertical = axis.is_vertical();
|
||||||
let (scroll_area_size, container_size, scroll_position) = if is_vertical {
|
let (scroll_area_size, container_size, scroll_position) = if is_vertical {
|
||||||
(
|
(
|
||||||
self.scroll_size.height,
|
scroll_size.height,
|
||||||
hitbox.size.height,
|
hitbox.size.height,
|
||||||
self.scroll_handle.offset().y,
|
self.scroll_handle.offset().y,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
self.scroll_size.width,
|
scroll_size.width,
|
||||||
hitbox.size.width,
|
hitbox.size.width,
|
||||||
self.scroll_handle.offset().x,
|
self.scroll_handle.offset().x,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 {
|
||||||
WIDTH
|
THUMB_ACTIVE_WIDTH
|
||||||
} else {
|
} else {
|
||||||
px(0.)
|
px(0.)
|
||||||
};
|
};
|
||||||
@@ -494,12 +512,27 @@ impl Element for Scrollbar {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
|
let is_always_to_show = cx.theme().scrollbar_mode.is_always();
|
||||||
|
let is_hover_to_show = cx.theme().scrollbar_mode.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);
|
||||||
|
|
||||||
let (thumb_bg, bar_bg, bar_border, inset, radius) =
|
let (thumb_bg, bar_bg, bar_border, thumb_width, 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 || is_hovered_on_thumb) {
|
||||||
|
if is_hovered_on_thumb {
|
||||||
|
Self::style_for_hovered_thumb(cx)
|
||||||
|
} else {
|
||||||
|
Self::style_for_hovered_bar(cx)
|
||||||
|
}
|
||||||
|
} else if is_always_to_show {
|
||||||
|
#[allow(clippy::if_same_then_else)]
|
||||||
|
if is_hovered_on_thumb {
|
||||||
|
Self::style_for_hovered_thumb(cx)
|
||||||
|
} else {
|
||||||
|
Self::style_for_hovered_bar(cx)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut idle_state = Self::style_for_idle(cx);
|
let mut idle_state = Self::style_for_idle(cx);
|
||||||
// Delay 2s to fade out the scrollbar thumb (in 1s)
|
// Delay 2s to fade out the scrollbar thumb (in 1s)
|
||||||
@@ -531,43 +564,39 @@ impl Element for Scrollbar {
|
|||||||
idle_state
|
idle_state
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The clickable area of the thumb
|
||||||
|
let thumb_length = thumb_end - thumb_start - inset * 2;
|
||||||
let thumb_bounds = if is_vertical {
|
let thumb_bounds = if is_vertical {
|
||||||
Bounds::from_corners(
|
Bounds::from_corner_and_size(
|
||||||
point(bounds.origin.x, bounds.origin.y + thumb_start),
|
Corner::TopRight,
|
||||||
point(bounds.origin.x + WIDTH, bounds.origin.y + thumb_end),
|
bounds.top_right() + point(-inset, inset + thumb_start),
|
||||||
|
size(WIDTH, thumb_length),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Bounds::from_corners(
|
Bounds::from_corner_and_size(
|
||||||
point(bounds.origin.x + thumb_start, bounds.origin.y),
|
Corner::BottomLeft,
|
||||||
point(bounds.origin.x + thumb_end, bounds.origin.y + WIDTH),
|
bounds.bottom_left() + point(inset + thumb_start, -inset),
|
||||||
|
size(thumb_length, WIDTH),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The actual render area of the thumb
|
||||||
let thumb_fill_bounds = if is_vertical {
|
let thumb_fill_bounds = if is_vertical {
|
||||||
Bounds::from_corners(
|
Bounds::from_corner_and_size(
|
||||||
point(
|
Corner::TopRight,
|
||||||
bounds.origin.x + inset + BORDER_WIDTH,
|
bounds.top_right() + point(-inset, inset + thumb_start),
|
||||||
bounds.origin.y + thumb_start + inset,
|
size(thumb_width, thumb_length),
|
||||||
),
|
|
||||||
point(
|
|
||||||
bounds.origin.x + WIDTH - inset,
|
|
||||||
bounds.origin.y + thumb_end - inset,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Bounds::from_corners(
|
Bounds::from_corner_and_size(
|
||||||
point(
|
Corner::BottomLeft,
|
||||||
bounds.origin.x + thumb_start + inset,
|
bounds.bottom_left() + point(inset + thumb_start, -inset),
|
||||||
bounds.origin.y + inset + BORDER_WIDTH,
|
size(thumb_length, thumb_width),
|
||||||
),
|
|
||||||
point(
|
|
||||||
bounds.origin.x + thumb_end - inset,
|
|
||||||
bounds.origin.y + WIDTH - inset,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let bar_hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
let bar_hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal)
|
||||||
});
|
});
|
||||||
|
|
||||||
states.push(AxisPrepaintState {
|
states.push(AxisPrepaintState {
|
||||||
@@ -592,16 +621,19 @@ impl Element for Scrollbar {
|
|||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Option<&gpui::GlobalElementId>,
|
_: Option<&GlobalElementId>,
|
||||||
_: Option<&gpui::InspectorElementId>,
|
_: Option<&InspectorElementId>,
|
||||||
_: Bounds<Pixels>,
|
_: Bounds<Pixels>,
|
||||||
_: &mut Self::RequestLayoutState,
|
_: &mut Self::RequestLayoutState,
|
||||||
prepaint: &mut Self::PrepaintState,
|
prepaint: &mut Self::PrepaintState,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
_cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
|
let view_id = window.current_view();
|
||||||
let hitbox_bounds = prepaint.hitbox.bounds;
|
let hitbox_bounds = prepaint.hitbox.bounds;
|
||||||
let is_visible = self.state.get().is_scrollbar_visible();
|
let is_visible =
|
||||||
|
self.state.get().is_scrollbar_visible() || cx.theme().scrollbar_mode.is_always();
|
||||||
|
let is_hover_to_show = cx.theme().scrollbar_mode.is_hover();
|
||||||
|
|
||||||
// Update last_scroll_time when offset is changed.
|
// Update last_scroll_time when offset is changed.
|
||||||
if self.scroll_handle.offset() != self.state.get().last_scroll_offset {
|
if self.scroll_handle.offset() != self.state.get().last_scroll_offset {
|
||||||
@@ -637,23 +669,14 @@ impl Element for Scrollbar {
|
|||||||
bounds,
|
bounds,
|
||||||
corner_radii: (0.).into(),
|
corner_radii: (0.).into(),
|
||||||
background: gpui::transparent_black().into(),
|
background: gpui::transparent_black().into(),
|
||||||
border_widths: if is_vertical {
|
border_widths: Edges {
|
||||||
Edges {
|
|
||||||
top: px(0.),
|
top: px(0.),
|
||||||
right: px(0.),
|
right: px(0.),
|
||||||
bottom: px(0.),
|
bottom: px(0.),
|
||||||
left: BORDER_WIDTH,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Edges {
|
|
||||||
top: BORDER_WIDTH,
|
|
||||||
right: px(0.),
|
|
||||||
bottom: px(0.),
|
|
||||||
left: px(0.),
|
left: px(0.),
|
||||||
}
|
|
||||||
},
|
},
|
||||||
border_color: state.border,
|
border_color: state.border,
|
||||||
border_style: BorderStyle::Solid,
|
border_style: BorderStyle::default(),
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.paint_quad(
|
cx.paint_quad(
|
||||||
@@ -663,7 +686,6 @@ impl Element for Scrollbar {
|
|||||||
|
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
let view_id = self.view_id;
|
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
let scroll_handle = self.scroll_handle.clone();
|
||||||
|
|
||||||
move |event: &ScrollWheelEvent, phase, _, cx| {
|
move |event: &ScrollWheelEvent, phase, _, cx| {
|
||||||
@@ -682,10 +704,9 @@ impl Element for Scrollbar {
|
|||||||
|
|
||||||
let safe_range = (-scroll_area_size + container_size)..px(0.);
|
let safe_range = (-scroll_area_size + container_size)..px(0.);
|
||||||
|
|
||||||
if is_visible {
|
if is_hover_to_show || is_visible {
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
let view_id = self.view_id;
|
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
let scroll_handle = self.scroll_handle.clone();
|
||||||
|
|
||||||
move |event: &MouseDownEvent, phase, _, cx| {
|
move |event: &MouseDownEvent, phase, _, cx| {
|
||||||
@@ -734,14 +755,13 @@ impl Element for Scrollbar {
|
|||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
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 max_fps_duration = Duration::from_millis((1000 / self.max_fps) as u64);
|
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;
|
let mut notify = false;
|
||||||
// When is hover to show mode or it was visible,
|
// When is hover to show mode or it was visible,
|
||||||
// we need to update the hovered state and increase the last_scroll_time.
|
// we need to update the hovered state and increase the last_scroll_time.
|
||||||
let need_hover_to_update = is_visible;
|
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) && need_hover_to_update {
|
if bounds.contains(&event.position) && need_hover_to_update {
|
||||||
state.set(state.get().with_hovered(Some(axis)));
|
state.set(state.get().with_hovered(Some(axis)));
|
||||||
@@ -815,7 +835,6 @@ impl Element for Scrollbar {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let view_id = self.view_id;
|
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
|
|
||||||
move |_event: &MouseUpEvent, phase, _, cx| {
|
move |_event: &MouseUpEvent, phase, _, cx| {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, App, Axis, Div, Element, ElementId, EntityId, Pixels, Refineable, StyleRefinement,
|
div, px, App, Axis, Div, Element, ElementId, Pixels, Refineable, StyleRefinement, Styled,
|
||||||
Styled, Window,
|
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
@@ -48,19 +47,15 @@ pub trait StyledExt: Styled + Sized {
|
|||||||
self.flex().flex_col()
|
self.flex().flex_col()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a border with a width of 1px, color ring color
|
|
||||||
fn outline(self, _window: &Window, cx: &App) -> Self {
|
|
||||||
self.border_color(cx.theme().ring)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps the element in a ScrollView.
|
/// Wraps the element in a ScrollView.
|
||||||
///
|
///
|
||||||
/// Current this is only have a vertical scrollbar.
|
/// Current this is only have a vertical scrollbar.
|
||||||
fn scrollable(self, view_id: EntityId, axis: ScrollbarAxis) -> Scrollable<Self>
|
#[inline]
|
||||||
|
fn scrollable(self, axis: impl Into<ScrollbarAxis>) -> Scrollable<Self>
|
||||||
where
|
where
|
||||||
Self: Element,
|
Self: Element,
|
||||||
{
|
{
|
||||||
Scrollable::new(view_id, self, axis)
|
Scrollable::new(axis, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
font_weight!(font_thin, THIN);
|
font_weight!(font_thin, THIN);
|
||||||
@@ -74,6 +69,7 @@ pub trait StyledExt: Styled + Sized {
|
|||||||
font_weight!(font_black, BLACK);
|
font_weight!(font_black, BLACK);
|
||||||
|
|
||||||
/// Set as Popover style
|
/// Set as Popover style
|
||||||
|
#[inline]
|
||||||
fn popover_style(self, cx: &mut App) -> Self {
|
fn popover_style(self, cx: &mut App) -> Self {
|
||||||
self.bg(cx.theme().background)
|
self.bg(cx.theme().background)
|
||||||
.border_1()
|
.border_1()
|
||||||
|
|||||||
1088
locales/app.yml
1088
locales/app.yml
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user