feat: add support for NIP-29 (#56)
Reviewed-on: #56
This commit was merged in pull request #56.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
use gpui::{App, AppContext, ParentElement, Styled, Window, px};
|
||||
use nip29::{GroupReference, GroupsRegistry};
|
||||
use ui::WindowExtension;
|
||||
use ui::input::{Input, InputState};
|
||||
use ui::notification::Notification;
|
||||
|
||||
pub fn open(window: &mut Window, cx: &mut App) {
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("naddr1…"));
|
||||
|
||||
window.open_dialog(cx, move |this, _window, _cx| {
|
||||
let input = input.clone();
|
||||
|
||||
this.width(px(380.))
|
||||
.confirm()
|
||||
.title("Join Group")
|
||||
.child(Input::new(&input).cleanable(true).flex_1().text_sm())
|
||||
.on_ok(move |_event, window, cx| {
|
||||
let groups = GroupsRegistry::global(cx);
|
||||
|
||||
let reference = match GroupReference::parse(input.read(cx).value().trim()) {
|
||||
Ok(reference) => reference,
|
||||
Err(error) => {
|
||||
window.push_notification(Notification::error(error.to_string()), cx);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
groups.update(cx, |registry, cx| {
|
||||
registry.join(reference, cx);
|
||||
});
|
||||
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod import;
|
||||
pub mod join_group;
|
||||
pub mod new_chat;
|
||||
pub mod new_community;
|
||||
pub mod new_group;
|
||||
pub mod profile;
|
||||
pub mod restore;
|
||||
pub mod screening;
|
||||
pub mod settings;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
use community::{CommunityMetadata, CommunityRegistry};
|
||||
use gpui::{App, AppContext, ParentElement, Window, px};
|
||||
use ui::WindowExtension;
|
||||
use ui::input::{Input, InputState};
|
||||
|
||||
pub fn open(window: &mut Window, cx: &mut App) {
|
||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Community name"));
|
||||
|
||||
window.open_dialog(cx, move |this, _window, _cx| {
|
||||
let name_input = name_input.clone();
|
||||
|
||||
this.width(px(380.))
|
||||
.confirm()
|
||||
.title("New community")
|
||||
.child(Input::new(&name_input))
|
||||
.on_ok(move |_event, _window, cx| {
|
||||
let community = CommunityRegistry::global(cx);
|
||||
let name = name_input.read(cx).value().trim().to_owned();
|
||||
|
||||
if name.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let metadata = CommunityMetadata {
|
||||
name,
|
||||
..CommunityMetadata::default()
|
||||
};
|
||||
|
||||
community.update(cx, |registry, cx| {
|
||||
registry.create(metadata, cx);
|
||||
});
|
||||
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use gpui::{App, AppContext, Div, Entity, ParentElement, Styled, Window, px};
|
||||
use nip29::{GroupId, GroupKey, GroupMetadata, GroupsRegistry};
|
||||
use nostr_sdk::prelude::*;
|
||||
use theme::ActiveTheme;
|
||||
use ui::input::{Input, InputState};
|
||||
use ui::notification::Notification;
|
||||
use ui::{WindowExtension, v_flex};
|
||||
|
||||
pub fn open(window: &mut Window, cx: &mut App) {
|
||||
let name = cx.new(|cx| InputState::new(window, cx).placeholder("Group name"));
|
||||
let id = cx.new(|cx| InputState::new(window, cx).placeholder("Group id"));
|
||||
let relay = cx.new(|cx| InputState::new(window, cx).placeholder("wss://relay.example.com"));
|
||||
|
||||
window.open_dialog(cx, move |this, _window, cx| {
|
||||
let name = name.clone();
|
||||
let id = id.clone();
|
||||
let relay = relay.clone();
|
||||
|
||||
this.width(px(380.))
|
||||
.confirm()
|
||||
.title("New group")
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.child(field("Name", &name, cx))
|
||||
.child(field("Group id", &id, cx))
|
||||
.child(field("Relay", &relay, cx)),
|
||||
)
|
||||
.on_ok(move |_event, window, cx| create(&name, &id, &relay, window, cx))
|
||||
});
|
||||
}
|
||||
|
||||
fn field(label: &'static str, input: &Entity<InputState>, cx: &App) -> Div {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(label)
|
||||
.child(Input::new(input))
|
||||
}
|
||||
|
||||
fn create(
|
||||
name: &Entity<InputState>,
|
||||
id: &Entity<InputState>,
|
||||
relay: &Entity<InputState>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> bool {
|
||||
let name_value = name.read(cx).value().trim().to_owned();
|
||||
let raw_id = id.read(cx).value().trim().to_owned();
|
||||
let raw_relay = relay.read(cx).value().trim().to_owned();
|
||||
|
||||
if name_value.is_empty() {
|
||||
window.push_notification(Notification::error("Enter a group name"), cx);
|
||||
return false;
|
||||
}
|
||||
|
||||
let id = if raw_id.is_empty() {
|
||||
slug(&name_value)
|
||||
} else {
|
||||
raw_id
|
||||
};
|
||||
|
||||
let id = match GroupId::new(id) {
|
||||
Ok(id) => id,
|
||||
Err(error) => {
|
||||
window.push_notification(Notification::error(error.to_string()), cx);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let relay = match RelayUrl::parse(&raw_relay) {
|
||||
Ok(relay) => relay,
|
||||
Err(error) => {
|
||||
window.push_notification(Notification::error(error.to_string()), cx);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let mut metadata = GroupMetadata::default();
|
||||
metadata.name = Some(name_value);
|
||||
|
||||
let registry = GroupsRegistry::global(cx);
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.create(GroupKey::new(relay, id), metadata, cx);
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn slug(name: &str) -> String {
|
||||
let mut slug = String::with_capacity(name.len());
|
||||
let mut separator = false;
|
||||
|
||||
for character in name.chars() {
|
||||
if character.is_ascii_alphanumeric() {
|
||||
slug.push(character.to_ascii_lowercase());
|
||||
separator = false;
|
||||
} else if !separator && !slug.is_empty() {
|
||||
slug.push('-');
|
||||
separator = true;
|
||||
}
|
||||
}
|
||||
|
||||
slug.trim_end_matches('-').to_owned()
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
use anyhow::Error;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, ClipboardItem, Context, Div, IntoElement, ParentElement, Render, SharedString,
|
||||
Styled, Task, Window, div, px,
|
||||
};
|
||||
use instant::Duration;
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{Person, PersonRegistry, shorten_pubkey};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::{IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
/// Open a read-only profile for `public_key`.
|
||||
pub fn open(public_key: PublicKey, window: &mut Window, cx: &mut App) {
|
||||
let profile = cx.new(|_| ProfileDialog::new(public_key));
|
||||
|
||||
window.open_dialog(cx, move |this, _window, _cx| {
|
||||
this.width(px(360.)).title("Profile").child(profile.clone())
|
||||
});
|
||||
}
|
||||
|
||||
struct ProfileDialog {
|
||||
public_key: PublicKey,
|
||||
copied: bool,
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
}
|
||||
|
||||
impl ProfileDialog {
|
||||
fn new(public_key: PublicKey) -> Self {
|
||||
Self {
|
||||
public_key,
|
||||
copied: false,
|
||||
tasks: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn profile(&self, cx: &App) -> Person {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
persons.read(cx).get(&self.public_key, cx)
|
||||
}
|
||||
|
||||
fn copy(&mut self, value: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let item = ClipboardItem::new_string(value);
|
||||
cx.write_to_clipboard(item);
|
||||
|
||||
self.set_copied(true, window, cx);
|
||||
}
|
||||
|
||||
fn set_copied(&mut self, status: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.copied = status;
|
||||
cx.notify();
|
||||
|
||||
if status {
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
|
||||
// Reset the copied state after a delay
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_copied(false, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProfileDialog {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let profile = self.profile(cx);
|
||||
let metadata = profile.metadata();
|
||||
|
||||
// Get the public key as short string
|
||||
let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8));
|
||||
|
||||
let about = metadata
|
||||
.about
|
||||
.filter(|about| !about.trim().is_empty())
|
||||
.map(SharedString::from);
|
||||
|
||||
let website = metadata
|
||||
.website
|
||||
.filter(|website| !website.trim().is_empty())
|
||||
.map(SharedString::from);
|
||||
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(
|
||||
v_flex()
|
||||
.h_40()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
Avatar::new(profile.avatar())
|
||||
.seed(profile.avatar_seed())
|
||||
.large(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(label("Name", cx))
|
||||
.child(div().text_sm().child(profile.name())),
|
||||
)
|
||||
.when_some(about, |this, about| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(label("About", cx))
|
||||
.child(div().text_sm().child(about)),
|
||||
)
|
||||
})
|
||||
.when_some(website, |this, website| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(label("Website", cx))
|
||||
.child(div().text_sm().child(website)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex().gap_1p5().child(label("Public Key", cx)).child(
|
||||
h_flex()
|
||||
.h_8()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.gap_3()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().secondary_background)
|
||||
.text_sm()
|
||||
.text_color(cx.theme().secondary_foreground)
|
||||
.child(shorten_pkey)
|
||||
.child(
|
||||
Button::new("copy")
|
||||
.icon({
|
||||
if self.copied {
|
||||
IconName::CheckCircle
|
||||
} else {
|
||||
IconName::Copy
|
||||
}
|
||||
})
|
||||
.xsmall()
|
||||
.secondary()
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.copy(this.public_key.to_bech32().unwrap(), window, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn label(label: &'static str, cx: &App) -> Div {
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(label)
|
||||
}
|
||||
Reference in New Issue
Block a user