From d54c08ee3db15a38534b3e9868b0a5394ca308f9 Mon Sep 17 00:00:00 2001 From: Tom Dwyer Date: Thu, 26 Mar 2026 02:00:03 +0000 Subject: [PATCH] messaging workflow updates --- gossip-bin/src/ui/dm_chat_list.rs | 189 ++++++++++++++++++++++++--- gossip-bin/src/ui/feed/note/mod.rs | 22 +++- gossip-bin/src/ui/feed/post.rs | 117 +++++++++++++---- gossip-bin/src/ui/mod.rs | 183 +++++++++++++++++++++++++- gossip-bin/src/ui/theme/default.rs | 4 + gossip-bin/src/ui/theme/test_page.rs | 4 + gossip-lib/src/comms.rs | 2 + gossip-lib/src/overlord.rs | 29 +++- gossip-lib/src/post.rs | 8 +- gossip-lib/src/storage/mod.rs | 1 + 10 files changed, 508 insertions(+), 51 deletions(-) diff --git a/gossip-bin/src/ui/dm_chat_list.rs b/gossip-bin/src/ui/dm_chat_list.rs index 9db59ea12..f6f737746 100644 --- a/gossip-bin/src/ui/dm_chat_list.rs +++ b/gossip-bin/src/ui/dm_chat_list.rs @@ -9,6 +9,7 @@ use gossip_lib::FeedKind; use gossip_lib::Person; use gossip_lib::GLOBALS; use gossip_lib::{PersonTable, Table}; +use nostr_types::PublicKey; use std::time::{Duration, Instant}; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { @@ -38,8 +39,21 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra let mut channels = app.dm_channel_cache.clone(); + let is_signer_ready = GLOBALS.identity.is_unlocked(); + widgets::page_header(ui, "Direct Messages", |ui| { ui.add_space(16.0); + if is_signer_ready { + if widgets::Button::bordered(&app.theme, "New message") + .small(true) + .show(ui) + .clicked() + { + app.dm_new_message = true; + app.dm_new_message_error = None; + } + } + ui.add_space(8.0); if widgets::Button::bordered(&app.theme, "Mark all read") .small(true) .show(ui) @@ -49,7 +63,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra } }); - let is_signer_ready = GLOBALS.identity.is_unlocked(); + if app.dm_new_message { + render_new_message_popup(app, ctx); + } app.vert_scroll_area() .id_salt("dm_chat_list") @@ -173,23 +189,164 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra .on_hover_cursor(egui::CursorIcon::PointingHand) .clicked() { - app.set_page( - ctx, - Page::Feed(FeedKind::DmChat(channeldata.dm_channel.clone())), - ); - app.draft_needs_focus = true; + open_dm_channel(app, ctx, channeldata.dm_channel.clone()); + } + } + }); +} - // Maybe clear the draft, if we are going into a different channel than last - // time - if let Some(oldtarget) = &app.dm_draft_data_target { - if *oldtarget != channeldata.dm_channel { - app.dm_draft_data.clear(); - } - } else { - app.dm_draft_data.clear(); - } - app.dm_draft_data_target = Some(channeldata.dm_channel.clone()); +fn open_dm_channel(app: &mut GossipUi, ctx: &Context, channel: gossip_lib::DmChannel) { + app.set_page(ctx, Page::Feed(FeedKind::DmChat(channel.clone()))); + app.draft_needs_focus = true; + + // Maybe clear the draft, if we are going into a different channel than last time. + if let Some(oldtarget) = &app.dm_draft_data_target { + if *oldtarget != channel { + app.request_dm_draft_states_save(); + app.dm_draft_data.clear(); + app.load_dm_draft_state(&channel); + } + } else { + app.dm_draft_data.clear(); + app.load_dm_draft_state(&channel); + } + app.dm_draft_data_target = Some(channel); +} + +fn render_new_message_popup(app: &mut GossipUi, ctx: &Context) { + const DLG_SIZE: eframe::egui::Vec2 = vec2(420.0, 320.0); + + let ret = widgets::modal_popup(ctx, DLG_SIZE, DLG_SIZE, true, |ui| { + ui.vertical(|ui| { + ui.heading("New direct message"); + ui.add_space(8.0); + + if let Some(err) = &app.dm_new_message_error { + ui.label(RichText::new(err).color(app.theme.warning_marker_text_color())); + ui.add_space(8.0); + } + + ui.label("Search for a known contact"); + let mut output = widgets::TextEdit::search( + &app.theme, + &app.assets, + &mut app.dm_new_message_search, + ) + .desired_width(f32::INFINITY) + .show(ui); + + let mut selected = app.dm_new_message_search_selected; + let mut enter_key = false; + if app.dm_new_message_search_results.is_empty() { + selected = None; + } else { + (selected, enter_key) = widgets::capture_keyboard_for_search( + ui, + app.dm_new_message_search_results.len(), + selected, + ); + } + + if app.dm_new_message_search.len() > 2 { + if Some(&app.dm_new_message_search) != app.dm_new_message_searched.as_ref() + && output.cursor_range.is_some() + { + let mut pairs = GLOBALS + .people + .search_people_to_tag(app.dm_new_message_search.as_str()) + .unwrap_or_default(); + pairs.sort_by(|(_, ak), (_, bk)| { + let af = GLOBALS + .db() + .is_person_in_list(ak, gossip_lib::PersonList::Followed) + .unwrap_or(false); + let bf = GLOBALS + .db() + .is_person_in_list(bk, gossip_lib::PersonList::Followed) + .unwrap_or(false); + bf.cmp(&af).then(std::cmp::Ordering::Greater) + }); + app.dm_new_message_searched = Some(app.dm_new_message_search.clone()); + app.dm_new_message_search_results = pairs.to_owned(); } + } else { + app.dm_new_message_searched = None; + app.dm_new_message_search_results.clear(); } + + widgets::show_contact_search( + ui, + app, + egui::AboveOrBelow::Below, + &mut output, + &mut selected, + app.dm_new_message_search_results.clone(), + enter_key, + |_, app, _, pair| { + app.dm_new_message_search = pair.0.clone(); + app.dm_new_message_search_results.clear(); + app.dm_new_message_search_selected = None; + app.dm_new_message_address = pair.1.as_bech32_string(); + }, + ); + app.dm_new_message_search_selected = selected; + + ui.add_space(10.0); + ui.label("Or enter an npub, hex key, or nprofile address"); + ui.add( + text_edit_line!(app, app.dm_new_message_address) + .desired_width(f32::INFINITY) + .hint_text("npub1, hex key, or nprofile1"), + ); + + ui.add_space(12.0); + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + ui.horizontal(|ui| { + if widgets::Button::secondary(&app.theme, "Cancel") + .show(ui) + .clicked() + { + app.clear_new_message_dialog(); + } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + if widgets::Button::primary(&app.theme, "Start chat") + .show(ui) + .clicked() + { + if let Some(pubkey) = parse_new_message_target( + app.dm_new_message_address.trim(), + ) { + open_dm_channel( + app, + ctx, + gossip_lib::DmChannel::new(&[pubkey]), + ); + app.clear_new_message_dialog(); + } else { + app.dm_new_message_error = + Some("Enter a valid recipient to start a chat.".to_owned()); + } + } + }); + }); + }); }); + }); + + if ret.inner.clicked() { + app.clear_new_message_dialog(); + } +} + +fn parse_new_message_target(value: &str) -> Option { + if let Ok(pubkey) = PublicKey::try_from_bech32_string(value, true) { + Some(pubkey) + } else if let Ok(pubkey) = PublicKey::try_from_hex_string(value, true) { + Some(pubkey) + } else if let Ok(profile) = nostr_types::Profile::try_from_bech32_string(value, true) { + Some(profile.pubkey) + } else { + None + } } diff --git a/gossip-bin/src/ui/feed/note/mod.rs b/gossip-bin/src/ui/feed/note/mod.rs index 32e91df84..e306aa468 100644 --- a/gossip-bin/src/ui/feed/note/mod.rs +++ b/gossip-bin/src/ui/feed/note/mod.rs @@ -57,6 +57,12 @@ pub struct NoteRenderData { /// Should hide nameline pub hide_nameline: bool, + + /// Is this note being rendered in the DM feed? + pub is_dm_feed: bool, + + /// Was this note authored by us? + pub is_our_event: bool, } pub(super) fn render_note( @@ -119,6 +125,11 @@ pub(super) fn render_note( thread_position: indent as i32, hide_footer: as_reply_to, hide_nameline: false, + is_dm_feed: matches!(app.page, Page::Feed(FeedKind::DmChat(_))), + is_our_event: GLOBALS + .identity + .public_key() + .is_some_and(|our_pubkey| note_data.event.pubkey == our_pubkey), }; let top = ui.next_widget_position(); @@ -231,6 +242,11 @@ pub fn render_dm_note(app: &mut GossipUi, ui: &mut Ui, feed_note_params: FeedNot if let Some(note_ref) = app.notecache.try_update_and_get(&id) { if let Ok(note_data) = note_ref.try_borrow() { + let is_our_event = GLOBALS + .identity + .public_key() + .is_some_and(|our_pubkey| note_data.event.pubkey == our_pubkey); + let viewed = GLOBALS .db() .is_event_viewed(note_data.event.id) @@ -252,6 +268,8 @@ pub fn render_dm_note(app: &mut GossipUi, ui: &mut Ui, feed_note_params: FeedNot thread_position: indent as i32, hide_footer: false, hide_nameline: true, + is_dm_feed: true, + is_our_event, }; let inner_response = @@ -385,7 +403,7 @@ pub fn render_note_inside_framing( EncryptionType::Nip04 => Some(EncryptionIndicator { color: app.theme.amber_400(), tooltip_ui: Box::new(|ui: &mut Ui| { - ui.label("NIP-04 encryption. It is recomended to upgrade [link to help page] to Giftwrap (NIP-44) encryption."); + ui.label("NIP-04 encryption. It is recommended to upgrade [link to help page] to Giftwrap (NIP-44) encryption."); }), }), EncryptionType::Giftwrap => None, // Giftwrap is the new good default, we won't show an indicator @@ -1412,6 +1430,8 @@ fn render_repost( thread_position: 0, hide_footer: false, hide_nameline: false, + is_dm_feed: false, + is_our_event: false, }; let row_height = ui.cursor().height(); diff --git a/gossip-bin/src/ui/feed/post.rs b/gossip-bin/src/ui/feed/post.rs index d1b868263..1453dbf9b 100644 --- a/gossip-bin/src/ui/feed/post.rs +++ b/gossip-bin/src/ui/feed/post.rs @@ -1,5 +1,5 @@ use super::FeedNoteParams; -use crate::ui::widgets::{InformationPopup, MoreMenuButton, MoreMenuItem}; +use crate::ui::widgets::{InformationPopup, MoreMenuButton, MoreMenuItem, MoreMenuSwitch}; use crate::ui::{widgets, you, FeedKind, GossipUi, HighlightType, Label, Page, Sense, Theme}; use eframe::egui; use eframe::epaint::text::LayoutJob; @@ -168,7 +168,10 @@ fn dm_posting_area( let compose_area_id: egui::Id = egui::Id::new("compose_area"); let mut send_now: bool = false; - let (bg_color, text_color, text, tooltip_text) = if dm_channel.can_use_nip17() { + let can_use_nip17 = dm_channel.can_use_nip17(); + let use_nip17 = app.dm_draft_data.use_nip17 + && (can_use_nip17 || app.dm_draft_data.use_nip17_force); + let (bg_color, text_color, text, tooltip_text) = if use_nip17 { let text = "STRONG ENCRYPTION"; let tt_text = "SECURED with Giftwrap DM technology (NIPs 17, 44, 59)"; if app.theme.dark_mode { @@ -190,11 +193,7 @@ fn dm_posting_area( let text = "BASIC ENCRYPTION"; let tt_text = "WARNING: Using older less-secure DM technology (NIP-04; recipient(s) have not signalled the ability to accept the newer method)"; - // if app.theme.dark_mode { - (app.theme.amber_400(), app.theme.neutral_50(), text, tt_text) - //} else { - // (app.theme.amber_400(), app.theme.neutral_50(), text, tt_text) - //} + (app.theme.amber_400(), app.theme.neutral_950(), text, tt_text) }; // Text area @@ -250,19 +249,10 @@ fn dm_posting_area( } if !app.dm_draft_data.draft.is_empty() { - let modifiers = if cfg!(target_os = "macos") { - Modifiers { - command: true, - ..Default::default() + if app.dm_draft_data.send_on_enter && draft_response.has_focus() { + if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Enter)) { + send_now = true; } - } else { - Modifiers { - ctrl: true, - ..Default::default() - } - }; - if ui.input_mut(|i| i.consume_key(modifiers, Key::Enter)) { - send_now = true; } } @@ -340,6 +330,28 @@ fn dm_posting_area( }), ))); } + items.push(MoreMenuItem::Switch(MoreMenuSwitch::new( + "Use NIP17", + app.dm_draft_data.use_nip17, + Box::new(move |_, app| { + app.dm_draft_data.use_nip17 = !app.dm_draft_data.use_nip17; + if !app.dm_draft_data.use_nip17 { + app.dm_draft_data.use_nip17_force = false; + app.dm_draft_data.use_nip17_force_confirm = false; + } else if !can_use_nip17 && !app.dm_draft_data.use_nip17_force { + app.dm_draft_data.use_nip17_force_confirm = true; + } + app.request_dm_draft_states_save(); + }), + ))); + items.push(MoreMenuItem::Switch(MoreMenuSwitch::new( + "Send on Enter", + app.dm_draft_data.send_on_enter, + Box::new(|_, app| { + app.dm_draft_data.send_on_enter = !app.dm_draft_data.send_on_enter; + app.request_dm_draft_states_save(); + }), + ))); menu.show_entries(ui, app, response, items); @@ -374,13 +386,15 @@ fn dm_posting_area( } }); } else { - if widgets::Button::primary(&app.theme, "Send") - .show(ui) - .clicked() - && !app.dm_draft_data.draft.is_empty() - { - send_now = true; - } + ui.horizontal(|ui| { + if widgets::Button::primary(&app.theme, "Send") + .show(ui) + .clicked() + && !app.dm_draft_data.draft.is_empty() + { + send_now = true; + } + }); } // Emoji picker @@ -394,6 +408,10 @@ fn dm_posting_area( }); }); + if app.dm_draft_data.use_nip17_force_confirm { + render_nip17_override_confirm(app, ctx); + } + if send_now { let mut tags: Vec = Vec::new(); if app.dm_draft_data.include_content_warning { @@ -415,6 +433,8 @@ fn dm_posting_area( in_reply_to: None, annotation: app.dm_draft_data.is_annotate, dm_channel: Some(dm_channel.to_owned()), + use_nip17, + force_nip17: app.dm_draft_data.use_nip17_force, }); app.reset_draft(); @@ -445,6 +465,47 @@ fn dm_posting_area( } } +fn render_nip17_override_confirm(app: &mut GossipUi, ctx: &Context) { + const DLG_SIZE: eframe::egui::Vec2 = vec2(460.0, 140.0); + + let ret = widgets::modal_popup(ctx, DLG_SIZE, DLG_SIZE, true, |ui| { + ui.vertical(|ui| { + ui.heading("Use NIP17 anyway?"); + ui.add_space(8.0); + ui.label( + "We do not know DM relays for one or more participants. Sending with NIP17 may not reach them.", + ); + ui.add_space(10.0); + ui.horizontal(|ui| { + if widgets::Button::secondary(&app.theme, "Use Basic Encryption") + .show(ui) + .clicked() + { + app.dm_draft_data.use_nip17 = false; + app.dm_draft_data.use_nip17_force = false; + app.dm_draft_data.use_nip17_force_confirm = false; + app.request_dm_draft_states_save(); + } + if widgets::Button::primary(&app.theme, "Use NIP17 Anyway") + .show(ui) + .clicked() + { + app.dm_draft_data.use_nip17_force = true; + app.dm_draft_data.use_nip17_force_confirm = false; + app.request_dm_draft_states_save(); + } + }); + }); + }); + + if ret.inner.clicked() { + app.dm_draft_data.use_nip17 = false; + app.dm_draft_data.use_nip17_force = false; + app.dm_draft_data.use_nip17_force_confirm = false; + app.request_dm_draft_states_save(); + } +} + fn real_posting_area(app: &mut GossipUi, ctx: &Context, ui: &mut Ui) { // Maybe render post we are replying to or reposting @@ -794,6 +855,8 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, ui: &mut Ui) { in_reply_to: Some(replying_to_id), annotation: app.draft_data.is_annotate, dm_channel: None, + use_nip17: false, + force_nip17: false, }); } None => { @@ -808,6 +871,8 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, ui: &mut Ui) { in_reply_to: None, annotation: app.draft_data.is_annotate, dm_channel: None, + use_nip17: false, + force_nip17: false, }); } } diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index b8b5f1f8a..872424b64 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -79,12 +79,14 @@ use nostr_types::RelayUrl; use nostr_types::{ EventKind, FileMetadata, Id, Metadata, MilliSatoshi, Profile, PublicKey, UncheckedUrl, Url, }; +use serde::{Deserialize, Serialize}; use widgets::ModalEntry; use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::ops::DerefMut; use std::path::PathBuf; +use std::sync::mpsc::{self, Receiver, TryRecvError}; use std::rc::Rc; use std::sync::atomic::Ordering; use std::time::{Duration, Instant}; @@ -373,6 +375,19 @@ pub struct DraftData { // If this is an annotation pub is_annotate: bool, + + // DM-only settings + pub use_nip17: bool, + pub send_on_enter: bool, + pub use_nip17_force: bool, + pub use_nip17_force_confirm: bool, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +struct DmDraftState { + use_nip17: bool, + send_on_enter: bool, + use_nip17_force: bool, } impl Default for DraftData { @@ -401,6 +416,10 @@ impl Default for DraftData { tagging_search_results: Vec::new(), is_annotate: false, + use_nip17: false, + send_on_enter: false, + use_nip17_force: false, + use_nip17_force_confirm: false, } } } @@ -425,6 +444,10 @@ impl DraftData { self.tagging_search_searched = None; self.tagging_search_results.clear(); self.is_annotate = false; + self.use_nip17 = false; + self.send_on_enter = false; + self.use_nip17_force = false; + self.use_nip17_force_confirm = false; } } @@ -519,6 +542,17 @@ struct GossipUi { previous_draft_data: DraftData, dm_draft_data: DraftData, dm_draft_data_target: Option, + dm_draft_states: HashMap, + dm_draft_states_dirty: bool, + dm_draft_states_save_after: Option, + dm_draft_states_save_rx: Option>>, + dm_new_message: bool, + dm_new_message_search: String, + dm_new_message_searched: Option, + dm_new_message_search_results: Vec<(String, PublicKey)>, + dm_new_message_search_selected: Option, + dm_new_message_address: String, + dm_new_message_error: Option, // User entry: metadata editing_metadata: bool, @@ -594,6 +628,7 @@ struct GossipUi { impl Drop for GossipUi { fn drop(&mut self) { + self.flush_dm_draft_states_save(); self.password.zeroize(); self.password2.zeroize(); self.password3.zeroize(); @@ -713,6 +748,7 @@ impl GossipUi { // Apply current theme let theme = Theme::from_settings(); theme::apply_theme(&theme, &cctx.egui_ctx); + let dm_draft_states = Self::load_dm_draft_states(); // Let gossip-lib know the max texture side so it can resize things that are // too large. @@ -800,6 +836,17 @@ impl GossipUi { previous_draft_data: DraftData::default(), dm_draft_data: DraftData::default(), dm_draft_data_target: None, + dm_draft_states, + dm_draft_states_dirty: false, + dm_draft_states_save_after: None, + dm_draft_states_save_rx: None, + dm_new_message: false, + dm_new_message_search: String::new(), + dm_new_message_searched: None, + dm_new_message_search_results: Vec::new(), + dm_new_message_search_selected: None, + dm_new_message_address: String::new(), + dm_new_message_error: None, editing_metadata: false, metadata: Metadata::new(), delegatee_tag_str: "".to_owned(), @@ -2131,8 +2178,13 @@ impl GossipUi { fn reset_draft(&mut self) { if let Page::Feed(FeedKind::DmChat(_)) = &self.page { + let current_target = self.dm_draft_data_target.clone(); + self.request_dm_draft_states_save(); self.dm_draft_data.clear(); - self.dm_draft_data_target = None; + if let Some(channel) = current_target.as_ref() { + self.load_dm_draft_state(channel); + } + self.dm_draft_data_target = current_target; } else { self.previous_draft_data = self.draft_data.clone(); self.draft_data.clear(); @@ -2141,6 +2193,134 @@ impl GossipUi { } } + fn save_dm_draft_state(&mut self) { + if let Some(channel) = self.dm_draft_data_target.clone() { + self.dm_draft_states.insert( + channel.unique_id(), + DmDraftState { + use_nip17: self.dm_draft_data.use_nip17, + send_on_enter: self.dm_draft_data.send_on_enter, + use_nip17_force: self.dm_draft_data.use_nip17_force, + }, + ); + } + } + + fn request_dm_draft_states_save(&mut self) { + self.save_dm_draft_state(); + self.dm_draft_states_dirty = true; + self.dm_draft_states_save_after = Some(Instant::now() + Duration::from_millis(250)); + } + + fn flush_dm_draft_states_save(&mut self) { + if let Some(rx) = self.dm_draft_states_save_rx.take() { + match rx.recv() { + Ok(result) => { + if let Err(e) = result { + tracing::error!("Error saving DM draft states: {}", e); + } + } + Err(_) => { + tracing::error!("DM draft state saver disconnected unexpectedly"); + } + } + } + + self.save_dm_draft_state(); + let json = serde_json::to_string(&self.dm_draft_states).unwrap_or_default(); + if let Err(e) = GLOBALS.db().write_setting_dm_draft_states(&json, None) { + tracing::error!("Error saving DM draft states: {}", e); + } + self.dm_draft_states_dirty = false; + self.dm_draft_states_save_after = None; + self.dm_draft_states_save_rx = None; + } + + fn load_dm_draft_state(&mut self, channel: &DmChannel) { + let state = self + .dm_draft_states + .get(&channel.unique_id()) + .cloned() + .unwrap_or_else(|| DmDraftState { + use_nip17: channel.can_use_nip17(), + send_on_enter: false, + use_nip17_force: false, + }); + + self.dm_draft_data.use_nip17 = state.use_nip17; + self.dm_draft_data.send_on_enter = state.send_on_enter; + self.dm_draft_data.use_nip17_force = state.use_nip17_force; + self.dm_draft_data.use_nip17_force_confirm = false; + } + + fn load_dm_draft_states() -> HashMap { + let json = GLOBALS.db().read_setting_dm_draft_states(); + if json.is_empty() { + return HashMap::new(); + } + + serde_json::from_str(&json).unwrap_or_default() + } + + fn poll_dm_draft_states_save(&mut self) { + if let Some(rx) = &self.dm_draft_states_save_rx { + match rx.try_recv() { + Ok(result) => { + if let Err(e) = result { + tracing::error!("Error saving DM draft states: {}", e); + } + self.dm_draft_states_save_rx = None; + self.dm_draft_states_dirty = false; + self.dm_draft_states_save_after = None; + } + Err(TryRecvError::Empty) => {} + Err(TryRecvError::Disconnected) => { + tracing::error!("DM draft state saver disconnected unexpectedly"); + self.dm_draft_states_save_rx = None; + self.dm_draft_states_dirty = true; + self.dm_draft_states_save_after = + Some(Instant::now() + Duration::from_millis(500)); + } + } + } + + if self.dm_draft_states_dirty + && self.dm_draft_states_save_rx.is_none() + && self + .dm_draft_states_save_after + .is_some_and(|when| Instant::now() >= when) + { + self.dm_draft_states_dirty = false; + self.dm_draft_states_save_after = None; + + let states = self.dm_draft_states.clone(); + let (tx, rx) = mpsc::channel(); + self.dm_draft_states_save_rx = Some(rx); + + GLOBALS.runtime.spawn_blocking(move || { + let result = (|| -> Result<(), String> { + let json = serde_json::to_string(&states).map_err(|e| e.to_string())?; + GLOBALS + .db() + .write_setting_dm_draft_states(&json, None) + .map_err(|e| e.to_string())?; + Ok(()) + })(); + let _ = tx.send(result); + }); + } + } + + fn clear_new_message_dialog(&mut self) { + self.dm_new_message = false; + self.dm_new_message_search.clear(); + self.dm_new_message_searched = None; + self.dm_new_message_search_results.clear(); + self.dm_new_message_search_selected = None; + self.dm_new_message_address.clear(); + self.dm_new_message_error = None; + } + fn show_post_area_fn(&self) -> bool { if self.page == Page::DmChatList { return false; @@ -2223,6 +2403,7 @@ impl eframe::App for GossipUi { } self.frame_count += 1; + self.poll_dm_draft_states_save(); // Enforce FPS limiting. // No amount of notifies or request_repaint()s can bypass this. diff --git a/gossip-bin/src/ui/theme/default.rs b/gossip-bin/src/ui/theme/default.rs index e14fb27ba..fc0a31ab2 100644 --- a/gossip-bin/src/ui/theme/default.rs +++ b/gossip-bin/src/ui/theme/default.rs @@ -834,6 +834,10 @@ impl ThemeDef for DefaultTheme { Shadow::default() } fn feed_frame_fill(dark_mode: bool, post: &NoteRenderData) -> Color32 { + if post.is_dm_feed && post.is_our_event { + return Color32::from_rgb(0x20, 0x33, 0x2c); + } + if post.is_main_event { if dark_mode { let mut hsva: ecolor::HsvaGamma = Self::highlighted_note_bgcolor(dark_mode).into(); diff --git a/gossip-bin/src/ui/theme/test_page.rs b/gossip-bin/src/ui/theme/test_page.rs index ce1ff31b3..be07a7bbd 100644 --- a/gossip-bin/src/ui/theme/test_page.rs +++ b/gossip-bin/src/ui/theme/test_page.rs @@ -99,6 +99,8 @@ pub(in crate::ui) fn update( thread_position: 0, hide_footer: false, hide_nameline: false, + is_dm_feed: false, + is_our_event: false, }; Frame::NONE .inner_margin(app.theme.feed_frame_inner_margin(&render_data)) @@ -124,6 +126,8 @@ pub(in crate::ui) fn update( thread_position: 0, hide_footer: false, hide_nameline: false, + is_dm_feed: false, + is_our_event: false, }; Frame::NONE .inner_margin(app.theme.feed_frame_inner_margin(&render_data)) diff --git a/gossip-lib/src/comms.rs b/gossip-lib/src/comms.rs index 2674ae5a0..14b270bcd 100644 --- a/gossip-lib/src/comms.rs +++ b/gossip-lib/src/comms.rs @@ -133,6 +133,8 @@ pub enum ToOverlordMessage { in_reply_to: Option, annotation: bool, dm_channel: Option, + use_nip17: bool, + force_nip17: bool, }, /// Calls [post_again](crate::Overlord::post_again) diff --git a/gossip-lib/src/overlord.rs b/gossip-lib/src/overlord.rs index 15990cb8a..b3c6a629a 100644 --- a/gossip-lib/src/overlord.rs +++ b/gossip-lib/src/overlord.rs @@ -717,9 +717,19 @@ impl Overlord { in_reply_to, annotation, dm_channel, + use_nip17, + force_nip17, } => { - self.post(content, tags, in_reply_to, annotation, dm_channel) - .await?; + self.post( + content, + tags, + in_reply_to, + annotation, + dm_channel, + use_nip17, + force_nip17, + ) + .await?; } ToOverlordMessage::PostAgain(event) => { self.post_again(event)?; @@ -1963,6 +1973,8 @@ impl Overlord { in_reply_to: Option, annotation: bool, dm_channel: Option, + use_nip17: bool, + force_nip17: bool, ) -> Result<(), Error> { let author = match GLOBALS.identity.public_key() { Some(pk) => pk, @@ -1975,9 +1987,16 @@ impl Overlord { // Prepare events for posting let mut prepared_events = match dm_channel { Some(channel) => { - if channel.can_use_nip17() { - crate::post::prepare_post_nip17(author, content, tags, channel, annotation) - .await? + if use_nip17 { + crate::post::prepare_post_nip17( + author, + content, + tags, + channel, + annotation, + force_nip17, + ) + .await? } else { crate::post::prepare_post_nip04(author, content, channel, annotation).await? } diff --git a/gossip-lib/src/post.rs b/gossip-lib/src/post.rs index 7b92ec38b..14da4520c 100644 --- a/gossip-lib/src/post.rs +++ b/gossip-lib/src/post.rs @@ -161,8 +161,9 @@ pub async fn prepare_post_nip17( mut tags: Vec, dm_channel: DmChannel, annotation: bool, + force_nip17: bool, ) -> Result)>, Error> { - if !dm_channel.can_use_nip17() { + if !dm_channel.can_use_nip17() && !force_nip17 { return Err(ErrorKind::UsersCantUseNip17.into()); } @@ -201,7 +202,10 @@ pub async fn prepare_post_nip17( // To all recipients for pk in dm_channel.keys() { let event = GLOBALS.identity.giftwrap(pre_event.clone(), *pk).await?; - let relays = relay::get_dm_relays(*pk)?; + let mut relays = relay::get_dm_relays(*pk)?; + if relays.is_empty() { + relays = Relay::choose_relay_urls(Relay::WRITE, |_| true)?; + } output.push((event, relays)); } diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index 656d1fe67..e129d1d65 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -708,6 +708,7 @@ impl Storage { def_setting!(reactions, b"reactions", bool, true); def_setting!(enable_zap_receipts, b"enable_zap_receipts", bool, true); def_setting!(show_media, b"show_media", bool, true); + def_setting!(dm_draft_states, b"dm_draft_states", String, String::new()); def_setting!( approve_content_warning, b"approve_content_warning",