add group preview mode to browse panel
This commit is contained in:
+46
-5
@@ -65,6 +65,7 @@ impl EventEmitter<GroupsEvent> for GroupsRegistry {}
|
|||||||
|
|
||||||
pub struct GroupsRegistry {
|
pub struct GroupsRegistry {
|
||||||
groups: Vec<Entity<Group>>,
|
groups: Vec<Entity<Group>>,
|
||||||
|
previews: HashMap<GroupKey, Entity<Group>>,
|
||||||
list: GroupList,
|
list: GroupList,
|
||||||
browse: HashMap<SubscriptionId, RelayUrl>,
|
browse: HashMap<SubscriptionId, RelayUrl>,
|
||||||
routes: HashMap<SubscriptionId, GroupKey>,
|
routes: HashMap<SubscriptionId, GroupKey>,
|
||||||
@@ -120,6 +121,7 @@ impl GroupsRegistry {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
|
previews: HashMap::new(),
|
||||||
list: GroupList::default(),
|
list: GroupList::default(),
|
||||||
browse: HashMap::new(),
|
browse: HashMap::new(),
|
||||||
routes: HashMap::new(),
|
routes: HashMap::new(),
|
||||||
@@ -145,6 +147,7 @@ impl GroupsRegistry {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|group| group.read(cx).key() == key)
|
.find(|group| group.read(cx).key() == key)
|
||||||
.cloned()
|
.cloned()
|
||||||
|
.or_else(|| self.previews.get(key).cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn join(&mut self, reference: GroupReference, cx: &mut Context<Self>) {
|
pub fn join(&mut self, reference: GroupReference, cx: &mut Context<Self>) {
|
||||||
@@ -268,6 +271,11 @@ impl GroupsRegistry {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the group is one of the user's groups, not a preview.
|
||||||
|
fn joined(&self, key: &GroupKey, cx: &App) -> bool {
|
||||||
|
self.groups.iter().any(|group| group.read(cx).key() == key)
|
||||||
|
}
|
||||||
|
|
||||||
/// Load the discoverable groups from the local database only.
|
/// Load the discoverable groups from the local database only.
|
||||||
pub fn cached_discovery(&self, cx: &App) -> Task<Discovery> {
|
pub fn cached_discovery(&self, cx: &App) -> Task<Discovery> {
|
||||||
let client = NostrRegistry::global(cx).read(cx).client();
|
let client = NostrRegistry::global(cx).read(cx).client();
|
||||||
@@ -340,6 +348,7 @@ impl GroupsRegistry {
|
|||||||
let routes = std::mem::take(&mut self.routes);
|
let routes = std::mem::take(&mut self.routes);
|
||||||
|
|
||||||
self.groups.clear();
|
self.groups.clear();
|
||||||
|
self.previews.clear();
|
||||||
self.observers.clear();
|
self.observers.clear();
|
||||||
self.forks.clear();
|
self.forks.clear();
|
||||||
self.fork_authors.clear();
|
self.fork_authors.clear();
|
||||||
@@ -532,6 +541,12 @@ impl GroupsRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ensure(&mut self, key: GroupKey, cx: &mut Context<Self>) {
|
fn ensure(&mut self, key: GroupKey, cx: &mut Context<Self>) {
|
||||||
|
// A previewed group becomes one of the user's groups, keeping its state.
|
||||||
|
if let Some(group) = self.previews.remove(&key) {
|
||||||
|
self.groups.push(group);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if self.group(&key, cx).is_some() {
|
if self.group(&key, cx).is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -539,9 +554,31 @@ impl GroupsRegistry {
|
|||||||
let group = cx.new(|_| Group::new(key.clone()));
|
let group = cx.new(|_| Group::new(key.clone()));
|
||||||
self.groups.push(group.clone());
|
self.groups.push(group.clone());
|
||||||
|
|
||||||
|
self.observe_group(key.clone(), &group, cx);
|
||||||
|
self.subscribe(key.clone(), cx);
|
||||||
|
self.load_state(key, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track a group opened for preview: connected, but not joined.
|
||||||
|
pub fn open(&mut self, key: GroupKey, cx: &mut Context<Self>) -> Entity<Group> {
|
||||||
|
if let Some(group) = self.group(&key, cx) {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
let group = cx.new(|_| Group::new(key.clone()));
|
||||||
|
self.previews.insert(key.clone(), group.clone());
|
||||||
|
|
||||||
|
self.observe_group(key.clone(), &group, cx);
|
||||||
|
self.subscribe(key.clone(), cx);
|
||||||
|
self.load_state(key, cx);
|
||||||
|
|
||||||
|
group
|
||||||
|
}
|
||||||
|
|
||||||
|
fn observe_group(&mut self, key: GroupKey, group: &Entity<Group>, cx: &mut Context<Self>) {
|
||||||
self.observers.insert(
|
self.observers.insert(
|
||||||
key.clone(),
|
key,
|
||||||
cx.subscribe(&group, |this, group, event, cx| match event {
|
cx.subscribe(group, |this, group, event, cx| match event {
|
||||||
GroupEvent::Sent(event) => {
|
GroupEvent::Sent(event) => {
|
||||||
if event.kind == Kind::GroupDeleteEvent {
|
if event.kind == Kind::GroupDeleteEvent {
|
||||||
this.forget_event(group, event.clone(), cx);
|
this.forget_event(group, event.clone(), cx);
|
||||||
@@ -551,7 +588,13 @@ impl GroupsRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
GroupEvent::Updated => {
|
GroupEvent::Updated => {
|
||||||
|
let key = group.read(cx).key().clone();
|
||||||
|
|
||||||
|
// Only the user's groups belong in their group list.
|
||||||
|
if this.joined(&key, cx) {
|
||||||
this.refresh_entry(group, cx);
|
this.refresh_entry(group, cx);
|
||||||
|
}
|
||||||
|
|
||||||
this.sync_forks(false, cx);
|
this.sync_forks(false, cx);
|
||||||
|
|
||||||
cx.emit(GroupsEvent::Updated);
|
cx.emit(GroupsEvent::Updated);
|
||||||
@@ -560,9 +603,6 @@ impl GroupsRegistry {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.subscribe(key.clone(), cx);
|
|
||||||
self.load_state(key, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe(&mut self, key: GroupKey, cx: &mut Context<Self>) {
|
fn subscribe(&mut self, key: GroupKey, cx: &mut Context<Self>) {
|
||||||
@@ -586,6 +626,7 @@ impl GroupsRegistry {
|
|||||||
self.routes.remove(&page);
|
self.routes.remove(&page);
|
||||||
self.observers.remove(&key);
|
self.observers.remove(&key);
|
||||||
self.groups.retain(|group| group.read(cx).key() != &key);
|
self.groups.retain(|group| group.read(cx).key() != &key);
|
||||||
|
self.previews.remove(&key);
|
||||||
self.list.remove(&key);
|
self.list.remove(&key);
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|||||||
+43
-52
@@ -9,7 +9,7 @@ use gpui::{
|
|||||||
PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div,
|
PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div,
|
||||||
list, px,
|
list, px,
|
||||||
};
|
};
|
||||||
use nip29::{Group, GroupEvent, GroupsRegistry, Membership, Pin, Row};
|
use nip29::{Group, GroupEvent, GroupKey, GroupsRegistry, Membership, Pin, Row};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -64,33 +64,8 @@ enum Notice {
|
|||||||
|
|
||||||
impl Notice {
|
impl Notice {
|
||||||
fn of(group: &Group) -> Option<Self> {
|
fn of(group: &Group) -> Option<Self> {
|
||||||
if let Some(reason) = group.refused() {
|
let reason = group.refused()?;
|
||||||
return Some(Self::Relay(reason.to_owned()));
|
Some(Self::Relay(reason.to_owned()))
|
||||||
}
|
|
||||||
|
|
||||||
let closed = group.metadata().is_some_and(|metadata| metadata.closed);
|
|
||||||
|
|
||||||
match group.membership() {
|
|
||||||
Membership::Member => {}
|
|
||||||
Membership::Pending { .. } => return Some(Self::Pending),
|
|
||||||
Membership::Refused { reason } => {
|
|
||||||
return Some(Self::Join {
|
|
||||||
refused: reason.clone(),
|
|
||||||
closed,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Membership::Unknown | Membership::Removed => {
|
|
||||||
return Some(Self::Join {
|
|
||||||
refused: None,
|
|
||||||
closed,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group
|
|
||||||
.metadata()
|
|
||||||
.is_some_and(|metadata| !metadata.supports(Kind::ChatMessage))
|
|
||||||
.then_some(Self::Unsupported)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,12 +629,8 @@ impl GroupPanel {
|
|||||||
Button::new("group-moved")
|
Button::new("group-moved")
|
||||||
.label("Join")
|
.label("Join")
|
||||||
.small()
|
.small()
|
||||||
.on_click(cx.listener(move |_this, _event, _window, cx| {
|
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||||
let registry = GroupsRegistry::global(cx);
|
this.request_join(moved.clone(), cx);
|
||||||
|
|
||||||
registry.update(cx, |registry, cx| {
|
|
||||||
registry.join_key(moved.clone(), None, cx);
|
|
||||||
});
|
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
@@ -758,6 +729,13 @@ impl GroupPanel {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ask the relay to admit the user into the group.
|
||||||
|
fn request_join(&self, key: GroupKey, cx: &mut App) {
|
||||||
|
GroupsRegistry::global(cx).update(cx, |registry, cx| {
|
||||||
|
registry.join_key(key, None, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn render_notice(&self, notice: &Notice, cx: &mut Context<Self>) -> AnyElement {
|
fn render_notice(&self, notice: &Notice, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let row = h_flex()
|
let row = h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -767,7 +745,7 @@ impl GroupPanel {
|
|||||||
.px_3()
|
.px_3()
|
||||||
.py_2()
|
.py_2()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_placeholder);
|
.text_color(cx.theme().text_muted);
|
||||||
|
|
||||||
match notice {
|
match notice {
|
||||||
Notice::Relay(reason) => row
|
Notice::Relay(reason) => row
|
||||||
@@ -781,7 +759,7 @@ impl GroupPanel {
|
|||||||
|
|
||||||
if *closed {
|
if *closed {
|
||||||
return row
|
return row
|
||||||
.child("This group is closed; an invite link is required.")
|
.child("Group is closed. An invite link is required.")
|
||||||
.into_any_element();
|
.into_any_element();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,14 +771,14 @@ impl GroupPanel {
|
|||||||
return row.into_any_element();
|
return row.into_any_element();
|
||||||
};
|
};
|
||||||
|
|
||||||
row.child(
|
row.child("You're in preview mode. Join this group to talk.")
|
||||||
Button::new("join-group")
|
.child(
|
||||||
.label("Request to join")
|
Button::new("join")
|
||||||
|
.label("Request to Join")
|
||||||
.small()
|
.small()
|
||||||
.on_click(cx.listener(move |_this, _event, _window, cx| {
|
.ghost_alt()
|
||||||
GroupsRegistry::global(cx).update(cx, |registry, cx| {
|
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||||
registry.join_key(key.clone(), None, cx);
|
this.request_join(key.clone(), cx);
|
||||||
});
|
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
@@ -871,18 +849,31 @@ impl GroupPanel {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_composer(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render_composer(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let can_send = self.group.upgrade().is_some_and(|group| {
|
let notice = self.group.upgrade().and_then(|group| {
|
||||||
let group = group.read(cx);
|
let group = group.read(cx);
|
||||||
let metadata = group.metadata();
|
|
||||||
let supports_text =
|
|
||||||
metadata.is_none_or(|metadata| metadata.supports(Kind::ChatMessage));
|
|
||||||
let writable = metadata.is_none_or(|metadata| !metadata.restricted)
|
|
||||||
|| matches!(group.membership(), Membership::Member);
|
|
||||||
|
|
||||||
supports_text && writable
|
if !group
|
||||||
|
.metadata()
|
||||||
|
.is_none_or(|metadata| metadata.supports(Kind::ChatMessage))
|
||||||
|
{
|
||||||
|
return Some(Notice::Unsupported);
|
||||||
|
}
|
||||||
|
|
||||||
|
match group.membership() {
|
||||||
|
Membership::Member => None,
|
||||||
|
Membership::Pending { .. } => Some(Notice::Pending),
|
||||||
|
_ => Some(Notice::Join {
|
||||||
|
refused: None,
|
||||||
|
closed: group.metadata().is_some_and(|metadata| metadata.closed),
|
||||||
|
}),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(notice) = notice {
|
||||||
|
return self.render_notice(¬ice, cx);
|
||||||
|
}
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -912,12 +903,12 @@ impl GroupPanel {
|
|||||||
.tooltip("Send")
|
.tooltip("Send")
|
||||||
.ghost()
|
.ghost()
|
||||||
.large()
|
.large()
|
||||||
.disabled(!can_send)
|
|
||||||
.on_click(cx.listener(|this, _event, window, cx| {
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
this.send(window, cx);
|
this.send(window, cx);
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_attachments(&self, cx: &mut Context<Self>) -> Vec<AnyElement> {
|
fn render_attachments(&self, cx: &mut Context<Self>) -> Vec<AnyElement> {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use gpui::{
|
|||||||
size,
|
size,
|
||||||
};
|
};
|
||||||
use gpui_base::{VirtualListScrollHandle, v_virtual_list};
|
use gpui_base::{VirtualListScrollHandle, v_virtual_list};
|
||||||
use nip29::{Discovery, GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry};
|
use nip29::{Discovery, GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry, Membership};
|
||||||
use nostr_sdk::prelude::{RelayUrl, Timestamp};
|
use nostr_sdk::prelude::{RelayUrl, Timestamp};
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
@@ -20,7 +20,7 @@ use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
|
|||||||
use ui::input::{Input, InputEvent, InputState};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::scroll::Scrollbar;
|
use ui::scroll::Scrollbar;
|
||||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
use ui::{Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
const TITLE: &str = "Browse";
|
const TITLE: &str = "Browse";
|
||||||
const INPUT_PLACEHOLDER: &str = "wss://relay.example.com";
|
const INPUT_PLACEHOLDER: &str = "wss://relay.example.com";
|
||||||
@@ -223,17 +223,10 @@ impl BrowsePanel {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Join a browsed group and open it.
|
/// Open the group's panel to preview (or revisit) it.
|
||||||
fn join(dock: &WeakEntity<DockArea>, key: GroupKey, window: &mut Window, cx: &mut App) {
|
fn open_panel(dock: &WeakEntity<DockArea>, key: GroupKey, window: &mut Window, cx: &mut App) {
|
||||||
let registry = GroupsRegistry::global(cx);
|
let registry = GroupsRegistry::global(cx);
|
||||||
|
let group = registry.update(cx, |registry, cx| registry.open(key, cx));
|
||||||
registry.update(cx, |registry, cx| {
|
|
||||||
registry.join_key(key.clone(), None, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
let Some(group) = registry.read(cx).group(&key, cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
ui::dock::add_panel_to(
|
ui::dock::add_panel_to(
|
||||||
dock,
|
dock,
|
||||||
@@ -244,19 +237,20 @@ impl BrowsePanel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The discovered groups the user has not joined yet.
|
/// The discovered groups the user is not a member of.
|
||||||
fn discovered_cards(&self, cx: &App) -> Vec<GroupCandidate> {
|
fn discovered_cards(&self, cx: &App) -> Vec<GroupCandidate> {
|
||||||
let joined: Vec<GroupKey> = GroupsRegistry::global(cx)
|
let member_of: Vec<GroupKey> = GroupsRegistry::global(cx)
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.groups()
|
.groups()
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|group| matches!(group.read(cx).membership(), Membership::Member))
|
||||||
.map(|group| group.read(cx).key().clone())
|
.map(|group| group.read(cx).key().clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.discovery
|
self.discovery
|
||||||
.discovered()
|
.discovered()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|candidate| !joined.contains(&candidate.key))
|
.filter(|candidate| !member_of.contains(&candidate.key))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -357,6 +351,10 @@ impl BrowsePanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_card(&self, candidate: &GroupCandidate, cx: &mut Context<Self>) -> AnyElement {
|
fn render_card(&self, candidate: &GroupCandidate, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
let registry = GroupsRegistry::global(cx);
|
||||||
|
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||||
|
let dock = self.dock.clone();
|
||||||
|
|
||||||
let key = candidate.key.clone();
|
let key = candidate.key.clone();
|
||||||
let seed = key.cache_tag();
|
let seed = key.cache_tag();
|
||||||
let members = self.discovery.members(&key).unwrap_or(0);
|
let members = self.discovery.members(&key).unwrap_or(0);
|
||||||
@@ -372,33 +370,19 @@ impl BrowsePanel {
|
|||||||
.display_about()
|
.display_about()
|
||||||
.map(|about| SharedString::from(about.to_owned()));
|
.map(|about| SharedString::from(about.to_owned()));
|
||||||
|
|
||||||
let registry = GroupsRegistry::global(cx);
|
let member = registry
|
||||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
.read(cx)
|
||||||
let joined = registry.read(cx).group(&key, cx).is_some();
|
.group(&key, cx)
|
||||||
|
.is_some_and(|group| matches!(group.read(cx).membership(), Membership::Member));
|
||||||
|
|
||||||
let action = if joined {
|
let action = Button::new(format!("open-{seed}"))
|
||||||
Button::new(format!("joined-{seed}"))
|
.label(if member { "Open" } else { "Preview" })
|
||||||
.label("Joined")
|
|
||||||
.small()
|
|
||||||
.ghost()
|
|
||||||
.disabled(true)
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
let panel = cx.entity().downgrade();
|
|
||||||
let dock = self.dock.clone();
|
|
||||||
let clicked = key;
|
|
||||||
|
|
||||||
Button::new(format!("join-{seed}"))
|
|
||||||
.label("Join")
|
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_click(move |_event, window, cx| {
|
.on_click(move |_event, window, cx| {
|
||||||
Self::join(&dock, clicked.clone(), window, cx);
|
Self::open_panel(&dock, key.clone(), window, cx);
|
||||||
|
|
||||||
let _ = panel.update(cx, |_, cx| cx.notify());
|
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element();
|
||||||
};
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
|||||||
Reference in New Issue
Block a user