add encrypted private section to group lists and channels
This commit is contained in:
@@ -23,22 +23,49 @@ const PRIVATE_DESC: &str = "Only members can read group messages";
|
||||
const CLOSED_DESC: &str = "Join requests are ignored (invite-only)";
|
||||
const RESTRICTED_DESC: &str = "Only members can post messages";
|
||||
const HIDDEN_DESC: &str = "Hidden from non-members, not discoverable";
|
||||
const LIST_PRIVATE_DESC: &str = "Kept encrypted in your group list so others can't see you joined";
|
||||
|
||||
pub fn open(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
|
||||
let view = cx.new(|cx| NewGroup::new(dock, window, cx));
|
||||
open_with(dock, None, window, cx);
|
||||
}
|
||||
|
||||
pub fn open_channel(
|
||||
dock: WeakEntity<DockArea>,
|
||||
parent: GroupKey,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
open_with(dock, Some(parent), window, cx);
|
||||
}
|
||||
|
||||
fn open_with(
|
||||
dock: WeakEntity<DockArea>,
|
||||
parent: Option<GroupKey>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let is_channel = parent.is_some();
|
||||
let view = cx.new(|cx| NewGroup::new(dock, parent, window, cx));
|
||||
|
||||
window.open_dialog(cx, {
|
||||
let view = view.clone();
|
||||
|
||||
move |this, _window, _cx| {
|
||||
this.width(px(450.))
|
||||
.title("Create a Group")
|
||||
.child(view.clone())
|
||||
let title = if is_channel {
|
||||
"Create Channel"
|
||||
} else {
|
||||
"Create a Group"
|
||||
};
|
||||
|
||||
this.width(px(440.)).title(title).child(view.clone())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
struct NewGroup {
|
||||
dock: WeakEntity<DockArea>,
|
||||
/// The parent group when creating a channel; the relay is locked to its own.
|
||||
parent: Option<GroupKey>,
|
||||
name_input: Entity<InputState>,
|
||||
id_input: Entity<InputState>,
|
||||
about_input: Entity<TextareaState>,
|
||||
@@ -49,6 +76,8 @@ struct NewGroup {
|
||||
selected_relay: Option<RelayUrl>,
|
||||
/// Whether the optional custom group id input is shown.
|
||||
show_custom_id: bool,
|
||||
/// Keep the group in the NIP-44 encrypted section of the user's group list.
|
||||
list_private: bool,
|
||||
/// The uploaded group picture, ready to be published with the metadata.
|
||||
picture: Option<SharedString>,
|
||||
/// Whether a picture upload is in flight.
|
||||
@@ -64,7 +93,12 @@ struct NewGroup {
|
||||
}
|
||||
|
||||
impl NewGroup {
|
||||
fn new(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
fn new(
|
||||
dock: WeakEntity<DockArea>,
|
||||
parent: Option<GroupKey>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("#example"));
|
||||
let id_input = cx.new(|cx| InputState::new(window, cx).placeholder("my-group"));
|
||||
|
||||
@@ -118,13 +152,17 @@ impl NewGroup {
|
||||
));
|
||||
}
|
||||
|
||||
// Load the user's relays once the view is mounted.
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.load_relays(window, cx);
|
||||
});
|
||||
// Load the user's relays once the view is mounted; a channel's relay
|
||||
// is locked to its parent's, so there is nothing to pick.
|
||||
if parent.is_none() {
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.load_relays(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
dock,
|
||||
parent,
|
||||
name_input,
|
||||
id_input,
|
||||
about_input,
|
||||
@@ -132,6 +170,7 @@ impl NewGroup {
|
||||
relays,
|
||||
selected_relay,
|
||||
show_custom_id: false,
|
||||
list_private: false,
|
||||
picture: None,
|
||||
uploading: false,
|
||||
private: false,
|
||||
@@ -226,12 +265,15 @@ impl NewGroup {
|
||||
return;
|
||||
}
|
||||
|
||||
let relay = match self.effective_relay(cx) {
|
||||
Ok(relay) => relay,
|
||||
Err(error) => {
|
||||
self.set_error(error, cx);
|
||||
return;
|
||||
}
|
||||
let relay = match &self.parent {
|
||||
Some(parent) => parent.relay().clone(),
|
||||
None => match self.effective_relay(cx) {
|
||||
Ok(relay) => relay,
|
||||
Err(error) => {
|
||||
self.set_error(error, cx);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let custom_id = self.id_input.read(cx).value().trim().to_lowercase();
|
||||
@@ -257,8 +299,9 @@ impl NewGroup {
|
||||
closed: self.closed,
|
||||
restricted: self.restricted,
|
||||
hidden: self.hidden,
|
||||
list_private: self.list_private,
|
||||
custom_id: (!custom_id.is_empty()).then_some(custom_id),
|
||||
..Default::default()
|
||||
parent: self.parent.as_ref().map(|parent| parent.id().clone()),
|
||||
};
|
||||
|
||||
self.creating = true;
|
||||
@@ -347,6 +390,20 @@ impl NewGroup {
|
||||
}
|
||||
|
||||
fn render_relay(&self, selected: &str, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
// A channel must live on its parent's relay: the parent tag carries a
|
||||
// relay-scoped group id the relay validates against its own state.
|
||||
if let Some(parent) = &self.parent {
|
||||
return v_flex().gap_1().child(field_label("Relay", cx)).child(
|
||||
div()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(format!(
|
||||
"Created on {} (same relay as the parent group).",
|
||||
relay_label(parent.relay())
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
let this = cx.entity().downgrade();
|
||||
let relays = self.relays.clone();
|
||||
|
||||
@@ -388,73 +445,65 @@ impl NewGroup {
|
||||
}
|
||||
|
||||
fn render_access(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let private = Switch::new("private")
|
||||
.label("Private")
|
||||
.description(PRIVATE_DESC)
|
||||
.checked(self.private)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.private = *checked;
|
||||
cx.notify();
|
||||
}));
|
||||
|
||||
let closed = Switch::new("closed")
|
||||
.label("Closed")
|
||||
.description(CLOSED_DESC)
|
||||
.checked(self.closed)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.closed = *checked;
|
||||
cx.notify();
|
||||
}));
|
||||
|
||||
let restricted = Switch::new("restricted")
|
||||
.label("Restricted")
|
||||
.description(RESTRICTED_DESC)
|
||||
.checked(self.restricted)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.restricted = *checked;
|
||||
cx.notify();
|
||||
}));
|
||||
|
||||
let hidden = Switch::new("hidden")
|
||||
.label("Hidden")
|
||||
.description(HIDDEN_DESC)
|
||||
.checked(self.hidden)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.hidden = *checked;
|
||||
cx.notify();
|
||||
}));
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(field_label("ACCESS SETTINGS", cx))
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded(px(control_radius()))
|
||||
.bg(cx.theme().surface_raised)
|
||||
.child(
|
||||
Switch::new("private")
|
||||
.label("Private")
|
||||
.description(PRIVATE_DESC)
|
||||
.checked(self.private)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.private = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded(px(control_radius()))
|
||||
.bg(cx.theme().surface_raised)
|
||||
.child(
|
||||
Switch::new("closed")
|
||||
.label("Closed")
|
||||
.description(CLOSED_DESC)
|
||||
.checked(self.closed)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.closed = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded(px(control_radius()))
|
||||
.bg(cx.theme().surface_raised)
|
||||
.child(
|
||||
Switch::new("restricted")
|
||||
.label("Restricted")
|
||||
.description(RESTRICTED_DESC)
|
||||
.checked(self.restricted)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.restricted = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded(px(control_radius()))
|
||||
.bg(cx.theme().surface_raised)
|
||||
.child(
|
||||
Switch::new("hidden")
|
||||
.label("Hidden")
|
||||
.description(HIDDEN_DESC)
|
||||
.checked(self.hidden)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.hidden = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(switch_row(private, cx))
|
||||
.child(switch_row(closed, cx))
|
||||
.child(switch_row(restricted, cx))
|
||||
.child(switch_row(hidden, cx))
|
||||
}
|
||||
|
||||
fn render_list_setting(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let list_private = Switch::new("list-private")
|
||||
.label("Private on my list")
|
||||
.description(LIST_PRIVATE_DESC)
|
||||
.checked(self.list_private)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.list_private = *checked;
|
||||
cx.notify();
|
||||
}));
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(field_label("YOUR LIST", cx))
|
||||
.child(switch_row(list_private, cx))
|
||||
}
|
||||
|
||||
fn render_error(
|
||||
@@ -586,6 +635,7 @@ impl Render for NewGroup {
|
||||
.child(self.render_relay(&selected, cx))
|
||||
.child(field("Description", Textarea::new(&self.about_input), cx))
|
||||
.child(self.render_access(cx))
|
||||
.child(self.render_list_setting(cx))
|
||||
.when_some(self.error.clone(), |this, error| {
|
||||
this.child(self.render_error(&error, web_url.as_deref(), cx))
|
||||
})
|
||||
@@ -601,7 +651,11 @@ impl Render for NewGroup {
|
||||
)
|
||||
.child(
|
||||
Button::new("create")
|
||||
.label("Create Group")
|
||||
.label(if self.parent.is_some() {
|
||||
"Create Channel"
|
||||
} else {
|
||||
"Create Group"
|
||||
})
|
||||
.primary()
|
||||
.font_semibold()
|
||||
.loading(self.creating)
|
||||
@@ -624,6 +678,15 @@ fn field_label(text: &'static str, cx: &App) -> Div {
|
||||
.child(text)
|
||||
}
|
||||
|
||||
fn switch_row(switch: impl IntoElement, cx: &App) -> Div {
|
||||
div()
|
||||
.w_full()
|
||||
.p_2()
|
||||
.rounded(px(control_radius()))
|
||||
.bg(cx.theme().surface_raised)
|
||||
.child(switch)
|
||||
}
|
||||
|
||||
fn relay_label(relay: &RelayUrl) -> SharedString {
|
||||
relay
|
||||
.as_str()
|
||||
|
||||
@@ -370,6 +370,25 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the channel creation dialog under the action's group.
|
||||
fn on_create_channel(
|
||||
&mut self,
|
||||
action: &nip29_ui::CreateChannel,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Ok(id) = GroupId::new(action.parent.clone()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
new_group::open_channel(
|
||||
self.dock.downgrade(),
|
||||
GroupKey::new(action.relay.clone(), id),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
/// Show the sidebar's information for the group a panel belongs to.
|
||||
fn show_group_meta(&mut self, action: &nip29_ui::ShowGroupMeta, cx: &mut Context<Self>) {
|
||||
let Ok(id) = GroupId::new(action.group.clone()) else {
|
||||
@@ -453,6 +472,7 @@ impl Render for Workspace {
|
||||
div()
|
||||
.id("workspace")
|
||||
.on_action(cx.listener(Self::on_command))
|
||||
.on_action(cx.listener(Self::on_create_channel))
|
||||
.relative()
|
||||
.size_full()
|
||||
.child(self.dock.clone())
|
||||
|
||||
Reference in New Issue
Block a user