feat: Redesign New Chat (#31)
* make subject is optional * redesign * search * fix * adjust
This commit is contained in:
76
Cargo.lock
generated
76
Cargo.lock
generated
@@ -966,6 +966,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"common",
|
"common",
|
||||||
|
"fuzzy-matcher",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
@@ -1136,7 +1137,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1171,13 +1172,14 @@ version = "0.1.5"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"futures",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"qrcode-generator",
|
"qrcode-generator",
|
||||||
"random_name_generator",
|
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"smol",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1527,7 +1529,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#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2169,6 +2171,15 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fuzzy-matcher"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
|
||||||
|
dependencies = [
|
||||||
|
"thread_local",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@@ -2318,7 +2329,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2410,7 +2421,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#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2634,7 +2645,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#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2651,7 +2662,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#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3109,12 +3120,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "joinery"
|
|
||||||
version = "2.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jpeg-decoder"
|
name = "jpeg-decoder"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -3392,7 +3397,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -4699,23 +4704,6 @@ dependencies = [
|
|||||||
"getrandom 0.3.2",
|
"getrandom 0.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "random_name_generator"
|
|
||||||
version = "0.3.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "83f35cf4ff1039c849a4d890c6aa4332df47f9def1e9398ef1e5959bc7f89992"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"bitflags 2.9.0",
|
|
||||||
"clap",
|
|
||||||
"lazy_static",
|
|
||||||
"log",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"regex",
|
|
||||||
"rust-embed",
|
|
||||||
"titlecase",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rangemap"
|
name = "rangemap"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
@@ -4843,7 +4831,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
@@ -4982,7 +4970,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#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5168,11 +5156,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.11.0"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
|
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"web-time",
|
"web-time",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5452,7 +5441,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#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5775,7 +5764,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#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -6196,17 +6185,6 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "titlecase"
|
|
||||||
version = "2.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "38397a8cdb017cfeb48bf6c154d6de975ac69ffeed35980fde199d2ee0842042"
|
|
||||||
dependencies = [
|
|
||||||
"joinery",
|
|
||||||
"lazy_static",
|
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.45.0"
|
version = "1.45.0"
|
||||||
@@ -6695,7 +6673,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util"
|
name = "util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24"
|
source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ nostr-keyring = { git = "https://github.com/rust-nostr/nostr" }
|
|||||||
# Others
|
# Others
|
||||||
emojis = "0.6.4"
|
emojis = "0.6.4"
|
||||||
smol = "2"
|
smol = "2"
|
||||||
|
futures = "0.3"
|
||||||
oneshot = "0.1.10"
|
oneshot = "0.1.10"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
futures = "0.3.30"
|
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.44"
|
||||||
|
|||||||
3
assets/icons/plus-fill.svg
Normal file
3
assets/icons/plus-fill.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 fill="currentColor" fill-rule="evenodd" d="M12 6a1 1 0 0 1 1 1v4h4a1 1 0 1 1 0 2h-4v4a1 1 0 1 1-2 0v-4H7a1 1 0 1 1 0-2h4V7a1 1 0 0 1 1-1Z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 272 B |
@@ -27,6 +27,10 @@ impl Account {
|
|||||||
cx.global::<GlobalAccount>().0.clone()
|
cx.global::<GlobalAccount>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_global(cx: &App) -> &Self {
|
||||||
|
cx.global::<GlobalAccount>().0.read(cx)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_global(account: Entity<Self>, cx: &mut App) {
|
pub fn set_global(account: Entity<Self>, cx: &mut App) {
|
||||||
cx.set_global(GlobalAccount(account));
|
cx.set_global(GlobalAccount(account));
|
||||||
}
|
}
|
||||||
@@ -162,6 +166,11 @@ impl Account {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the reference to profile.
|
||||||
|
pub fn profile_ref(&self) -> Option<&Profile> {
|
||||||
|
self.profile.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the profile for the account.
|
/// Sets the profile for the account.
|
||||||
pub fn profile(&mut self, profile: Profile, cx: &mut Context<Self>) {
|
pub fn profile(&mut self, profile: Profile, cx: &mut Context<Self>) {
|
||||||
self.profile = Some(profile);
|
self.profile = Some(profile);
|
||||||
|
|||||||
@@ -20,3 +20,5 @@ smallvec.workspace = true
|
|||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
oneshot.workspace = true
|
oneshot.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
||||||
|
fuzzy-matcher = "0.3.7"
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
use std::{cmp::Reverse, collections::HashMap};
|
use std::{
|
||||||
|
cmp::Reverse,
|
||||||
|
collections::{BTreeMap, BTreeSet, HashMap},
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use common::room_hash;
|
use common::room_hash;
|
||||||
|
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||||
use global::get_client;
|
use global::get_client;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
|
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@@ -33,14 +37,11 @@ impl Global for GlobalChatRegistry {}
|
|||||||
/// - Handling messages and room creation
|
/// - Handling messages and room creation
|
||||||
pub struct ChatRegistry {
|
pub struct ChatRegistry {
|
||||||
/// Collection of all chat rooms
|
/// Collection of all chat rooms
|
||||||
rooms: Vec<Entity<Room>>,
|
rooms: BTreeSet<Entity<Room>>,
|
||||||
|
|
||||||
/// Map of user public keys to their profile metadata
|
/// Map of user public keys to their profile metadata
|
||||||
profiles: Entity<HashMap<PublicKey, Option<Metadata>>>,
|
profiles: Entity<BTreeMap<PublicKey, Option<Metadata>>>,
|
||||||
|
|
||||||
/// Indicates if rooms are currently being loaded
|
/// Indicates if rooms are currently being loaded
|
||||||
loading: bool,
|
pub loading: bool,
|
||||||
|
|
||||||
/// Subscriptions for observing changes
|
/// Subscriptions for observing changes
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
subscriptions: SmallVec<[Subscription; 1]>,
|
subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
@@ -52,6 +53,11 @@ impl ChatRegistry {
|
|||||||
cx.global::<GlobalChatRegistry>().0.clone()
|
cx.global::<GlobalChatRegistry>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve the ChatRegistry instance
|
||||||
|
pub fn get_global(cx: &App) -> &Self {
|
||||||
|
cx.global::<GlobalChatRegistry>().0.read(cx)
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the global ChatRegistry instance
|
/// Set the global ChatRegistry instance
|
||||||
pub fn set_global(state: Entity<Self>, cx: &mut App) {
|
pub fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||||
cx.set_global(GlobalChatRegistry(state));
|
cx.set_global(GlobalChatRegistry(state));
|
||||||
@@ -59,7 +65,7 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
/// Create a new ChatRegistry instance
|
/// Create a new ChatRegistry instance
|
||||||
fn new(cx: &mut Context<Self>) -> Self {
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
let profiles = cx.new(|_| HashMap::new());
|
let profiles = cx.new(|_| BTreeMap::new());
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
// Observe new Room creations to collect profile metadata
|
// Observe new Room creations to collect profile metadata
|
||||||
@@ -82,18 +88,13 @@ impl ChatRegistry {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
rooms: vec![],
|
rooms: BTreeSet::new(),
|
||||||
loading: true,
|
loading: true,
|
||||||
profiles,
|
profiles,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the global loading status
|
|
||||||
pub fn loading(&self) -> bool {
|
|
||||||
self.loading
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a room by its ID.
|
/// Get a room by its ID.
|
||||||
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
|
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
|
||||||
self.rooms
|
self.rooms
|
||||||
@@ -103,33 +104,43 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get all rooms grouped by their kind.
|
/// Get all rooms grouped by their kind.
|
||||||
pub fn rooms(&self, cx: &App) -> HashMap<RoomKind, Vec<&Entity<Room>>> {
|
pub fn rooms(&self, cx: &App) -> BTreeMap<RoomKind, Vec<Entity<Room>>> {
|
||||||
let mut groups = HashMap::new();
|
let mut groups = BTreeMap::new();
|
||||||
groups.insert(RoomKind::Ongoing, Vec::new());
|
groups.insert(RoomKind::Ongoing, Vec::new());
|
||||||
groups.insert(RoomKind::Trusted, Vec::new());
|
groups.insert(RoomKind::Trusted, Vec::new());
|
||||||
groups.insert(RoomKind::Unknown, Vec::new());
|
groups.insert(RoomKind::Unknown, Vec::new());
|
||||||
|
|
||||||
for room in self.rooms.iter() {
|
for room in self.rooms.iter() {
|
||||||
let kind = room.read(cx).kind;
|
let kind = room.read(cx).kind;
|
||||||
groups.entry(kind).or_insert_with(Vec::new).push(room);
|
groups
|
||||||
|
.entry(kind)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(room.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
groups
|
groups
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get rooms by their kind.
|
|
||||||
pub fn rooms_by_kind(&self, kind: RoomKind, cx: &App) -> Vec<&Entity<Room>> {
|
|
||||||
self.rooms
|
|
||||||
.iter()
|
|
||||||
.filter(|room| room.read(cx).kind == kind)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the IDs of all rooms.
|
/// Get the IDs of all rooms.
|
||||||
pub fn room_ids(&self, cx: &mut Context<Self>) -> Vec<u64> {
|
pub fn room_ids(&self, cx: &mut Context<Self>) -> Vec<u64> {
|
||||||
self.rooms.iter().map(|room| room.read(cx).id).collect()
|
self.rooms.iter().map(|room| room.read(cx).id).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Search rooms by their name.
|
||||||
|
pub fn search(&self, query: &str, cx: &App) -> Vec<Entity<Room>> {
|
||||||
|
let matcher = SkimMatcherV2::default();
|
||||||
|
|
||||||
|
self.rooms
|
||||||
|
.iter()
|
||||||
|
.filter(|room| {
|
||||||
|
matcher
|
||||||
|
.fuzzy_match(room.read(cx).display_name(cx).as_ref(), query)
|
||||||
|
.is_some()
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Load all rooms from the lmdb.
|
/// Load all rooms from the lmdb.
|
||||||
///
|
///
|
||||||
/// This method:
|
/// This method:
|
||||||
@@ -215,7 +226,6 @@ impl ChatRegistry {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
this.rooms.extend(rooms);
|
this.rooms.extend(rooms);
|
||||||
this.rooms.sort_by_key(|r| Reverse(r.read(cx).created_at));
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -259,15 +269,20 @@ impl ChatRegistry {
|
|||||||
Profile::new(*public_key, metadata)
|
Profile::new(*public_key, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new room to the registry
|
/// Parse a Nostr event into a Room and push it to the registry
|
||||||
///
|
///
|
||||||
/// Returns an error if the room already exists
|
/// Returns the ID of the new room
|
||||||
pub fn push(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) -> u64 {
|
pub fn push_event(
|
||||||
|
&mut self,
|
||||||
|
event: &Event,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> u64 {
|
||||||
let room = Room::new(event).kind(RoomKind::Ongoing);
|
let room = Room::new(event).kind(RoomKind::Ongoing);
|
||||||
let id = room.id;
|
let id = room.id;
|
||||||
|
|
||||||
if !self.rooms.iter().any(|r| r.read(cx) == &room) {
|
if !self.rooms.iter().any(|this| this.read(cx) == &room) {
|
||||||
self.rooms.insert(0, cx.new(|_| room));
|
self.rooms.insert(cx.new(|_| room));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
} else {
|
} else {
|
||||||
window.push_notification("Room already exists", cx);
|
window.push_notification("Room already exists", cx);
|
||||||
@@ -276,6 +291,20 @@ impl ChatRegistry {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a nostr event into Room and push to the registry
|
||||||
|
///
|
||||||
|
/// Returns the ID of the new room
|
||||||
|
pub fn push_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) -> u64 {
|
||||||
|
let id = room.read(cx).id;
|
||||||
|
|
||||||
|
if !self.rooms.iter().any(|this| this.read(cx) == room.read(cx)) {
|
||||||
|
self.rooms.insert(room);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
/// Push a new message to a room
|
/// Push a new message to a room
|
||||||
///
|
///
|
||||||
/// If the room doesn't exist, it will be created.
|
/// If the room doesn't exist, it will be created.
|
||||||
@@ -291,14 +320,9 @@ impl ChatRegistry {
|
|||||||
this.emit_message(event, window, cx);
|
this.emit_message(event, window, cx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.defer_in(window, |this, _, cx| {
|
|
||||||
this.rooms
|
|
||||||
.sort_by_key(|room| Reverse(room.read(cx).created_at));
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Push the new room to the front of the list
|
// Push the new room to the front of the list
|
||||||
self.rooms.insert(0, cx.new(|_| Room::new(&event)));
|
self.rooms.insert(cx.new(|_| Room::new(&event)));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::sync::Arc;
|
use std::{cmp::Ordering, sync::Arc};
|
||||||
|
|
||||||
use account::Account;
|
use account::Account;
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
@@ -27,7 +27,7 @@ pub enum SendStatus {
|
|||||||
Failed(Error),
|
Failed(Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, Default)]
|
#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||||
pub enum RoomKind {
|
pub enum RoomKind {
|
||||||
Ongoing,
|
Ongoing,
|
||||||
Trusted,
|
Trusted,
|
||||||
@@ -35,6 +35,7 @@ pub enum RoomKind {
|
|||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub created_at: Timestamp,
|
pub created_at: Timestamp,
|
||||||
@@ -48,7 +49,19 @@ pub struct Room {
|
|||||||
pub kind: RoomKind,
|
pub kind: RoomKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<IncomingEvent> for Room {}
|
impl Ord for Room {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.created_at.cmp(&other.created_at)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Room {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Room {}
|
||||||
|
|
||||||
impl PartialEq for Room {
|
impl PartialEq for Room {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
@@ -56,6 +69,8 @@ impl PartialEq for Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<IncomingEvent> for Room {}
|
||||||
|
|
||||||
impl Room {
|
impl Room {
|
||||||
/// Creates a new Room instance from a Nostr event
|
/// Creates a new Room instance from a Nostr event
|
||||||
///
|
///
|
||||||
@@ -186,28 +201,6 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets all avatars for members in the room
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cx` - The App context
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A vector of SharedString containing all members' avatars
|
|
||||||
pub fn avatars(&self, cx: &App) -> Vec<SharedString> {
|
|
||||||
let profiles: Vec<Profile> = self
|
|
||||||
.members
|
|
||||||
.iter()
|
|
||||||
.map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
profiles
|
|
||||||
.iter()
|
|
||||||
.map(|member| member.shared_avatar())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a formatted string of member names
|
/// Gets a formatted string of member names
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ anyhow.workspace = true
|
|||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
|
||||||
random_name_generator = "0.3.6"
|
|
||||||
qrcode-generator = "5.0.0"
|
qrcode-generator = "5.0.0"
|
||||||
|
|||||||
55
crates/common/src/debounced_delay.rs
Normal file
55
crates/common/src/debounced_delay.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use futures::{channel::oneshot, FutureExt};
|
||||||
|
use gpui::{Context, Task};
|
||||||
|
use std::{marker::PhantomData, time::Duration};
|
||||||
|
|
||||||
|
pub struct DebouncedDelay<E: 'static> {
|
||||||
|
task: Option<Task<()>>,
|
||||||
|
cancel_channel: Option<oneshot::Sender<()>>,
|
||||||
|
_phantom_data: PhantomData<E>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: 'static> Default for DebouncedDelay<E> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: 'static> DebouncedDelay<E> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
task: None,
|
||||||
|
cancel_channel: None,
|
||||||
|
_phantom_data: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fire_new<F>(&mut self, delay: Duration, cx: &mut Context<E>, func: F)
|
||||||
|
where
|
||||||
|
F: 'static + Send + FnOnce(&mut E, &mut Context<E>) -> Task<()>,
|
||||||
|
{
|
||||||
|
if let Some(channel) = self.cancel_channel.take() {
|
||||||
|
_ = channel.send(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (sender, mut receiver) = oneshot::channel::<()>();
|
||||||
|
self.cancel_channel = Some(sender);
|
||||||
|
|
||||||
|
let previous_task = self.task.take();
|
||||||
|
self.task = Some(cx.spawn(async move |entity, cx| {
|
||||||
|
let mut timer = cx.background_executor().timer(delay).fuse();
|
||||||
|
|
||||||
|
if let Some(previous_task) = previous_task {
|
||||||
|
previous_task.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
futures::select_biased! {
|
||||||
|
_ = receiver => return,
|
||||||
|
_ = timer => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(task) = entity.update(cx, |project, cx| (func)(project, cx)) {
|
||||||
|
task.await;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,13 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use global::constants::NIP96_SERVER;
|
use global::constants::NIP96_SERVER;
|
||||||
use gpui::Image;
|
use gpui::Image;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use qrcode_generator::QrCodeEcc;
|
use qrcode_generator::QrCodeEcc;
|
||||||
use rnglib::{Language, RNG};
|
|
||||||
|
|
||||||
|
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, file: Vec<u8>) -> anyhow::Result<Url, anyhow::Error> {
|
||||||
@@ -43,19 +42,6 @@ pub fn room_hash(event: &Event) -> u64 {
|
|||||||
hasher.finish()
|
hasher.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn device_pubkey(event: &Event) -> Result<PublicKey, anyhow::Error> {
|
|
||||||
let n_tag = event.tags.find(TagKind::custom("n")).context("Invalid")?;
|
|
||||||
let hex = n_tag.content().context("Invalid")?;
|
|
||||||
let pubkey = PublicKey::parse(hex)?;
|
|
||||||
|
|
||||||
Ok(pubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn random_name(length: usize) -> String {
|
|
||||||
let rng = RNG::from(&Language::Roman);
|
|
||||||
rng.generate_names(length, true).join("-").to_lowercase()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_qr(data: &str) -> Result<Arc<Image>, anyhow::Error> {
|
pub fn create_qr(data: &str) -> Result<Arc<Image>, anyhow::Error> {
|
||||||
let qr = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256)?;
|
let qr = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256)?;
|
||||||
let img = Arc::new(Image {
|
let img = Arc::new(Image {
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ rust-embed.workspace = true
|
|||||||
log.workspace = true
|
log.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
oneshot.workspace = true
|
oneshot.workspace = true
|
||||||
|
|
||||||
webbrowser = "1.0.4"
|
webbrowser = "1.0.4"
|
||||||
rustls = "0.23.23"
|
rustls = "0.23.23"
|
||||||
futures = "0.3"
|
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||||
|
|||||||
@@ -20,12 +20,10 @@ use ui::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
lru_cache::cache_provider,
|
lru_cache::cache_provider,
|
||||||
views::{
|
views::{chat, compose, login, new_account, onboarding, profile, relays, sidebar, welcome},
|
||||||
chat, compose, login, new_account, onboarding, profile, relays, search, sidebar, welcome,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CACHE_SIZE: usize = 200;
|
const IMAGE_CACHE_SIZE: usize = 200;
|
||||||
const MODAL_WIDTH: f32 = 420.;
|
const MODAL_WIDTH: f32 = 420.;
|
||||||
const SIDEBAR_WIDTH: f32 = 280.;
|
const SIDEBAR_WIDTH: f32 = 280.;
|
||||||
|
|
||||||
@@ -53,7 +51,6 @@ pub enum PanelKind {
|
|||||||
pub enum ModalKind {
|
pub enum ModalKind {
|
||||||
Profile,
|
Profile,
|
||||||
Compose,
|
Compose,
|
||||||
Search,
|
|
||||||
Relay,
|
Relay,
|
||||||
Onboarding,
|
Onboarding,
|
||||||
SetupRelay,
|
SetupRelay,
|
||||||
@@ -242,16 +239,6 @@ impl ChatSpace {
|
|||||||
.child(compose.clone())
|
.child(compose.clone())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ModalKind::Search => {
|
|
||||||
let search = search::init(window, cx);
|
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, _, _| {
|
|
||||||
modal
|
|
||||||
.closable(false)
|
|
||||||
.width(px(MODAL_WIDTH))
|
|
||||||
.child(search.clone())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ModalKind::Relay => {
|
ModalKind::Relay => {
|
||||||
let relays = relays::init(window, cx);
|
let relays = relays::init(window, cx);
|
||||||
|
|
||||||
@@ -299,7 +286,7 @@ impl Render for ChatSpace {
|
|||||||
.relative()
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(
|
.child(
|
||||||
image_cache(cache_provider("image-cache", CACHE_SIZE))
|
image_cache(cache_provider("image-cache", IMAGE_CACHE_SIZE))
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ impl Chat {
|
|||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
if let Ok(result) = task.await {
|
if let Ok(result) = task.await {
|
||||||
cx.update(|_, cx| {
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
result.into_iter().for_each(|item| {
|
result.into_iter().for_each(|item| {
|
||||||
if !item.1 {
|
if !item.1 {
|
||||||
@@ -164,8 +163,6 @@ impl Chat {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -235,8 +232,8 @@ impl Chat {
|
|||||||
|
|
||||||
// Update input state
|
// Update input state
|
||||||
self.input.update(cx, |this, cx| {
|
self.input.update(cx, |this, cx| {
|
||||||
this.set_loading(true, window, cx);
|
this.set_loading(true, cx);
|
||||||
this.set_disabled(true, window, cx);
|
this.set_disabled(true, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let room = self.room.read(cx);
|
let room = self.room.read(cx);
|
||||||
@@ -261,8 +258,8 @@ impl Chat {
|
|||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.input.update(cx, |this, cx| {
|
this.input.update(cx, |this, cx| {
|
||||||
this.set_loading(false, window, cx);
|
this.set_loading(false, cx);
|
||||||
this.set_disabled(false, window, cx);
|
this.set_disabled(false, cx);
|
||||||
this.set_text("", window, cx);
|
this.set_text("", window, cx);
|
||||||
});
|
});
|
||||||
received = true;
|
received = true;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::{
|
|||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use chats::ChatRegistry;
|
use chats::ChatRegistry;
|
||||||
use common::{profile::SharedProfile, random_name};
|
use common::profile::SharedProfile;
|
||||||
use global::get_client;
|
use global::get_client;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, impl_internal_actions, prelude::FluentBuilder, px, red, relative, uniform_list, App,
|
div, img, impl_internal_actions, prelude::FluentBuilder, px, red, relative, uniform_list, App,
|
||||||
@@ -56,14 +56,10 @@ impl Compose {
|
|||||||
let error_message = cx.new(|_| None);
|
let error_message = cx.new(|_| None);
|
||||||
|
|
||||||
let title_input = cx.new(|cx| {
|
let title_input = cx.new(|cx| {
|
||||||
let name = random_name(2);
|
TextInput::new(window, cx)
|
||||||
let mut input = TextInput::new(window, cx)
|
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
.text_size(Size::Small);
|
.placeholder("Family... . (Optional)")
|
||||||
|
.text_size(Size::Small)
|
||||||
input.set_placeholder("Family... . (Optional)");
|
|
||||||
input.set_text(name, window, cx);
|
|
||||||
input
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let user_input = cx.new(|cx| {
|
let user_input = cx.new(|cx| {
|
||||||
@@ -151,6 +147,7 @@ impl Compose {
|
|||||||
let event: Task<Result<Event, anyhow::Error>> = cx.background_spawn(async move {
|
let event: Task<Result<Event, anyhow::Error>> = cx.background_spawn(async move {
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
|
|
||||||
// [IMPORTANT]
|
// [IMPORTANT]
|
||||||
// Make sure this event is never send,
|
// Make sure this event is never send,
|
||||||
// this event existed just use for convert to Coop's Room later.
|
// this event existed just use for convert to Coop's Room later.
|
||||||
@@ -166,7 +163,7 @@ impl Compose {
|
|||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
ChatRegistry::global(cx).update(cx, |chats, cx| {
|
ChatRegistry::global(cx).update(cx, |chats, cx| {
|
||||||
let id = chats.push(&event, window, cx);
|
let id = chats.push_event(&event, window, cx);
|
||||||
window.close_modal(cx);
|
window.close_modal(cx);
|
||||||
window.dispatch_action(
|
window.dispatch_action(
|
||||||
Box::new(AddPanel::new(PanelKind::Room(id), DockPlacement::Center)),
|
Box::new(AddPanel::new(PanelKind::Room(id), DockPlacement::Center)),
|
||||||
@@ -351,7 +348,7 @@ impl Render for Compose {
|
|||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(div().pb_0p5().text_sm().font_semibold().child("Title:"))
|
.child(div().pb_0p5().text_sm().font_semibold().child("Subject:"))
|
||||||
.child(self.title_input.clone()),
|
.child(self.title_input.clone()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ pub mod new_account;
|
|||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod relays;
|
pub mod relays;
|
||||||
pub mod search;
|
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
pub mod subject;
|
pub mod subject;
|
||||||
pub mod welcome;
|
pub mod welcome;
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ impl Render for Profile {
|
|||||||
.child(self.bio_input.clone()),
|
.child(self.bio_input.clone()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().p_3().child(
|
div().py_3().child(
|
||||||
Button::new("submit")
|
Button::new("submit")
|
||||||
.label("Update")
|
.label("Update")
|
||||||
.primary()
|
.primary()
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::{
|
|
||||||
div, prelude::FluentBuilder, App, ClickEvent, Div, InteractiveElement, IntoElement,
|
|
||||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::Icon;
|
|
||||||
|
|
||||||
type Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct SidebarButton {
|
|
||||||
base: Div,
|
|
||||||
label: SharedString,
|
|
||||||
icon: Option<Icon>,
|
|
||||||
handler: Handler,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SidebarButton {
|
|
||||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
|
||||||
Self {
|
|
||||||
base: div().flex().items_center().gap_3().px_3().h_8(),
|
|
||||||
label: label.into(),
|
|
||||||
icon: None,
|
|
||||||
handler: Rc::new(|_, _, _| {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
|
||||||
self.icon = Some(icon.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_click(
|
|
||||||
mut self,
|
|
||||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handler = Rc::new(handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for SidebarButton {
|
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let handler = self.handler.clone();
|
|
||||||
|
|
||||||
self.base
|
|
||||||
.id(self.label.clone())
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.when_some(self.icon, |this, icon| {
|
|
||||||
this.child(div().text_color(cx.theme().text_muted).child(icon))
|
|
||||||
})
|
|
||||||
.child(self.label.clone())
|
|
||||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
|
||||||
.on_click(move |ev, window, cx| handler(ev, window, cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -299,7 +299,7 @@ impl RenderOnce for FolderItem {
|
|||||||
.font_medium()
|
.font_medium()
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if let Some(img) = self.img {
|
if let Some(img) = self.img {
|
||||||
this.child(img.size_5().flex_shrink_0())
|
this.child(img.size_6().flex_shrink_0())
|
||||||
} else {
|
} else {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -1,27 +1,35 @@
|
|||||||
use std::{cmp::Reverse, collections::HashSet};
|
use std::{
|
||||||
|
cmp::Reverse,
|
||||||
|
collections::{BTreeSet, HashSet},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use account::Account;
|
use account::Account;
|
||||||
use button::SidebarButton;
|
use async_utility::task::spawn;
|
||||||
use chats::{
|
use chats::{
|
||||||
room::{Room, RoomKind},
|
room::{Room, RoomKind},
|
||||||
ChatRegistry,
|
ChatRegistry,
|
||||||
};
|
};
|
||||||
use common::profile::SharedProfile;
|
|
||||||
|
use common::{debounced_delay::DebouncedDelay, profile::SharedProfile};
|
||||||
use folder::{Folder, FolderItem, Parent};
|
use folder::{Folder, FolderItem, Parent};
|
||||||
use global::get_client;
|
use global::{constants::SEARCH_RELAYS, get_client};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity,
|
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, ScrollHandle,
|
||||||
ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Task, Window,
|
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonCustomVariant, ButtonVariants},
|
button::{Button, ButtonCustomVariant, ButtonRounded, ButtonVariants},
|
||||||
dock_area::{
|
dock_area::{
|
||||||
dock::DockPlacement,
|
dock::DockPlacement,
|
||||||
panel::{Panel, PanelEvent},
|
panel::{Panel, PanelEvent},
|
||||||
},
|
},
|
||||||
|
input::{InputEvent, TextInput},
|
||||||
popup_menu::{PopupMenu, PopupMenuExt},
|
popup_menu::{PopupMenu, PopupMenuExt},
|
||||||
skeleton::Skeleton,
|
skeleton::Skeleton,
|
||||||
IconName, Sizable, StyledExt,
|
IconName, Sizable, StyledExt,
|
||||||
@@ -29,10 +37,10 @@ use ui::{
|
|||||||
|
|
||||||
use crate::chatspace::{AddPanel, ModalKind, PanelKind, ToggleModal};
|
use crate::chatspace::{AddPanel, ModalKind, PanelKind, ToggleModal};
|
||||||
|
|
||||||
mod button;
|
|
||||||
mod folder;
|
mod folder;
|
||||||
|
|
||||||
actions!(profile, [Logout]);
|
const FIND_DELAY: u64 = 600;
|
||||||
|
const FIND_LIMIT: usize = 10;
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
||||||
Sidebar::new(window, cx)
|
Sidebar::new(window, cx)
|
||||||
@@ -52,11 +60,21 @@ pub enum SubItem {
|
|||||||
|
|
||||||
pub struct Sidebar {
|
pub struct Sidebar {
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
|
// Search
|
||||||
|
find_input: Entity<TextInput>,
|
||||||
|
find_debouncer: DebouncedDelay<Self>,
|
||||||
|
finding: bool,
|
||||||
|
local_result: Entity<Option<Vec<Entity<Room>>>>,
|
||||||
|
global_result: Entity<Option<Vec<Entity<Room>>>>,
|
||||||
|
// Layout
|
||||||
split_into_folders: bool,
|
split_into_folders: bool,
|
||||||
active_items: HashSet<Item>,
|
active_items: HashSet<Item>,
|
||||||
active_subitems: HashSet<SubItem>,
|
active_subitems: HashSet<SubItem>,
|
||||||
|
// GPUI
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
scroll_handle: ScrollHandle,
|
scroll_handle: ScrollHandle,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sidebar {
|
impl Sidebar {
|
||||||
@@ -64,7 +82,7 @@ impl Sidebar {
|
|||||||
cx.new(|cx| Self::view(window, cx))
|
cx.new(|cx| Self::view(window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
let scroll_handle = ScrollHandle::default();
|
let scroll_handle = ScrollHandle::default();
|
||||||
|
|
||||||
@@ -75,13 +93,65 @@ impl Sidebar {
|
|||||||
active_subitems.insert(SubItem::Trusted);
|
active_subitems.insert(SubItem::Trusted);
|
||||||
active_subitems.insert(SubItem::Unknown);
|
active_subitems.insert(SubItem::Unknown);
|
||||||
|
|
||||||
|
let local_result = cx.new(|_| None);
|
||||||
|
let global_result = cx.new(|_| None);
|
||||||
|
let find_input = cx.new(|cx| {
|
||||||
|
TextInput::new(window, cx)
|
||||||
|
.small()
|
||||||
|
.text_size(ui::Size::XSmall)
|
||||||
|
.suffix(|window, cx| {
|
||||||
|
Button::new("find")
|
||||||
|
.icon(IconName::Search)
|
||||||
|
.tooltip("Press Enter to search")
|
||||||
|
.small()
|
||||||
|
.custom(
|
||||||
|
ButtonCustomVariant::new(window, cx)
|
||||||
|
.active(gpui::transparent_black())
|
||||||
|
.color(gpui::transparent_black())
|
||||||
|
.hover(gpui::transparent_black())
|
||||||
|
.foreground(cx.theme().text_placeholder),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.placeholder("Find or start a conversation")
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
cx.subscribe_in(&find_input, window, |this, _, event, _, cx| {
|
||||||
|
match event {
|
||||||
|
InputEvent::PressEnter => this.search(cx),
|
||||||
|
InputEvent::Change(text) => {
|
||||||
|
// Clear the result when input is empty
|
||||||
|
if text.is_empty() {
|
||||||
|
this.clear_search_results(cx);
|
||||||
|
} else {
|
||||||
|
// Run debounced search
|
||||||
|
this.find_debouncer.fire_new(
|
||||||
|
Duration::from_millis(FIND_DELAY),
|
||||||
|
cx,
|
||||||
|
|this, cx| this.debounced_search(cx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name: "Chat Sidebar".into(),
|
name: "Chat Sidebar".into(),
|
||||||
split_into_folders: false,
|
split_into_folders: false,
|
||||||
|
find_debouncer: DebouncedDelay::new(),
|
||||||
|
finding: false,
|
||||||
|
find_input,
|
||||||
|
local_result,
|
||||||
|
global_result,
|
||||||
active_items,
|
active_items,
|
||||||
active_subitems,
|
active_subitems,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
scroll_handle,
|
scroll_handle,
|
||||||
|
subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,32 +169,167 @@ impl Sidebar {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_into_folders(&mut self, cx: &mut Context<Self>) {
|
fn toggle_folder(&mut self, cx: &mut Context<Self>) {
|
||||||
self.split_into_folders = !self.split_into_folders;
|
self.split_into_folders = !self.split_into_folders;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_logout(&mut self, _: &Logout, window: &mut Window, cx: &mut Context<Self>) {
|
fn debounced_search(&self, cx: &mut Context<Self>) -> Task<()> {
|
||||||
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
cx.spawn(async move |this, cx| {
|
||||||
let client = get_client();
|
this.update(cx, |this, cx| {
|
||||||
_ = client.reset().await;
|
this.search(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
fn nip50_search(&self, cx: &App) -> Task<Result<BTreeSet<Room>, Error>> {
|
||||||
|
let query = self.find_input.read(cx).text();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = get_client();
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::Metadata)
|
||||||
|
.search(query.to_lowercase())
|
||||||
|
.limit(FIND_LIMIT);
|
||||||
|
|
||||||
|
let events = client
|
||||||
|
.fetch_events_from(SEARCH_RELAYS, filter, Duration::from_secs(3))
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.unique_by(|event| event.pubkey)
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
let mut rooms = BTreeSet::new();
|
||||||
|
let (tx, rx) = smol::channel::bounded::<Room>(10);
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
let client = get_client();
|
||||||
|
let signer = client.signer().await.unwrap();
|
||||||
|
|
||||||
|
for event in events.into_iter() {
|
||||||
|
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||||
|
|
||||||
|
if let Some(target) = metadata.nip05.as_ref() {
|
||||||
|
if let Ok(verify) = nip05::verify(&event.pubkey, target, None).await {
|
||||||
|
if verify {
|
||||||
|
if let Ok(event) = EventBuilder::private_msg_rumor(event.pubkey, "")
|
||||||
|
.sign(&signer)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let room = Room::new(&event);
|
||||||
|
_ = tx.send(room).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
while let Ok(room) = rx.recv().await {
|
||||||
if task.await.is_ok() {
|
rooms.insert(room);
|
||||||
cx.update(|_, cx| {
|
}
|
||||||
Account::global(cx).update(cx, |this, cx| {
|
|
||||||
this.profile = None;
|
Ok(rooms)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let query = self.find_input.read(cx).text();
|
||||||
|
let result = ChatRegistry::get_global(cx).search(query.as_ref(), cx);
|
||||||
|
|
||||||
|
// Return if query is empty
|
||||||
|
if query.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if search is in progress
|
||||||
|
if self.finding {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block the UI until the search process completes
|
||||||
|
self.set_finding(true, cx);
|
||||||
|
|
||||||
|
// Disable the search input to prevent duplicate requests
|
||||||
|
self.find_input.update(cx, |this, cx| {
|
||||||
|
this.set_disabled(true, cx);
|
||||||
|
this.set_loading(true, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
if !result.is_empty() {
|
||||||
|
self.set_finding(false, cx);
|
||||||
|
|
||||||
|
self.find_input.update(cx, |this, cx| {
|
||||||
|
this.set_disabled(false, cx);
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.local_result.update(cx, |this, cx| {
|
||||||
|
*this = Some(result);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let task = self.nip50_search(cx);
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
if let Ok(result) = task.await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
let result = result
|
||||||
|
.into_iter()
|
||||||
|
.map(|room| cx.new(|_| room))
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
this.set_finding(false, cx);
|
||||||
|
|
||||||
|
this.find_input.update(cx, |this, cx| {
|
||||||
|
this.set_disabled(false, cx);
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.global_result.update(cx, |this, cx| {
|
||||||
|
*this = Some(result);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
};
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_finding(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
|
self.finding = status;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_search_results(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.local_result.update(cx, |this, cx| {
|
||||||
|
*this = None;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
self.global_result.update(cx, |this, cx| {
|
||||||
|
*this = None;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some(result) = self.global_result.read(cx).as_ref() {
|
||||||
|
if let Some(room) = result.iter().find(|this| this.read(cx).id == id).cloned() {
|
||||||
|
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||||
|
this.push_room(room, cx);
|
||||||
|
});
|
||||||
|
window.dispatch_action(
|
||||||
|
Box::new(AddPanel::new(PanelKind::Room(id), DockPlacement::Center)),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
self.clear_search_results(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_skeleton(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
|
fn render_skeleton(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
|
||||||
(0..total).map(|_| {
|
(0..total).map(|_| {
|
||||||
@@ -140,10 +345,34 @@ impl Sidebar {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_items(rooms: &Vec<&Entity<Room>>, cx: &Context<Self>) -> Vec<FolderItem> {
|
fn render_global_items(rooms: &[Entity<Room>], cx: &Context<Self>) -> Vec<FolderItem> {
|
||||||
let mut items = Vec::with_capacity(rooms.len());
|
let mut items = Vec::with_capacity(rooms.len());
|
||||||
|
|
||||||
for room in rooms {
|
for room in rooms.iter() {
|
||||||
|
let this = room.read(cx);
|
||||||
|
let id = this.id;
|
||||||
|
let label = this.display_name(cx);
|
||||||
|
let img = this.display_image(cx).map(img);
|
||||||
|
|
||||||
|
let item = FolderItem::new(id as usize)
|
||||||
|
.label(label)
|
||||||
|
.img(img)
|
||||||
|
.on_click({
|
||||||
|
cx.listener(move |this, _, window, cx| {
|
||||||
|
this.push_room(id, window, cx);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_items(rooms: &[Entity<Room>], cx: &Context<Self>) -> Vec<FolderItem> {
|
||||||
|
let mut items = Vec::with_capacity(rooms.len());
|
||||||
|
|
||||||
|
for room in rooms.iter() {
|
||||||
let room = room.read(cx);
|
let room = room.read(cx);
|
||||||
let id = room.id;
|
let id = room.id;
|
||||||
let ago = room.ago();
|
let ago = room.ago();
|
||||||
@@ -198,30 +427,33 @@ impl Focusable for Sidebar {
|
|||||||
|
|
||||||
impl Render for Sidebar {
|
impl Render for Sidebar {
|
||||||
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 account = Account::global(cx).read(cx).profile.as_ref();
|
let account = Account::get_global(cx).profile_ref();
|
||||||
let registry = ChatRegistry::global(cx).read(cx);
|
let registry = ChatRegistry::get_global(cx);
|
||||||
|
|
||||||
|
// Get all rooms
|
||||||
let rooms = registry.rooms(cx);
|
let rooms = registry.rooms(cx);
|
||||||
let loading = registry.loading();
|
let loading = registry.loading;
|
||||||
|
|
||||||
|
// Get search result
|
||||||
|
let local_result = self.local_result.read(cx);
|
||||||
|
let global_result = self.global_result.read(cx);
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id("sidebar")
|
.id("sidebar")
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.track_scroll(&self.scroll_handle)
|
.track_scroll(&self.scroll_handle)
|
||||||
.on_action(cx.listener(Self::on_logout))
|
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
.size_full()
|
.size_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.pt_1()
|
.py_1()
|
||||||
.px_2()
|
|
||||||
.pb_2()
|
|
||||||
.when_some(account, |this, profile| {
|
.when_some(account, |this, profile| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
|
.px_3()
|
||||||
.h_7()
|
.h_7()
|
||||||
.px_1p5()
|
.flex_none()
|
||||||
.flex()
|
.flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -236,10 +468,16 @@ impl Render for Sidebar {
|
|||||||
.child(profile.shared_name()),
|
.child(profile.shared_name()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("user_dropdown")
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Button::new("user")
|
||||||
.icon(IconName::Ellipsis)
|
.icon(IconName::Ellipsis)
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
|
.rounded(ButtonRounded::Full)
|
||||||
.popup_menu(|this, _window, _cx| {
|
.popup_menu(|this, _window, _cx| {
|
||||||
this.menu(
|
this.menu(
|
||||||
"Profile",
|
"Profile",
|
||||||
@@ -253,34 +491,15 @@ impl Render for Sidebar {
|
|||||||
modal: ModalKind::Relay,
|
modal: ModalKind::Relay,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.separator()
|
|
||||||
.menu("Logout", Box::new(Logout))
|
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.font_medium()
|
|
||||||
.child(
|
|
||||||
SidebarButton::new("Find")
|
|
||||||
.icon(IconName::Search)
|
|
||||||
.on_click(cx.listener(|_, _, window, cx| {
|
|
||||||
window.dispatch_action(
|
|
||||||
Box::new(ToggleModal {
|
|
||||||
modal: ModalKind::Search,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
SidebarButton::new("New Chat")
|
Button::new("compose")
|
||||||
.icon(IconName::PlusCircleFill)
|
.icon(IconName::PlusFill)
|
||||||
|
.tooltip("Create DM or Group DM")
|
||||||
|
.small()
|
||||||
|
.primary()
|
||||||
|
.rounded(ButtonRounded::Full)
|
||||||
.on_click(cx.listener(|_, _, window, cx| {
|
.on_click(cx.listener(|_, _, window, cx| {
|
||||||
window.dispatch_action(
|
window.dispatch_action(
|
||||||
Box::new(ToggleModal {
|
Box::new(ToggleModal {
|
||||||
@@ -290,20 +509,40 @@ impl Render for Sidebar {
|
|||||||
);
|
);
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.px_3()
|
||||||
|
.h_7()
|
||||||
|
.flex_none()
|
||||||
|
.child(self.find_input.clone()),
|
||||||
|
)
|
||||||
|
.when_some(global_result.as_ref(), |this, rooms| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.px_1()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_2()
|
.gap_1()
|
||||||
|
.children(Self::render_global_items(rooms, cx)),
|
||||||
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.pl_2()
|
.px_1()
|
||||||
.pr_1()
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.mb_1()
|
||||||
|
.px_2()
|
||||||
.flex()
|
.flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.items_center()
|
.items_center()
|
||||||
.text_xs()
|
.text_sm()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_placeholder)
|
.text_color(cx.theme().text_placeholder)
|
||||||
.child("Messages")
|
.child("Messages")
|
||||||
@@ -326,19 +565,20 @@ impl Render for Sidebar {
|
|||||||
.active(cx.theme().ghost_element_background),
|
.active(cx.theme().ghost_element_background),
|
||||||
)
|
)
|
||||||
.on_click(cx.listener(move |this, _, _, cx| {
|
.on_click(cx.listener(move |this, _, _, cx| {
|
||||||
this.split_into_folders(cx);
|
this.toggle_folder(cx);
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.when(loading, |this| this.children(self.render_skeleton(6)))
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if loading {
|
if let Some(rooms) = local_result {
|
||||||
this.children(self.render_skeleton(6))
|
this.children(Self::render_items(rooms, cx))
|
||||||
} else if !self.split_into_folders {
|
} else if !self.split_into_folders {
|
||||||
let rooms: Vec<_> = rooms
|
let rooms = rooms
|
||||||
.values()
|
.values()
|
||||||
.flat_map(|v| v.iter().cloned())
|
.flat_map(|v| v.iter().cloned())
|
||||||
.sorted_by_key(|e| Reverse(e.read(cx).created_at))
|
.sorted_by_key(|e| Reverse(e.read(cx).created_at))
|
||||||
.collect();
|
.collect_vec();
|
||||||
|
|
||||||
this.children(Self::render_items(&rooms, cx))
|
this.children(Self::render_items(&rooms, cx))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -53,11 +53,6 @@ impl Subject {
|
|||||||
let registry = ChatRegistry::global(cx).read(cx);
|
let registry = ChatRegistry::global(cx).read(cx);
|
||||||
let subject = self.input.read(cx).text();
|
let subject = self.input.read(cx).text();
|
||||||
|
|
||||||
if subject.is_empty() {
|
|
||||||
window.push_notification("Subject cannot be empty", cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(room) = registry.room(&self.id, cx) {
|
if let Some(room) = registry.room(&self.id, cx) {
|
||||||
room.update(cx, |this, cx| {
|
room.update(cx, |this, cx| {
|
||||||
this.subject = Some(subject);
|
this.subject = Some(subject);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use gpui::{hsla, Hsla, Rgba};
|
use gpui::{hsla, Hsla, Rgba};
|
||||||
|
|
||||||
use crate::scale::{ColorScale, ColorScaleSet, ColorScales};
|
use crate::scale::{ColorScale, ColorScaleSet, ColorScales};
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ impl ThemeColor {
|
|||||||
ghost_element_hover: neutral().light_alpha().step_3(),
|
ghost_element_hover: neutral().light_alpha().step_3(),
|
||||||
ghost_element_active: neutral().light_alpha().step_4(),
|
ghost_element_active: neutral().light_alpha().step_4(),
|
||||||
ghost_element_selected: neutral().light_alpha().step_5(),
|
ghost_element_selected: neutral().light_alpha().step_5(),
|
||||||
ghost_element_disabled: neutral().light_alpha().step_3(),
|
ghost_element_disabled: neutral().light_alpha().step_2(),
|
||||||
text: neutral().light().step_12(),
|
text: neutral().light().step_12(),
|
||||||
text_muted: neutral().light().step_11(),
|
text_muted: neutral().light().step_11(),
|
||||||
text_placeholder: neutral().light().step_10(),
|
text_placeholder: neutral().light().step_10(),
|
||||||
@@ -202,7 +202,7 @@ impl ThemeColor {
|
|||||||
elevated_surface_background: neutral().dark().step_3(),
|
elevated_surface_background: neutral().dark().step_3(),
|
||||||
surface_background: neutral().dark().step_2(),
|
surface_background: neutral().dark().step_2(),
|
||||||
background: neutral().dark().step_1(),
|
background: neutral().dark().step_1(),
|
||||||
element_foreground: brand().dark().step_12(),
|
element_foreground: brand().dark().step_1(),
|
||||||
element_background: brand().dark().step_9(),
|
element_background: brand().dark().step_9(),
|
||||||
element_hover: brand().dark_alpha().step_10(),
|
element_hover: brand().dark_alpha().step_10(),
|
||||||
element_active: brand().dark().step_10(),
|
element_active: brand().dark().step_10(),
|
||||||
@@ -213,7 +213,7 @@ impl ThemeColor {
|
|||||||
ghost_element_hover: neutral().dark_alpha().step_3(),
|
ghost_element_hover: neutral().dark_alpha().step_3(),
|
||||||
ghost_element_active: neutral().dark_alpha().step_4(),
|
ghost_element_active: neutral().dark_alpha().step_4(),
|
||||||
ghost_element_selected: neutral().dark_alpha().step_5(),
|
ghost_element_selected: neutral().dark_alpha().step_5(),
|
||||||
ghost_element_disabled: neutral().dark_alpha().step_3(),
|
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
||||||
text: neutral().dark().step_12(),
|
text: neutral().dark().step_12(),
|
||||||
text_muted: neutral().dark().step_11(),
|
text_muted: neutral().dark().step_11(),
|
||||||
text_placeholder: neutral().dark().step_10(),
|
text_placeholder: neutral().dark().step_10(),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use gpui::{Hsla, SharedString};
|
use gpui::{Hsla, SharedString};
|
||||||
|
|
||||||
/// A collection of colors that are used to style the UI.
|
/// A collection of colors that are used to style the UI.
|
||||||
|
|||||||
@@ -308,9 +308,15 @@ impl RenderOnce for Button {
|
|||||||
// Normal Button
|
// Normal Button
|
||||||
match self.size {
|
match self.size {
|
||||||
Size::Size(size) => this.px(size * 0.2),
|
Size::Size(size) => this.px(size * 0.2),
|
||||||
Size::XSmall => this.h_6().px_1p5(),
|
Size::XSmall => this.h_6().px_2(),
|
||||||
Size::Small => this.h_7().px_2(),
|
Size::Small => {
|
||||||
Size::Large => this.h_10().px_3(),
|
if self.icon.is_some() {
|
||||||
|
this.h_7().pl_2().pr_3()
|
||||||
|
} else {
|
||||||
|
this.h_7().px_3()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Size::Large => this.h_10().px_4(),
|
||||||
_ => this.h_9().px_2(),
|
_ => this.h_9().px_2(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ impl_internal_actions!(emoji, [EmitEmoji]);
|
|||||||
pub struct EmojiPicker {
|
pub struct EmojiPicker {
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
anchor: Option<Corner>,
|
anchor: Option<Corner>,
|
||||||
input: WeakEntity<TextInput>,
|
target_input: WeakEntity<TextInput>,
|
||||||
emojis: Rc<Vec<SharedString>>,
|
emojis: Rc<Vec<SharedString>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EmojiPicker {
|
impl EmojiPicker {
|
||||||
pub fn new(input: WeakEntity<TextInput>) -> Self {
|
pub fn new(target_input: WeakEntity<TextInput>) -> Self {
|
||||||
let mut emojis: Vec<SharedString> = vec![];
|
let mut emojis: Vec<SharedString> = vec![];
|
||||||
|
|
||||||
emojis.extend(
|
emojis.extend(
|
||||||
@@ -39,15 +39,8 @@ impl EmojiPicker {
|
|||||||
.collect::<Vec<SharedString>>(),
|
.collect::<Vec<SharedString>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
emojis.extend(
|
|
||||||
emojis::Group::Symbols
|
|
||||||
.emojis()
|
|
||||||
.map(|e| SharedString::from(e.as_str()))
|
|
||||||
.collect::<Vec<SharedString>>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
input,
|
target_input,
|
||||||
emojis: emojis.into(),
|
emojis: emojis.into(),
|
||||||
anchor: None,
|
anchor: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
@@ -82,7 +75,7 @@ impl RenderOnce for EmojiPicker {
|
|||||||
)
|
)
|
||||||
.content(move |window, cx| {
|
.content(move |window, cx| {
|
||||||
let emojis = self.emojis.clone();
|
let emojis = self.emojis.clone();
|
||||||
let input = self.input.clone();
|
let input = self.target_input.clone();
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
PopoverContent::new(window, cx, move |_window, cx| {
|
PopoverContent::new(window, cx, move |_window, cx| {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ pub enum IconName {
|
|||||||
PanelRightClose,
|
PanelRightClose,
|
||||||
PanelRightOpen,
|
PanelRightOpen,
|
||||||
Plus,
|
Plus,
|
||||||
|
PlusFill,
|
||||||
PlusCircleFill,
|
PlusCircleFill,
|
||||||
Relays,
|
Relays,
|
||||||
ResizeCorner,
|
ResizeCorner,
|
||||||
@@ -124,6 +125,7 @@ impl IconName {
|
|||||||
Self::PanelRightClose => "icons/panel-right-close.svg",
|
Self::PanelRightClose => "icons/panel-right-close.svg",
|
||||||
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
||||||
Self::Plus => "icons/plus.svg",
|
Self::Plus => "icons/plus.svg",
|
||||||
|
Self::PlusFill => "icons/plus-fill.svg",
|
||||||
Self::PlusCircleFill => "icons/plus-circle-fill.svg",
|
Self::PlusCircleFill => "icons/plus-circle-fill.svg",
|
||||||
Self::Relays => "icons/relays.svg",
|
Self::Relays => "icons/relays.svg",
|
||||||
Self::ResizeCorner => "icons/resize-corner.svg",
|
Self::ResizeCorner => "icons/resize-corner.svg",
|
||||||
|
|||||||
@@ -464,13 +464,13 @@ impl TextInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the disabled state of the input field.
|
/// Set the disabled state of the input field.
|
||||||
pub fn set_disabled(&mut self, disabled: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
|
||||||
self.disabled = disabled;
|
self.disabled = disabled;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the masked state of the input field.
|
/// Set the masked state of the input field.
|
||||||
pub fn set_masked(&mut self, masked: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_masked(&mut self, masked: bool, cx: &mut Context<Self>) {
|
||||||
self.masked = masked;
|
self.masked = masked;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -576,7 +576,7 @@ impl TextInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set true to show indicator at the input right.
|
/// Set true to show indicator at the input right.
|
||||||
pub fn set_loading(&mut self, loading: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
|
||||||
self.loading = loading;
|
self.loading = loading;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,10 +286,10 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_loading(&mut self, loading: bool, window: &mut Window, cx: &mut Context<Self>) {
|
fn set_loading(&mut self, loading: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.loading = loading;
|
self.loading = loading;
|
||||||
if let Some(input) = &self.query_input {
|
if let Some(input) = &self.query_input {
|
||||||
input.update(cx, |input, cx| input.set_loading(loading, window, cx))
|
input.update(cx, |input, cx| input.set_loading(loading, cx))
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ impl RenderOnce for ListItem {
|
|||||||
let is_active = self.selected || self.confirmed;
|
let is_active = self.selected || self.confirmed;
|
||||||
|
|
||||||
self.base
|
self.base
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text)
|
||||||
.relative()
|
.relative()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
@@ -147,7 +147,7 @@ impl RenderOnce for ListItem {
|
|||||||
})
|
})
|
||||||
.when(is_active, |this| this.bg(cx.theme().element_active))
|
.when(is_active, |this| this.bg(cx.theme().element_active))
|
||||||
.when(!is_active && !self.disabled, |this| {
|
.when(!is_active && !self.disabled, |this| {
|
||||||
this.hover(|this| this.bg(cx.theme().surface_background))
|
this.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||||
})
|
})
|
||||||
// Mouse enter
|
// Mouse enter
|
||||||
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
|
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
|
||||||
|
|||||||
@@ -590,7 +590,7 @@ impl Render for PopupMenu {
|
|||||||
.h(px(1.))
|
.h(px(1.))
|
||||||
.mx_neg_1()
|
.mx_neg_1()
|
||||||
.my_0p5()
|
.my_0p5()
|
||||||
.bg(cx.theme().border_variant),
|
.bg(cx.theme().border_disabled),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PopupMenuItem::ElementItem { render, .. } => this
|
PopupMenuItem::ElementItem { render, .. } => this
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ impl RenderOnce for Skeleton {
|
|||||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
|
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
|
||||||
div().child(
|
div().child(
|
||||||
self.base
|
self.base
|
||||||
.bg(cx.theme().ghost_element_disabled)
|
.bg(cx.theme().ghost_element_active)
|
||||||
.with_animation(
|
.with_animation(
|
||||||
"skeleton",
|
"skeleton",
|
||||||
Animation::new(Duration::from_secs(2))
|
Animation::new(Duration::from_secs(2))
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ impl Render for Tooltip {
|
|||||||
.p_2()
|
.p_2()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.bg(cx.theme().surface_background)
|
.bg(cx.theme().background)
|
||||||
.shadow_lg()
|
.shadow_md()
|
||||||
.rounded_lg()
|
.rounded_lg()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text)
|
||||||
.line_height(relative(1.25))
|
.line_height(relative(1.25))
|
||||||
.child(self.text.clone()),
|
.child(self.text.clone()),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user