@@ -12,7 +12,7 @@ nip29 = { path = "../nip29" }
|
||||
person = { path = "../person" }
|
||||
settings = { path = "../settings" }
|
||||
state = { path = "../state" }
|
||||
theme = { path = "../theme" }
|
||||
gpui-kit.workspace = true
|
||||
ui = { path = "../ui" }
|
||||
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -2,15 +2,15 @@ use gpui::{
|
||||
AnyElement, App, IntoElement, ParentElement, SharedString, Styled, WeakEntity, Window, div, px,
|
||||
relative,
|
||||
};
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex};
|
||||
use nip29::{Group, GroupMetadata};
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _, control_radius};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::{Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
use ui::Avatar;
|
||||
|
||||
pub(crate) fn open(group: WeakEntity<Group>, window: &mut Window, cx: &mut App) {
|
||||
window.open_dialog(cx, move |this, _window, cx| {
|
||||
this.max_h(px(600.))
|
||||
.show_close(true)
|
||||
.close_button(true)
|
||||
.title("About Group")
|
||||
.child(body(&group, cx))
|
||||
});
|
||||
@@ -22,13 +22,13 @@ fn body(group: &WeakEntity<Group>, cx: &App) -> AnyElement {
|
||||
};
|
||||
|
||||
let group = entity.read(cx);
|
||||
let mut body = v_flex().gap_3().text_style(TextStyle::Callout).child(header(group, cx));
|
||||
let mut body = v_flex().gap_3().text_xs().child(header(group, cx));
|
||||
|
||||
if let Some(metadata) = group.metadata() {
|
||||
if let Some(about) = metadata.about.as_ref() {
|
||||
body = body.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(about.clone())),
|
||||
);
|
||||
}
|
||||
@@ -76,7 +76,7 @@ fn body(group: &WeakEntity<Group>, cx: &App) -> AnyElement {
|
||||
"Supported kinds",
|
||||
if items.is_empty() {
|
||||
div()
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("None")
|
||||
.into_any_element()
|
||||
} else {
|
||||
@@ -107,7 +107,7 @@ fn body(group: &WeakEntity<Group>, cx: &App) -> AnyElement {
|
||||
.map(|pin| {
|
||||
div()
|
||||
.truncate()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(crate::util::pin_label(group, pin))
|
||||
.into_any_element()
|
||||
})
|
||||
@@ -128,7 +128,7 @@ fn header(group: &Group, cx: &App) -> AnyElement {
|
||||
.gap_3()
|
||||
.items_center()
|
||||
.child(
|
||||
Avatar::from_source(group.display_image())
|
||||
Avatar::new(group.display_image())
|
||||
.seed(key.cache_tag())
|
||||
.large(),
|
||||
)
|
||||
@@ -137,15 +137,15 @@ fn header(group: &Group, cx: &App) -> AnyElement {
|
||||
.min_w_0()
|
||||
.child(
|
||||
div()
|
||||
.text_style(TextStyle::Body)
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.truncate()
|
||||
.child(group.display_name()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.truncate()
|
||||
.child(SharedString::from(format!(
|
||||
"{} · {}",
|
||||
@@ -162,9 +162,9 @@ fn section(label: &'static str, content: AnyElement, cx: &App) -> AnyElement {
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_xs()
|
||||
.font_medium()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(label),
|
||||
)
|
||||
.child(content)
|
||||
@@ -183,10 +183,10 @@ fn chip(label: impl Into<SharedString>, cx: &App) -> AnyElement {
|
||||
div()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(px(control_radius()))
|
||||
.bg(cx.theme().surface_raised)
|
||||
.text_color(cx.theme().text)
|
||||
.text_style(TextStyle::Caption)
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().secondary)
|
||||
.text_color(cx.theme().foreground)
|
||||
.text_xs()
|
||||
.line_height(relative(1.))
|
||||
.child(label.into())
|
||||
.into_any_element()
|
||||
|
||||
@@ -5,13 +5,12 @@ use gpui::{
|
||||
App, AppContext, ClipboardItem, Entity, IntoElement, ParentElement, SharedString, Styled,
|
||||
WeakEntity, Window, div, px,
|
||||
};
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::input::InputState;
|
||||
use gpui_kit::component::notification::Notification;
|
||||
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex};
|
||||
use nip29::{Group, Invite};
|
||||
use nostr_sdk::prelude::SecretKey;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::InputState;
|
||||
use ui::notification::Notification;
|
||||
use ui::{Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
use crate::util::{field, report};
|
||||
|
||||
@@ -26,7 +25,7 @@ pub(crate) fn open(group: WeakEntity<Group>, window: &mut Window, cx: &mut App)
|
||||
|
||||
window.open_dialog(cx, move |this, _window, cx| {
|
||||
this.max_h(px(600.))
|
||||
.show_close(true)
|
||||
.close_button(true)
|
||||
.title("Invites")
|
||||
.child(body(group.clone(), input.clone(), cx))
|
||||
});
|
||||
@@ -48,16 +47,16 @@ fn body(group: WeakEntity<Group>, input: Entity<InputState>, cx: &App) -> impl I
|
||||
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_xs()
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Anyone with a code can ask to join. Revoking a code stops new joins, it doesn't remove anyone."),
|
||||
)
|
||||
.when(invites.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("No active invites"),
|
||||
)
|
||||
})
|
||||
@@ -68,8 +67,8 @@ fn body(group: WeakEntity<Group>, input: Entity<InputState>, cx: &App) -> impl I
|
||||
fn invite_row(group: WeakEntity<Group>, invite: &Invite, cx: &App) -> impl IntoElement {
|
||||
let created = div()
|
||||
.flex_shrink_0()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(invite.created_at.to_ago());
|
||||
|
||||
h_flex()
|
||||
|
||||
+98
-103
@@ -1,14 +1,22 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use chat_ui::text::rendered_text;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
FollowMode, ImageSource, IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement,
|
||||
FollowMode, IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement,
|
||||
PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div,
|
||||
list, px,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariant, ButtonVariants};
|
||||
use gpui_kit::component::dialog::DialogButtonProps;
|
||||
use gpui_kit::component::dock::{Panel, PanelEvent};
|
||||
use gpui_kit::component::input::{InputEvent, Textarea, TextareaState};
|
||||
use gpui_kit::component::notification::Notification;
|
||||
use gpui_kit::component::scroll::Scrollbar;
|
||||
use gpui_kit::component::{ActiveTheme, Disableable, Icon, Sizable, WindowExt, h_flex, v_flex};
|
||||
use nip29::{Group, GroupEvent, GroupKey, GroupsRegistry, Membership, Pin, Row};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::Directory;
|
||||
@@ -16,17 +24,7 @@ use serde::Deserialize;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{NostrRegistry, upload};
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _, control_radius};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariant, ButtonVariants};
|
||||
use ui::dialog::DialogButtonProps;
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, Textarea, TextareaState};
|
||||
use ui::markdown::RenderedText;
|
||||
use ui::message::WelcomeMessage;
|
||||
use ui::notification::Notification;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
use ui::{Avatar, RenderedText, WelcomeMessage, rendered_text};
|
||||
use util::{attachment_name, display_name, opens_run, pin_label, report};
|
||||
|
||||
mod details;
|
||||
@@ -123,7 +121,7 @@ pub fn actions(group: WeakEntity<Group>, cx: &App) -> AnyElement {
|
||||
.when(admin, |this| {
|
||||
this.child(
|
||||
Button::new("edit")
|
||||
.icon(IconName::Edit)
|
||||
.icon(IconName::SquarePen)
|
||||
.tooltip("Edit group")
|
||||
.ghost()
|
||||
.small()
|
||||
@@ -133,7 +131,7 @@ pub fn actions(group: WeakEntity<Group>, cx: &App) -> AnyElement {
|
||||
)
|
||||
.child(
|
||||
Button::new("group-invite")
|
||||
.icon(IconName::Invite)
|
||||
.icon(IconName::UserRoundPlus)
|
||||
.tooltip("Invite people")
|
||||
.ghost()
|
||||
.small()
|
||||
@@ -164,7 +162,7 @@ pub fn actions(group: WeakEntity<Group>, cx: &App) -> AnyElement {
|
||||
)
|
||||
.child(
|
||||
Button::new("group-delete")
|
||||
.icon(IconName::CloseCircle)
|
||||
.icon(IconName::CircleX)
|
||||
.tooltip("Delete group")
|
||||
.ghost()
|
||||
.small()
|
||||
@@ -175,7 +173,7 @@ pub fn actions(group: WeakEntity<Group>, cx: &App) -> AnyElement {
|
||||
})
|
||||
.child(
|
||||
Button::new("leave")
|
||||
.icon(IconName::Logout)
|
||||
.icon(IconName::LogOut)
|
||||
.tooltip("Leave group")
|
||||
.ghost()
|
||||
.small()
|
||||
@@ -195,7 +193,7 @@ pub fn confirm_leave(group: WeakEntity<Group>, window: &mut Window, cx: &mut App
|
||||
return;
|
||||
};
|
||||
|
||||
window.open_dialog(cx, move |this, _window, _cx| {
|
||||
window.open_alert_dialog(cx, move |this, _window, _cx| {
|
||||
let key = key.clone();
|
||||
|
||||
this.confirm()
|
||||
@@ -209,7 +207,7 @@ pub fn confirm_leave(group: WeakEntity<Group>, window: &mut Window, cx: &mut App
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_xs()
|
||||
.child("You will stop receiving messages from this group."),
|
||||
)
|
||||
.on_ok(move |_event, _window, cx| {
|
||||
@@ -231,7 +229,7 @@ pub fn confirm_delete(group: WeakEntity<Group>, window: &mut Window, cx: &mut Ap
|
||||
return;
|
||||
};
|
||||
|
||||
window.open_dialog(cx, move |this, _window, _cx| {
|
||||
window.open_alert_dialog(cx, move |this, _window, _cx| {
|
||||
let key = key.clone();
|
||||
|
||||
this.confirm()
|
||||
@@ -245,7 +243,7 @@ pub fn confirm_delete(group: WeakEntity<Group>, window: &mut Window, cx: &mut Ap
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_xs()
|
||||
.child("The relay will remove this group for everyone."),
|
||||
)
|
||||
.on_ok(move |_event, window, cx| {
|
||||
@@ -305,7 +303,6 @@ fn put_user(
|
||||
}
|
||||
|
||||
pub struct GroupPanel {
|
||||
id: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
group: WeakEntity<Group>,
|
||||
/// Person directory for members, message authors, and activity subjects
|
||||
@@ -328,16 +325,11 @@ pub struct GroupPanel {
|
||||
|
||||
impl GroupPanel {
|
||||
pub(crate) fn new(group: Entity<Group>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let (id, name, count, head) = {
|
||||
let (name, count, head) = {
|
||||
let group = group.read(cx);
|
||||
let rows = group.rows();
|
||||
|
||||
(
|
||||
SharedString::from(format!("group-{}", group.key().cache_tag())),
|
||||
display_name(group),
|
||||
rows.len(),
|
||||
rows.first().map(Row::id),
|
||||
)
|
||||
(display_name(group), rows.len(), rows.first().map(Row::id))
|
||||
};
|
||||
|
||||
let input = cx.new(|cx| {
|
||||
@@ -383,7 +375,6 @@ impl GroupPanel {
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
id,
|
||||
focus_handle: cx.focus_handle(),
|
||||
group: group.downgrade(),
|
||||
directory: Directory::new(cx),
|
||||
@@ -721,16 +712,16 @@ impl GroupPanel {
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.w_full()
|
||||
.gap(px(theme::SPACE))
|
||||
.gap_2()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_xs()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
Icon::new(IconName::Info)
|
||||
.small()
|
||||
.text_color(cx.theme().text_faint),
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().truncate().child(message))
|
||||
.child(
|
||||
@@ -759,11 +750,11 @@ impl GroupPanel {
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.w_full()
|
||||
.gap(px(theme::SPACE))
|
||||
.gap_2()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.text_style(TextStyle::Callout)
|
||||
.bg(cx.theme().surface_raised)
|
||||
.text_xs()
|
||||
.bg(cx.theme().secondary)
|
||||
.child(
|
||||
Icon::new(IconName::Info)
|
||||
.small()
|
||||
@@ -788,7 +779,7 @@ impl GroupPanel {
|
||||
)
|
||||
.child(
|
||||
Button::new("dismiss")
|
||||
.icon(IconName::Close)
|
||||
.icon(IconName::X)
|
||||
.small()
|
||||
.ghost()
|
||||
.tooltip("Close")
|
||||
@@ -826,10 +817,10 @@ impl GroupPanel {
|
||||
div()
|
||||
.px_1p5()
|
||||
.py_0p5()
|
||||
.rounded(px(control_radius()))
|
||||
.bg(cx.theme().solid)
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().primary)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(pin_label(group, pin))
|
||||
}))
|
||||
.into_any_element()
|
||||
@@ -839,17 +830,18 @@ impl GroupPanel {
|
||||
let (name, avatar) = match self.group.upgrade() {
|
||||
Some(group) => {
|
||||
let group = group.read(cx);
|
||||
let seed = group.key().cache_tag();
|
||||
let picture = group
|
||||
.metadata()
|
||||
.and_then(|metadata| metadata.picture.clone());
|
||||
|
||||
(
|
||||
display_name(group),
|
||||
Avatar::from_source(picture).seed(seed).large(),
|
||||
Avatar::new(picture.map(Into::into))
|
||||
.seed(group.key().cache_tag())
|
||||
.large(),
|
||||
)
|
||||
}
|
||||
None => (SharedString::from("this group"), Avatar::new(None)),
|
||||
None => (SharedString::from("this group"), Avatar::new(None).large()),
|
||||
};
|
||||
|
||||
WelcomeMessage::new("welcome")
|
||||
@@ -906,11 +898,11 @@ impl GroupPanel {
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap(px(theme::SPACE))
|
||||
.gap_2()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text_muted);
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground);
|
||||
|
||||
match notice {
|
||||
Notice::Relay(reason) => row
|
||||
@@ -941,7 +933,7 @@ impl GroupPanel {
|
||||
Button::new("join")
|
||||
.label("Request to Join")
|
||||
.small()
|
||||
.ghost_alt()
|
||||
.ghost()
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
this.request_join(key.clone(), cx);
|
||||
})),
|
||||
@@ -993,7 +985,10 @@ impl GroupPanel {
|
||||
.or_insert_with(|| {
|
||||
rendered_text(
|
||||
&message.content,
|
||||
&message.mentions,
|
||||
message
|
||||
.mentions
|
||||
.iter()
|
||||
.map(|mention| (mention.range.clone(), mention.public_key)),
|
||||
&message.emojis,
|
||||
&directory,
|
||||
cx,
|
||||
@@ -1071,7 +1066,6 @@ impl GroupPanel {
|
||||
.loading(self.uploading)
|
||||
.disabled(self.uploading)
|
||||
.ghost()
|
||||
.large()
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.upload(window, cx);
|
||||
})),
|
||||
@@ -1079,10 +1073,9 @@ impl GroupPanel {
|
||||
.child(Textarea::new(&self.input).appearance(false).flex_1())
|
||||
.child(
|
||||
Button::new("send")
|
||||
.icon(IconName::PaperPlaneFill)
|
||||
.icon(ui::IconName::PaperPlaneFill)
|
||||
.tooltip("Send")
|
||||
.ghost()
|
||||
.large()
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.send(window, cx);
|
||||
})),
|
||||
@@ -1119,13 +1112,13 @@ impl GroupPanel {
|
||||
.gap_1()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(px(control_radius()))
|
||||
.bg(cx.theme().surface_raised)
|
||||
.text_style(TextStyle::Caption)
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().secondary)
|
||||
.text_xs()
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Replying to"),
|
||||
)
|
||||
.child(
|
||||
@@ -1134,7 +1127,7 @@ impl GroupPanel {
|
||||
.max_w(px(140.))
|
||||
.truncate()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text)
|
||||
.text_color(cx.theme().foreground)
|
||||
.child(author),
|
||||
)
|
||||
.child(
|
||||
@@ -1142,12 +1135,12 @@ impl GroupPanel {
|
||||
.min_w_0()
|
||||
.flex_1()
|
||||
.truncate()
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(snippet),
|
||||
)
|
||||
.child(
|
||||
Button::new("cancel-reply")
|
||||
.icon(IconName::Close)
|
||||
.icon(ui::IconName::Close)
|
||||
.tooltip("Cancel reply")
|
||||
.ghost()
|
||||
.small()
|
||||
@@ -1171,10 +1164,10 @@ impl GroupPanel {
|
||||
.h_8()
|
||||
.px_2()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border_faint)
|
||||
.rounded(px(control_radius()))
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.border_color(cx.theme().border)
|
||||
.rounded(cx.theme().radius)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Icon::new(IconName::Link).xsmall())
|
||||
.child(
|
||||
div()
|
||||
@@ -1184,7 +1177,7 @@ impl GroupPanel {
|
||||
)
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("remove-attachment-{ix}")))
|
||||
.icon(IconName::Close)
|
||||
.icon(ui::IconName::Close)
|
||||
.tooltip("Remove")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
@@ -1198,45 +1191,12 @@ impl GroupPanel {
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for GroupPanel {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
self.id.clone()
|
||||
impl gpui_kit::component::dock::BasePanel for GroupPanel {
|
||||
fn panel_name(&self) -> &'static str {
|
||||
"GroupPanel"
|
||||
}
|
||||
|
||||
fn title(&self, cx: &App) -> AnyElement {
|
||||
self.group
|
||||
.upgrade()
|
||||
.map(|group| {
|
||||
let group = group.read(cx);
|
||||
let seed = group.key().cache_tag();
|
||||
let picture = group
|
||||
.metadata()
|
||||
.and_then(|metadata| metadata.picture.clone());
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_style(TextStyle::Caption)
|
||||
.child(Avatar::from_source(picture).seed(seed).xsmall())
|
||||
.child(display_name(group))
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
div()
|
||||
.text_style(TextStyle::Caption)
|
||||
.child("Unknown")
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn background(&self, cx: &App) -> Option<ImageSource> {
|
||||
AppSettings::panel_background_source(cx)
|
||||
}
|
||||
|
||||
fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) {
|
||||
fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(group) = self.group.upgrade() else {
|
||||
return;
|
||||
};
|
||||
@@ -1262,6 +1222,41 @@ impl Panel for GroupPanel {
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for GroupPanel {
|
||||
fn title(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.group
|
||||
.upgrade()
|
||||
.map(|group| {
|
||||
let group = group.read(cx);
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.child(
|
||||
Avatar::new(
|
||||
group
|
||||
.metadata()
|
||||
.and_then(|metadata| metadata.picture.clone())
|
||||
.map(Into::into),
|
||||
)
|
||||
.seed(group.key().cache_tag())
|
||||
.xsmall(),
|
||||
)
|
||||
.child(display_name(group))
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or_else(|| div().text_xs().child("Unknown").into_any_element())
|
||||
}
|
||||
|
||||
fn toolbar_buttons(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Vec<Button>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for GroupPanel {}
|
||||
|
||||
impl Focusable for GroupPanel {
|
||||
@@ -1294,8 +1289,8 @@ impl Render for GroupPanel {
|
||||
None => h_flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("No messages yet")
|
||||
.into_any_element(),
|
||||
}),
|
||||
|
||||
@@ -3,23 +3,24 @@ use common::TimestampExt;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, Entity, InteractiveElement, IntoElement, ParentElement, SharedString, Styled,
|
||||
WeakEntity, div, px,
|
||||
WeakEntity, div,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex};
|
||||
use nip29::{Activity, Group, Reaction};
|
||||
use nostr_sdk::prelude::{EventId, Kind};
|
||||
use person::Directory;
|
||||
use settings::AppSettings;
|
||||
use state::NostrRegistry;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::menu::{ContextMenu, PopupMenu, PopupMenuItem};
|
||||
use ui::message::{MessageMedia, MessageRow, ReactionBar, ReplyPreview, group_reactions};
|
||||
use ui::{Icon, IconName, Sizable, StyledExt, h_flex};
|
||||
use ui::{
|
||||
Avatar, MessageMedia, MessageRow, REACTION_EMOJIS, ReactionBar, ReplyPreview, group_reactions,
|
||||
react_menu,
|
||||
};
|
||||
|
||||
use crate::GroupPanel;
|
||||
|
||||
const REACTIONS: [&str; 6] = ["👍", "❤️", "😂", "🎉", "😮", "😢"];
|
||||
|
||||
pub(crate) struct Actions {
|
||||
pub group: WeakEntity<Group>,
|
||||
pub panel: WeakEntity<GroupPanel>,
|
||||
@@ -60,6 +61,7 @@ pub(crate) fn render(
|
||||
.avatar(
|
||||
Avatar::new(author.avatar())
|
||||
.seed(author.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.author(author.name())
|
||||
@@ -79,8 +81,7 @@ pub(crate) fn render(
|
||||
})
|
||||
.when(!reaction_groups.is_empty(), |this| {
|
||||
this.child(ReactionBar::new(reaction_groups))
|
||||
})
|
||||
.into_any_element();
|
||||
});
|
||||
|
||||
let id = message.id;
|
||||
let admin = actions.admin;
|
||||
@@ -88,10 +89,27 @@ pub(crate) fn render(
|
||||
let group = actions.group;
|
||||
let panel = actions.panel;
|
||||
|
||||
ContextMenu::new(
|
||||
format!("message-menu-{}", id.to_hex()),
|
||||
row,
|
||||
move |menu, window, cx| {
|
||||
// Reading the panel here would re-enter its ongoing update, so resolve
|
||||
// the current user's custom emojis through the shared person directory.
|
||||
let custom = nostr
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.map(|current_user| {
|
||||
persons
|
||||
.read(cx)
|
||||
.person(¤t_user, cx)
|
||||
.emojis()
|
||||
.iter()
|
||||
.map(|(shortcode, _)| format!(":{shortcode}:"))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let row = div()
|
||||
.id(SharedString::from(format!("message-menu-{}", id.to_hex())))
|
||||
.w_full()
|
||||
.child(row)
|
||||
.context_menu(move |menu, window, cx| {
|
||||
let menu = menu
|
||||
.item({
|
||||
let panel = panel.clone();
|
||||
@@ -105,8 +123,20 @@ pub(crate) fn render(
|
||||
.submenu("React", window, cx, {
|
||||
let group = group.clone();
|
||||
let panel = panel.clone();
|
||||
let custom = custom.clone();
|
||||
|
||||
move |menu, _window, cx| react_menu(menu, group.clone(), panel.clone(), id, cx)
|
||||
move |menu, _window, _cx| {
|
||||
let group = group.clone();
|
||||
let panel = panel.clone();
|
||||
|
||||
react_menu(menu, REACTION_EMOJIS, &custom, move |emoji, _window, cx| {
|
||||
if let Err(error) = panel.update_in(cx, |panel, window, cx| {
|
||||
panel.react(group.clone(), id, emoji.to_owned(), window, cx);
|
||||
}) {
|
||||
log::warn!("nip29: sending a reaction failed: {error}");
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if !admin {
|
||||
@@ -136,70 +166,9 @@ pub(crate) fn render(
|
||||
});
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
fn react_menu(
|
||||
menu: PopupMenu,
|
||||
group: WeakEntity<Group>,
|
||||
panel: WeakEntity<GroupPanel>,
|
||||
id: EventId,
|
||||
cx: &App,
|
||||
) -> PopupMenu {
|
||||
let menu = REACTIONS.iter().fold(menu, |menu, emoji| {
|
||||
let emoji = *emoji;
|
||||
let group = group.clone();
|
||||
let panel = panel.clone();
|
||||
|
||||
menu.item(
|
||||
PopupMenuItem::new(emoji).on_click(move |_event, _window, cx| {
|
||||
if let Err(error) = panel.update_in(cx, |panel, window, cx| {
|
||||
panel.react(group.clone(), id, emoji.to_owned(), window, cx);
|
||||
}) {
|
||||
log::warn!("nip29: sending a reaction failed: {error}");
|
||||
}
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
let emojis = panel
|
||||
.upgrade()
|
||||
.and_then(|panel| {
|
||||
let current_user = NostrRegistry::global(cx).read(cx).current_user()?;
|
||||
Some(
|
||||
panel
|
||||
.read(cx)
|
||||
.directory
|
||||
.read(cx)
|
||||
.person(¤t_user, cx)
|
||||
.emojis()
|
||||
.to_vec(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if emojis.is_empty() {
|
||||
return menu;
|
||||
}
|
||||
|
||||
let menu = menu.separator();
|
||||
emojis.into_iter().fold(menu, |menu, (shortcode, _)| {
|
||||
let emoji = format!(":{shortcode}:");
|
||||
let group = group.clone();
|
||||
let panel = panel.clone();
|
||||
|
||||
menu.item(
|
||||
PopupMenuItem::new(emoji.clone()).on_click(move |_event, _window, cx| {
|
||||
if let Err(error) = panel.update_in(cx, |panel, window, cx| {
|
||||
panel.react(group.clone(), id, emoji.clone(), window, cx);
|
||||
}) {
|
||||
log::warn!("nip29: sending a reaction failed: {error}");
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
row.into_any_element()
|
||||
}
|
||||
|
||||
pub(crate) fn render_activity(
|
||||
@@ -214,10 +183,10 @@ pub(crate) fn render_activity(
|
||||
h_flex()
|
||||
.id(ix)
|
||||
.w_full()
|
||||
.gap(px(theme::SPACE))
|
||||
.gap_2()
|
||||
.py_1()
|
||||
.px_3()
|
||||
.hover(|this| this.bg(cx.theme().element_hover))
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.child(badge(activity, cx))
|
||||
.when(!hide_avatar, |this| {
|
||||
this.child(
|
||||
@@ -231,7 +200,7 @@ pub(crate) fn render_activity(
|
||||
div()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.child(subject.name()),
|
||||
)
|
||||
@@ -239,16 +208,16 @@ pub(crate) fn render_activity(
|
||||
div()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(action(activity))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.ml_1()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(activity.created_at.to_ago()),
|
||||
)
|
||||
.into_any_element()
|
||||
@@ -256,11 +225,11 @@ pub(crate) fn render_activity(
|
||||
|
||||
fn badge(activity: &Activity, cx: &App) -> AnyElement {
|
||||
let (icon, color) = match activity.kind {
|
||||
Kind::GroupJoinRequest => (IconName::Login, cx.theme().accent),
|
||||
Kind::GroupPutUser => (IconName::Login, cx.theme().accent),
|
||||
Kind::GroupLeaveRequest => (IconName::Logout, cx.theme().danger),
|
||||
Kind::GroupJoinRequest => (IconName::LogIn, cx.theme().primary),
|
||||
Kind::GroupPutUser => (IconName::LogIn, cx.theme().primary),
|
||||
Kind::GroupLeaveRequest => (IconName::LogOut, cx.theme().danger),
|
||||
Kind::GroupRemoveUser => (IconName::Ban, cx.theme().danger),
|
||||
_ => (IconName::Info, cx.theme().text_faint),
|
||||
_ => (IconName::Info, cx.theme().muted_foreground),
|
||||
};
|
||||
|
||||
h_flex()
|
||||
|
||||
@@ -4,17 +4,17 @@ use gpui::http_client::{AsyncBody, HttpClient};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, Entity, IntoElement, ParentElement, SharedString, Styled,
|
||||
WeakEntity, Window, div, px,
|
||||
WeakEntity, Window, div,
|
||||
};
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::dialog::DialogButtonProps;
|
||||
use gpui_kit::component::input::InputState;
|
||||
use gpui_kit::component::notification::Notification;
|
||||
use gpui_kit::component::switch::Switch;
|
||||
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex};
|
||||
use nip29::{Group, GroupId, GroupMetadata};
|
||||
use nostr_sdk::prelude::RelayUrl;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dialog::DialogButtonProps;
|
||||
use ui::input::InputState;
|
||||
use ui::notification::Notification;
|
||||
use ui::switch::Switch;
|
||||
use ui::{Sizable, WindowExtension, h_flex, v_flex};
|
||||
use ui::settings_row;
|
||||
|
||||
use crate::util::field;
|
||||
|
||||
@@ -99,13 +99,22 @@ pub(crate) fn open(group: WeakEntity<Group>, window: &mut Window, cx: &mut App)
|
||||
let current = *flags.read(cx);
|
||||
let supported = *livekit.read(cx) != Some(false);
|
||||
|
||||
this.confirm()
|
||||
this.close_button(true)
|
||||
.title("Edit Group")
|
||||
.button_props(
|
||||
DialogButtonProps::default()
|
||||
.cancel_text("Cancel")
|
||||
.ok_text("Save"),
|
||||
)
|
||||
.footer(
|
||||
gpui_kit::component::dialog::DialogFooter::new().child(
|
||||
gpui_kit::component::dialog::DialogAction::new().child(
|
||||
gpui_kit::component::button::Button::new("save")
|
||||
.label("Save")
|
||||
.primary(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_3()
|
||||
@@ -116,59 +125,62 @@ pub(crate) fn open(group: WeakEntity<Group>, window: &mut Window, cx: &mut App)
|
||||
.child(children_section(&children, cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap(px(theme::SPACE))
|
||||
.child(
|
||||
.gap_2()
|
||||
.child(settings_row(
|
||||
"Private",
|
||||
"Only members can read group messages",
|
||||
Switch::new("private")
|
||||
.label("Private")
|
||||
.description("Only members can read group messages")
|
||||
.checked(current.private)
|
||||
.on_click(toggle(&flags, |flags| &mut flags.private)),
|
||||
)
|
||||
.child(
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Restricted",
|
||||
"Only members can post messages",
|
||||
Switch::new("restricted")
|
||||
.label("Restricted")
|
||||
.description("Only members can post messages")
|
||||
.checked(current.restricted)
|
||||
.on_click(toggle(&flags, |flags| &mut flags.restricted)),
|
||||
)
|
||||
.child(
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Hidden",
|
||||
"Hidden from non-members, not discoverable",
|
||||
Switch::new("hidden")
|
||||
.label("Hidden")
|
||||
.description("Hidden from non-members, not discoverable")
|
||||
.checked(current.hidden)
|
||||
.on_click(toggle(&flags, |flags| &mut flags.hidden)),
|
||||
)
|
||||
.child(
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Closed",
|
||||
"Join requests are ignored (invite-only)",
|
||||
Switch::new("closed")
|
||||
.label("Closed")
|
||||
.description("Join requests are ignored (invite-only)")
|
||||
.checked(current.closed)
|
||||
.on_click(toggle(&flags, |flags| &mut flags.closed)),
|
||||
)
|
||||
.child(
|
||||
Switch::new("livekit")
|
||||
.label("LiveKit")
|
||||
.description(if supported {
|
||||
"Announce audio and video support."
|
||||
} else {
|
||||
"The relay doesn't announce LiveKit support."
|
||||
})
|
||||
.checked(current.livekit)
|
||||
.on_click({
|
||||
let flags = flags.clone();
|
||||
let livekit = livekit.clone();
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"LiveKit",
|
||||
if supported {
|
||||
"Announce audio and video support."
|
||||
} else {
|
||||
"The relay doesn't announce LiveKit support."
|
||||
},
|
||||
Switch::new("livekit").checked(current.livekit).on_click({
|
||||
let flags = flags.clone();
|
||||
let livekit = livekit.clone();
|
||||
|
||||
move |next, _window, cx| {
|
||||
if *next && *livekit.read(cx) == Some(false) {
|
||||
return;
|
||||
}
|
||||
flags.update(cx, |flags, cx| {
|
||||
flags.livekit = *next;
|
||||
cx.notify();
|
||||
});
|
||||
move |next, _window, cx| {
|
||||
if *next && *livekit.read(cx) == Some(false) {
|
||||
return;
|
||||
}
|
||||
}),
|
||||
),
|
||||
flags.update(cx, |flags, cx| {
|
||||
flags.livekit = *next;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
.on_ok({
|
||||
@@ -262,15 +274,15 @@ fn children_section(children: &Entity<Vec<GroupId>>, cx: &App) -> AnyElement {
|
||||
|
||||
let mut section = v_flex()
|
||||
.gap_1()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Children");
|
||||
|
||||
if list.is_empty() {
|
||||
return section
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("No subgroups"),
|
||||
)
|
||||
.into_any_element();
|
||||
@@ -295,7 +307,7 @@ fn children_section(children: &Entity<Vec<GroupId>>, cx: &App) -> AnyElement {
|
||||
div()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_color(cx.theme().text)
|
||||
.text_color(cx.theme().foreground)
|
||||
.child(SharedString::from(child.as_str().to_owned())),
|
||||
)
|
||||
.child(order),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use gpui::{App, Div, Entity, ParentElement, SharedString, Styled, Task, Window};
|
||||
use gpui_kit::component::input::{Input, InputState};
|
||||
use gpui_kit::component::notification::Notification;
|
||||
use gpui_kit::component::{ActiveTheme, WindowExt, v_flex};
|
||||
use nip29::{Group, Pin, Row};
|
||||
use nostr_sdk::prelude::Url;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
use ui::input::{Input, InputState};
|
||||
use ui::notification::Notification;
|
||||
use ui::{WindowExtension, v_flex};
|
||||
|
||||
pub(crate) fn display_name(group: &Group) -> SharedString {
|
||||
group
|
||||
@@ -36,7 +35,7 @@ pub(crate) fn opens_run(rows: &[Row], index: usize) -> bool {
|
||||
};
|
||||
|
||||
match (current, previous) {
|
||||
(Row::Message(current), Row::Message(previous)) => ui::message::opens_run(
|
||||
(Row::Message(current), Row::Message(previous)) => ui::opens_run(
|
||||
Some((previous.author, previous.created_at.as_secs())),
|
||||
(current.author, current.created_at.as_secs()),
|
||||
),
|
||||
@@ -47,8 +46,8 @@ pub(crate) fn opens_run(rows: &[Row], index: usize) -> bool {
|
||||
pub(crate) fn field(label: &'static str, input: &Entity<InputState>, cx: &App) -> Div {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(label)
|
||||
.child(Input::new(input))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user