Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
314fdcb827 | ||
|
|
77eba03ae1 | ||
|
|
9b06428f44 | ||
|
|
d3c3263c58 |
Generated
+42
-35
@@ -699,11 +699,11 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin-consensus-encoding"
|
name = "bitcoin-consensus-encoding"
|
||||||
version = "1.2.0"
|
version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6712f9c6fd6785b3b270884e57c441c403dc5d7e19ca45368c97c7a1de3000ec"
|
checksum = "9daa31138eb443d5751b207f3f64154e2bb09cd59960562ccc7a7112be38147f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin-internals",
|
"bitcoin-internals 0.7.0",
|
||||||
"hex-conservative 1.3.0",
|
"hex-conservative 1.3.0",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -714,6 +714,12 @@ version = "0.6.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5"
|
checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitcoin-internals"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8bea3a9f0cfece4564e37cb49a38cc245ca5184719e50d7d0dda3268722c4e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin-io"
|
name = "bitcoin-io"
|
||||||
version = "0.1.101"
|
version = "0.1.101"
|
||||||
@@ -740,7 +746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5304e53726dbe5f93141535e102ed97b5bf4714fbecefdda8f9fb98d7fdaff0e"
|
checksum = "5304e53726dbe5f93141535e102ed97b5bf4714fbecefdda8f9fb98d7fdaff0e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin-consensus-encoding",
|
"bitcoin-consensus-encoding",
|
||||||
"bitcoin-internals",
|
"bitcoin-internals 0.6.0",
|
||||||
"hex-conservative 1.3.0",
|
"hex-conservative 1.3.0",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -987,9 +993,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.4.7"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313"
|
checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -2229,9 +2235,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.13"
|
version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b"
|
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fixedbitset"
|
name = "fixedbitset"
|
||||||
@@ -4052,9 +4058,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.105"
|
version = "0.3.106"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
|
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -4753,9 +4759,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify-rust"
|
name = "notify-rust"
|
||||||
version = "4.18.0"
|
version = "4.18.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c5b4c1b4f2aa9f25f63a7a49d3dd0ed567b3670da15330a66b29434be899b891"
|
checksum = "fe430c879247371c47fdedd328ad4e301e6b30b533c5fd5db1c06d75253d0e46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"log",
|
"log",
|
||||||
@@ -6996,9 +7002,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "skrifa"
|
name = "skrifa"
|
||||||
@@ -7037,9 +7043,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.16.1"
|
version = "1.16.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
|
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smol"
|
name = "smol"
|
||||||
@@ -7441,12 +7447,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-winrt-notification"
|
name = "tauri-winrt-notification"
|
||||||
version = "0.7.3"
|
version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade"
|
checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.21",
|
"thiserror 2.0.21",
|
||||||
"windows 0.61.3",
|
"windows 0.62.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8363,9 +8369,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.128"
|
version = "0.2.129"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
|
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -8376,19 +8382,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.78"
|
version = "0.4.79"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
|
checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"tokio",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.128"
|
version = "0.2.129"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
|
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -8396,9 +8403,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.128"
|
version = "0.2.129"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
|
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -8409,9 +8416,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.128"
|
version = "0.2.129"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
|
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -8542,9 +8549,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.105"
|
version = "0.3.106"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
|
checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -9832,18 +9839,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.58"
|
version = "0.8.59"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44"
|
checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.58"
|
version = "0.8.59"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2"
|
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
+13
-6
@@ -401,20 +401,25 @@ impl RenderOnce for Modal {
|
|||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
|
.relative()
|
||||||
.when_some(self.title, |this, title| {
|
.when_some(self.title, |this, title| {
|
||||||
this.h_10().font_semibold().text_center().child(title)
|
this.h_10().font_semibold().text_center().child(title)
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
.when(self.show_close, |this| {
|
.when(self.show_close, |this| {
|
||||||
let on_cancel = on_cancel.clone();
|
let on_cancel = on_cancel.clone();
|
||||||
let on_close = on_close.clone();
|
let on_close = on_close.clone();
|
||||||
|
|
||||||
this.child(
|
this.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.top_0()
|
||||||
|
.bottom_0()
|
||||||
|
.right_2()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.child(
|
||||||
Button::new("close")
|
Button::new("close")
|
||||||
.icon(IconName::CloseCircleFill)
|
.icon(IconName::CloseCircleFill)
|
||||||
.absolute()
|
|
||||||
.top_1p5()
|
|
||||||
.right_2()
|
|
||||||
.custom(
|
.custom(
|
||||||
ButtonCustomVariant::new(window, cx)
|
ButtonCustomVariant::new(window, cx)
|
||||||
.foreground(cx.theme().icon_muted)
|
.foreground(cx.theme().icon_muted)
|
||||||
@@ -427,8 +432,10 @@ impl RenderOnce for Modal {
|
|||||||
on_close(&ClickEvent::default(), window, cx);
|
on_close(&ClickEvent::default(), window, cx);
|
||||||
window.close_modal(cx);
|
window.close_modal(cx);
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.pt_px()
|
.pt_px()
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ impl RenderOnce for Nav {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.child(div().truncate().min_w_0().child(self.label))
|
.min_w_0()
|
||||||
.child(div().flex_1())
|
.child(div().flex_1().min_w_0().truncate().child(self.label))
|
||||||
.when_some(self.suffix, |this, suffix| {
|
.when_some(self.suffix, |this, suffix| {
|
||||||
this.child(div().flex_shrink_0().child(suffix))
|
this.child(div().flex_shrink_0().child(suffix))
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,79 +1,306 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
use chat::{ChatRegistry, Room, RoomKind};
|
use chat::{ChatRegistry, Room, RoomKind};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
AnyElement, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
|
||||||
Subscription, WeakEntity, Window, div, px,
|
Styled, Subscription, Task, WeakEntity, Window, div, px, uniform_list,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use person::PersonRegistry;
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{DockArea, DockPlacement, PanelHandle};
|
use ui::dock::{DockArea, DockPlacement, PanelHandle};
|
||||||
use ui::input::{Input, InputEvent, InputState};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
use ui::{StyledExt, WindowExtension, v_flex};
|
use ui::modal::ModalButtonProps;
|
||||||
|
use ui::nav::Nav;
|
||||||
|
use ui::{Disableable, IconName, Selectable, Sizable, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
|
use crate::sidebar::nav_avatar;
|
||||||
|
|
||||||
|
/// Maximum number of members a chat can have.
|
||||||
|
const MAX_MEMBERS: usize = 3;
|
||||||
|
|
||||||
pub fn open(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
|
pub fn open(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
|
||||||
let view = cx.new(|cx| NewChat::new(dock, window, cx));
|
let view = cx.new(|cx| NewChat::new(dock, window, cx));
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
window.open_modal(cx, {
|
||||||
this.width(px(420.)).title("New chat").child(view.clone())
|
let view = view.clone();
|
||||||
|
move |this, _window, _cx| {
|
||||||
|
this.width(px(420.))
|
||||||
|
.title("New chat")
|
||||||
|
.confirm()
|
||||||
|
.button_props(
|
||||||
|
ModalButtonProps::default()
|
||||||
|
.cancel_text("Cancel")
|
||||||
|
.ok_text("Start"),
|
||||||
|
)
|
||||||
|
.on_ok({
|
||||||
|
let view = view.clone();
|
||||||
|
move |_, window, cx| view.update(cx, |this, cx| this.start_chat(window, cx))
|
||||||
|
})
|
||||||
|
.child(view.clone())
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NewChat {
|
pub struct NewChat {
|
||||||
/// The dock a started chat opens in
|
|
||||||
dock: WeakEntity<DockArea>,
|
dock: WeakEntity<DockArea>,
|
||||||
|
/// Find input, used to filter contacts or enter a new public key
|
||||||
/// Public key input
|
|
||||||
input: Entity<InputState>,
|
input: Entity<InputState>,
|
||||||
|
/// Current user's contacts
|
||||||
/// Error message
|
contacts: Option<Vec<PublicKey>>,
|
||||||
|
/// Public keys selected as members of the chat
|
||||||
|
members: Vec<PublicKey>,
|
||||||
|
/// Whether a NIP-05 address is being resolved
|
||||||
|
resolving: bool,
|
||||||
error: Option<SharedString>,
|
error: Option<SharedString>,
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
/// Input subscription
|
|
||||||
_subscription: Option<Subscription>,
|
_subscription: Option<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewChat {
|
impl NewChat {
|
||||||
fn new(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("npub"));
|
let input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx).placeholder("Search or enter npub or user@address.me")
|
||||||
|
});
|
||||||
|
|
||||||
let subscription = cx.subscribe_in(&input, window, |this, _input, event, window, cx| {
|
let subscription = cx.subscribe_in(&input, window, |this, _input, event, window, cx| {
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
this.start_chat(window, cx);
|
this.submit(window, cx)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load the contacts once the view is mounted
|
||||||
|
cx.defer_in(window, |this, window, cx| {
|
||||||
|
this.load_contacts(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
dock,
|
dock,
|
||||||
input,
|
input,
|
||||||
|
contacts: None,
|
||||||
|
members: Vec::new(),
|
||||||
|
resolving: false,
|
||||||
error: None,
|
error: None,
|
||||||
|
tasks: Vec::new(),
|
||||||
_subscription: Some(subscription),
|
_subscription: Some(subscription),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_chat(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
/// Load the signed-in user's contact list.
|
||||||
let value = self.input.read(cx).value().to_string();
|
fn load_contacts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.contacts.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Ok(peer) = PublicKey::parse(&value) else {
|
let nostr = NostrRegistry::global(cx);
|
||||||
self.set_error("Public key is invalid", cx);
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let Some(current_user) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.author(current_user)
|
||||||
|
.kind(Kind::ContactList)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let contacts: Vec<PublicKey> = client
|
||||||
|
.database()
|
||||||
|
.query(filter)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.map(|event| event.tags.public_keys().collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(contacts)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(contacts) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.contacts = Some(contacts);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_error(error.to_string(), cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The contacts matching the current input, or every contact when empty.
|
||||||
|
fn filtered_contacts(&self, cx: &Context<Self>) -> Vec<PublicKey> {
|
||||||
|
let Some(contacts) = self.contacts.as_ref() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let persons = PersonRegistry::global(cx);
|
||||||
|
let query = self.input.read(cx).value().trim().to_lowercase();
|
||||||
|
|
||||||
|
if query.is_empty() {
|
||||||
|
return contacts.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
contacts
|
||||||
|
.iter()
|
||||||
|
.filter(|public_key| {
|
||||||
|
if public_key.to_hex().to_lowercase().contains(&query) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let name = persons.read(cx).get(public_key, cx).name();
|
||||||
|
name.to_lowercase().contains(&query)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the input into a public key and add it as a member.
|
||||||
|
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.resolving {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = self.input.read(cx).value().trim().to_string();
|
||||||
|
|
||||||
|
if query.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A raw public key or an npub can be added directly.
|
||||||
|
if let Ok(public_key) = PublicKey::parse(&query) {
|
||||||
|
if self.try_add(public_key, cx) {
|
||||||
|
self.clear_input(window, cx);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A NIP-05 address is resolved before being added.
|
||||||
|
if let Ok(address) = Nip05Address::parse(&query) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let task = nostr.read(cx).query_address(address, cx);
|
||||||
|
|
||||||
|
self.set_resolving(true, window, cx);
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result = task.await;
|
||||||
|
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.set_resolving(false, window, cx);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(public_key) => {
|
||||||
|
if this.try_add(public_key, cx) {
|
||||||
|
this.clear_input(window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => this.set_error(error.to_string(), cx),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to a local search by contact name.
|
||||||
|
let mut matches = self.filtered_contacts(cx);
|
||||||
|
match matches.len() {
|
||||||
|
0 => self.set_error("Enter a valid npub or NIP-05 address", cx),
|
||||||
|
1 => {
|
||||||
|
let public_key = matches.remove(0);
|
||||||
|
if self.try_add(public_key, cx) {
|
||||||
|
self.clear_input(window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => self.set_error("Multiple contacts match, pick one from the list", cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a public key to the members if possible, otherwise show an error.
|
||||||
|
fn try_add(&mut self, public_key: PublicKey, cx: &mut Context<Self>) -> bool {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
|
let Some(current_user) = nostr.read(cx).current_user() else {
|
||||||
|
self.set_error("You are not signed in", cx);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if public_key == current_user {
|
||||||
|
self.set_error("You cannot add yourself", cx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.members.contains(&public_key) {
|
||||||
|
self.set_error("This person is already added", cx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.members.len() >= MAX_MEMBERS {
|
||||||
|
self.set_error(format!("A chat can have up to {MAX_MEMBERS} members"), cx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.members.push(public_key);
|
||||||
|
self.error = None;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle a contact's membership.
|
||||||
|
fn toggle_member(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||||
|
if self.members.iter().any(|member| member == public_key) {
|
||||||
|
self.members.retain(|member| member != public_key);
|
||||||
|
self.error = None;
|
||||||
|
cx.notify();
|
||||||
|
} else {
|
||||||
|
self.try_add(public_key.to_owned(), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.input.update(cx, |this, cx| {
|
||||||
|
this.set_value("", window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle the input's loading spinner while a NIP-05 address is resolved.
|
||||||
|
fn set_resolving(&mut self, resolving: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.input.update(cx, |this, cx| {
|
||||||
|
this.set_loading(resolving, window, cx);
|
||||||
|
});
|
||||||
|
self.resolving = resolving;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_chat(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||||
|
if self.members.is_empty() {
|
||||||
|
self.set_error("Add at least one person to chat with", cx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
|
|
||||||
let Some(current_user) = nostr.read(cx).current_user() else {
|
let Some(current_user) = nostr.read(cx).current_user() else {
|
||||||
self.set_error("You are not signed in", cx);
|
self.set_error("You are not signed in", cx);
|
||||||
return;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if peer == current_user {
|
let room = Room::new(current_user, self.members.clone())
|
||||||
self.set_error("You cannot chat with yourself", cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let room = Room::new(current_user, [peer])
|
|
||||||
.organize(¤t_user)
|
.organize(¤t_user)
|
||||||
.kind(RoomKind::Ongoing);
|
.kind(RoomKind::Ongoing);
|
||||||
|
|
||||||
@@ -91,36 +318,79 @@ impl NewChat {
|
|||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
window.close_modal(cx);
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_error(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) {
|
fn set_error(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||||
self.error = Some(message.into());
|
self.error = Some(message.into());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_contacts(
|
||||||
|
&self,
|
||||||
|
range: Range<usize>,
|
||||||
|
contacts: &[PublicKey],
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Vec<AnyElement> {
|
||||||
|
let persons = PersonRegistry::global(cx);
|
||||||
|
let start = range.start;
|
||||||
|
|
||||||
|
contacts
|
||||||
|
.get(range)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(offset, public_key)| {
|
||||||
|
let selected = self.members.contains(public_key);
|
||||||
|
let profile = persons.read(cx).get(public_key, cx);
|
||||||
|
let public_key = *public_key;
|
||||||
|
|
||||||
|
Nav::new(SharedString::from(format!("contact-{}", start + offset)))
|
||||||
|
.label(profile.name())
|
||||||
|
.text_sm()
|
||||||
|
.when_some(
|
||||||
|
nav_avatar(Some(profile.avatar_seed()), profile.avatar(), cx),
|
||||||
|
|this, avatar| this.prefix(avatar),
|
||||||
|
)
|
||||||
|
.selected(selected)
|
||||||
|
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||||
|
this.toggle_member(&public_key, cx);
|
||||||
|
}))
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for NewChat {
|
impl Render for NewChat {
|
||||||
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 contacts = Rc::new(self.filtered_contacts(cx));
|
||||||
|
let contact_count = contacts.len();
|
||||||
|
let notice = SharedString::from(format!("Group Chat can have up to {MAX_MEMBERS} members"));
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_4()
|
.gap_3()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_color(cx.theme().text_muted)
|
.w_full()
|
||||||
.child("Public key of the person you want to chat with")
|
.child(Input::new(&self.input).flex_1().text_sm().cleanable(true))
|
||||||
.child(Input::new(&self.input)),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
Button::new("start-chat")
|
Button::new("add")
|
||||||
.label("Start chat")
|
.icon(IconName::Plus)
|
||||||
.primary()
|
.tooltip("Add person")
|
||||||
.font_semibold()
|
.ghost()
|
||||||
|
.large()
|
||||||
|
.disabled(self.resolving)
|
||||||
.on_click(cx.listener(|this, _event, window, cx| {
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
this.start_chat(window, cx);
|
this.submit(window, cx);
|
||||||
})),
|
})),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.when_some(self.error.clone(), |this, error| {
|
.map(|this| {
|
||||||
|
if let Some(error) = self.error.clone() {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
@@ -128,6 +398,25 @@ impl Render for NewChat {
|
|||||||
.text_color(cx.theme().text_danger)
|
.text_color(cx.theme().text_danger)
|
||||||
.child(error),
|
.child(error),
|
||||||
)
|
)
|
||||||
})
|
} else {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_placeholder)
|
||||||
|
.child(notice),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
uniform_list(
|
||||||
|
"contacts",
|
||||||
|
contact_count,
|
||||||
|
cx.processor(move |this, range, _window, cx| {
|
||||||
|
this.render_contacts(range, contacts.as_slice(), cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.h_80(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
use auto_update::AutoUpdater;
|
use auto_update::AutoUpdater;
|
||||||
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
||||||
use common::TimestampExt;
|
use common::TimestampExt;
|
||||||
use community::{ChannelId, Community, CommunityEvent, CommunityRegistry};
|
use community::{ChannelId, Community, CommunityEvent, CommunityRegistry};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Context, Div, Entity, EventEmitter, FocusHandle, Focusable,
|
AnyElement, App, AppContext, Context, Div, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString, Stateful,
|
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString, Stateful,
|
||||||
Styled, StyledImage, Subscription, UniformListScrollHandle, WeakEntity, Window, div, img, px,
|
Styled, StyledImage, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, div, img,
|
||||||
retain_all, uniform_list,
|
px, retain_all, uniform_list,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
@@ -45,6 +47,7 @@ pub(crate) use utils::{nav_avatar, nav_icon, pick_banner};
|
|||||||
pub enum SidebarRow {
|
pub enum SidebarRow {
|
||||||
Room { room: Entity<Room> },
|
Room { room: Entity<Room> },
|
||||||
Community { community: Entity<Community> },
|
Community { community: Entity<Community> },
|
||||||
|
Contact { public_key: PublicKey },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A collapsible group of rows in the sidebar's community view.
|
/// A collapsible group of rows in the sidebar's community view.
|
||||||
@@ -81,10 +84,23 @@ pub struct Sidebar {
|
|||||||
active_tab: SidebarTab,
|
active_tab: SidebarTab,
|
||||||
/// The community the sidebar is browsing, if any
|
/// The community the sidebar is browsing, if any
|
||||||
community: Option<WeakEntity<Community>>,
|
community: Option<WeakEntity<Community>>,
|
||||||
|
/// Whether the inbox list is showing contacts to start a chat with
|
||||||
|
contacts_open: bool,
|
||||||
|
/// The signed-in user's contacts, loaded when the contact picker opens
|
||||||
|
contacts: Option<Vec<PublicKey>>,
|
||||||
|
/// Contacts selected in the contact picker
|
||||||
|
selected_contacts: HashSet<PublicKey>,
|
||||||
|
/// Scroll state of the contact picker's list
|
||||||
|
contacts_scroll: UniformListScrollHandle,
|
||||||
|
/// Whether the inbox list is showing chat requests instead of rooms
|
||||||
|
requests_open: bool,
|
||||||
|
/// Scroll state of the requests list
|
||||||
|
requests_scroll: UniformListScrollHandle,
|
||||||
channels_open: bool,
|
channels_open: bool,
|
||||||
admins_open: bool,
|
admins_open: bool,
|
||||||
members_open: bool,
|
members_open: bool,
|
||||||
new_requests: bool,
|
new_requests: bool,
|
||||||
|
tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
|
||||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +121,11 @@ impl Sidebar {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Keep the room and request counts in sync with the chat registry.
|
||||||
|
subscriptions.push(cx.observe(&chat, |_this, _chat, cx| {
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
|
||||||
subscriptions.push(cx.subscribe_in(
|
subscriptions.push(cx.subscribe_in(
|
||||||
&communities,
|
&communities,
|
||||||
window,
|
window,
|
||||||
@@ -136,10 +157,17 @@ impl Sidebar {
|
|||||||
banner: pick_banner(),
|
banner: pick_banner(),
|
||||||
active_tab: SidebarTab::Inbox,
|
active_tab: SidebarTab::Inbox,
|
||||||
community: None,
|
community: None,
|
||||||
|
contacts_open: false,
|
||||||
|
contacts: None,
|
||||||
|
selected_contacts: HashSet::new(),
|
||||||
|
contacts_scroll: UniformListScrollHandle::new(),
|
||||||
|
requests_open: false,
|
||||||
|
requests_scroll: UniformListScrollHandle::new(),
|
||||||
channels_open: true,
|
channels_open: true,
|
||||||
admins_open: true,
|
admins_open: true,
|
||||||
members_open: true,
|
members_open: true,
|
||||||
new_requests: false,
|
new_requests: false,
|
||||||
|
tasks: smallvec![],
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,9 +177,155 @@ impl Sidebar {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.active_tab = tab;
|
self.active_tab = tab;
|
||||||
|
self.close_contacts();
|
||||||
|
self.close_requests();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Leave the contact picker and clear its selection.
|
||||||
|
fn close_contacts(&mut self) {
|
||||||
|
self.contacts_open = false;
|
||||||
|
self.selected_contacts.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show or hide the contact picker in the inbox list.
|
||||||
|
fn toggle_contacts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.contacts_open = !self.contacts_open;
|
||||||
|
|
||||||
|
if !self.contacts_open {
|
||||||
|
self.selected_contacts.clear();
|
||||||
|
} else {
|
||||||
|
self.close_requests();
|
||||||
|
if self.contacts.is_none() {
|
||||||
|
self.load_contacts(window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Leave the requests list in the inbox.
|
||||||
|
fn close_requests(&mut self) {
|
||||||
|
self.requests_open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show or hide the chat requests in the inbox list.
|
||||||
|
fn toggle_requests(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.requests_open = !self.requests_open;
|
||||||
|
|
||||||
|
if self.requests_open {
|
||||||
|
self.close_contacts();
|
||||||
|
self.new_requests = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the signed-in user's contact list into the contact picker.
|
||||||
|
fn load_contacts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.author(public_key)
|
||||||
|
.kind(Kind::ContactList)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let contacts: Vec<PublicKey> = client
|
||||||
|
.database()
|
||||||
|
.query(filter)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.map(|event| event.tags.public_keys().collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(contacts)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(contacts) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.contacts = Some(contacts);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
window.push_notification(
|
||||||
|
Notification::error(error.to_string()).autohide(false),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle a contact's selection in the contact picker.
|
||||||
|
fn toggle_contact(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||||
|
if !self.selected_contacts.remove(public_key) {
|
||||||
|
self.selected_contacts.insert(public_key.to_owned());
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a chat with the contacts selected in the picker.
|
||||||
|
fn create_chat(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_contacts.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let chat = ChatRegistry::global(cx);
|
||||||
|
let async_chat = chat.downgrade();
|
||||||
|
let dock = self.dock.clone();
|
||||||
|
|
||||||
|
let receivers: Vec<PublicKey> = self.selected_contacts.iter().copied().collect();
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let room = async_chat.update_in(cx, |chat, _window, cx| {
|
||||||
|
let room = cx.new(|_| {
|
||||||
|
Room::new(public_key, receivers)
|
||||||
|
.organize(&public_key)
|
||||||
|
.kind(RoomKind::Ongoing)
|
||||||
|
});
|
||||||
|
chat.track_room(&room, cx);
|
||||||
|
room
|
||||||
|
})?;
|
||||||
|
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
ui::dock::add_panel_to(
|
||||||
|
&dock,
|
||||||
|
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
|
||||||
|
DockPlacement::Center,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Leave the picker now that the chat is open
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.close_contacts();
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/// Leave the community view, returning the sidebar to its tab list.
|
/// Leave the community view, returning the sidebar to its tab list.
|
||||||
fn reset_community(&mut self, cx: &mut Context<Self>) {
|
fn reset_community(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.community.take().is_none() {
|
if self.community.take().is_none() {
|
||||||
@@ -293,13 +467,34 @@ impl Sidebar {
|
|||||||
fn render_tabs(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_tabs(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let sidebar = cx.entity().downgrade();
|
let sidebar = cx.entity().downgrade();
|
||||||
let active_tab = self.active_tab;
|
let active_tab = self.active_tab;
|
||||||
|
let requests_open = self.requests_open && active_tab == SidebarTab::Inbox;
|
||||||
|
let contacts_open = self.contacts_open && active_tab == SidebarTab::Inbox && !requests_open;
|
||||||
let rows = Rc::new(self.rows_for(active_tab, cx));
|
let rows = Rc::new(self.rows_for(active_tab, cx));
|
||||||
let scroll_handle = &self.scroll_handles[active_tab.index()];
|
|
||||||
|
// Whether to show the create button for the active tab
|
||||||
|
let create_button = contacts_open && !self.selected_contacts.is_empty();
|
||||||
|
|
||||||
|
let (list_id, scroll_handle, list_title) = if contacts_open {
|
||||||
|
("sidebar-contacts", &self.contacts_scroll, "Contacts")
|
||||||
|
} else if requests_open {
|
||||||
|
("sidebar-requests", &self.requests_scroll, "Requests")
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
active_tab.list_id(),
|
||||||
|
&self.scroll_handles[active_tab.index()],
|
||||||
|
active_tab.list_title(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_count = ChatRegistry::global(cx)
|
||||||
|
.read(cx)
|
||||||
|
.count(&RoomKind::Request, cx);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_h_0()
|
.min_h_0()
|
||||||
|
.relative()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div().px_2().child(
|
div().px_2().child(
|
||||||
@@ -329,38 +524,58 @@ impl Sidebar {
|
|||||||
.px_2()
|
.px_2()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
NavItem::new(
|
NavItem::new("chat", "New Chat", Icon::new(IconName::Message).small())
|
||||||
"new-chat",
|
|
||||||
"New Chat",
|
|
||||||
Icon::new(IconName::Message).small(),
|
|
||||||
)
|
|
||||||
.on_click(|_event, window, cx| {
|
.on_click(|_event, window, cx| {
|
||||||
window.dispatch_action(Box::new(Command::NewChat), cx)
|
window.dispatch_action(Box::new(Command::NewChat), cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
NavItem::new("reqs", "Requests", Icon::new(IconName::Invite).small())
|
NavItem::new("reqs", "Requests", Icon::new(IconName::Invite).small())
|
||||||
.when(self.new_requests, |this| {
|
.when(request_count > 0, |this| {
|
||||||
this.suffix(div().size_1().rounded_full().bg(cx.theme().cursor))
|
this.suffix(
|
||||||
})
|
h_flex()
|
||||||
.on_click({
|
.gap_1()
|
||||||
let sidebar = sidebar.clone();
|
.items_center()
|
||||||
move |_event, window, cx| {
|
.child(
|
||||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
h_flex()
|
||||||
this.new_requests = false;
|
.py_0p5()
|
||||||
cx.notify();
|
.px_1()
|
||||||
}) {
|
.min_w_6()
|
||||||
log::error!("Failed to clear new requests: {error}");
|
.justify_center()
|
||||||
}
|
.text_size(px(10.))
|
||||||
window.dispatch_action(Box::new(Command::ShowRequests), cx);
|
.text_color(cx.theme().text_muted)
|
||||||
}
|
.text_center()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().elevated_surface_background)
|
||||||
|
.child(SharedString::from(
|
||||||
|
request_count.to_string(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.when(self.new_requests && !requests_open, |this| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.size_1()
|
||||||
|
.rounded_full()
|
||||||
|
.bg(cx.theme().cursor),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
.when(requests_open, |this| {
|
||||||
|
this.bg(cx.theme().ghost_element_active)
|
||||||
|
})
|
||||||
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
|
this.toggle_requests(cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
NavItem::new("contacts", "Contacts", Icon::new(IconName::Book).small())
|
NavItem::new("contacts", "Contacts", Icon::new(IconName::Book).small())
|
||||||
.on_click(|_event, window, cx| {
|
.when(contacts_open, |this| {
|
||||||
window.dispatch_action(Box::new(Command::ShowContactList), cx)
|
this.bg(cx.theme().ghost_element_active)
|
||||||
}),
|
})
|
||||||
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
|
this.toggle_contacts(window, cx);
|
||||||
|
})),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SidebarTab::Communities => this.child(
|
SidebarTab::Communities => this.child(
|
||||||
@@ -391,7 +606,7 @@ impl Sidebar {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_placeholder)
|
.text_color(cx.theme().text_placeholder)
|
||||||
.child(active_tab.list_title()),
|
.child(list_title),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -399,7 +614,7 @@ impl Sidebar {
|
|||||||
.flex_1()
|
.flex_1()
|
||||||
.child(
|
.child(
|
||||||
uniform_list(
|
uniform_list(
|
||||||
active_tab.list_id(),
|
list_id,
|
||||||
rows.len(),
|
rows.len(),
|
||||||
cx.processor(move |this, range, _window, cx| {
|
cx.processor(move |this, range, _window, cx| {
|
||||||
this.render_rows(range, rows.as_slice(), cx)
|
this.render_rows(range, rows.as_slice(), cx)
|
||||||
@@ -411,6 +626,43 @@ impl Sidebar {
|
|||||||
)
|
)
|
||||||
.child(Scrollbar::vertical(scroll_handle)),
|
.child(Scrollbar::vertical(scroll_handle)),
|
||||||
)
|
)
|
||||||
|
.when(create_button, |this| {
|
||||||
|
this.child(
|
||||||
|
v_flex()
|
||||||
|
.absolute()
|
||||||
|
.bottom_0()
|
||||||
|
.left_0()
|
||||||
|
.justify_center()
|
||||||
|
.h_20()
|
||||||
|
.w_full()
|
||||||
|
.px_4()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.bg(cx.theme().background)
|
||||||
|
.child(
|
||||||
|
Button::new("create-chat")
|
||||||
|
.label(if self.selected_contacts.len() > 1 {
|
||||||
|
"Create Group Chat"
|
||||||
|
} else {
|
||||||
|
"Create Chat"
|
||||||
|
})
|
||||||
|
.primary()
|
||||||
|
.on_click(cx.listener(|this, _ev, window, cx| {
|
||||||
|
this.create_chat(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child("Selected")
|
||||||
|
.child(div().font_semibold().child(SharedString::from(
|
||||||
|
self.selected_contacts.len().to_string(),
|
||||||
|
))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,6 +789,21 @@ impl Sidebar {
|
|||||||
|
|
||||||
fn rows_for(&self, tab: SidebarTab, cx: &App) -> Vec<SidebarRow> {
|
fn rows_for(&self, tab: SidebarTab, cx: &App) -> Vec<SidebarRow> {
|
||||||
match tab {
|
match tab {
|
||||||
|
SidebarTab::Inbox if self.contacts_open => self
|
||||||
|
.contacts
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|public_key| SidebarRow::Contact { public_key })
|
||||||
|
.collect(),
|
||||||
|
SidebarTab::Inbox if self.requests_open => {
|
||||||
|
let chat = ChatRegistry::global(cx);
|
||||||
|
chat.read(cx)
|
||||||
|
.rooms(&RoomKind::Request, cx)
|
||||||
|
.into_iter()
|
||||||
|
.map(|room| SidebarRow::Room { room })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
SidebarTab::Inbox => {
|
SidebarTab::Inbox => {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
chat.read(cx)
|
chat.read(cx)
|
||||||
@@ -605,6 +872,31 @@ impl Sidebar {
|
|||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
SidebarRow::Contact { public_key } => {
|
||||||
|
let persons = PersonRegistry::global(cx);
|
||||||
|
let person = persons.read(cx).get(public_key, cx);
|
||||||
|
let selected = self.selected_contacts.contains(public_key);
|
||||||
|
let sidebar = cx.entity().downgrade();
|
||||||
|
let public_key = public_key.to_owned();
|
||||||
|
|
||||||
|
Nav::new(SharedString::from(format!("contact-{index}")))
|
||||||
|
.label(person.name())
|
||||||
|
.text_sm()
|
||||||
|
.font_medium()
|
||||||
|
.when_some(
|
||||||
|
nav_avatar(Some(person.avatar_seed()), person.avatar(), cx),
|
||||||
|
|this, avatar| this.prefix(avatar),
|
||||||
|
)
|
||||||
|
.selected(selected)
|
||||||
|
.on_click(move |_event, _window, cx| {
|
||||||
|
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||||
|
this.toggle_contact(&public_key, cx);
|
||||||
|
}) {
|
||||||
|
log::error!("Failed to select contact: {error}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
SidebarRow::Community { community } => {
|
SidebarRow::Community { community } => {
|
||||||
let name = community.read(cx).name();
|
let name = community.read(cx).name();
|
||||||
let seed = community.read(cx).id().to_hex();
|
let seed = community.read(cx).id().to_hex();
|
||||||
|
|||||||
Reference in New Issue
Block a user