feat: audit codebase (#72)
Reviewed-on: #72
This commit was merged in pull request #72.
This commit is contained in:
@@ -18,6 +18,7 @@ gpui.workspace = true
|
||||
instant.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
itertools.workspace = true
|
||||
smallvec.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
+112
-28
@@ -1,3 +1,4 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::sync::{Arc, LazyLock, RwLock};
|
||||
use std::time::Duration;
|
||||
@@ -56,6 +57,7 @@ static CUSTOM_EMOJI_RE: LazyLock<Regex> =
|
||||
|
||||
mod actions;
|
||||
mod file;
|
||||
pub mod trash;
|
||||
|
||||
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
||||
cx.new(|cx| ChatPanel::new(room, window, cx))
|
||||
@@ -144,7 +146,6 @@ impl ChatPanel {
|
||||
})
|
||||
.unwrap_or(("Unknown".into(), "Message...".into()));
|
||||
|
||||
// Define input state
|
||||
let input = cx.new(|cx| {
|
||||
TextareaState::new(window, cx)
|
||||
.placeholder(format!("Message {}", name))
|
||||
@@ -152,7 +153,6 @@ impl ChatPanel {
|
||||
.clean_on_escape()
|
||||
});
|
||||
|
||||
// Define dictation state
|
||||
let speech = cx.new(|cx| {
|
||||
let state = SpeechState::new(cx)
|
||||
.recognizer(speech::recognizer())
|
||||
@@ -165,11 +165,9 @@ impl ChatPanel {
|
||||
state
|
||||
});
|
||||
|
||||
// Define subject input state
|
||||
let subject_input = cx.new(|cx| InputState::new(window, cx).placeholder("New subject..."));
|
||||
let subject_bar = cx.new(|_cx| false);
|
||||
|
||||
// Define subscriptions
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
@@ -218,9 +216,8 @@ impl ChatPanel {
|
||||
}),
|
||||
);
|
||||
|
||||
// Define all functions that will run after the current cycle
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.connect(cx);
|
||||
this.connect(window, cx);
|
||||
this.subscribe_room_events(window, cx);
|
||||
this.get_messages(window, cx);
|
||||
});
|
||||
@@ -269,11 +266,24 @@ impl ChatPanel {
|
||||
}
|
||||
|
||||
/// Get messaging relays and announcement for each member
|
||||
fn connect(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(room) = self.room.upgrade() {
|
||||
let task = room.read(cx).connect(cx);
|
||||
self.tasks.push(task);
|
||||
}
|
||||
fn connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(room) = self.room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let connect = room.read(cx).connect(cx);
|
||||
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
if let Err(error) = connect.await {
|
||||
log::error!("Connecting room relays failed: {error}");
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.notify_failure("Connection failed", error, window, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Subscribe to room events
|
||||
@@ -317,23 +327,40 @@ impl ChatPanel {
|
||||
}
|
||||
|
||||
/// Load all messages belonging to this room
|
||||
fn get_messages(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn get_messages(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Ok(get_messages) = self.room.read_with(cx, |this, cx| this.get_messages(cx)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let events = get_messages.await?;
|
||||
|
||||
// Update message list
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_messages(&events, cx);
|
||||
})?;
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
match get_messages.await {
|
||||
Ok(events) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_messages(&events, cx);
|
||||
})?;
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("Loading room messages failed: {error}");
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.notify_failure("Failed to load messages", error, window, cx);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Show a failure of a background operation to the user until dismissed
|
||||
fn notify_failure(&self, title: &'static str, error: Error, window: &mut Window, cx: &mut App) {
|
||||
window.push_notification(
|
||||
Notification::error(error.to_string())
|
||||
.title(title)
|
||||
.autohide(false),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get user input content and merged all attachments if available
|
||||
fn get_input_value(&self, cx: &Context<Self>) -> String {
|
||||
// Get input's value
|
||||
@@ -566,8 +593,9 @@ impl ChatPanel {
|
||||
for (i, message) in self.messages.iter().enumerate().skip(pos) {
|
||||
self.message_index.insert(message.id, i);
|
||||
}
|
||||
let len = self.list_state.item_count();
|
||||
self.list_state.splice(len..len, 1);
|
||||
// The welcome row occupies row zero, so message rows sit one ahead
|
||||
let row = pos + 1;
|
||||
self.list_state.splice(row..row, 1);
|
||||
|
||||
if scroll {
|
||||
self.list_state.scroll_to(ListOffset {
|
||||
@@ -582,13 +610,62 @@ impl ChatPanel {
|
||||
|
||||
/// Convert and insert a vector of nostr events into the chat panel
|
||||
fn insert_messages(&mut self, events: &[UnsignedEvent], cx: &mut Context<Self>) {
|
||||
let mut incoming: Vec<Message> = Vec::with_capacity(events.len());
|
||||
|
||||
for event in events.iter() {
|
||||
if event.kind == Kind::Reaction {
|
||||
self.insert_reaction(event, cx);
|
||||
} else {
|
||||
incoming.push(event.into());
|
||||
}
|
||||
}
|
||||
|
||||
incoming.sort();
|
||||
|
||||
let old_len = self.messages.len();
|
||||
let mut stored = std::mem::take(&mut self.messages).into_iter().peekable();
|
||||
let mut merged: Vec<Message> = Vec::with_capacity(old_len + incoming.len());
|
||||
let mut insert_start = None;
|
||||
let mut inserted = 0usize;
|
||||
|
||||
for message in incoming {
|
||||
while let Some(old) = stored.next_if(|old| old.cmp(&message) == Ordering::Less) {
|
||||
merged.push(old);
|
||||
}
|
||||
|
||||
let duplicate = stored
|
||||
.peek()
|
||||
.is_some_and(|old| old.cmp(&message) == Ordering::Equal)
|
||||
|| merged
|
||||
.last()
|
||||
.is_some_and(|last| last.cmp(&message) == Ordering::Equal);
|
||||
|
||||
if duplicate {
|
||||
continue;
|
||||
}
|
||||
// Bulk inserting messages, so no need to scroll to the latest message
|
||||
self.insert_message(event, false, cx);
|
||||
|
||||
if inserted == 0 {
|
||||
insert_start = Some(merged.len());
|
||||
}
|
||||
inserted += 1;
|
||||
merged.push(message);
|
||||
}
|
||||
|
||||
merged.extend(stored);
|
||||
|
||||
self.messages = merged;
|
||||
|
||||
if let Some(start) = insert_start {
|
||||
self.message_index = self
|
||||
.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, message)| (message.id, index))
|
||||
.collect();
|
||||
|
||||
let row = start + 1;
|
||||
self.list_state.splice(row..row, inserted);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,16 +675,23 @@ impl ChatPanel {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = SharedString::from(&event.content);
|
||||
let emojis = emojis_from_tags(&event.tags);
|
||||
|
||||
let image = Message::reaction_image(&event.content, &emojis)
|
||||
.map(|url| SharedUri::from(url.to_string()));
|
||||
|
||||
for id in event.tags.event_ids() {
|
||||
self.reactions.entry(id).or_default().push((
|
||||
SharedString::from(&event.content),
|
||||
image.clone(),
|
||||
event.pubkey,
|
||||
));
|
||||
let entries = self.reactions.entry(id).or_default();
|
||||
// NIP-25: a newer reaction by the same author replaces their previous one
|
||||
if let Some(existing) = entries
|
||||
.iter_mut()
|
||||
.find(|(_, _, author)| *author == event.pubkey)
|
||||
{
|
||||
*existing = (content.clone(), image.clone(), event.pubkey);
|
||||
} else {
|
||||
entries.push((content.clone(), image.clone(), event.pubkey));
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use chat::{ChatRegistry, TrashedMessage};
|
||||
use common::TimestampExt;
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, Context, EventEmitter, FocusHandle, Focusable, IntoElement,
|
||||
ParentElement, Render, Styled, Window, div, uniform_list,
|
||||
};
|
||||
use gpui_kit::component::button::{Button, ButtonVariants as _};
|
||||
use gpui_kit::component::dock::{Panel, PanelEvent};
|
||||
use gpui_kit::component::{ActiveTheme, Disableable, Icon, Sizable, h_flex, v_flex};
|
||||
use ui::IconName;
|
||||
|
||||
/// Initialize the trash panel.
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> gpui::Entity<TrashPanel> {
|
||||
cx.new(|cx| TrashPanel::new(window, cx))
|
||||
}
|
||||
|
||||
/// The messages that could not be decrypted, shown like a chat room.
|
||||
pub struct TrashPanel {
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl TrashPanel {
|
||||
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_entry(&self, message: &TrashedMessage, cx: &Context<Self>) -> AnyElement {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.px_2()
|
||||
.py_1p5()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().secondary.opacity(0.5))
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.small()
|
||||
.text_color(cx.theme().warning),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(div().text_sm().child("Could not be decrypted"))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.truncate()
|
||||
.child(message.reason.clone()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(message.created_at.to_ago()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_empty(&self, cx: &Context<Self>) -> AnyElement {
|
||||
div()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Trash is empty")
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui_kit::component::dock::BasePanel for TrashPanel {
|
||||
fn panel_name(&self) -> &'static str {
|
||||
"TrashPanel"
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for TrashPanel {
|
||||
fn title(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
.child("Trash")
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for TrashPanel {}
|
||||
|
||||
impl Focusable for TrashPanel {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TrashPanel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let messages = Rc::new(ChatRegistry::global(cx).read(cx).trashed(cx));
|
||||
let count = messages.len();
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.p_3()
|
||||
.gap_3()
|
||||
.text_xs()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Messages that could not be decrypted. New encryption keys may make them readable."),
|
||||
)
|
||||
.child(
|
||||
Button::new("trash-retry")
|
||||
.label("Try again")
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(count == 0)
|
||||
.on_click(|_, _window, cx| {
|
||||
ChatRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.reprocess_trash(cx);
|
||||
});
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when(count == 0, |this| this.child(self.render_empty(cx)))
|
||||
.when(count > 0, |this| {
|
||||
this.child(
|
||||
uniform_list(
|
||||
"trash-list",
|
||||
count,
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_entries(range, messages.as_slice(), cx)
|
||||
}),
|
||||
)
|
||||
.min_h_0()
|
||||
.flex_1(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TrashPanel {
|
||||
fn render_entries(
|
||||
&self,
|
||||
range: std::ops::Range<usize>,
|
||||
messages: &[TrashedMessage],
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
messages
|
||||
.get(range)
|
||||
.map(|slice| {
|
||||
slice
|
||||
.iter()
|
||||
.map(|message| {
|
||||
// Keep a small gap between entries so they read as separate rows
|
||||
div()
|
||||
.pb_1p5()
|
||||
.child(self.render_entry(message, cx))
|
||||
.into_any_element()
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user