52
Cargo.lock
generated
52
Cargo.lock
generated
@@ -745,9 +745,9 @@ checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.23.0"
|
version = "1.23.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c"
|
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck_derive",
|
"bytemuck_derive",
|
||||||
]
|
]
|
||||||
@@ -930,6 +930,7 @@ dependencies = [
|
|||||||
"nostr",
|
"nostr",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
]
|
]
|
||||||
@@ -1062,7 +1063,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1157,6 +1158,7 @@ dependencies = [
|
|||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"theme",
|
"theme",
|
||||||
@@ -1442,7 +1444,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#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2264,7 +2266,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2357,7 +2359,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#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2410,9 +2412,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.3"
|
version = "0.15.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foldhash",
|
"foldhash",
|
||||||
]
|
]
|
||||||
@@ -2580,7 +2582,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#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2597,7 +2599,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#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -2873,7 +2875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.15.3",
|
"hashbrown 0.15.4",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3338,7 +3340,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -3451,7 +3453,7 @@ dependencies = [
|
|||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"codespan-reporting 0.12.0",
|
"codespan-reporting 0.12.0",
|
||||||
"half",
|
"half",
|
||||||
"hashbrown 0.15.3",
|
"hashbrown 0.15.4",
|
||||||
"hexf-parse",
|
"hexf-parse",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
@@ -4661,7 +4663,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
@@ -4798,7 +4800,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#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5269,7 +5271,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#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5380,6 +5382,20 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "settings"
|
||||||
|
version = "0.1.5"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"global",
|
||||||
|
"gpui",
|
||||||
|
"log",
|
||||||
|
"nostr-sdk",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -5621,7 +5637,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#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -6536,7 +6552,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#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
source = "git+https://github.com/zed-industries/zed#b15aef4310e86aa31c2ceab74184ec7e5627a2c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use global::shared_state;
|
|||||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
|
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use settings::AppSettings;
|
||||||
|
|
||||||
use crate::constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE};
|
use crate::constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE};
|
||||||
use crate::message::Message;
|
use crate::message::Message;
|
||||||
@@ -249,10 +250,12 @@ impl Room {
|
|||||||
/// - For a direct message: the other person's avatar
|
/// - For a direct message: the other person's avatar
|
||||||
/// - For a group chat: None
|
/// - For a group chat: None
|
||||||
pub fn display_image(&self, cx: &App) -> SharedString {
|
pub fn display_image(&self, cx: &App) -> SharedString {
|
||||||
|
let proxy = AppSettings::get_global(cx).settings().proxy_user_avatars;
|
||||||
|
|
||||||
if let Some(picture) = self.picture.as_ref() {
|
if let Some(picture) = self.picture.as_ref() {
|
||||||
picture.clone()
|
picture.clone()
|
||||||
} else if !self.is_group() {
|
} else if !self.is_group() {
|
||||||
self.first_member(cx).render_avatar()
|
self.first_member(cx).render_avatar(proxy)
|
||||||
} else {
|
} else {
|
||||||
"brand/group.png".into()
|
"brand/group.png".into()
|
||||||
}
|
}
|
||||||
@@ -630,6 +633,7 @@ impl Room {
|
|||||||
let subject = self.subject.clone();
|
let subject = self.subject.clone();
|
||||||
let picture = self.picture.clone();
|
let picture = self.picture.clone();
|
||||||
let public_keys = Arc::clone(&self.members);
|
let public_keys = Arc::clone(&self.members);
|
||||||
|
let backup = AppSettings::get_global(cx).settings().backup_messages;
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let signer = shared_state().client.signer().await?;
|
let signer = shared_state().client.signer().await?;
|
||||||
@@ -697,7 +701,7 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only send a backup message to current user if there are no issues when sending to others
|
// Only send a backup message to current user if there are no issues when sending to others
|
||||||
if reports.is_empty() {
|
if backup && reports.is_empty() {
|
||||||
if let Err(e) = shared_state()
|
if let Err(e) = shared_state()
|
||||||
.client
|
.client
|
||||||
.send_private_msg(*current_user, &content, tags.clone())
|
.send_private_msg(*current_user, &content, tags.clone())
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::collections::HashSet;
|
|||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use global::constants::NIP96_SERVER;
|
|
||||||
use gpui::{Image, ImageFormat};
|
use gpui::{Image, ImageFormat};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -11,11 +10,14 @@ use qrcode_generator::QrCodeEcc;
|
|||||||
pub mod debounced_delay;
|
pub mod debounced_delay;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
|
|
||||||
pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url, anyhow::Error> {
|
pub async fn nip96_upload(
|
||||||
|
client: &Client,
|
||||||
|
upload_to: Url,
|
||||||
|
file: Vec<u8>,
|
||||||
|
) -> anyhow::Result<Url, anyhow::Error> {
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let server_url = Url::parse(NIP96_SERVER)?;
|
|
||||||
|
|
||||||
let config: ServerConfig = nip96::get_server_config(server_url, None).await?;
|
let config: ServerConfig = nip96::get_server_config(upload_to.to_owned(), None).await?;
|
||||||
let url = nip96::upload_data(&signer, &config, file, None, None).await?;
|
let url = nip96::upload_data(&signer, &config, file, None, None).await?;
|
||||||
|
|
||||||
Ok(url)
|
Ok(url)
|
||||||
|
|||||||
@@ -2,23 +2,29 @@ use global::constants::IMAGE_RESIZE_SERVICE;
|
|||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
const FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png";
|
||||||
|
|
||||||
pub trait RenderProfile {
|
pub trait RenderProfile {
|
||||||
fn render_avatar(&self) -> SharedString;
|
fn render_avatar(&self, proxy: bool) -> SharedString;
|
||||||
fn render_name(&self) -> SharedString;
|
fn render_name(&self) -> SharedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderProfile for Profile {
|
impl RenderProfile for Profile {
|
||||||
fn render_avatar(&self) -> SharedString {
|
fn render_avatar(&self, proxy: bool) -> SharedString {
|
||||||
self.metadata()
|
self.metadata()
|
||||||
.picture
|
.picture
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|picture| !picture.is_empty())
|
.filter(|picture| !picture.is_empty())
|
||||||
.map(|picture| {
|
.map(|picture| {
|
||||||
|
if proxy {
|
||||||
format!(
|
format!(
|
||||||
"{}/?url={}&w=100&h=100&fit=cover&mask=circle&default=https://image.nostr.build/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png&n=-1",
|
"{}/?url={}&w=100&h=100&fit=cover&mask=circle&default={}&n=-1",
|
||||||
IMAGE_RESIZE_SERVICE, picture
|
IMAGE_RESIZE_SERVICE, picture, FALLBACK_IMG
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
|
} else {
|
||||||
|
picture.into()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "brand/avatar.png".into())
|
.unwrap_or_else(|| "brand/avatar.png".into())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ theme = { path = "../theme" }
|
|||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
chats = { path = "../chats" }
|
chats = { path = "../chats" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
|
|||||||
use global::shared_state;
|
use global::shared_state;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, impl_internal_actions, px, App, AppContext, Axis, Context, Entity, InteractiveElement,
|
div, impl_internal_actions, px, App, AppContext, Axis, Context, Entity, IntoElement,
|
||||||
IntoElement, ParentElement, Render, Styled, Subscription, Task, Window,
|
ParentElement, Render, Styled, Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -20,9 +20,7 @@ use ui::dock_area::{DockArea, DockItem};
|
|||||||
use ui::{ContextModal, IconName, Root, Sizable, TitleBar};
|
use ui::{ContextModal, IconName, Root, Sizable, TitleBar};
|
||||||
|
|
||||||
use crate::views::chat::{self, Chat};
|
use crate::views::chat::{self, Chat};
|
||||||
use crate::views::{
|
use crate::views::{login, new_account, onboarding, preferences, sidebar, startup, welcome};
|
||||||
compose, login, new_account, onboarding, profile, relays, sidebar, startup, welcome,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl_internal_actions!(dock, [ToggleModal]);
|
impl_internal_actions!(dock, [ToggleModal]);
|
||||||
|
|
||||||
@@ -181,6 +179,17 @@ impl ChatSpace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_settings(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let settings = preferences::init(window, cx);
|
||||||
|
|
||||||
|
window.open_modal(cx, move |modal, _, _| {
|
||||||
|
modal
|
||||||
|
.title("Preferences")
|
||||||
|
.width(px(DEFAULT_MODAL_WIDTH))
|
||||||
|
.child(settings.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn titlebar(&mut self, status: bool, cx: &mut Context<Self>) {
|
fn titlebar(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
self.titlebar = status;
|
self.titlebar = status;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -206,53 +215,19 @@ impl ChatSpace {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_modal_action(
|
fn toggle_appearance(&self, window: &mut Window, cx: &mut App) {
|
||||||
&mut self,
|
if cx.theme().mode.is_dark() {
|
||||||
action: &ToggleModal,
|
Theme::change(ThemeMode::Light, Some(window), cx);
|
||||||
window: &mut Window,
|
} else {
|
||||||
cx: &mut Context<Self>,
|
Theme::change(ThemeMode::Dark, Some(window), cx);
|
||||||
) {
|
}
|
||||||
match action.modal {
|
}
|
||||||
ModalKind::Profile => {
|
|
||||||
let profile = profile::init(window, cx);
|
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, _, _| {
|
fn logout(&self, _window: &mut Window, cx: &mut App) {
|
||||||
modal
|
cx.background_spawn(async move {
|
||||||
.title("Profile")
|
shared_state().unset_signer().await;
|
||||||
.width(px(DEFAULT_MODAL_WIDTH))
|
|
||||||
.child(profile.clone())
|
|
||||||
})
|
})
|
||||||
}
|
.detach();
|
||||||
ModalKind::Compose => {
|
|
||||||
let compose = compose::init(window, cx);
|
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, _, _| {
|
|
||||||
modal
|
|
||||||
.title("Direct Messages")
|
|
||||||
.width(px(DEFAULT_MODAL_WIDTH))
|
|
||||||
.child(compose.clone())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ModalKind::Relay => {
|
|
||||||
let relays = relays::init(window, cx);
|
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _, _| {
|
|
||||||
this.width(px(DEFAULT_MODAL_WIDTH))
|
|
||||||
.title("Edit your Messaging Relays")
|
|
||||||
.child(relays.clone())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ModalKind::SetupRelay => {
|
|
||||||
let relays = relays::init(window, cx);
|
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _, _| {
|
|
||||||
this.width(px(DEFAULT_MODAL_WIDTH))
|
|
||||||
.title("Your Messaging Relays are not configured")
|
|
||||||
.child(relays.clone())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_center_panel<P: PanelView>(panel: P, window: &mut Window, cx: &mut App) {
|
pub(crate) fn set_center_panel<P: PanelView>(panel: P, window: &mut Window, cx: &mut App) {
|
||||||
@@ -310,40 +285,28 @@ impl Render for ChatSpace {
|
|||||||
this.icon(IconName::Moon)
|
this.icon(IconName::Moon)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on_click(cx.listener(|_, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
if cx.theme().mode.is_dark() {
|
this.toggle_appearance(window, cx);
|
||||||
Theme::change(
|
|
||||||
ThemeMode::Light,
|
|
||||||
Some(window),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Theme::change(
|
|
||||||
ThemeMode::Dark,
|
|
||||||
Some(window),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("settings")
|
Button::new("preferences")
|
||||||
.tooltip("Open settings")
|
.tooltip("Open Preferences")
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.icon(IconName::Settings),
|
.icon(IconName::Settings)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.open_settings(window, cx);
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("logout")
|
Button::new("logout")
|
||||||
.tooltip("Log out")
|
.tooltip("Log Out")
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.icon(IconName::Logout)
|
.icon(IconName::Logout)
|
||||||
.on_click(cx.listener(move |_, _, _window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
cx.background_spawn(async move {
|
this.logout(window, cx);
|
||||||
shared_state().unset_signer().await;
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -356,7 +319,5 @@ impl Render for ChatSpace {
|
|||||||
.child(div().absolute().top_8().children(notification_layer))
|
.child(div().absolute().top_8().children(notification_layer))
|
||||||
// Modals
|
// Modals
|
||||||
.children(modal_layer)
|
.children(modal_layer)
|
||||||
// Actions
|
|
||||||
.on_action(cx.listener(Self::on_modal_action))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ fn main() {
|
|||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
// Initialize components
|
// Initialize components
|
||||||
ui::init(cx);
|
ui::init(cx);
|
||||||
|
// Initialize settings
|
||||||
|
settings::init(cx);
|
||||||
// Initialize auto update
|
// Initialize auto update
|
||||||
auto_update::init(cx);
|
auto_update::init(cx);
|
||||||
// Initialize chat state
|
// Initialize chat state
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use gpui::{
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
@@ -371,6 +372,7 @@ impl Chat {
|
|||||||
|
|
||||||
self.uploading(true, cx);
|
self.uploading(true, cx);
|
||||||
|
|
||||||
|
let nip96 = AppSettings::get_global(cx).settings().media_server.clone();
|
||||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||||
files: true,
|
files: true,
|
||||||
directories: false,
|
directories: false,
|
||||||
@@ -389,7 +391,9 @@ impl Chat {
|
|||||||
|
|
||||||
// Spawn task via async utility instead of GPUI context
|
// Spawn task via async utility instead of GPUI context
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let url = match nip96_upload(&shared_state().client, file_data).await {
|
let url = match nip96_upload(&shared_state().client, nip96, file_data)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(url) => Some(url),
|
Ok(url) => Some(url),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Upload error: {e}");
|
log::error!("Upload error: {e}");
|
||||||
@@ -542,6 +546,9 @@ impl Chat {
|
|||||||
return div().id(ix);
|
return div().id(ix);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let proxy = AppSettings::get_global(cx).settings().proxy_user_avatars;
|
||||||
|
let hide_avatar = AppSettings::get_global(cx).settings().hide_user_avatars;
|
||||||
|
|
||||||
let message = message.borrow();
|
let message = message.borrow();
|
||||||
|
|
||||||
// Message without ID, Author probably the placeholder
|
// Message without ID, Author probably the placeholder
|
||||||
@@ -590,7 +597,9 @@ impl Chat {
|
|||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.child(Avatar::new(author.render_avatar()).size(rems(2.)))
|
.when(!hide_avatar, |this| {
|
||||||
|
this.child(Avatar::new(author.render_avatar(proxy)).size(rems(2.)))
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
@@ -305,6 +306,8 @@ impl Render for Compose {
|
|||||||
const DESCRIPTION: &str =
|
const DESCRIPTION: &str =
|
||||||
"Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
|
"Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
|
||||||
|
|
||||||
|
let proxy = AppSettings::get_global(cx).settings().proxy_user_avatars;
|
||||||
|
|
||||||
let label: SharedString = if self.selected.read(cx).len() > 1 {
|
let label: SharedString = if self.selected.read(cx).len() > 1 {
|
||||||
"Create Group DM".into()
|
"Create Group DM".into()
|
||||||
} else {
|
} else {
|
||||||
@@ -413,7 +416,7 @@ impl Render for Compose {
|
|||||||
.gap_3()
|
.gap_3()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(
|
.child(
|
||||||
img(item.render_avatar())
|
img(item.render_avatar(proxy))
|
||||||
.size_7()
|
.size_7()
|
||||||
.flex_shrink_0(),
|
.flex_shrink_0(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub mod compose;
|
|||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod new_account;
|
pub mod new_account;
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
|
pub mod preferences;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod relays;
|
pub mod relays;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use gpui::{
|
|||||||
Styled, Window,
|
Styled, Window,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use settings::AppSettings;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -94,6 +95,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 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,
|
||||||
@@ -122,7 +124,9 @@ impl NewAccount {
|
|||||||
let (tx, rx) = oneshot::channel::<Url>();
|
let (tx, rx) = oneshot::channel::<Url>();
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
if let Ok(url) = nip96_upload(&shared_state().client, file_data).await {
|
if let Ok(url) =
|
||||||
|
nip96_upload(&shared_state().client, nip96, file_data).await
|
||||||
|
{
|
||||||
_ = tx.send(url);
|
_ = tx.send(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
308
crates/coop/src/views/preferences.rs
Normal file
308
crates/coop/src/views/preferences.rs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
use common::profile::RenderProfile;
|
||||||
|
use global::{
|
||||||
|
constants::{DEFAULT_MODAL_WIDTH, NIP96_SERVER},
|
||||||
|
shared_state,
|
||||||
|
};
|
||||||
|
use gpui::{
|
||||||
|
div, http_client::Url, prelude::FluentBuilder, px, relative, rems, App, AppContext, Context,
|
||||||
|
Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, Render,
|
||||||
|
StatefulInteractiveElement, Styled, Window,
|
||||||
|
};
|
||||||
|
use settings::AppSettings;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::{
|
||||||
|
avatar::Avatar,
|
||||||
|
button::{Button, ButtonVariants},
|
||||||
|
input::{InputState, TextInput},
|
||||||
|
switch::Switch,
|
||||||
|
ContextModal, IconName, Sizable, Size, StyledExt,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::views::{profile, relays};
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
||||||
|
Preferences::new(window, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Preferences {
|
||||||
|
media_input: Entity<InputState>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Preferences {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
|
cx.new(|cx| {
|
||||||
|
let media_server = AppSettings::get_global(cx)
|
||||||
|
.settings()
|
||||||
|
.media_server
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let media_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.default_value(media_server)
|
||||||
|
.placeholder(NIP96_SERVER)
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
media_input,
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_profile(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let profile = profile::init(window, cx);
|
||||||
|
|
||||||
|
window.open_modal(cx, move |modal, _, _| {
|
||||||
|
modal
|
||||||
|
.title("Profile")
|
||||||
|
.width(px(DEFAULT_MODAL_WIDTH))
|
||||||
|
.child(profile.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let relays = relays::init(window, cx);
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _, _| {
|
||||||
|
this.width(px(DEFAULT_MODAL_WIDTH))
|
||||||
|
.title("Edit your Messaging Relays")
|
||||||
|
.child(relays.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Preferences {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
const MEDIA_DESCRIPTION: &str = "Coop only supports NIP-96 media servers for now. If you're not sure about it, please keep the default value.";
|
||||||
|
const BACKUP_DESCRIPTION: &str =
|
||||||
|
"When a user sends a message, Coop won't back it up to the user's messaging relays";
|
||||||
|
const TRUSTED_DESCRIPTION: &str = "Show trusted requests by default";
|
||||||
|
const HIDE_AVATAR_DESCRIPTION: &str =
|
||||||
|
"Unload all avatar pictures to improve performance and reduce memory usage";
|
||||||
|
const PROXY_DESCRIPTION: &str =
|
||||||
|
"Use wsrv.nl to resize and downscale avatar pictures (saves ~50MB of data)";
|
||||||
|
|
||||||
|
let input_state = self.media_input.downgrade();
|
||||||
|
let settings = AppSettings::get_global(cx).settings();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.track_focus(&self.focus_handle)
|
||||||
|
.size_full()
|
||||||
|
.px_3()
|
||||||
|
.pb_3()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.py_2()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_placeholder)
|
||||||
|
.font_semibold()
|
||||||
|
.child("Account"),
|
||||||
|
)
|
||||||
|
.when_some(shared_state().identity(), |this, profile| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
|
.flex()
|
||||||
|
.justify_between()
|
||||||
|
.items_center()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id("current-user")
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Avatar::new(
|
||||||
|
profile.render_avatar(settings.proxy_user_avatars),
|
||||||
|
)
|
||||||
|
.size(rems(2.4)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.line_height(relative(1.3))
|
||||||
|
.font_semibold()
|
||||||
|
.child(profile.render_name()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.line_height(relative(1.3))
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child("See your profile"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.open_profile(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("relays")
|
||||||
|
.label("DM Relays")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.open_relays(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.py_2()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_placeholder)
|
||||||
|
.font_semibold()
|
||||||
|
.child("Media Server"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_start()
|
||||||
|
.gap_1()
|
||||||
|
.child(TextInput::new(&self.media_input).xsmall())
|
||||||
|
.child(
|
||||||
|
Button::new("update")
|
||||||
|
.icon(IconName::CheckCircleFill)
|
||||||
|
.ghost()
|
||||||
|
.with_size(Size::Size(px(26.)))
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
if let Some(input) = input_state.upgrade() {
|
||||||
|
let value = input.read(cx).value();
|
||||||
|
let Ok(url) = Url::parse(value) else {
|
||||||
|
window.push_notification("URL is not valid", cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
AppSettings::global(cx).update(cx, |this, cx| {
|
||||||
|
this.settings.media_server = url;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(MEDIA_DESCRIPTION),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.py_2()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_placeholder)
|
||||||
|
.font_semibold()
|
||||||
|
.child("Messages"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Switch::new("backup_messages")
|
||||||
|
.label("Backup messages")
|
||||||
|
.description(BACKUP_DESCRIPTION)
|
||||||
|
.checked(settings.backup_messages)
|
||||||
|
.on_click(|_, _window, cx| {
|
||||||
|
AppSettings::global(cx).update(cx, |this, cx| {
|
||||||
|
this.settings.backup_messages =
|
||||||
|
!this.settings.backup_messages;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Switch::new("only_show_trusted")
|
||||||
|
.label("Only trusted")
|
||||||
|
.description(TRUSTED_DESCRIPTION)
|
||||||
|
.checked(settings.only_show_trusted)
|
||||||
|
.on_click(|_, _window, cx| {
|
||||||
|
AppSettings::global(cx).update(cx, |this, cx| {
|
||||||
|
this.settings.only_show_trusted =
|
||||||
|
!this.settings.only_show_trusted;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.py_2()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_placeholder)
|
||||||
|
.font_semibold()
|
||||||
|
.child("Display"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Switch::new("hide_user_avatars")
|
||||||
|
.label("Hide user avatars")
|
||||||
|
.description(HIDE_AVATAR_DESCRIPTION)
|
||||||
|
.checked(settings.hide_user_avatars)
|
||||||
|
.on_click(|_, _window, cx| {
|
||||||
|
AppSettings::global(cx).update(cx, |this, cx| {
|
||||||
|
this.settings.hide_user_avatars =
|
||||||
|
!this.settings.hide_user_avatars;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Switch::new("proxy_user_avatars")
|
||||||
|
.label("Proxy user avatars")
|
||||||
|
.description(PROXY_DESCRIPTION)
|
||||||
|
.checked(settings.proxy_user_avatars)
|
||||||
|
.on_click(|_, _window, cx| {
|
||||||
|
AppSettings::global(cx).update(cx, |this, cx| {
|
||||||
|
this.settings.proxy_user_avatars =
|
||||||
|
!this.settings.proxy_user_avatars;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use gpui::{
|
|||||||
PathPromptOptions, Render, Styled, Task, Window,
|
PathPromptOptions, Render, Styled, Task, Window,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use settings::AppSettings;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -104,6 +105,7 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 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,
|
||||||
@@ -123,7 +125,9 @@ impl Profile {
|
|||||||
let (tx, rx) = oneshot::channel::<Url>();
|
let (tx, rx) = oneshot::channel::<Url>();
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
if let Ok(url) = nip96_upload(&shared_state().client, file_data).await {
|
if let Ok(url) =
|
||||||
|
nip96_upload(&shared_state().client, nip96, file_data).await
|
||||||
|
{
|
||||||
_ = tx.send(url);
|
_ = tx.send(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use gpui::{
|
|||||||
div, img, rems, App, ClickEvent, Div, InteractiveElement, IntoElement, ParentElement as _,
|
div, img, rems, App, ClickEvent, Div, InteractiveElement, IntoElement, ParentElement as _,
|
||||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||||
};
|
};
|
||||||
|
use settings::AppSettings;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::StyledExt;
|
use ui::StyledExt;
|
||||||
@@ -59,6 +60,7 @@ impl DisplayRoom {
|
|||||||
impl RenderOnce for DisplayRoom {
|
impl RenderOnce for DisplayRoom {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let handler = self.handler.clone();
|
let handler = self.handler.clone();
|
||||||
|
let hide_avatar = AppSettings::get_global(cx).settings().hide_user_avatars;
|
||||||
|
|
||||||
self.base
|
self.base
|
||||||
.id(self.ix)
|
.id(self.ix)
|
||||||
@@ -67,7 +69,8 @@ impl RenderOnce for DisplayRoom {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.child(
|
.when(!hide_avatar, |this| {
|
||||||
|
this.child(
|
||||||
div()
|
div()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.size_6()
|
.size_6()
|
||||||
@@ -86,6 +89,7 @@ impl RenderOnce for DisplayRoom {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
|||||||
@@ -8,27 +8,29 @@ use chats::{ChatRegistry, RoomEmitter};
|
|||||||
use common::debounced_delay::DebouncedDelay;
|
use common::debounced_delay::DebouncedDelay;
|
||||||
use common::profile::RenderProfile;
|
use common::profile::RenderProfile;
|
||||||
use element::DisplayRoom;
|
use element::DisplayRoom;
|
||||||
use global::constants::SEARCH_RELAYS;
|
use global::constants::{DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
||||||
use global::shared_state;
|
use global::shared_state;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, rems, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
div, px, rems, uniform_list, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
|
||||||
FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString,
|
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
||||||
Styled, Subscription, Task, Window,
|
RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
|
||||||
|
Window,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
use ui::popup_menu::{PopupMenu, PopupMenuExt};
|
use ui::popup_menu::PopupMenu;
|
||||||
use ui::skeleton::Skeleton;
|
use ui::skeleton::Skeleton;
|
||||||
use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
|
use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::chatspace::{ModalKind, ToggleModal};
|
use crate::views::compose;
|
||||||
|
|
||||||
mod element;
|
mod element;
|
||||||
|
|
||||||
@@ -68,6 +70,7 @@ impl Sidebar {
|
|||||||
let indicator = cx.new(|_| None);
|
let indicator = cx.new(|_| None);
|
||||||
let local_result = cx.new(|_| None);
|
let local_result = cx.new(|_| None);
|
||||||
let global_result = cx.new(|_| None);
|
let global_result = cx.new(|_| None);
|
||||||
|
let trusted_only = AppSettings::get_global(cx).settings().only_show_trusted;
|
||||||
|
|
||||||
let find_input =
|
let find_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("Find or start a conversation"));
|
cx.new(|cx| InputState::new(window, cx).placeholder("Find or start a conversation"));
|
||||||
@@ -118,7 +121,7 @@ impl Sidebar {
|
|||||||
image_cache: RetainAllImageCache::new(cx),
|
image_cache: RetainAllImageCache::new(cx),
|
||||||
find_debouncer: DebouncedDelay::new(),
|
find_debouncer: DebouncedDelay::new(),
|
||||||
finding: false,
|
finding: false,
|
||||||
trusted_only: false,
|
trusted_only,
|
||||||
indicator,
|
indicator,
|
||||||
active_filter,
|
active_filter,
|
||||||
find_input,
|
find_input,
|
||||||
@@ -334,7 +337,20 @@ impl Sidebar {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_compose(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let compose = compose::init(window, cx);
|
||||||
|
|
||||||
|
window.open_modal(cx, move |modal, _window, _cx| {
|
||||||
|
modal
|
||||||
|
.title("Direct Messages")
|
||||||
|
.width(px(DEFAULT_MODAL_WIDTH))
|
||||||
|
.child(compose.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn render_account(&self, profile: &Profile, cx: &Context<Self>) -> impl IntoElement {
|
fn render_account(&self, profile: &Profile, cx: &Context<Self>) -> impl IntoElement {
|
||||||
|
let proxy = AppSettings::get_global(cx).settings().proxy_user_avatars;
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.px_3()
|
.px_3()
|
||||||
.h_8()
|
.h_8()
|
||||||
@@ -344,39 +360,27 @@ impl Sidebar {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.id("current-user")
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child(Avatar::new(profile.render_avatar()).size(rems(1.75)))
|
.child(Avatar::new(profile.render_avatar(proxy)).size(rems(1.75)))
|
||||||
.child(profile.render_name()),
|
.child(profile.render_name())
|
||||||
)
|
.on_click(cx.listener({
|
||||||
.child(
|
let Ok(public_key) = profile.public_key().to_bech32();
|
||||||
div()
|
let item = ClipboardItem::new_string(public_key);
|
||||||
.flex()
|
|
||||||
.items_center()
|
move |_, _, window, cx| {
|
||||||
.gap_2()
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
.child(
|
cx.write_to_primary(item.clone());
|
||||||
Button::new("user")
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
.icon(IconName::Ellipsis)
|
cx.write_to_clipboard(item.clone());
|
||||||
.small()
|
|
||||||
.ghost()
|
window.push_notification("User's NPUB is copied", cx);
|
||||||
.rounded(ButtonRounded::Full)
|
}
|
||||||
.popup_menu(|this, _window, _cx| {
|
})),
|
||||||
this.menu(
|
|
||||||
"Profile",
|
|
||||||
Box::new(ToggleModal {
|
|
||||||
modal: ModalKind::Profile,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.menu(
|
|
||||||
"Relays",
|
|
||||||
Box::new(ToggleModal {
|
|
||||||
modal: ModalKind::Relay,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("compose")
|
Button::new("compose")
|
||||||
@@ -385,15 +389,9 @@ impl Sidebar {
|
|||||||
.small()
|
.small()
|
||||||
.primary()
|
.primary()
|
||||||
.rounded(ButtonRounded::Full)
|
.rounded(ButtonRounded::Full)
|
||||||
.on_click(cx.listener(|_, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
window.dispatch_action(
|
this.open_compose(window, cx);
|
||||||
Box::new(ToggleModal {
|
|
||||||
modal: ModalKind::Compose,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})),
|
})),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ pub const DEFAULT_SIDEBAR_WIDTH: f32 = 280.;
|
|||||||
/// Image Resize Service
|
/// Image Resize Service
|
||||||
pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
|
pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
|
||||||
|
|
||||||
/// NIP96 Media Server.
|
/// Default NIP96 Media Server.
|
||||||
pub const NIP96_SERVER: &str = "https://nostrmedia.com";
|
pub const NIP96_SERVER: &str = "https://nostrmedia.com";
|
||||||
|
|
||||||
pub(crate) const GLOBAL_CHANNEL_LIMIT: usize = 2048;
|
pub(crate) const GLOBAL_CHANNEL_LIMIT: usize = 2048;
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ impl Globals {
|
|||||||
/// Stores an unwrapped event in local database with reference to original
|
/// Stores an unwrapped event in local database with reference to original
|
||||||
async fn set_unwrapped(&self, root: EventId, event: &Event, keys: &Keys) -> Result<(), Error> {
|
async fn set_unwrapped(&self, root: EventId, event: &Event, keys: &Keys) -> Result<(), Error> {
|
||||||
// Must be use the random generated keys to sign this event
|
// Must be use the random generated keys to sign this event
|
||||||
let event = EventBuilder::new(Kind::Custom(30078), event.as_json())
|
let event = EventBuilder::new(Kind::ApplicationSpecificData, event.as_json())
|
||||||
.tags(vec![Tag::identifier(root), Tag::event(root)])
|
.tags(vec![Tag::identifier(root), Tag::event(root)])
|
||||||
.sign(keys)
|
.sign(keys)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
16
crates/settings/Cargo.toml
Normal file
16
crates/settings/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "settings"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
global = { path = "../global" }
|
||||||
|
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
146
crates/settings/src/lib.rs
Normal file
146
crates/settings/src/lib.rs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
|
use global::shared_state;
|
||||||
|
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
let state = cx.new(AppSettings::new);
|
||||||
|
// Observe for state changes and save settings to database
|
||||||
|
state.update(cx, |this, cx| {
|
||||||
|
this.subscriptions
|
||||||
|
.push(cx.observe(&state, |this, _state, cx| {
|
||||||
|
this.set_settings(cx);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
AppSettings::set_global(state, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub media_server: Url,
|
||||||
|
pub proxy_user_avatars: bool,
|
||||||
|
pub hide_user_avatars: bool,
|
||||||
|
pub only_show_trusted: bool,
|
||||||
|
pub backup_messages: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<Settings> for Settings {
|
||||||
|
fn as_ref(&self) -> &Settings {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalAppSettings(Entity<AppSettings>);
|
||||||
|
|
||||||
|
impl Global for GlobalAppSettings {}
|
||||||
|
|
||||||
|
pub struct AppSettings {
|
||||||
|
pub settings: Settings,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppSettings {
|
||||||
|
/// Retrieve the Global Settings instance
|
||||||
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
|
cx.global::<GlobalAppSettings>().0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the Settings instance
|
||||||
|
pub fn get_global(cx: &App) -> &Self {
|
||||||
|
cx.global::<GlobalAppSettings>().0.read(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the global Settings instance
|
||||||
|
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||||
|
cx.set_global(GlobalAppSettings(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
let settings = Settings {
|
||||||
|
media_server: Url::parse("https://nostrmedia.com").expect("it's fine"),
|
||||||
|
proxy_user_avatars: true,
|
||||||
|
hide_user_avatars: false,
|
||||||
|
only_show_trusted: false,
|
||||||
|
backup_messages: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(cx.observe_new::<Self>(|this, _window, cx| {
|
||||||
|
this.get_settings_from_db(cx);
|
||||||
|
}));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
settings,
|
||||||
|
subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settings(&self) -> &Settings {
|
||||||
|
self.settings.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_settings_from_db(&self, cx: &mut Context<Self>) {
|
||||||
|
let task: Task<Result<Settings, anyhow::Error>> = cx.background_spawn(async move {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::ApplicationSpecificData)
|
||||||
|
.identifier("coop-settings")
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if let Some(event) = shared_state()
|
||||||
|
.client
|
||||||
|
.database()
|
||||||
|
.query(filter)
|
||||||
|
.await?
|
||||||
|
.first_owned()
|
||||||
|
{
|
||||||
|
log::info!("Successfully loaded settings from database");
|
||||||
|
Ok(serde_json::from_str(&event.content)?)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Not found"))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
if let Ok(settings) = task.await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.settings = settings;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_settings(&self, cx: &mut Context<Self>) {
|
||||||
|
if let Ok(content) = serde_json::to_string(&self.settings) {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let Some(identity) = shared_state().identity() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let keys = Keys::generate();
|
||||||
|
let ident = Tag::identifier("coop-settings");
|
||||||
|
|
||||||
|
if let Ok(event) = EventBuilder::new(Kind::ApplicationSpecificData, content)
|
||||||
|
.tags(vec![ident])
|
||||||
|
.build(identity.public_key())
|
||||||
|
.sign(&keys)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if let Err(e) = shared_state().client.database().save_event(&event).await {
|
||||||
|
log::error!("Failed to save user settings: {e}");
|
||||||
|
} else {
|
||||||
|
log::info!("New settings have been saved successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -373,6 +373,7 @@ impl Theme {
|
|||||||
Self::change(appearance, window, cx);
|
Self::change(appearance, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change the app's appearance
|
||||||
pub fn change(mode: impl Into<ThemeMode>, window: Option<&mut Window>, cx: &mut App) {
|
pub fn change(mode: impl Into<ThemeMode>, window: Option<&mut Window>, cx: &mut App) {
|
||||||
let mode = mode.into();
|
let mode = mode.into();
|
||||||
let colors = match mode {
|
let colors = match mode {
|
||||||
|
|||||||
@@ -1,54 +1,117 @@
|
|||||||
use std::rc::Rc;
|
use std::{rc::Rc, time::Duration};
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, anchored, div, point, px, relative, Animation, AnimationExt as _, AnyElement, App,
|
anchored, div, point, prelude::FluentBuilder, px, relative, Animation, AnimationExt as _,
|
||||||
Bounds, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding, MouseButton,
|
AnyElement, App, Bounds, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement,
|
||||||
ParentElement, Pixels, Point, RenderOnce, SharedString, Styled, Window,
|
KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, Styled,
|
||||||
|
Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::animation::cubic_bezier;
|
use crate::{
|
||||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariants as _};
|
actions::{Cancel, Confirm},
|
||||||
use crate::{v_flex, ContextModal, IconName, StyledExt};
|
animation::cubic_bezier,
|
||||||
|
button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _},
|
||||||
actions!(modal, [Escape]);
|
h_flex, v_flex, ContextModal, IconName, Root, StyledExt,
|
||||||
|
};
|
||||||
|
|
||||||
const CONTEXT: &str = "Modal";
|
const CONTEXT: &str = "Modal";
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))])
|
cx.bind_keys([
|
||||||
|
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
||||||
|
KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
type OnClose = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
type OnClose = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||||
|
type OnOk = Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>>;
|
||||||
|
type OnCancel = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>;
|
||||||
|
type RenderButtonFn = Box<dyn FnOnce(&mut Window, &mut App) -> AnyElement>;
|
||||||
|
type FooterFn =
|
||||||
|
Box<dyn Fn(RenderButtonFn, RenderButtonFn, &mut Window, &mut App) -> Vec<AnyElement>>;
|
||||||
|
|
||||||
|
/// Modal button props.
|
||||||
|
pub struct ModalButtonProps {
|
||||||
|
ok_text: Option<SharedString>,
|
||||||
|
ok_variant: ButtonVariant,
|
||||||
|
cancel_text: Option<SharedString>,
|
||||||
|
cancel_variant: ButtonVariant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ModalButtonProps {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ok_text: None,
|
||||||
|
ok_variant: ButtonVariant::Primary,
|
||||||
|
cancel_text: None,
|
||||||
|
cancel_variant: ButtonVariant::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModalButtonProps {
|
||||||
|
/// Sets the text of the OK button. Default is `OK`.
|
||||||
|
pub fn ok_text(mut self, ok_text: impl Into<SharedString>) -> Self {
|
||||||
|
self.ok_text = Some(ok_text.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the variant of the OK button. Default is `ButtonVariant::Primary`.
|
||||||
|
pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self {
|
||||||
|
self.ok_variant = ok_variant;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text of the Cancel button. Default is `Cancel`.
|
||||||
|
pub fn cancel_text(mut self, cancel_text: impl Into<SharedString>) -> Self {
|
||||||
|
self.cancel_text = Some(cancel_text.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the variant of the Cancel button. Default is `ButtonVariant::default()`.
|
||||||
|
pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self {
|
||||||
|
self.cancel_variant = cancel_variant;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Modal {
|
pub struct Modal {
|
||||||
base: Div,
|
base: Div,
|
||||||
title: Option<AnyElement>,
|
title: Option<AnyElement>,
|
||||||
footer: Option<AnyElement>,
|
footer: Option<FooterFn>,
|
||||||
content: Div,
|
content: Div,
|
||||||
width: Pixels,
|
width: Pixels,
|
||||||
max_width: Option<Pixels>,
|
max_width: Option<Pixels>,
|
||||||
margin_top: Option<Pixels>,
|
margin_top: Option<Pixels>,
|
||||||
|
|
||||||
on_close: OnClose,
|
on_close: OnClose,
|
||||||
closable: bool,
|
on_ok: OnOk,
|
||||||
|
on_cancel: OnCancel,
|
||||||
|
button_props: ModalButtonProps,
|
||||||
|
show_close: bool,
|
||||||
|
overlay: bool,
|
||||||
|
overlay_closable: bool,
|
||||||
keyboard: bool,
|
keyboard: bool,
|
||||||
|
|
||||||
/// 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,
|
||||||
pub(crate) layer_ix: usize,
|
pub(crate) layer_ix: usize,
|
||||||
pub(crate) overlay: bool,
|
pub(crate) overlay_visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
let base = v_flex()
|
||||||
.bg(cx.theme().background)
|
.bg(cx.theme().background)
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.rounded_xl()
|
.rounded(radius)
|
||||||
.shadow_md();
|
.shadow_xl()
|
||||||
|
.min_h_24();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
base,
|
base,
|
||||||
@@ -61,9 +124,14 @@ impl Modal {
|
|||||||
max_width: None,
|
max_width: None,
|
||||||
overlay: true,
|
overlay: true,
|
||||||
keyboard: true,
|
keyboard: true,
|
||||||
closable: true,
|
|
||||||
layer_ix: 0,
|
layer_ix: 0,
|
||||||
|
overlay_visible: false,
|
||||||
on_close: Rc::new(|_, _, _| {}),
|
on_close: Rc::new(|_, _, _| {}),
|
||||||
|
on_ok: None,
|
||||||
|
on_cancel: Rc::new(|_, _, _| true),
|
||||||
|
button_props: ModalButtonProps::default(),
|
||||||
|
show_close: true,
|
||||||
|
overlay_closable: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,12 +142,54 @@ impl Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the footer of the modal.
|
/// Set the footer of the modal.
|
||||||
pub fn footer(mut self, footer: impl IntoElement) -> Self {
|
///
|
||||||
self.footer = Some(footer.into_any_element());
|
/// The `footer` is a function that takes two `RenderButtonFn` and a `WindowContext` and returns a list of `AnyElement`.
|
||||||
|
///
|
||||||
|
/// - First `RenderButtonFn` is the render function for the OK button.
|
||||||
|
/// - Second `RenderButtonFn` is the render function for the CANCEL button.
|
||||||
|
///
|
||||||
|
/// When you set the footer, the footer will be placed default footer buttons.
|
||||||
|
pub fn footer<E, F>(mut self, footer: F) -> Self
|
||||||
|
where
|
||||||
|
E: IntoElement,
|
||||||
|
F: Fn(RenderButtonFn, RenderButtonFn, &mut Window, &mut App) -> Vec<E> + 'static,
|
||||||
|
{
|
||||||
|
self.footer = Some(Box::new(move |ok, cancel, window, cx| {
|
||||||
|
footer(ok, cancel, window, cx)
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| e.into_any_element())
|
||||||
|
.collect()
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set to use confirm modal, with OK and Cancel buttons.
|
||||||
|
///
|
||||||
|
/// See also [`Self::alert`]
|
||||||
|
pub fn confirm(self) -> Self {
|
||||||
|
self.footer(|ok, cancel, window, cx| vec![cancel(window, cx), ok(window, cx)])
|
||||||
|
.overlay_closable(false)
|
||||||
|
.show_close(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set to as a alter modal, with OK button.
|
||||||
|
///
|
||||||
|
/// See also [`Self::confirm`]
|
||||||
|
pub fn alert(self) -> Self {
|
||||||
|
self.footer(|ok, _, window, cx| vec![ok(window, cx)])
|
||||||
|
.overlay_closable(false)
|
||||||
|
.show_close(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the button props of the modal.
|
||||||
|
pub fn button_props(mut self, button_props: ModalButtonProps) -> Self {
|
||||||
|
self.button_props = button_props;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the callback for when the modal is closed.
|
/// Sets the callback for when the modal is closed.
|
||||||
|
///
|
||||||
|
/// Called after [`Self::on_ok`] or [`Self::on_cancel`] callback.
|
||||||
pub fn on_close(
|
pub fn on_close(
|
||||||
mut self,
|
mut self,
|
||||||
on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
@@ -88,9 +198,31 @@ impl Modal {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the false to make modal unclosable, default: true
|
/// Sets the callback for when the modal is has been confirmed.
|
||||||
pub fn closable(mut self, closable: bool) -> Self {
|
///
|
||||||
self.closable = closable;
|
/// The callback should return `true` to close the modal, if return `false` the modal will not be closed.
|
||||||
|
pub fn on_ok(
|
||||||
|
mut self,
|
||||||
|
on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_ok = Some(Rc::new(on_ok));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the callback for when the modal is has been canceled.
|
||||||
|
///
|
||||||
|
/// The callback should return `true` to close the modal, if return `false` the modal will not be closed.
|
||||||
|
pub fn on_cancel(
|
||||||
|
mut self,
|
||||||
|
on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_cancel = Rc::new(on_cancel);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the false to hide close icon, default: true
|
||||||
|
pub fn show_close(mut self, show_close: bool) -> Self {
|
||||||
|
self.show_close = show_close;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +250,14 @@ impl Modal {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the overlay closable of the modal, defaults to `true`.
|
||||||
|
///
|
||||||
|
/// When the overlay is clicked, the modal will be closed.
|
||||||
|
pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
|
||||||
|
self.overlay_closable = overlay_closable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set whether to support keyboard esc to close the modal, defaults to `true`.
|
/// Set whether to support keyboard esc to close the modal, defaults to `true`.
|
||||||
pub fn keyboard(mut self, keyboard: bool) -> Self {
|
pub fn keyboard(mut self, keyboard: bool) -> Self {
|
||||||
self.keyboard = keyboard;
|
self.keyboard = keyboard;
|
||||||
@@ -145,6 +285,64 @@ impl RenderOnce for Modal {
|
|||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
|
||||||
let layer_ix = self.layer_ix;
|
let layer_ix = self.layer_ix;
|
||||||
let on_close = self.on_close.clone();
|
let on_close = self.on_close.clone();
|
||||||
|
let on_ok = self.on_ok.clone();
|
||||||
|
let on_cancel = self.on_cancel.clone();
|
||||||
|
|
||||||
|
let render_ok: RenderButtonFn = Box::new({
|
||||||
|
let on_ok = on_ok.clone();
|
||||||
|
let on_close = on_close.clone();
|
||||||
|
let ok_text = self.button_props.ok_text.unwrap_or_else(|| "Ok".into());
|
||||||
|
let ok_variant = self.button_props.ok_variant;
|
||||||
|
move |_, _| {
|
||||||
|
Button::new("ok")
|
||||||
|
.label(ok_text)
|
||||||
|
.with_variant(ok_variant)
|
||||||
|
.on_click({
|
||||||
|
let on_ok = on_ok.clone();
|
||||||
|
let on_close = on_close.clone();
|
||||||
|
|
||||||
|
move |_, window, cx| {
|
||||||
|
if let Some(on_ok) = &on_ok {
|
||||||
|
if !on_ok(&ClickEvent::default(), window, cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on_close(&ClickEvent::default(), window, cx);
|
||||||
|
window.close_modal(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let render_cancel: RenderButtonFn = Box::new({
|
||||||
|
let on_cancel = on_cancel.clone();
|
||||||
|
let on_close = on_close.clone();
|
||||||
|
let cancel_text = self
|
||||||
|
.button_props
|
||||||
|
.cancel_text
|
||||||
|
.unwrap_or_else(|| "Cancel".into());
|
||||||
|
let cancel_variant = self.button_props.cancel_variant;
|
||||||
|
move |_, _| {
|
||||||
|
Button::new("cancel")
|
||||||
|
.label(cancel_text)
|
||||||
|
.with_variant(cancel_variant)
|
||||||
|
.on_click({
|
||||||
|
let on_cancel = on_cancel.clone();
|
||||||
|
let on_close = on_close.clone();
|
||||||
|
move |_, window, cx| {
|
||||||
|
if !on_cancel(&ClickEvent::default(), window, cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
on_close(&ClickEvent::default(), window, cx);
|
||||||
|
window.close_modal(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let window_paddings = crate::window_border::window_paddings(window, cx);
|
let window_paddings = crate::window_border::window_paddings(window, cx);
|
||||||
let view_size = window.viewport_size()
|
let view_size = window.viewport_size()
|
||||||
- gpui::size(
|
- gpui::size(
|
||||||
@@ -155,8 +353,8 @@ impl RenderOnce for Modal {
|
|||||||
origin: Point::default(),
|
origin: Point::default(),
|
||||||
size: view_size,
|
size: view_size,
|
||||||
};
|
};
|
||||||
let offset_top = px(layer_ix as f32 * 2.);
|
let offset_top = px(layer_ix as f32 * 16.);
|
||||||
let y = self.margin_top.unwrap_or(view_size.height / 16.) + 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.;
|
||||||
|
|
||||||
anchored()
|
anchored()
|
||||||
@@ -164,14 +362,22 @@ impl RenderOnce for Modal {
|
|||||||
.snap_to_window()
|
.snap_to_window()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.occlude()
|
|
||||||
.w(view_size.width)
|
.w(view_size.width)
|
||||||
.h(view_size.height)
|
.h(view_size.height)
|
||||||
.when(self.overlay, |this| this.bg(cx.theme().overlay))
|
.when(self.overlay_visible, |this| {
|
||||||
.when(self.keyboard, |this| {
|
this.occlude().bg(cx.theme().overlay)
|
||||||
|
})
|
||||||
|
.when(self.overlay_closable, |this| {
|
||||||
|
// Only the last modal owns the `mouse down - close modal` event.
|
||||||
|
if (self.layer_ix + 1) != Root::read(window, cx).active_modals.len() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
this.on_mouse_down(MouseButton::Left, {
|
this.on_mouse_down(MouseButton::Left, {
|
||||||
let on_close = self.on_close.clone();
|
let on_cancel = on_cancel.clone();
|
||||||
|
let on_close = on_close.clone();
|
||||||
move |_, window, cx| {
|
move |_, 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);
|
||||||
}
|
}
|
||||||
@@ -182,8 +388,39 @@ impl RenderOnce for Modal {
|
|||||||
.id(SharedString::from(format!("modal-{layer_ix}")))
|
.id(SharedString::from(format!("modal-{layer_ix}")))
|
||||||
.key_context(CONTEXT)
|
.key_context(CONTEXT)
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
|
.when(self.keyboard, |this| {
|
||||||
|
this.on_action({
|
||||||
|
let on_cancel = on_cancel.clone();
|
||||||
|
let on_close = on_close.clone();
|
||||||
|
move |_: &Cancel, window, cx| {
|
||||||
|
// FIXME:
|
||||||
|
//
|
||||||
|
// Here some Modal have no focus_handle, so it will not work will Escape key.
|
||||||
|
// But by now, we `cx.close_modal()` going to close the last active model, so the Escape is unexpected to work.
|
||||||
|
on_cancel(&ClickEvent::default(), window, cx);
|
||||||
|
on_close(&ClickEvent::default(), window, cx);
|
||||||
|
window.close_modal(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_action({
|
||||||
|
let on_ok = on_ok.clone();
|
||||||
|
let on_close = on_close.clone();
|
||||||
|
let has_footer = self.footer.is_some();
|
||||||
|
move |_: &Confirm, window, cx| {
|
||||||
|
if let Some(on_ok) = &on_ok {
|
||||||
|
if on_ok(&ClickEvent::default(), window, cx) {
|
||||||
|
on_close(&ClickEvent::default(), window, cx);
|
||||||
|
window.close_modal(cx);
|
||||||
|
}
|
||||||
|
} else if has_footer {
|
||||||
|
window.close_modal(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
.absolute()
|
.absolute()
|
||||||
.occlude()
|
.occlude()
|
||||||
|
.relative()
|
||||||
.left(x)
|
.left(x)
|
||||||
.top(y)
|
.top(y)
|
||||||
.w(self.width)
|
.w(self.width)
|
||||||
@@ -203,7 +440,7 @@ impl RenderOnce for Modal {
|
|||||||
.child(title),
|
.child(title),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(self.closable, |this| {
|
.when(self.show_close, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new(SharedString::from(format!(
|
Button::new(SharedString::from(format!(
|
||||||
"modal-close-{layer_ix}"
|
"modal-close-{layer_ix}"
|
||||||
@@ -221,26 +458,23 @@ impl RenderOnce for Modal {
|
|||||||
)
|
)
|
||||||
.on_click(
|
.on_click(
|
||||||
move |_, window, cx| {
|
move |_, 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(self.content)
|
.child(div().w_full().flex_1().child(self.content))
|
||||||
.children(self.footer)
|
.when(self.footer.is_some(), |this| {
|
||||||
.when(self.keyboard, |this| {
|
let footer = self.footer.unwrap();
|
||||||
this.on_action({
|
|
||||||
let on_close = self.on_close.clone();
|
this.child(h_flex().gap_2().justify_end().children(footer(
|
||||||
move |_: &Escape, window, cx| {
|
render_ok,
|
||||||
// FIXME:
|
render_cancel,
|
||||||
//
|
window,
|
||||||
// Here some Modal have no focus_handle, so it will not work will Escape key.
|
cx,
|
||||||
// But by now, we `cx.close_modal()` going to close the last active model, so the Escape is unexpected to work.
|
)))
|
||||||
on_close(&ClickEvent::default(), window, cx);
|
|
||||||
window.close_modal(cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.with_animation(
|
.with_animation(
|
||||||
"slide-down",
|
"slide-down",
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ impl ContextModal for Window {
|
|||||||
type Builder = Rc<dyn Fn(Modal, &mut Window, &mut App) -> Modal + 'static>;
|
type Builder = Rc<dyn Fn(Modal, &mut Window, &mut App) -> Modal + 'static>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ActiveModal {
|
pub(crate) struct ActiveModal {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
builder: Builder,
|
builder: Builder,
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ pub struct ActiveModal {
|
|||||||
///
|
///
|
||||||
/// It is used to manage the Modal, and Notification.
|
/// It is used to manage the Modal, and Notification.
|
||||||
pub struct Root {
|
pub struct Root {
|
||||||
pub active_modals: Vec<ActiveModal>,
|
pub(crate) active_modals: Vec<ActiveModal>,
|
||||||
pub notification: Entity<NotificationList>,
|
pub notification: Entity<NotificationList>,
|
||||||
pub focused_input: Option<Entity<InputState>>,
|
pub focused_input: Option<Entity<InputState>>,
|
||||||
/// Used to store the focus handle of the previous view.
|
/// Used to store the focus handle of the previous view.
|
||||||
@@ -194,36 +194,46 @@ impl Root {
|
|||||||
/// Render the Modal layer.
|
/// Render the Modal layer.
|
||||||
pub fn render_modal_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
|
pub fn render_modal_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
|
||||||
let root = window.root::<Root>()??;
|
let root = window.root::<Root>()??;
|
||||||
|
|
||||||
let active_modals = root.read(cx).active_modals.clone();
|
let active_modals = root.read(cx).active_modals.clone();
|
||||||
let mut has_overlay = false;
|
|
||||||
|
|
||||||
if active_modals.is_empty() {
|
if active_modals.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(
|
let mut show_overlay_ix = None;
|
||||||
div().children(active_modals.iter().enumerate().map(|(i, active_modal)| {
|
|
||||||
|
let mut modals = active_modals
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, active_modal)| {
|
||||||
let mut modal = Modal::new(window, cx);
|
let mut modal = Modal::new(window, cx);
|
||||||
|
|
||||||
modal = (active_modal.builder)(modal, window, cx);
|
modal = (active_modal.builder)(modal, window, cx);
|
||||||
modal.layer_ix = i;
|
|
||||||
// Give the modal the focus handle, because `modal` is a temporary value, is not possible to
|
// Give the modal the focus handle, because `modal` is a temporary value, is not possible to
|
||||||
// keep the focus handle in the modal.
|
// keep the focus handle in the modal.
|
||||||
//
|
//
|
||||||
// So we keep the focus handle in the `active_modal`, this is owned by the `Root`.
|
// So we keep the focus handle in the `active_modal`, this is owned by the `Root`.
|
||||||
modal.focus_handle = active_modal.focus_handle.clone();
|
modal.focus_handle = active_modal.focus_handle.clone();
|
||||||
|
|
||||||
// Keep only have one overlay, we only render the first modal with overlay.
|
modal.layer_ix = i;
|
||||||
if has_overlay {
|
// Find the modal which one needs to show overlay.
|
||||||
modal.overlay = false;
|
|
||||||
}
|
|
||||||
if modal.has_overlay() {
|
if modal.has_overlay() {
|
||||||
has_overlay = true;
|
show_overlay_ix = Some(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
modal
|
modal
|
||||||
})),
|
})
|
||||||
)
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if let Some(ix) = show_overlay_ix {
|
||||||
|
if let Some(modal) = modals.get_mut(ix) {
|
||||||
|
modal.overlay_visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(div().children(modals))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the root view of the Root.
|
/// Return the root view of the Root.
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId,
|
div, px, white, Animation, AnimationExt as _, AnyElement, App, Element, ElementId,
|
||||||
InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _,
|
GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString,
|
||||||
Window,
|
Styled as _, Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::{h_flex, Disableable, Side, Sizable, Size};
|
use crate::{Disableable, Side, Sizable, Size};
|
||||||
|
|
||||||
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
|
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ pub struct Switch {
|
|||||||
checked: bool,
|
checked: bool,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
label: Option<SharedString>,
|
label: Option<SharedString>,
|
||||||
|
description: Option<SharedString>,
|
||||||
label_side: Side,
|
label_side: Side,
|
||||||
on_click: OnClick,
|
on_click: OnClick,
|
||||||
size: Size,
|
size: Size,
|
||||||
@@ -27,13 +28,15 @@ pub struct Switch {
|
|||||||
impl Switch {
|
impl Switch {
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
let id: ElementId = id.into();
|
let id: ElementId = id.into();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
checked: false,
|
checked: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
label: None,
|
label: None,
|
||||||
|
description: None,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
label_side: Side::Right,
|
label_side: Side::Left,
|
||||||
size: Size::Medium,
|
size: Size::Medium,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,6 +51,11 @@ impl Switch {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
|
||||||
|
self.description = Some(description.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_click<F>(mut self, handler: F) -> Self
|
pub fn on_click<F>(mut self, handler: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(&bool, &mut Window, &mut App) + 'static,
|
F: Fn(&bool, &mut Window, &mut App) + 'static,
|
||||||
@@ -116,8 +124,8 @@ impl Element for Switch {
|
|||||||
let on_click = self.on_click.clone();
|
let on_click = self.on_click.clone();
|
||||||
|
|
||||||
let (bg, toggle_bg) = match self.checked {
|
let (bg, toggle_bg) = match self.checked {
|
||||||
true => (theme.icon_accent, theme.background),
|
true => (theme.element_background, white()),
|
||||||
false => (theme.element_background, theme.background),
|
false => (theme.elevated_surface_background, white()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (bg, toggle_bg) = match self.disabled {
|
let (bg, toggle_bg) = match self.disabled {
|
||||||
@@ -138,17 +146,28 @@ impl Element for Switch {
|
|||||||
let inset = px(2.);
|
let inset = px(2.);
|
||||||
|
|
||||||
let mut element = div()
|
let mut element = div()
|
||||||
.flex()
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.items_center()
|
|
||||||
.gap_2()
|
|
||||||
.when(self.label_side.is_left(), |this| this.flex_row_reverse())
|
.when(self.label_side.is_left(), |this| this.flex_row_reverse())
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
|
.flex()
|
||||||
|
.justify_between()
|
||||||
|
.items_center()
|
||||||
|
.gap_4()
|
||||||
|
.when_some(self.label.clone(), |this, label| {
|
||||||
|
// Label
|
||||||
|
this.child(
|
||||||
|
div().text_sm().text_color(cx.theme().text).child(label),
|
||||||
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
// Switch Bar
|
// Switch Bar
|
||||||
div()
|
div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
|
.flex_shrink_0()
|
||||||
.w(bg_width)
|
.w(bg_width)
|
||||||
.h(bg_height)
|
.h(bg_height)
|
||||||
.rounded(bg_height / 2.)
|
.rounded(bg_height / 2.)
|
||||||
@@ -160,8 +179,12 @@ impl Element for Switch {
|
|||||||
.when(!self.disabled, |this| this.cursor_pointer())
|
.when(!self.disabled, |this| this.cursor_pointer())
|
||||||
.child(
|
.child(
|
||||||
// Switch Toggle
|
// Switch Toggle
|
||||||
div().rounded_full().bg(toggle_bg).size(bar_width).map(
|
div()
|
||||||
|this| {
|
.rounded_full()
|
||||||
|
.shadow_sm()
|
||||||
|
.bg(toggle_bg)
|
||||||
|
.size(bar_width)
|
||||||
|
.map(|this| {
|
||||||
let prev_checked = state.prev_checked.clone();
|
let prev_checked = state.prev_checked.clone();
|
||||||
if !self.disabled
|
if !self.disabled
|
||||||
&& prev_checked
|
&& prev_checked
|
||||||
@@ -170,8 +193,11 @@ impl Element for Switch {
|
|||||||
{
|
{
|
||||||
let dur = Duration::from_secs_f64(0.15);
|
let dur = Duration::from_secs_f64(0.15);
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
cx.background_executor().timer(dur).await;
|
cx.background_executor()
|
||||||
*prev_checked.borrow_mut() = Some(checked);
|
.timer(dur)
|
||||||
|
.await;
|
||||||
|
*prev_checked.borrow_mut() =
|
||||||
|
Some(checked);
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
this.with_animation(
|
this.with_animation(
|
||||||
@@ -181,8 +207,9 @@ impl Element for Switch {
|
|||||||
),
|
),
|
||||||
Animation::new(dur),
|
Animation::new(dur),
|
||||||
move |this, delta| {
|
move |this, delta| {
|
||||||
let max_x =
|
let max_x = bg_width
|
||||||
bg_width - bar_width - inset * 2;
|
- bar_width
|
||||||
|
- inset * 2;
|
||||||
let x = if checked {
|
let x = if checked {
|
||||||
max_x * delta
|
max_x * delta
|
||||||
} else {
|
} else {
|
||||||
@@ -193,19 +220,24 @@ impl Element for Switch {
|
|||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
let max_x = bg_width - bar_width - inset * 2;
|
let max_x =
|
||||||
let x = if checked { max_x } else { px(0.) };
|
bg_width - bar_width - inset * 2;
|
||||||
|
let x =
|
||||||
|
if checked { max_x } else { px(0.) };
|
||||||
this.left(x).into_any_element()
|
this.left(x).into_any_element()
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when_some(self.label.clone(), |this, label| {
|
.when_some(self.description.clone(), |this, description| {
|
||||||
this.child(div().child(label).map(|this| match self.size {
|
this.child(
|
||||||
Size::XSmall | Size::Small => this.text_sm(),
|
div()
|
||||||
_ => this.text_base(),
|
.w_3_4()
|
||||||
}))
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(description),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.when_some(
|
.when_some(
|
||||||
on_click
|
on_click
|
||||||
|
|||||||
Reference in New Issue
Block a user