Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/chatd/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1508,7 +1508,7 @@ impl Store {
let pattern = format!("%{}%", like_prefix(query));
self.db
.prepare(
"SELECT m.network, m.account, m.conversation, m.id, m.sent, m.text, m.body, c.title
"SELECT m.network, m.account, m.conversation, m.id, m.sent, m.text, m.body, c.title, c.space_title
FROM message m
JOIN conversation c ON c.network = m.network AND c.account = m.account
AND c.id = m.conversation
Expand Down Expand Up @@ -1542,6 +1542,7 @@ impl Store {
sender: message.sender.name,
snippet: snippet(&text, query),
title: row.get(7)?,
space_title: row.get(8)?,
outgoing: message.outgoing,
})
},
Expand Down
4 changes: 4 additions & 0 deletions crates/protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,9 @@ pub struct MessageHit {
/// The conversation's title, so a result row says where it is without a second request.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub title: String,
/// The title of the Space the conversation sits in, when it sits in one.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub space_title: String,
/// Mine, so the row can say "You".
#[serde(default, skip_serializing_if = "is_false")]
pub outgoing: bool,
Expand Down Expand Up @@ -1768,6 +1771,7 @@ mod tests {
sender: "mara".into(),
snippet: "hello world".into(),
title: String::new(),
space_title: String::new(),
outgoing: false,
};
assert_eq!(
Expand Down
59 changes: 54 additions & 5 deletions crates/tinyclient/src/gui/conversation/side.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::client::ask;
use crate::gui::components::{empty_state, glyph};
use crate::gui::input::{InputEvent, InputKey, TextInput};
use crate::gui::layout::{RightKind, RightPane};
use crate::gui::sidebar::chat;
use crate::gui::state::Shell;
use crate::gui::theme::{self, Type};
use crate::gui::time;
Expand All @@ -31,6 +32,9 @@ pub struct Side {
pub search: Option<SearchPage>,
/// The search pane's field and its wiring, alive while the pane is.
search_input: Option<SearchField>,
/// How often the cache was asked for the search pane, counted across pages: a page remade
/// starts at nothing, and its number must never meet an older ask's.
search_asks: u32,
/// The conversation the stack was last fitted to.
fitted: Option<Key>,
/// The kind this view last put in the layout's slot, so another view's change can be told
Expand Down Expand Up @@ -525,11 +529,13 @@ impl Shell {
/// conversation open now, so a switch of conversation re-resolves the next ask.
fn reask_search(&mut self, cx: &mut Context<Self>) {
let (words, scope, asked) = {
let Some(page) = self.conversation_state.side.search.as_mut() else {
let side = &mut self.conversation_state.side;
let Some(page) = side.search.as_mut() else {
return;
};
side.search_asks += 1;
page.failed = false;
page.asked += 1;
page.asked = side.search_asks;
(page.words.clone(), page.scope, page.asked)
};
if words.trim().is_empty() {
Expand All @@ -541,12 +547,18 @@ impl Shell {
}
let open = self.open_with_space();
let (conversation, space) = scope_filters(scope, open.as_ref().map(|(key, space)| (key, space.as_deref())));
let request = Request::SearchMessages { conversation, network: None, space, query: words, limit: protocol::PAGE_LIMIT };
let request = Request::SearchMessages { conversation, network: None, space, query: words.clone(), limit: protocol::PAGE_LIMIT };
self.off_thread(
cx,
move || ask(&request),
move |this, answer, cx| {
let Some(page) = this.conversation_state.side.search.as_mut().filter(|page| page.asked == asked) else {
let Some(page) = this
.conversation_state
.side
.search
.as_mut()
.filter(|page| page.asked == asked && page.words == words && page.scope == scope)
else {
return;
};
match answer {
Expand Down Expand Up @@ -955,7 +967,8 @@ fn draw_search(shell: &Shell, _window: &mut Window, cx: &mut Context<Shell>) ->
.flex()
.items_center()
.gap(px(6.))
.child(div().flex_1().min_w_0().truncate().type_ui().text_color(theme::text()).child(hit.title.clone()))
.child(chat::mark(hit.conversation.network, 13., theme::text_faint()))
.child(div().flex_1().min_w_0().truncate().type_small().text_color(theme::text_faint()).child(said_at(hit)))
.child(div().flex_none().type_small().text_color(theme::text_soft()).child(age(now(), hit.sent))),
)
.child(
Expand Down Expand Up @@ -1047,6 +1060,21 @@ fn draw_pins(shell: &Shell, conversation: &Key, cx: &mut Context<Shell>) -> AnyE
div().id("pins").size_full().overflow_y_scroll().py(px(4.)).children(rows).into_any_element()
}

/// Where the message was said, as the spec's result line has it: the Space it sits in and the
/// conversation, or the conversation's id when it has no title yet.
fn said_at(hit: &MessageHit) -> String {
let name = if hit.title.is_empty() {
hit.conversation.id.as_str()
} else {
hit.title.as_str()
};
if hit.space_title.is_empty() {
name.to_owned()
} else {
format!("{} › {name}", hit.space_title)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -1089,6 +1117,27 @@ mod tests {
assert_eq!(scope_filters(Scope::Space, None), (None, None));
}

#[test]
fn a_hit_row_names_its_space_and_conversation_or_falls_back_to_its_id() {
let mut hit = MessageHit {
conversation: key("C_a"),
message_id: "1".into(),
sent: 0,
sender: "Ines".into(),
snippet: "rules".into(),
title: "general".into(),
space_title: "Northwind".into(),
outgoing: false,
};
assert_eq!(said_at(&hit), "Northwind › general");
hit.space_title.clear();
assert_eq!(said_at(&hit), "general");
hit.title.clear();
assert_eq!(said_at(&hit), "C_a");
hit.space_title = "Northwind".into();
assert_eq!(said_at(&hit), "Northwind › C_a");
}

#[test]
fn the_switcher_s_words_start_asked_for() {
// The switcher's row seeds its words into the page, so the first ask carries them
Expand Down
Loading