update
This commit is contained in:
+66
-79
@@ -31,13 +31,11 @@ use ui::button::{Button, ButtonVariants};
|
|||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
use ui::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
||||||
use ui::menu::DropdownMenu;
|
use ui::menu::DropdownMenu;
|
||||||
|
use ui::message::{MessageRow, WelcomeMessage};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::scroll::Scrollbar;
|
use ui::scroll::Scrollbar;
|
||||||
use ui::tooltip::Tooltip;
|
use ui::tooltip::Tooltip;
|
||||||
use ui::{
|
use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, WindowExtension,
|
|
||||||
h_flex, v_flex,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::file::*;
|
use crate::file::*;
|
||||||
use crate::text::RenderedText;
|
use crate::text::RenderedText;
|
||||||
@@ -45,6 +43,10 @@ use crate::text::RenderedText;
|
|||||||
const REACTION_EMOJIS: &[&str] = &["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"];
|
const REACTION_EMOJIS: &[&str] = &["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"];
|
||||||
const COMPACT_REACTION_EMOJIS: &[&str] = &["👍", "❤️", "👀"];
|
const COMPACT_REACTION_EMOJIS: &[&str] = &["👍", "❤️", "👀"];
|
||||||
|
|
||||||
|
/// Notice shown when a conversation has no messages, and above the message list.
|
||||||
|
const PRIVATE_NOTICE: &str =
|
||||||
|
"This conversation is private. Only members can see each other's messages.";
|
||||||
|
|
||||||
/// Regex matching strings that consist entirely of emoji characters,
|
/// Regex matching strings that consist entirely of emoji characters,
|
||||||
/// zero-width joiners, variation selectors, and keycap combiners.
|
/// zero-width joiners, variation selectors, and keycap combiners.
|
||||||
static EMOJI_RE: LazyLock<Regex> =
|
static EMOJI_RE: LazyLock<Regex> =
|
||||||
@@ -126,9 +128,8 @@ impl ChatPanel {
|
|||||||
let replies_to = cx.new(|_| HashSet::new());
|
let replies_to = cx.new(|_| HashSet::new());
|
||||||
let reports_by_id = Arc::new(RwLock::new(BTreeMap::new()));
|
let reports_by_id = Arc::new(RwLock::new(BTreeMap::new()));
|
||||||
|
|
||||||
// Define list of messages
|
|
||||||
let messages = Vec::new();
|
let messages = Vec::new();
|
||||||
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
|
let list_state = ListState::new(messages.len() + 1, ListAlignment::Bottom, px(1024.));
|
||||||
|
|
||||||
// Get room id and name
|
// Get room id and name
|
||||||
let (id, name) = room
|
let (id, name) = room
|
||||||
@@ -577,7 +578,6 @@ impl ChatPanel {
|
|||||||
where
|
where
|
||||||
E: Into<Message>,
|
E: Into<Message>,
|
||||||
{
|
{
|
||||||
let old_len = self.messages.len();
|
|
||||||
let msg: Message = m.into();
|
let msg: Message = m.into();
|
||||||
|
|
||||||
if let Err(pos) = self.messages.binary_search(&msg) {
|
if let Err(pos) = self.messages.binary_search(&msg) {
|
||||||
@@ -586,7 +586,8 @@ impl ChatPanel {
|
|||||||
for (i, message) in self.messages.iter().enumerate().skip(pos) {
|
for (i, message) in self.messages.iter().enumerate().skip(pos) {
|
||||||
self.message_index.insert(message.id, i);
|
self.message_index.insert(message.id, i);
|
||||||
}
|
}
|
||||||
self.list_state.splice(old_len..old_len, 1);
|
let len = self.list_state.item_count();
|
||||||
|
self.list_state.splice(len..len, 1);
|
||||||
|
|
||||||
if scroll {
|
if scroll {
|
||||||
self.list_state.scroll_to(ListOffset {
|
self.list_state.scroll_to(ListOffset {
|
||||||
@@ -657,7 +658,8 @@ impl ChatPanel {
|
|||||||
/// Scroll to a message by its ID
|
/// Scroll to a message by its ID
|
||||||
fn scroll_to(&self, id: &EventId) {
|
fn scroll_to(&self, id: &EventId) {
|
||||||
if let Some(ix) = self.messages.iter().position(|msg| &msg.id == id) {
|
if let Some(ix) = self.messages.iter().position(|msg| &msg.id == id) {
|
||||||
self.list_state.scroll_to_reveal_item(ix);
|
// The welcome message occupies the first row, so message rows sit one ahead.
|
||||||
|
self.list_state.scroll_to_reveal_item(ix + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1051,10 +1053,19 @@ impl ChatPanel {
|
|||||||
cx.open_url(&content);
|
cx.open_url(&content);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_announcement(&self, cx: &Context<Self>) -> AnyElement {
|
fn render_welcome(&self, cx: &Context<Self>) -> AnyElement {
|
||||||
const MSG: &str =
|
WelcomeMessage::new("welcome")
|
||||||
"This conversation is private. Only members can see each other's messages.";
|
.icon(
|
||||||
|
svg()
|
||||||
|
.path("brand/coop.svg")
|
||||||
|
.size_12()
|
||||||
|
.text_color(cx.theme().ghost_element_background_alt),
|
||||||
|
)
|
||||||
|
.message(PRIVATE_NOTICE)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_announcement(&self, cx: &Context<Self>) -> AnyElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.h_40()
|
.h_40()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -1072,7 +1083,7 @@ impl ChatPanel {
|
|||||||
.size_12()
|
.size_12()
|
||||||
.text_color(cx.theme().ghost_element_active),
|
.text_color(cx.theme().ghost_element_active),
|
||||||
)
|
)
|
||||||
.child(MSG)
|
.child(PRIVATE_NOTICE)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1141,7 +1152,14 @@ impl ChatPanel {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let file = self.messages.get(ix).and_then(|message| {
|
if ix == 0 {
|
||||||
|
return self.render_welcome(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The welcome message occupies the first row, so message rows sit one ahead.
|
||||||
|
let index = ix - 1;
|
||||||
|
|
||||||
|
let file = self.messages.get(index).and_then(|message| {
|
||||||
let file = message.file.clone()?;
|
let file = message.file.clone()?;
|
||||||
(!self.decrypted_files.contains_key(&message.id) && file.is_image())
|
(!self.decrypted_files.contains_key(&message.id) && file.is_image())
|
||||||
.then_some((message.id, file))
|
.then_some((message.id, file))
|
||||||
@@ -1151,9 +1169,9 @@ impl ChatPanel {
|
|||||||
self.load_file(id, file, cx);
|
self.load_file(id, file, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = self.messages.get(ix) {
|
if let Some(message) = self.messages.get(index) {
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let show_author = self.is_group_start(ix);
|
let show_author = self.is_group_start(index);
|
||||||
let text = self
|
let text = self
|
||||||
.rendered_texts_by_id
|
.rendered_texts_by_id
|
||||||
.entry(message.id)
|
.entry(message.id)
|
||||||
@@ -1188,68 +1206,38 @@ impl ChatPanel {
|
|||||||
// Hide avatar setting
|
// Hide avatar setting
|
||||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||||
|
|
||||||
div()
|
MessageRow::new(ix)
|
||||||
.id(ix)
|
.show_author(show_author)
|
||||||
.group("")
|
.hide_avatar(hide_avatar)
|
||||||
.relative()
|
.avatar(
|
||||||
.w_full()
|
Avatar::new(author.avatar())
|
||||||
.py_1()
|
.seed(author.avatar_seed())
|
||||||
.px_3()
|
.flex_shrink_0()
|
||||||
.child(
|
.relative()
|
||||||
div()
|
.dropdown_menu(move |this, _window, _cx| {
|
||||||
.flex()
|
this.menu("Public Key", Box::new(Command::Copy(pk)))
|
||||||
.gap_3()
|
.menu("View Relays", Box::new(Command::Relays(pk)))
|
||||||
.when(!hide_avatar, |this| {
|
.separator()
|
||||||
if show_author {
|
.menu("View on njump.me", Box::new(Command::Njump(pk)))
|
||||||
this.child(
|
}),
|
||||||
Avatar::new(author.avatar())
|
|
||||||
.seed(author.avatar_seed())
|
|
||||||
.flex_shrink_0()
|
|
||||||
.relative()
|
|
||||||
.dropdown_menu(move |this, _window, _cx| {
|
|
||||||
this.menu("Public Key", Box::new(Command::Copy(pk)))
|
|
||||||
.menu("View Relays", Box::new(Command::Relays(pk)))
|
|
||||||
.separator()
|
|
||||||
.menu("View on njump.me", Box::new(Command::Njump(pk)))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.child(div().flex_shrink_0().w(px(32.)))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.flex_1()
|
|
||||||
.w_full()
|
|
||||||
.flex_initial()
|
|
||||||
.overflow_hidden()
|
|
||||||
.when(show_author, |this| {
|
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_placeholder)
|
|
||||||
.child(div().font_semibold().child(author.name()))
|
|
||||||
.child(message.created_at.to_human_time())
|
|
||||||
.when(has_reports, |this| {
|
|
||||||
this.child(self.render_sent_reports(&id, cx))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(has_replies, |this| {
|
|
||||||
this.children(self.render_message_replies(replies, cx))
|
|
||||||
})
|
|
||||||
.when(message.file.is_none(), |this| this.child(rendered_text))
|
|
||||||
.child(self.render_media(&message.media, cx))
|
|
||||||
.when_some(message.file.as_ref(), |this, file| {
|
|
||||||
this.child(self.render_message_file(&id, file, cx))
|
|
||||||
})
|
|
||||||
.when(has_reactions, |this| {
|
|
||||||
this.child(self.render_reactions(&id, cx))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.author(author.name())
|
||||||
|
.timestamp(message.created_at.to_human_time())
|
||||||
|
.when(has_reports, |this| {
|
||||||
|
this.header_extra(self.render_sent_reports(&id, cx))
|
||||||
|
})
|
||||||
|
.when(has_replies, |this| {
|
||||||
|
this.children(self.render_message_replies(replies, cx))
|
||||||
|
})
|
||||||
|
.when(message.file.is_none(), |this| this.child(rendered_text))
|
||||||
|
.child(self.render_media(&message.media, cx))
|
||||||
|
.when_some(message.file.as_ref(), |this, file| {
|
||||||
|
this.child(self.render_message_file(&id, file, cx))
|
||||||
|
})
|
||||||
|
.when(has_reactions, |this| {
|
||||||
|
this.child(self.render_reactions(&id, cx))
|
||||||
|
})
|
||||||
|
.overlay(
|
||||||
div()
|
div()
|
||||||
.group_hover("", |this| this.bg(cx.theme().element_active))
|
.group_hover("", |this| this.bg(cx.theme().element_active))
|
||||||
.absolute()
|
.absolute()
|
||||||
@@ -1259,7 +1247,7 @@ impl ChatPanel {
|
|||||||
.h_full()
|
.h_full()
|
||||||
.bg(cx.theme().border_transparent),
|
.bg(cx.theme().border_transparent),
|
||||||
)
|
)
|
||||||
.child(self.render_actions(&id, &pk, cx))
|
.overlay(self.render_actions(&id, &pk, cx))
|
||||||
.on_mouse_down(
|
.on_mouse_down(
|
||||||
MouseButton::Middle,
|
MouseButton::Middle,
|
||||||
cx.listener(move |this, _, _window, cx| {
|
cx.listener(move |this, _, _window, cx| {
|
||||||
@@ -1269,7 +1257,6 @@ impl ChatPanel {
|
|||||||
.on_double_click(cx.listener(move |this, _, _window, cx| {
|
.on_double_click(cx.listener(move |this, _, _window, cx| {
|
||||||
this.reply_to(&id, cx);
|
this.reply_to(&id, cx);
|
||||||
}))
|
}))
|
||||||
.hover(|this| this.bg(cx.theme().surface_background))
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use ui::avatar::Avatar;
|
|||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{InputEvent, Textarea, TextareaState};
|
use ui::input::{InputEvent, Textarea, TextareaState};
|
||||||
|
use ui::message::WelcomeMessage;
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::scroll::Scrollbar;
|
use ui::scroll::Scrollbar;
|
||||||
use ui::{Disableable, IconName, Sizable, WindowExtension, h_flex, v_flex};
|
use ui::{Disableable, IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||||
@@ -205,15 +206,15 @@ impl CommunityPanel {
|
|||||||
self.rows.clear();
|
self.rows.clear();
|
||||||
self.has_more = false;
|
self.has_more = false;
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
self.list_state.reset(1);
|
self.list_state.reset(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
channel
|
channel
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The list's item count: the load-older row, then every message row.
|
/// The list's item count: the welcome row, the load-older row, then every message row.
|
||||||
fn item_count(&self) -> usize {
|
fn item_count(&self) -> usize {
|
||||||
self.rows.len() + 1
|
self.rows.len() + 2
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A timeline read, `before_ms` exclusive, or `None` for the newest rows.
|
/// A timeline read, `before_ms` exclusive, or `None` for the newest rows.
|
||||||
@@ -542,7 +543,8 @@ impl CommunityPanel {
|
|||||||
|
|
||||||
shown.insert(message.id, at);
|
shown.insert(message.id, at);
|
||||||
self.rows.insert(at, message);
|
self.rows.insert(at, message);
|
||||||
self.list_state.splice(at + 1..at + 1, 1);
|
// The welcome and load-older rows sit above the messages.
|
||||||
|
self.list_state.splice(at + 2..at + 2, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,7 +626,29 @@ impl CommunityPanel {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The row at index 0: the affordance that pages older history in.
|
/// The row at index 0: the welcome message for the channel.
|
||||||
|
fn render_welcome(&self, cx: &Context<Self>) -> AnyElement {
|
||||||
|
let (name, avatar) = self
|
||||||
|
.community
|
||||||
|
.read_with(cx, |community, _cx| {
|
||||||
|
let seed = community.id().to_hex();
|
||||||
|
let avatar = match community.icon() {
|
||||||
|
Some(path) => Avatar::from_source(path).seed(seed).large(),
|
||||||
|
None => Avatar::new(None).seed(seed).large(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(community.name(), avatar)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|_| (SharedString::from("this community"), Avatar::new(None)));
|
||||||
|
|
||||||
|
WelcomeMessage::new("welcome")
|
||||||
|
.icon(avatar)
|
||||||
|
.title(format!("Welcome to {}", name))
|
||||||
|
.message(format!("This is the start of the {} channel.", name))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The row at index 1: the affordance that pages older history in.
|
||||||
fn render_older(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_older(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
if !self.has_more {
|
if !self.has_more {
|
||||||
return div().into_any_element();
|
return div().into_any_element();
|
||||||
@@ -670,14 +694,19 @@ impl CommunityPanel {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
if ix == 0 {
|
if ix == 0 {
|
||||||
|
return self.render_welcome(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ix == 1 {
|
||||||
return self.render_older(cx);
|
return self.render_older(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(message) = self.rows.get(ix - 1) else {
|
// The welcome and load-older rows sit above the messages.
|
||||||
|
let Some(message) = self.rows.get(ix - 2) else {
|
||||||
return div().into_any_element();
|
return div().into_any_element();
|
||||||
};
|
};
|
||||||
|
|
||||||
let show_author = self.opens_run(ix - 1);
|
let show_author = self.opens_run(ix - 2);
|
||||||
|
|
||||||
message::render(ix, message, show_author, cx)
|
message::render(ix, message, show_author, cx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,68 +3,37 @@ use std::collections::BTreeMap;
|
|||||||
use common::TimestampExt;
|
use common::TimestampExt;
|
||||||
use community::ChatMessage;
|
use community::ChatMessage;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, div};
|
||||||
AnyElement, App, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, px,
|
|
||||||
};
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::{StyledExt, h_flex, v_flex};
|
use ui::h_flex;
|
||||||
|
use ui::message::MessageRow;
|
||||||
|
|
||||||
pub(crate) fn render(ix: usize, message: &ChatMessage, show_author: bool, cx: &App) -> AnyElement {
|
pub(crate) fn render(ix: usize, message: &ChatMessage, show_author: bool, cx: &App) -> AnyElement {
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let author = persons.read(cx).get(&message.author, cx);
|
let author = persons.read(cx).get(&message.author, cx);
|
||||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||||
|
|
||||||
div()
|
MessageRow::new(ix)
|
||||||
.id(ix)
|
.show_author(show_author)
|
||||||
.w_full()
|
.hide_avatar(hide_avatar)
|
||||||
.py_1()
|
.avatar(
|
||||||
.px_3()
|
Avatar::new(author.avatar())
|
||||||
.hover(|this| this.bg(cx.theme().surface_background))
|
.seed(author.avatar_seed())
|
||||||
.child(
|
.flex_shrink_0(),
|
||||||
h_flex()
|
|
||||||
.items_start()
|
|
||||||
.gap_3()
|
|
||||||
.when(!hide_avatar, |this| {
|
|
||||||
if show_author {
|
|
||||||
this.child(
|
|
||||||
Avatar::new(author.avatar())
|
|
||||||
.seed(author.avatar_seed())
|
|
||||||
.flex_shrink_0(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.child(div().flex_shrink_0().w(px(32.)))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.flex_1()
|
|
||||||
.min_w_0()
|
|
||||||
.gap_0p5()
|
|
||||||
.when(show_author, |this| {
|
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_placeholder)
|
|
||||||
.child(div().font_semibold().child(author.name()))
|
|
||||||
.child(
|
|
||||||
Timestamp::from_secs(message.at_ms / 1000).to_human_time(),
|
|
||||||
)
|
|
||||||
.when(message.edited_at.is_some(), |this| {
|
|
||||||
this.child(div().child("(edited)"))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(content(message, cx))
|
|
||||||
.when(!message.reactions.is_empty(), |this| {
|
|
||||||
this.child(reactions(message, cx))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
.author(author.name())
|
||||||
|
.timestamp(Timestamp::from_secs(message.at_ms / 1000).to_human_time())
|
||||||
|
.when(message.edited_at.is_some(), |this| {
|
||||||
|
this.header_extra(div().child("(edited)"))
|
||||||
|
})
|
||||||
|
.child(content(message, cx))
|
||||||
|
.when(!message.reactions.is_empty(), |this| {
|
||||||
|
this.child(reactions(message, cx))
|
||||||
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub mod group_box;
|
|||||||
pub mod indicator;
|
pub mod indicator;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
|
pub mod message;
|
||||||
pub mod modal;
|
pub mod modal;
|
||||||
pub mod nav_item;
|
pub mod nav_item;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, MouseButton,
|
||||||
|
MouseDownEvent, ParentElement as _, RenderOnce, SharedString, StyleRefinement, Styled, Window,
|
||||||
|
div, px,
|
||||||
|
};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
|
use crate::{InteractiveElementExt as _, StyledExt as _, h_flex, v_flex};
|
||||||
|
|
||||||
|
type MouseDownListener = Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>;
|
||||||
|
type DoubleClickListener = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||||
|
|
||||||
|
/// A single message row in a chat-like list.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct MessageRow {
|
||||||
|
id: ElementId,
|
||||||
|
style: StyleRefinement,
|
||||||
|
show_author: bool,
|
||||||
|
hide_avatar: bool,
|
||||||
|
avatar: Option<AnyElement>,
|
||||||
|
author: Option<SharedString>,
|
||||||
|
timestamp: Option<SharedString>,
|
||||||
|
header_extras: SmallVec<[AnyElement; 1]>,
|
||||||
|
body: SmallVec<[AnyElement; 4]>,
|
||||||
|
overlays: SmallVec<[AnyElement; 2]>,
|
||||||
|
on_mouse_down: Option<(MouseButton, MouseDownListener)>,
|
||||||
|
on_double_click: Option<DoubleClickListener>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageRow {
|
||||||
|
/// Create a message row with the given element id.
|
||||||
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
style: StyleRefinement::default(),
|
||||||
|
show_author: false,
|
||||||
|
hide_avatar: false,
|
||||||
|
avatar: None,
|
||||||
|
author: None,
|
||||||
|
timestamp: None,
|
||||||
|
header_extras: SmallVec::new(),
|
||||||
|
body: SmallVec::new(),
|
||||||
|
overlays: SmallVec::new(),
|
||||||
|
on_mouse_down: None,
|
||||||
|
on_double_click: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this row opens a run of messages from one author.
|
||||||
|
#[must_use]
|
||||||
|
pub fn show_author(mut self, show_author: bool) -> Self {
|
||||||
|
self.show_author = show_author;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hide the avatar column entirely.
|
||||||
|
#[must_use]
|
||||||
|
pub fn hide_avatar(mut self, hide_avatar: bool) -> Self {
|
||||||
|
self.hide_avatar = hide_avatar;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The avatar shown when this row opens a run of messages.
|
||||||
|
#[must_use]
|
||||||
|
pub fn avatar(mut self, avatar: impl IntoElement) -> Self {
|
||||||
|
self.avatar = Some(avatar.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The author's display name, shown next to the timestamp.
|
||||||
|
#[must_use]
|
||||||
|
pub fn author(mut self, author: impl Into<SharedString>) -> Self {
|
||||||
|
self.author = Some(author.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The time the message was sent.
|
||||||
|
#[must_use]
|
||||||
|
pub fn timestamp(mut self, timestamp: impl Into<SharedString>) -> Self {
|
||||||
|
self.timestamp = Some(timestamp.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append an element to the header row.
|
||||||
|
#[must_use]
|
||||||
|
pub fn header_extra(mut self, extra: impl IntoElement) -> Self {
|
||||||
|
self.header_extras.push(extra.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append an element to the message body, in order.
|
||||||
|
#[must_use]
|
||||||
|
pub fn child(mut self, child: impl IntoElement) -> Self {
|
||||||
|
self.body.push(child.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append several elements to the message body, in order.
|
||||||
|
#[must_use]
|
||||||
|
pub fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self {
|
||||||
|
self.body
|
||||||
|
.extend(children.into_iter().map(|child| child.into_any_element()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append an element positioned over the row.
|
||||||
|
#[must_use]
|
||||||
|
pub fn overlay(mut self, overlay: impl IntoElement) -> Self {
|
||||||
|
self.overlays.push(overlay.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a mouse button press anywhere on the row.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_mouse_down(
|
||||||
|
mut self,
|
||||||
|
button: MouseButton,
|
||||||
|
listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_mouse_down = Some((button, Box::new(listener)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a double click anywhere on the row.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_double_click(
|
||||||
|
mut self,
|
||||||
|
listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_double_click = Some(Box::new(listener));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for MessageRow {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for MessageRow {
|
||||||
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let Self {
|
||||||
|
id,
|
||||||
|
style,
|
||||||
|
show_author,
|
||||||
|
hide_avatar,
|
||||||
|
avatar,
|
||||||
|
author,
|
||||||
|
timestamp,
|
||||||
|
header_extras,
|
||||||
|
body,
|
||||||
|
overlays,
|
||||||
|
on_mouse_down,
|
||||||
|
on_double_click,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id(id)
|
||||||
|
.group("")
|
||||||
|
.relative()
|
||||||
|
.w_full()
|
||||||
|
.py_1()
|
||||||
|
.when(show_author, |this| this.pt_2())
|
||||||
|
.px_3()
|
||||||
|
.refine_style(&style)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.items_start()
|
||||||
|
.gap_3()
|
||||||
|
.when(!hide_avatar, |this| {
|
||||||
|
if show_author {
|
||||||
|
match avatar {
|
||||||
|
Some(avatar) => this.child(avatar),
|
||||||
|
None => this.child(div().flex_shrink_0().w(px(32.))),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.child(div().flex_shrink_0().w(px(32.)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.flex_1()
|
||||||
|
.w_full()
|
||||||
|
.min_w_0()
|
||||||
|
.flex_initial()
|
||||||
|
.overflow_hidden()
|
||||||
|
.when(show_author, |this| {
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_placeholder)
|
||||||
|
.when_some(author, |this, author| {
|
||||||
|
this.child(div().font_semibold().child(author))
|
||||||
|
})
|
||||||
|
.when_some(timestamp, |this, timestamp| {
|
||||||
|
this.child(timestamp)
|
||||||
|
})
|
||||||
|
.children(header_extras),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.children(body),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.children(overlays)
|
||||||
|
.when_some(on_mouse_down, |this, (button, listener)| {
|
||||||
|
this.on_mouse_down(button, listener)
|
||||||
|
})
|
||||||
|
.when_some(on_double_click, |this, listener| {
|
||||||
|
this.on_double_click(listener)
|
||||||
|
})
|
||||||
|
.hover(|this| this.bg(cx.theme().surface_background))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A welcome message shown at the top of a message list.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct WelcomeMessage {
|
||||||
|
id: ElementId,
|
||||||
|
style: StyleRefinement,
|
||||||
|
icon: Option<AnyElement>,
|
||||||
|
title: Option<SharedString>,
|
||||||
|
message: Option<SharedString>,
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WelcomeMessage {
|
||||||
|
/// Create a welcome message with the given element id.
|
||||||
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
style: StyleRefinement::default(),
|
||||||
|
icon: None,
|
||||||
|
title: None,
|
||||||
|
message: None,
|
||||||
|
children: SmallVec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The icon shown above the title.
|
||||||
|
#[must_use]
|
||||||
|
pub fn icon(mut self, icon: impl IntoElement) -> Self {
|
||||||
|
self.icon = Some(icon.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The welcome title.
|
||||||
|
#[must_use]
|
||||||
|
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
|
||||||
|
self.title = Some(title.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The welcome body text.
|
||||||
|
#[must_use]
|
||||||
|
pub fn message(mut self, message: impl Into<SharedString>) -> Self {
|
||||||
|
self.message = Some(message.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append an element below the body text.
|
||||||
|
#[must_use]
|
||||||
|
pub fn child(mut self, child: impl IntoElement) -> Self {
|
||||||
|
self.children.push(child.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for WelcomeMessage {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for WelcomeMessage {
|
||||||
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let Self {
|
||||||
|
id,
|
||||||
|
style,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
children,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.id(id)
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.p_3()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_center()
|
||||||
|
.refine_style(&style)
|
||||||
|
.when_some(icon, |this, icon| this.child(icon))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_center()
|
||||||
|
.when_some(title, |this, title| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(cx.theme().text)
|
||||||
|
.child(title),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(message, |this, message| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_placeholder)
|
||||||
|
.child(message),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.children(children)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,6 +127,7 @@ impl RenderOnce for TabBar {
|
|||||||
let selected_index = self.selected_index;
|
let selected_index = self.selected_index;
|
||||||
let on_click = self.on_click.clone();
|
let on_click = self.on_click.clone();
|
||||||
let segmented = self.segmented;
|
let segmented = self.segmented;
|
||||||
|
let has_prefix = self.prefix.is_some();
|
||||||
|
|
||||||
self.base
|
self.base
|
||||||
.group("tab-bar")
|
.group("tab-bar")
|
||||||
@@ -145,7 +146,10 @@ impl RenderOnce for TabBar {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.id("tabs")
|
.id("tabs")
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.when(!segmented, |this| this.overflow_x_scroll())
|
.when(!segmented, |this| {
|
||||||
|
this.overflow_x_scroll()
|
||||||
|
.when(!has_prefix, |this| this.pl_2())
|
||||||
|
})
|
||||||
.when_some(self.scroll_handle, |this, scroll_handle| {
|
.when_some(self.scroll_handle, |this, scroll_handle| {
|
||||||
this.track_scroll(&scroll_handle)
|
this.track_scroll(&scroll_handle)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user