From 049ca9181bc26d7e2b6892edfcc65eb264a77c3b Mon Sep 17 00:00:00 2001 From: boxdot Date: Fri, 11 Mar 2022 21:52:23 +0100 Subject: [PATCH] Sync contacts Now we resolve user names from the synced address book, if possible. We fallback to the profile name, if contact is not in the address book. And eventually to contacts phone number, if profile retrieval failed. The contacts are synced on start-up of the app, however max one time per hour. Resolved #140 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app.rs | 208 +++++++++++++++++++++++++++++++------------------ src/main.rs | 3 + src/signal.rs | 49 +++++++++--- src/storage.rs | 49 ++++-------- src/ui.rs | 111 +++++++++++++------------- src/util.rs | 11 +-- 8 files changed, 254 insertions(+), 181 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0138333..f96a17d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1970,7 +1970,7 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "presage" version = "0.2.0" -source = "git+https://github.com/whisperfish/presage.git?branch=main#ee1aa4163393c0e53e80c021ca59e2c78b2ea807" +source = "git+https://github.com/whisperfish/presage.git?rev=f09db39#f09db3926fd02f61f94cc6fc786d81ca52f9f245" dependencies = [ "async-trait", "base64 0.12.3", diff --git a/Cargo.toml b/Cargo.toml index 7bc9562..c871eb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ debug = 0 lto = "thin" [dependencies] -presage = { git = "https://github.com/whisperfish/presage.git", branch = "main" } +presage = { git = "https://github.com/whisperfish/presage.git", rev = "f09db39" } anyhow = "1.0.40" async-trait = "0.1.51" diff --git a/src/app.rs b/src/app.rs index fb28b81..8a44c08 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,8 +9,10 @@ use crate::util::{ }; use anyhow::{anyhow, Context as _}; +use chrono::{DateTime, Duration, Utc}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent}; use itertools::Itertools; +use log::info; use notify_rust::Notification; use phonenumber::{Mode, PhoneNumber}; use presage::prelude::proto::{AttachmentPointer, ReceiptMessage, TypingMessage}; @@ -34,6 +36,9 @@ use std::convert::{TryFrom, TryInto}; use std::path::Path; use std::str::FromStr; +/// Amount of time to skip contacts sync after the last sync +const CONTACTS_SYNC_DEADLINE_SEC: i64 = 60 * 60; // 1h + pub struct App { pub config: Config, signal_manager: Box, @@ -255,6 +260,9 @@ impl BoxData { #[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct AppData { pub channels: FilteredStatefulList, + /// Names retrieved from profiles or phone number if it failed + /// + /// Do not use directly, use [`App::name_by_id`] instead. pub names: HashMap, #[serde(skip)] // ! We may want to save it pub input: BoxData, @@ -262,6 +270,8 @@ pub struct AppData { pub search_box: BoxData, #[serde(skip)] pub is_multiline_input: bool, + #[serde(default)] + pub contacts_sync_request_at: Option>, } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -338,23 +348,6 @@ pub struct GroupData { } impl Channel { - pub fn contains_user(&self, name: &str, hm: &HashMap) -> bool { - match self.group_data { - Some(ref gd) => gd.members.iter().any(|u| name_by_id(hm, *u).contains(name)), - None => self.name.contains(name), - } - } - - pub fn match_pattern(&self, pattern: &str, hm: &HashMap) -> bool { - if pattern.is_empty() { - return true; - } - match pattern.chars().next().unwrap() { - '@' => self.contains_user(&pattern[1..], hm), - _ => self.name.contains(pattern), - } - } - pub fn reset_writing(&mut self, user: Uuid) { match &mut self.typing { TypingSet::GroupTyping(ref mut hash_set) => { @@ -551,7 +544,7 @@ impl App { storage: Box, ) -> anyhow::Result { let user_id = signal_manager.user_id(); - let data = storage.load_app_data(user_id, config.user.name.clone())?; + let data = storage.load_app_data()?; Ok(Self { config, signal_manager, @@ -576,35 +569,59 @@ impl App { } } - pub fn writing_people(&self, channel: &Channel) -> String { - if !channel.is_writing() { - return String::from(""); - } - let uuids: Vec = match &channel.typing { - TypingSet::GroupTyping(hash_set) => hash_set.clone().into_iter().collect(), - TypingSet::SingleTyping(a) => { - if *a { - vec![channel.user_id().unwrap()] - } else { - Vec::new() + pub fn writing_people(&self, channel: &Channel) -> Option { + if channel.is_writing() { + let uuids: Box> = match &channel.typing { + TypingSet::GroupTyping(uuids) => Box::new(uuids.iter().copied()), + TypingSet::SingleTyping(a) => { + if *a { + Box::new(std::iter::once(channel.user_id().unwrap())) + } else { + Box::new(std::iter::empty()) + } } - } - }; - format!( - "{:?} writing...", - uuids - .into_iter() - .map(|u| self.name_by_id(u)) - .collect::>() - ) + }; + Some(format!( + "[{}] writing...", + uuids.map(|id| self.name_by_id(id)).format(", ") + )) + } else { + None + } } pub fn save(&self) -> anyhow::Result<()> { self.storage.save_app_data(&self.data) } - pub fn name_by_id(&self, id: Uuid) -> &str { - name_by_id(&self.data.names, id) + pub fn name_by_id(&self, id: Uuid) -> String { + if self.user_id == id { + // it's me + self.config.user.name.clone() + } else if let Some(contact) = self + .signal_manager + .contact_by_id(id) + .ok() + .flatten() + .filter(|contact| !contact.name.is_empty()) + { + // user is known via our contact list + contact.name + } else if let Some(name) = self.data.names.get(&id) { + // user should be at least known via their profile or phone number + name.clone() + } else { + // give up + "Unknown User".to_string() + } + } + + pub fn channel_name<'a>(&self, channel: &'a Channel) -> Cow<'a, str> { + if let Some(id) = channel.user_id() { + self.name_by_id(id).into() + } else { + (&channel.name).into() + } } pub fn on_key(&mut self, key: KeyEvent) -> anyhow::Result<()> { @@ -911,18 +928,17 @@ impl App { .ensure_group_channel_exists(master_key, revision) .await .context("failed to create group channel")?; - let from = self - .ensure_user_is_known(uuid, profile_key, phone_number) - .await - .to_string(); + + self.ensure_user_is_known(uuid, profile_key, phone_number) + .await; + let from = self.name_by_id(uuid); (channel_idx, from) } else { // incoming direct message - let name = self - .ensure_user_is_known(uuid, profile_key, phone_number) - .await - .to_string(); + self.ensure_user_is_known(uuid, profile_key, phone_number) + .await; + let name = self.name_by_id(uuid); let channel_idx = self.ensure_contact_channel_exists(uuid, &name).await; let from = self.data.channels.items[channel_idx].name.clone(); // Reset typing notification as the Tipyng::Stop are not always sent by the server when a message is sent. @@ -1257,11 +1273,13 @@ impl App { .iter() .position(|channel| channel.id == channel_id)?; let channel = &mut self.data.channels.items[channel_idx]; + let message = channel .messages .items .iter_mut() .find(|m| m.arrived_at == target_sent_timestamp)?; + let reaction_idx = message .reactions .iter() @@ -1281,19 +1299,25 @@ impl App { if is_added && channel_id != ChannelId::User(self.user_id) { // Notification - let sender_name = name_by_id(&self.data.names, sender_uuid); - let summary = if let ChannelId::Group(_) = channel.id { - Cow::from(format!("{} in {}", sender_name, channel.name)) - } else { - Cow::from(sender_name) - }; - let mut notification = format!("{} reacted {}", summary, emoji); + let mut notification = format!("reacted {}", emoji); if let Some(text) = message.message.as_ref() { notification.push_str(" to: "); notification.push_str(text); } + + // makes borrow checker happy + let channel_id = channel.id; + let channel_name = channel.name.clone(); + + let sender_name = self.name_by_id(sender_uuid); + let summary = if let ChannelId::Group(_) = channel_id { + Cow::from(format!("{} in {}", sender_name, channel_name)) + } else { + Cow::from(sender_name) + }; + if notify { - self.notify(&summary, ¬ification); + self.notify(&summary, &format!("{summary} {notification}")); } self.touch_channel(channel_idx); @@ -1375,19 +1399,15 @@ impl App { uuid: Uuid, profile_key: Vec, phone_number: PhoneNumber, - ) -> &str { - if self - .try_ensure_user_is_known(uuid, profile_key) - .await - .is_none() - { + ) { + if !self.try_ensure_user_is_known(uuid, profile_key).await { let phone_number_name = phone_number.format().mode(Mode::E164).to_string(); self.data.names.insert(uuid, phone_number_name); } - self.data.names.get(&uuid).unwrap() } - async fn try_ensure_user_is_known(&mut self, uuid: Uuid, profile_key: Vec) -> Option<&str> { + /// Returns `true`, if user name was resolved successfully, otherwise `false` + async fn try_ensure_user_is_known(&mut self, uuid: Uuid, profile_key: Vec) -> bool { let is_phone_number_or_unknown = self .data .names @@ -1396,12 +1416,18 @@ impl App { .unwrap_or(true); if is_phone_number_or_unknown { let name = match profile_key.try_into() { - Ok(key) => self.signal_manager.contact_name(uuid, key).await, + Ok(key) => { + self.signal_manager + .resolve_name_from_profile(uuid, key) + .await + } Err(_) => None, }; - self.data.names.insert(uuid, name?); + if let Some(name) = name { + self.data.names.insert(uuid, name); + } } - self.data.names.get(&uuid).map(|s| s.as_str()) + self.data.names.contains_key(&uuid) } async fn try_ensure_users_are_known( @@ -1445,11 +1471,9 @@ impl App { .iter() .position(|channel| channel.user_id() == Some(uuid)) { - if let Some(name) = self.data.names.get(&uuid) { - let channel = &mut self.data.channels.items[channel_idx]; - if &channel.name != name { - channel.name = name.clone(); - } + let channel = &mut self.data.channels.items[channel_idx]; + if channel.name != name { + channel.name = name.to_string(); } channel_idx } else { @@ -1580,10 +1604,44 @@ impl App { pub fn is_help(&self) -> bool { self.display_help } -} -pub fn name_by_id(names: &HashMap, id: Uuid) -> &str { - names.get(&id).map(|s| s.as_ref()).unwrap_or("Unknown Name") + pub(crate) async fn request_contacts_sync(&mut self) -> anyhow::Result<()> { + let now = Utc::now(); + let do_sync = self + .data + .contacts_sync_request_at + .map(|dt| dt + Duration::seconds(CONTACTS_SYNC_DEADLINE_SEC) < now) + .unwrap_or(true); + if do_sync { + info!("requesting contact sync"); + self.signal_manager.request_contacts_sync().await?; + self.data.contacts_sync_request_at = Some(now); + self.save().unwrap(); + } + Ok(()) + } + + /// Filters visible channel based on the provided `pattern` + /// + /// `pattern` is compared to channel name or channel member contact names, case insensitively. + pub(crate) fn filter_channels(&mut self, pattern: &str) { + let pattern = pattern.to_lowercase(); + + // move out `channels` temporarily to make borrow checker happy + let mut channels = std::mem::take(&mut self.data.channels); + channels.filter(|channel: &Channel| match pattern.chars().next() { + None => true, + Some('@') => match channel.group_data.as_ref() { + Some(group_data) => group_data + .members + .iter() + .any(|&id| self.name_by_id(id).to_lowercase().contains(&pattern[1..])), + None => channel.name.to_lowercase().contains(&pattern[1..]), + }, + _ => channel.name.to_lowercase().contains(&pattern), + }); + self.data.channels = channels; + } } /// Returns an emoji string if `s` is an emoji or if `s` is a GitHub emoji shortcode. diff --git a/src/main.rs b/src/main.rs index 56b5b8f..074e835 100644 --- a/src/main.rs +++ b/src/main.rs @@ -89,6 +89,7 @@ async fn is_online() -> bool { async fn run_single_threaded(relink: bool) -> anyhow::Result<()> { let (signal_manager, config) = signal::ensure_linked_device(relink).await?; + let storage = JsonStorage::new(config.data_path.clone(), config::fallback_data_path()); let mut app = App::try_new( config, @@ -96,6 +97,8 @@ async fn run_single_threaded(relink: bool) -> anyhow::Result<()> { Box::new(storage), )?; + app.request_contacts_sync().await?; + enable_raw_mode()?; let _raw_mode_guard = scopeguard::guard((), |_| { disable_raw_mode().unwrap(); diff --git a/src/signal.rs b/src/signal.rs index d70258d..350495c 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -12,7 +12,8 @@ use presage::prelude::content::Reaction; use presage::prelude::proto::data_message::Quote; use presage::prelude::proto::{AttachmentPointer, ReceiptMessage}; use presage::prelude::{ - AttachmentSpec, ContentBody, DataMessage, GroupContextV2, GroupMasterKey, SignalServers, + AttachmentSpec, Contact, ContentBody, DataMessage, GroupContextV2, GroupMasterKey, + SignalServers, }; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -32,8 +33,6 @@ pub type Manager = presage::Manager; pub trait SignalManager { fn user_id(&self) -> Uuid; - async fn contact_name(&self, id: Uuid, profile_key: [u8; 32]) -> Option; - async fn resolve_group( &mut self, master_key_bytes: GroupMasterKeyBytes, @@ -55,6 +54,18 @@ pub trait SignalManager { ) -> Message; fn send_reaction(&self, channel: &Channel, message: &Message, emoji: String, remove: bool); + + /// Resolves contact name from its profile + async fn resolve_name_from_profile(&self, id: Uuid, profile_key: [u8; 32]) -> Option; + + async fn request_contacts_sync(&self) -> anyhow::Result<()>; + + /// Retrieves contact information store in the manager + /// + /// The information is based on the contact book of the client and is only available after + /// [`request_contacts_sync`] was called **and** contacts where received from Signal server. + /// This usually happens shortly after the latter method is called. + fn contact_by_id(&self, id: Uuid) -> anyhow::Result>; } pub struct ResolvedGroup { @@ -245,7 +256,7 @@ impl SignalManager for PresageManager { } } - async fn contact_name(&self, id: Uuid, profile_key: [u8; 32]) -> Option { + async fn resolve_name_from_profile(&self, id: Uuid, profile_key: [u8; 32]) -> Option { match self.manager.retrieve_profile_by_uuid(id, profile_key).await { Ok(profile) => Some(profile.name?.given_name), Err(e) => { @@ -320,6 +331,14 @@ impl SignalManager for PresageManager { size: attachment_pointer.size.unwrap(), }) } + + async fn request_contacts_sync(&self) -> anyhow::Result<()> { + Ok(self.manager.request_contacts_sync().await?) + } + + fn contact_by_id(&self, id: Uuid) -> anyhow::Result> { + Ok(self.manager.get_contact_by_id(id)?) + } } async fn upload_attachments( @@ -361,7 +380,7 @@ fn get_signal_manager(db_path: PathBuf) -> anyhow::Result { Ok(manager) } -/// Makes sure that we have linked device. +/// Makes sure that we have a linked device. /// /// Either, /// @@ -466,10 +485,6 @@ pub mod test { fn send_receipt(&self, _: Uuid, _: Vec, _: Receipt) {} - async fn contact_name(&self, _id: Uuid, _profile_key: [u8; 32]) -> Option { - None - } - async fn resolve_group( &mut self, _master_key_bytes: super::GroupMasterKeyBytes, @@ -523,5 +538,21 @@ pub mod test { ) -> anyhow::Result { bail!("mocked signal manager cannot save attachments"); } + + async fn resolve_name_from_profile( + &self, + _id: Uuid, + _profile_key: [u8; 32], + ) -> Option { + None + } + + async fn request_contacts_sync(&self) -> anyhow::Result<()> { + Ok(()) + } + + fn contact_by_id(&self, _id: Uuid) -> anyhow::Result> { + Ok(None) + } } } diff --git a/src/storage.rs b/src/storage.rs index bf3b1eb..996455f 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -3,7 +3,6 @@ use crate::cursor::Cursor; use anyhow::Context; use log::info; -use uuid::Uuid; use std::fs::File; use std::io::BufReader; @@ -21,10 +20,7 @@ pub trait Storage { /// In case, the app data exists, but can't be deserialized/loaded, this method should fail with /// an error, instead of returning a *new* app data which would override the old incompatible /// one. - /// - /// After the app data is loaded, this method must ensure that the user with the given`user_id` - /// and `user_name` is indexed in the app data names. - fn load_app_data(&self, user_id: Uuid, user_name: String) -> anyhow::Result; + fn load_app_data(&self) -> anyhow::Result; } /// Storage based on a single JSON file. @@ -38,12 +34,9 @@ impl Storage for JsonStorage { Self::save_to(data, &self.data_path) } - fn load_app_data(&self, user_id: Uuid, user_name: String) -> anyhow::Result { + fn load_app_data(&self) -> anyhow::Result { let mut data = self.load_app_data_impl()?; - // ensure that our name is up to date - data.names.insert(user_id, user_name); - // select the first channel if none is selected if data.channels.state.selected().is_none() && !data.channels.items.is_empty() { data.channels.state.select(Some(0)); @@ -125,11 +118,8 @@ pub mod test { Ok(()) } - fn load_app_data(&self, user_id: uuid::Uuid, user_name: String) -> anyhow::Result { - Ok(AppData { - names: IntoIterator::into_iter([(user_id, user_name)]).collect(), - ..Default::default() - }) + fn load_app_data(&self) -> anyhow::Result { + Ok(Default::default()) } } } @@ -144,22 +134,20 @@ mod tests { use super::*; use tempfile::NamedTempFile; + use uuid::Uuid; #[test] fn test_json_storage_load_existing_app_data() -> anyhow::Result<()> { - let user_id = Uuid::new_v4(); - let user_name = "Tyler Durden".to_string(); let app_data = AppData { input: BoxData::empty(), search_box: BoxData::empty(), - names: [(user_id, user_name.clone())].iter().cloned().collect(), ..Default::default() }; let file = NamedTempFile::new()?; let storage = JsonStorage::new(file.path().to_owned(), None); storage.save_app_data(&app_data)?; - let loaded_app_data = storage.load_app_data(user_id, user_name)?; + let loaded_app_data = storage.load_app_data()?; assert_eq!(loaded_app_data, app_data); assert_eq!(loaded_app_data.channels.state.selected(), None); @@ -173,29 +161,17 @@ mod tests { let storage = JsonStorage::new(data_path, None); - let user_id = Uuid::new_v4(); - let user_name = "Tyler Durden".to_string(); - let app_data = storage.load_app_data(user_id, user_name.clone())?; - - assert_eq!( - app_data, - AppData { - names: [(user_id, user_name)].iter().cloned().collect(), - ..Default::default() - } - ); + let app_data = storage.load_app_data()?; + assert_eq!(app_data, Default::default()); Ok(()) } #[test] fn test_json_storage_load_app_data_from_fallback() -> anyhow::Result<()> { - let user_id = Uuid::new_v4(); - let user_name = "Tyler Durden".to_string(); let app_data = AppData { input: BoxData::empty(), search_box: BoxData::empty(), - names: [(user_id, user_name.clone())].iter().cloned().collect(), ..Default::default() }; @@ -205,7 +181,7 @@ mod tests { let storage = JsonStorage::new(data_path, Some(fallback_data_path.path().to_owned())); - let loaded_app_data = storage.load_app_data(user_id, user_name)?; + let loaded_app_data = storage.load_app_data()?; assert_eq!(loaded_app_data, app_data); @@ -226,21 +202,22 @@ mod tests { cursor: Cursor::end("some search"), }, is_multiline_input: false, - names: [(user_id, user_name.clone())].iter().cloned().collect(), + names: Default::default(), channels: FilteredStatefulList::_with_items(vec![Channel { id: ChannelId::User(user_id), - name: user_name.clone(), + name: user_name, group_data: None, messages: Default::default(), unread_messages: 0, typing: TypingSet::SingleTyping(false), }]), + contacts_sync_request_at: None, }; let file = NamedTempFile::new()?; let storage = JsonStorage::new(file.path().to_owned(), None); storage.save_app_data(&app_data)?; - let app_data = storage.load_app_data(user_id, user_name)?; + let app_data = storage.load_app_data()?; assert_eq!(app_data.channels.state.selected(), Some(0)); diff --git a/src/ui.rs b/src/ui.rs index acaa23e..292cd1b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -16,6 +16,7 @@ use tui::Frame; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use uuid::Uuid; +use std::borrow::Cow; use std::fmt; pub const CHANNEL_VIEW_RATIO: u32 = 4; @@ -125,9 +126,10 @@ fn draw_channels_column(f: &mut Frame, app: &mut App, area: Rect) fn draw_channels(f: &mut Frame, app: &mut App, area: Rect) { let channel_list_width = area.width.saturating_sub(2) as usize; - let pattern = app.data.search_box.data.as_str(); + let pattern = app.data.search_box.data.clone(); app.channel_text_width = channel_list_width; - app.data.channels.filter_channels(pattern, &app.data.names); + app.filter_channels(&pattern); + let channels: Vec = app .data .channels @@ -138,7 +140,7 @@ fn draw_channels(f: &mut Frame, app: &mut App, area: Rect) { } else { String::new() }; - let label = format!("{}{}", channel.name, unread_messages_label); + let label = format!("{}{}", app.channel_name(channel), unread_messages_label); let label_width = label.width(); let label = if label.width() <= channel_list_width || unread_messages_label.is_empty() { label @@ -384,7 +386,11 @@ fn draw_messages(f: &mut Frame, app: &mut App, area: Rect) { items.insert(unread_messages, ListItem::new(Span::from(new_message_line))); } - let title = format!("Messages {}", writing_people); + let title: String = if let Some(writing_people) = writing_people { + format!("Messages {}", writing_people) + } else { + "Messages".to_string() + }; let list = List::new(items) .block(Block::default().title(title).borders(Borders::ALL)) @@ -475,7 +481,7 @@ fn display_message( let from = Span::styled( textwrap::indent( - from, + &from, &" ".repeat( names .max_name_width() @@ -665,10 +671,10 @@ fn draw_help(f: &mut Frame, app: &mut App, area: Rect) { f.render_stateful_widget(shorts_widget, area, &mut app.data.channels.state); } -fn displayed_name(name: &str, first_name_only: bool) -> &str { +fn displayed_name(name: String, first_name_only: bool) -> String { if first_name_only { let space_pos = name.find(' ').unwrap_or(name.len()); - &name[0..space_pos] + name[0..space_pos].to_string() } else { name } @@ -695,55 +701,56 @@ fn user_color(username: &str) -> Color { /// Resolves names in a channel struct NameResolver<'a> { app: Option<&'a App>, - names_and_colors: Vec<(Uuid, &'a str, Color)>, + names_and_colors: Vec<(Uuid, String, Color)>, max_name_width: usize, } impl<'a> NameResolver<'a> { fn compute_for_channel<'b>(app: &'a app::App, channel: &'b app::Channel) -> Self { let first_name_only = app.config.first_name_only; - let mut names_and_colors = if let Some(group_data) = channel.group_data.as_ref() { - group_data - .members - .iter() - .map(|&uuid| { - let name = app.name_by_id(uuid); - let color = user_color(name); - let name = displayed_name(name, first_name_only); - (uuid, name, color) - }) - .collect() - } else { - let user_id = app.user_id; - let user_name = app.name_by_id(user_id); - let mut self_color = user_color(user_name); - let user_name = displayed_name(user_name, first_name_only); - - let contact_uuid = match channel.id { - app::ChannelId::User(uuid) => uuid, - _ => unreachable!("logic error"), - }; - - if contact_uuid == user_id { - vec![(user_id, user_name, self_color)] + let mut names_and_colors: Vec<(Uuid, String, Color)> = + if let Some(group_data) = channel.group_data.as_ref() { + group_data + .members + .iter() + .map(|&uuid| { + let name = app.name_by_id(uuid); + let color = user_color(&name); + let name = displayed_name(name, first_name_only); + (uuid, name, color) + }) + .collect() } else { - let contact_name = app.name_by_id(contact_uuid); - let contact_color = user_color(contact_name); - let contact_name = displayed_name(contact_name, first_name_only); - - if self_color == contact_color { - // use differnt color for our user name - if let Some(idx) = USER_COLORS.iter().position(|&c| c == self_color) { - self_color = USER_COLORS[(idx + 1) % USER_COLORS.len()]; + let user_id = app.user_id; + let user_name = app.name_by_id(user_id); + let mut self_color = user_color(&user_name); + let user_name = displayed_name(user_name, first_name_only); + + let contact_uuid = match channel.id { + app::ChannelId::User(uuid) => uuid, + _ => unreachable!("logic error"), + }; + + if contact_uuid == user_id { + vec![(user_id, user_name, self_color)] + } else { + let contact_name = app.name_by_id(contact_uuid); + let contact_color = user_color(&contact_name); + let contact_name = displayed_name(contact_name, first_name_only); + + if self_color == contact_color { + // use differnt color for our user name + if let Some(idx) = USER_COLORS.iter().position(|&c| c == self_color) { + self_color = USER_COLORS[(idx + 1) % USER_COLORS.len()]; + } } - } - vec![ - (user_id, user_name, self_color), - (contact_uuid, contact_name, contact_color), - ] - } - }; + vec![ + (user_id, user_name, self_color), + (contact_uuid, contact_name, contact_color), + ] + } + }; names_and_colors.sort_unstable_by_key(|&(id, _, _)| id); let max_name_width = names_and_colors @@ -759,17 +766,17 @@ impl<'a> NameResolver<'a> { } } - fn resolve(&self, id: Uuid) -> (&str, Color) { + fn resolve(&self, id: Uuid) -> (Cow, Color) { match self .names_and_colors .binary_search_by_key(&id, |&(id, _, _)| id) { Ok(idx) => { - let (_, from, from_color) = self.names_and_colors[idx]; - (from, from_color) + let (_, from, from_color) = &self.names_and_colors[idx]; + (from.into(), *from_color) } Err(_) => ( - app::App::name_by_id(self.app.expect("logic error"), id), + self.app.expect("logic error").name_by_id(id).into(), Color::Magenta, ), } @@ -802,7 +809,7 @@ mod tests { fn name_resolver(user_id: Uuid) -> NameResolver<'static> { NameResolver { app: None, - names_and_colors: vec![(user_id, "boxdot", Color::Green)], + names_and_colors: vec![(user_id, "boxdot".to_string(), Color::Green)], max_name_width: 6, } } diff --git a/src/util.rs b/src/util.rs index 5932297..1e74f50 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,14 +1,12 @@ -use std::collections::HashMap; - use crate::app::Channel; -use super::MESSAGE_SCROLL_BACK; use chrono::{DateTime, Local, NaiveDateTime, TimeZone as _, Utc}; use presage::prelude::PhoneNumber; use regex_automata::Regex; use serde::{Deserialize, Serialize}; use tui::widgets::ListState; -use uuid::Uuid; + +use crate::MESSAGE_SCROLL_BACK; #[derive(Debug, Serialize, Deserialize)] pub struct StatefulList { @@ -62,9 +60,8 @@ impl Default for StatefulList { } impl FilteredStatefulList { - pub fn filter_channels(&mut self, pattern: &str, hm: &HashMap) { - let lambda = |c: &Channel| c.match_pattern(pattern, hm); - self.filter_elements(lambda); + pub fn filter(&mut self, filter: impl Fn(&Channel) -> bool) { + self.filter_elements(filter); // Update the selected message to not got past the bound of `self.filtered_items` self.state.select(if self.filtered_items.is_empty() { None