diff --git a/src/comms.rs b/src/comms.rs index 370096dc..1ef46013 100644 --- a/src/comms.rs +++ b/src/comms.rs @@ -1,3 +1,4 @@ +use crate::dm_channel::DmChannel; use nostr_types::{ Event, Id, IdHex, Metadata, MilliSatoshi, PublicKey, RelayUrl, Tag, UncheckedUrl, }; @@ -28,7 +29,7 @@ pub enum ToOverlordMessage { MinionJobComplete(RelayUrl, u64), MinionJobUpdated(RelayUrl, u64, u64), PickRelays, - Post(String, Vec, Option), + Post(String, Vec, Option, Option), PruneCache, PruneDatabase, PullFollow, diff --git a/src/dm_channel.rs b/src/dm_channel.rs index 851d3eba..87e0fbb4 100644 --- a/src/dm_channel.rs +++ b/src/dm_channel.rs @@ -1,4 +1,5 @@ -use nostr_types::{PublicKey, Unixtime}; +use crate::globals::GLOBALS; +use nostr_types::{Event, EventKind, PublicKey, Unixtime}; use sha2::Digest; /// This represents a DM (direct message) channel which includes a set @@ -46,6 +47,57 @@ impl DmChannel { } hex::encode(hasher.finalize()) } + + pub fn from_event(event: &Event, my_pubkey: Option) -> Option { + let my_pubkey = match my_pubkey { + Some(pk) => pk, + None => match GLOBALS.signer.public_key() { + Some(pk) => pk, + None => return None, + }, + }; + + if event.kind == EventKind::EncryptedDirectMessage { + let mut people: Vec = event + .people() + .iter() + .filter_map(|(pk, _, _)| PublicKey::try_from(pk).ok()) + .collect(); + people.push(event.pubkey); + people.retain(|p| *p != my_pubkey); + if people.len() > 1 { + None + } else { + Some(Self::new(&people)) + } + } else if event.kind == EventKind::GiftWrap { + if let Ok(rumor) = GLOBALS.signer.unwrap_giftwrap(event) { + let rumor_event = rumor.into_event_with_bad_signature(); + let mut people: Vec = rumor_event + .people() + .iter() + .filter_map(|(pk, _, _)| PublicKey::try_from(pk).ok()) + .collect(); + people.push(rumor_event.pubkey); // include author too + people.retain(|p| *p != my_pubkey); + Some(Self::new(&people)) + } else { + None + } + } else if event.kind == EventKind::DmChat { + // unwrapped rumor + let mut people: Vec = event + .people() + .iter() + .filter_map(|(pk, _, _)| PublicKey::try_from(pk).ok()) + .collect(); + people.push(event.pubkey); // include author too + people.retain(|p| *p != my_pubkey); + Some(Self::new(&people)) + } else { + None + } + } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] diff --git a/src/error.rs b/src/error.rs index 4e5eb0f3..8e3c5f2d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,6 +8,7 @@ pub enum ErrorKind { Empty(String), EventNotFound, General(String), + GroupDmsNotYetSupported, HttpError(http::Error), JoinError(tokio::task::JoinError), Lmdb(heed::Error), @@ -65,6 +66,7 @@ impl std::fmt::Display for Error { Delegation(s) => write!(f, "NIP-26 Delegation Error: {s}"), Empty(s) => write!(f, "{s} is empty"), EventNotFound => write!(f, "Event not found"), + GroupDmsNotYetSupported => write!(f, "Group DMs are not yet supported"), General(s) => write!(f, "{s}"), HttpError(e) => write!(f, "HTTP error: {e}"), JoinError(e) => write!(f, "Task join error: {e}"), diff --git a/src/overlord/mod.rs b/src/overlord/mod.rs index 09c8a72d..1c348ab6 100644 --- a/src/overlord/mod.rs +++ b/src/overlord/mod.rs @@ -4,6 +4,7 @@ use crate::comms::{ RelayConnectionReason, RelayJob, ToMinionMessage, ToMinionPayload, ToMinionPayloadDetail, ToOverlordMessage, }; +use crate::dm_channel::DmChannel; use crate::error::{Error, ErrorKind}; use crate::globals::{ZapState, GLOBALS}; use crate::people::Person; @@ -709,8 +710,8 @@ impl Overlord { count )); } - ToOverlordMessage::Post(content, tags, reply_to) => { - self.post(content, tags, reply_to).await?; + ToOverlordMessage::Post(content, tags, reply_to, dm_channel) => { + self.post(content, tags, reply_to, dm_channel).await?; } ToOverlordMessage::PullFollow => { self.pull_following().await?; @@ -935,155 +936,181 @@ impl Overlord { content: String, mut tags: Vec, reply_to: Option, + dm_channel: Option, ) -> Result<(), Error> { - // We will fill this just before we create the event - let mut tagged_pubkeys: Vec; + let public_key = match GLOBALS.signer.public_key() { + Some(pk) => pk, + None => { + tracing::warn!("No public key! Not posting"); + return Ok(()); + } + }; - let event = { - let public_key = match GLOBALS.signer.public_key() { - Some(pk) => pk, - None => { - tracing::warn!("No public key! Not posting"); - return Ok(()); + let pre_event = match dm_channel { + Some(dmc) => { + if dmc.keys().len() > 1 { + return Err((ErrorKind::GroupDmsNotYetSupported, file!(), line!()).into()); } - }; - if GLOBALS.storage.read_setting_set_client_tag() { - tags.push(Tag::Other { - tag: "client".to_owned(), - data: vec!["gossip".to_owned()], - }); - } - - // Add Tags based on references in the content - // - // FIXME - this function takes a 'tags' variable. We may want to let - // the user determine which tags to keep and which to delete, so we - // should probably move this processing into the post editor instead. - // For now, I'm just trying to remove the old #[0] type substitutions - // and use the new NostrBech32 parsing. - for bech32 in NostrBech32::find_all_in_string(&content).iter() { - match bech32 { - NostrBech32::EventAddr(ea) => { - add_addr_to_tags( - &mut tags, - ea.kind, - ea.author.into(), - ea.d.clone(), - ea.relays.get(0).cloned(), - ) - .await; - } - NostrBech32::EventPointer(ep) => { - // NIP-10: "Those marked with "mention" denote a quoted or reposted event id." - add_event_to_tags(&mut tags, ep.id, "mention").await; - } - NostrBech32::Id(id) => { - // NIP-10: "Those marked with "mention" denote a quoted or reposted event id." - add_event_to_tags(&mut tags, *id, "mention").await; - } - NostrBech32::Profile(prof) => { - add_pubkey_to_tags(&mut tags, &prof.pubkey).await; - } - NostrBech32::Pubkey(pk) => { - add_pubkey_to_tags(&mut tags, pk).await; - } - NostrBech32::Relay(_) => { - // we don't need to add this to tags I don't think. - } - } - } - - // Standardize nostr links (prepend 'nostr:' where missing) - // (This was a bad idea to do this late in the process, it breaks links that contain - // nostr urls) - // content = NostrUrl::urlize(&content); - - // Find and tag all hashtags - for capture in GLOBALS.hashtag_regex.captures_iter(&content) { - tags.push(Tag::Hashtag { - hashtag: capture[1][1..].to_string(), - trailing: Vec::new(), - }); - } - - if let Some(parent_id) = reply_to { - // Get the event we are replying to - let parent = match GLOBALS.storage.read_event(parent_id)? { - Some(e) => e, - None => return Err("Cannot find event we are replying to.".into()), + let recipient = if dmc.keys().is_empty() { + public_key // must be to yourself + } else { + dmc.keys()[0] }; - // Add a 'p' tag for the author we are replying to (except if it is our own key) - if parent.pubkey != public_key { - add_pubkey_to_tags(&mut tags, &parent.pubkey).await; + // On a DM, we ignore tags and reply_to + + GLOBALS.signer.new_nip04(recipient, &content)? + } + _ => { + if GLOBALS.storage.read_setting_set_client_tag() { + tags.push(Tag::Other { + tag: "client".to_owned(), + data: vec!["gossip".to_owned()], + }); } - // Add all the 'p' tags from the note we are replying to (except our own) - // FIXME: Should we avoid taging people who are muted? - for tag in &parent.tags { - if let Tag::Pubkey { pubkey, .. } = tag { - if pubkey.as_str() != public_key.as_hex_string() { - add_pubkey_hex_to_tags(&mut tags, pubkey).await; + // Add Tags based on references in the content + // + // FIXME - this function takes a 'tags' variable. We may want to let + // the user determine which tags to keep and which to delete, so we + // should probably move this processing into the post editor instead. + // For now, I'm just trying to remove the old #[0] type substitutions + // and use the new NostrBech32 parsing. + for bech32 in NostrBech32::find_all_in_string(&content).iter() { + match bech32 { + NostrBech32::EventAddr(ea) => { + add_addr_to_tags( + &mut tags, + ea.kind, + ea.author.into(), + ea.d.clone(), + ea.relays.get(0).cloned(), + ) + .await; + } + NostrBech32::EventPointer(ep) => { + // NIP-10: "Those marked with "mention" denote a quoted or reposted event id." + add_event_to_tags(&mut tags, ep.id, "mention").await; + } + NostrBech32::Id(id) => { + // NIP-10: "Those marked with "mention" denote a quoted or reposted event id." + add_event_to_tags(&mut tags, *id, "mention").await; + } + NostrBech32::Profile(prof) => { + if dm_channel.is_none() { + add_pubkey_to_tags(&mut tags, &prof.pubkey).await; + } + } + NostrBech32::Pubkey(pk) => { + if dm_channel.is_none() { + add_pubkey_to_tags(&mut tags, pk).await; + } + } + NostrBech32::Relay(_) => { + // we don't need to add this to tags I don't think. } } } - if let Some((root, _maybeurl)) = parent.replies_to_root() { - // Add an 'e' tag for the root - add_event_to_tags(&mut tags, root, "root").await; + // Standardize nostr links (prepend 'nostr:' where missing) + // (This was a bad idea to do this late in the process, it breaks links that contain + // nostr urls) + // content = NostrUrl::urlize(&content); + + // Find and tag all hashtags + for capture in GLOBALS.hashtag_regex.captures_iter(&content) { + tags.push(Tag::Hashtag { + hashtag: capture[1][1..].to_string(), + trailing: Vec::new(), + }); + } + + if let Some(parent_id) = reply_to { + // Get the event we are replying to + let parent = match GLOBALS.storage.read_event(parent_id)? { + Some(e) => e, + None => return Err("Cannot find event we are replying to.".into()), + }; + + // Add a 'p' tag for the author we are replying to (except if it is our own key) + if parent.pubkey != public_key { + if dm_channel.is_none() { + add_pubkey_to_tags(&mut tags, &parent.pubkey).await; + } + } + + // Add all the 'p' tags from the note we are replying to (except our own) + // FIXME: Should we avoid taging people who are muted? + if dm_channel.is_none() { + for tag in &parent.tags { + if let Tag::Pubkey { pubkey, .. } = tag { + if pubkey.as_str() != public_key.as_hex_string() { + add_pubkey_hex_to_tags(&mut tags, pubkey).await; + } + } + } + } + + if let Some((root, _maybeurl)) = parent.replies_to_root() { + // Add an 'e' tag for the root + add_event_to_tags(&mut tags, root, "root").await; - // Add an 'e' tag for the note we are replying to - add_event_to_tags(&mut tags, parent_id, "reply").await; - } else { - let ancestors = parent.referred_events(); - if ancestors.is_empty() { - // parent is the root - add_event_to_tags(&mut tags, parent_id, "root").await; - } else { // Add an 'e' tag for the note we are replying to - // (and we don't know about the root, the parent is malformed). add_event_to_tags(&mut tags, parent_id, "reply").await; + } else { + let ancestors = parent.referred_events(); + if ancestors.is_empty() { + // parent is the root + add_event_to_tags(&mut tags, parent_id, "root").await; + } else { + // Add an 'e' tag for the note we are replying to + // (and we don't know about the root, the parent is malformed). + add_event_to_tags(&mut tags, parent_id, "reply").await; + } + } + + // Possibly propagate a subject tag + for tag in &parent.tags { + if let Tag::Subject { subject, .. } = tag { + let mut subject = subject.to_owned(); + if !subject.starts_with("Re: ") { + subject = format!("Re: {}", subject); + } + subject = subject.chars().take(80).collect(); + add_subject_to_tags_if_missing(&mut tags, subject); + } } } - // Possibly propagate a subject tag - for tag in &parent.tags { - if let Tag::Subject { subject, .. } = tag { - let mut subject = subject.to_owned(); - if !subject.starts_with("Re: ") { - subject = format!("Re: {}", subject); - } - subject = subject.chars().take(80).collect(); - add_subject_to_tags_if_missing(&mut tags, subject); - } + PreEvent { + pubkey: public_key, + created_at: Unixtime::now().unwrap(), + kind: EventKind::TextNote, + tags, + content, + ots: None, } } + }; - // Copy the tagged pubkeys for determine which relays to send to - tagged_pubkeys = tags - .iter() - .filter_map(|t| { - if let Tag::Pubkey { pubkey, .. } = t { - match PublicKey::try_from_hex_string(pubkey, true) { - Ok(pk) => Some(pk), - _ => None, - } - } else { - None + // Copy the tagged pubkeys for determine which relays to send to + let mut tagged_pubkeys: Vec = pre_event + .tags + .iter() + .filter_map(|t| { + if let Tag::Pubkey { pubkey, .. } = t { + match PublicKey::try_from_hex_string(pubkey, true) { + Ok(pk) => Some(pk), + _ => None, } - }) - .collect(); - - let pre_event = PreEvent { - pubkey: public_key, - created_at: Unixtime::now().unwrap(), - kind: EventKind::TextNote, - tags, - content, - ots: None, - }; + } else { + None + } + }) + .collect(); + let event = { let powint = GLOBALS.storage.read_setting_pow(); let pow = if powint > 0 { Some(powint) } else { None }; let (work_sender, work_receiver) = mpsc::channel(); diff --git a/src/signer.rs b/src/signer.rs index 7cd91a96..79b03633 100644 --- a/src/signer.rs +++ b/src/signer.rs @@ -197,6 +197,17 @@ impl Signer { } } + pub fn new_nip04( + &self, + recipient_public_key: PublicKey, + message: &str, + ) -> Result { + match &*self.private.read() { + Some(pk) => Ok(PreEvent::new_nip04(pk, recipient_public_key, message)?), + _ => Err((ErrorKind::NoPrivateKey, file!(), line!()).into()), + } + } + pub fn export_private_key_bech32(&self, pass: &str) -> Result { let maybe_encrypted = self.encrypted.read().to_owned(); match maybe_encrypted { diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 12556f59..22e9678e 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -2324,27 +2324,9 @@ impl Storage { let unread = 1 - self.is_event_viewed(event.id)? as usize; if event.kind == EventKind::EncryptedDirectMessage { let time = event.created_at; - let dmchannel = { - if event.pubkey != my_pubkey { - // DM sent to me - DmChannel::new(&[event.pubkey]) - } else { - // DM sent from me - let mut maybe_channel: Option = None; - for tag in event.tags.iter() { - if let Tag::Pubkey { pubkey, .. } = tag { - if let Ok(pk) = PublicKey::try_from(pubkey) { - if pk != my_pubkey { - maybe_channel = Some(DmChannel::new(&[pk])); - } - } - } - } - match maybe_channel { - Some(dmchannel) => dmchannel, - None => continue, - } - } + let dmchannel = match DmChannel::from_event(event, Some(my_pubkey)) { + Some(dmc) => dmc, + None => continue, }; map.entry(dmchannel.clone()) .and_modify(|d| { @@ -2362,15 +2344,9 @@ impl Storage { if let Ok(rumor) = GLOBALS.signer.unwrap_giftwrap(event) { let rumor_event = rumor.into_event_with_bad_signature(); let time = rumor_event.created_at; - let dmchannel = { - let mut people: Vec = rumor_event - .people() - .iter() - .filter_map(|(pk, _, _)| PublicKey::try_from(pk).ok()) - .filter(|pk| *pk != my_pubkey) - .collect(); - people.push(rumor_event.pubkey); // include author too - DmChannel::new(&people) + let dmchannel = match DmChannel::from_event(&rumor_event, Some(my_pubkey)) { + Some(dmc) => dmc, + None => continue, }; map.entry(dmchannel.clone()) .and_modify(|d| { @@ -2400,61 +2376,25 @@ impl Storage { None => return Ok(Vec::new()), }; - let mut pass1 = self.find_events( + let mut output: Vec = self.find_events( &[EventKind::EncryptedDirectMessage, EventKind::GiftWrap], &[], Some(Unixtime(0)), |event| { - if event.kind == EventKind::EncryptedDirectMessage { - if channel.keys().len() > 1 { return false; } - if channel.keys().len() == 0 { return true; } // self-channel - let other = &channel.keys()[0]; - let people = event.people(); - match people.len() { - 1 => (event.pubkey == my_pubkey && event.is_tagged(other)) - || (event.pubkey == *other && event.is_tagged(&my_pubkey)), - 2 => (event.pubkey == my_pubkey || event.pubkey == *other) - && (event.is_tagged(&my_pubkey) && event.is_tagged(other)), - _ => false, + if let Some(event_dm_channel) = DmChannel::from_event(event, Some(my_pubkey)) { + if event_dm_channel == *channel { + return true; } - } else if event.kind == EventKind::GiftWrap { - // Decrypt in next pass, else we would have to decrypt twice - true - } else { - false } + false }, false, )?; - let mut pass2: Vec = Vec::new(); - - for event in pass1.drain(..) { - if event.kind == EventKind::EncryptedDirectMessage { - pass2.push(event); // already validated - } else if event.kind == EventKind::GiftWrap { - if let Ok(rumor) = GLOBALS.signer.unwrap_giftwrap(&event) { - let mut rumor_event = rumor.into_event_with_bad_signature(); - rumor_event.id = event.id; // lie, so it indexes it under the giftwrap - let mut tagged: Vec = rumor_event - .people() - .drain(..) - .filter_map(|(pkh, _, _)| PublicKey::try_from(pkh).ok()) - .collect(); - tagged.push(rumor_event.pubkey); // include author - tagged.retain(|pk| *pk != my_pubkey); // never include user - let this_channel = DmChannel::new(&tagged); - if this_channel == *channel { - pass2.push(event); - } - } - } - } - // sort - pass2.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + output.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - Ok(pass2.iter().map(|e| e.id).collect()) + Ok(output.iter().map(|e| e.id).collect()) } pub fn rebuild_event_indices(&self) -> Result<(), Error> { diff --git a/src/ui/dm_chat_list.rs b/src/ui/dm_chat_list.rs index 47ea7871..327b98ec 100644 --- a/src/ui/dm_chat_list.rs +++ b/src/ui/dm_chat_list.rs @@ -16,7 +16,6 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr ui.heading("Direct Private Message Channels"); ui.add_space(12.0); - ui.separator(); ScrollArea::vertical() .id_source("dm_chat_list") diff --git a/src/ui/feed/note/mod.rs b/src/ui/feed/note/mod.rs index 9096eb02..30ec2f58 100644 --- a/src/ui/feed/note/mod.rs +++ b/src/ui/feed/note/mod.rs @@ -8,6 +8,7 @@ use super::notedata::{NoteData, RepostType}; use super::FeedNoteParams; use crate::comms::ToOverlordMessage; +use crate::dm_channel::DmChannel; use crate::feed::FeedKind; use crate::globals::{ZapState, GLOBALS}; use crate::ui::widgets::CopyButton; @@ -615,61 +616,65 @@ fn render_note_inner( ui.add_space(24.0); - if render_data.can_post - && note.event.kind != EventKind::EncryptedDirectMessage - { - // Button to Repost - if ui - .add( - Label::new(RichText::new("↻").size(18.0)) - .sense(Sense::click()), - ) - .on_hover_text("Repost") - .clicked() + if render_data.can_post { + if note.event.kind != EventKind::EncryptedDirectMessage + && note.event.kind != EventKind::DmChat { - app.draft_repost = Some(note.event.id); - app.replying_to = None; - app.show_post_area = true; - } - - ui.add_space(24.0); - - // Button to quote note - if ui - .add( - Label::new(RichText::new("“…”").size(18.0)) - .sense(Sense::click()), - ) - .on_hover_text("Quote") - .clicked() - { - if !app.draft.ends_with(' ') && !app.draft.is_empty() { - app.draft.push(' '); + // Button to Repost + if ui + .add( + Label::new(RichText::new("↻").size(18.0)) + .sense(Sense::click()), + ) + .on_hover_text("Repost") + .clicked() + { + app.draft_repost = Some(note.event.id); + app.replying_to = None; + app.draft_dm_channel = None; + app.show_post_area = true; } - let event_pointer = EventPointer { - id: note.event.id, - relays: match GLOBALS - .storage - .get_event_seen_on_relay(note.event.id) - { - Err(_) => vec![], - Ok(vec) => vec - .iter() - .map(|(url, _)| url.to_unchecked_url()) - .collect(), - }, - author: None, - kind: None, - }; - let nostr_url: NostrUrl = event_pointer.into(); - app.draft.push_str(&format!("{}", nostr_url)); - app.draft_repost = None; - app.replying_to = None; - app.show_post_area = true; - app.draft_needs_focus = true; - } - ui.add_space(24.0); + ui.add_space(24.0); + + // Button to quote note + if ui + .add( + Label::new(RichText::new("“…”").size(18.0)) + .sense(Sense::click()), + ) + .on_hover_text("Quote") + .clicked() + { + if !app.draft.ends_with(' ') && !app.draft.is_empty() { + app.draft.push(' '); + } + let event_pointer = EventPointer { + id: note.event.id, + relays: match GLOBALS + .storage + .get_event_seen_on_relay(note.event.id) + { + Err(_) => vec![], + Ok(vec) => vec + .iter() + .map(|(url, _)| url.to_unchecked_url()) + .collect(), + }, + author: None, + kind: None, + }; + let nostr_url: NostrUrl = event_pointer.into(); + app.draft.push_str(&format!("{}", nostr_url)); + app.draft_repost = None; + app.replying_to = None; + app.draft_dm_channel = None; + app.show_post_area = true; + app.draft_needs_focus = true; + } + + ui.add_space(24.0); + } // Button to reply if ui @@ -683,6 +688,8 @@ fn render_note_inner( app.replying_to = Some(note.event.id); app.draft_repost = None; app.show_post_area = true; + app.draft_dm_channel = + DmChannel::from_event(¬e.event, None); app.draft_needs_focus = true; } diff --git a/src/ui/feed/post.rs b/src/ui/feed/post.rs index d0ed47fc..cbe1202d 100644 --- a/src/ui/feed/post.rs +++ b/src/ui/feed/post.rs @@ -163,6 +163,12 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram }); } + if let Some(dm_channel) = &app.draft_dm_channel { + ui.horizontal(|ui| { + ui.label(format!("DIRECT MESSAGE TO: {}", dm_channel.name())); + }); + } + let draft_response = ui.add( text_edit_multiline!(app, app.draft) .id_source("compose_area") @@ -296,6 +302,7 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram app.draft.clone(), tags, Some(replying_to_id), + app.draft_dm_channel.clone(), )); } None => { @@ -308,6 +315,7 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram app.draft.clone(), tags, None, + app.draft_dm_channel.clone(), )); } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 6207522e..0f65460d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -26,6 +26,7 @@ mod you; use crate::about::About; use crate::comms::ToOverlordMessage; +use crate::dm_channel::DmChannel; use crate::error::Error; use crate::feed::FeedKind; use crate::globals::{ZapState, GLOBALS}; @@ -243,6 +244,7 @@ struct GossipUi { subject: String, include_content_warning: bool, content_warning: String, + draft_dm_channel: Option, replying_to: Option, // User entry: metadata @@ -460,6 +462,7 @@ impl GossipUi { subject: "".to_owned(), include_content_warning: false, content_warning: "".to_owned(), + draft_dm_channel: None, replying_to: None, editing_metadata: false, metadata: Metadata::new(), @@ -551,6 +554,7 @@ impl GossipUi { self.include_subject = false; self.subject = "".to_owned(); self.replying_to = None; + self.draft_dm_channel = None; self.include_content_warning = false; self.content_warning = "".to_owned(); } @@ -802,6 +806,11 @@ impl eframe::App for GossipUi { let response = ui.add_sized([crate::AVATAR_SIZE_F32, crate::AVATAR_SIZE_F32], egui::Button::new(text.color(self.settings.theme.navigation_text_color())).stroke(egui::Stroke::NONE).rounding(egui::Rounding::same(crate::AVATAR_SIZE_F32)).fill(self.settings.theme.navigation_bg_fill())); if response.clicked() { self.show_post_area = true; + if let Page::Feed(FeedKind::DmChat(channel)) = &self.page { + self.draft_dm_channel = Some(channel.clone()); + } else { + self.draft_dm_channel = None; + } if GLOBALS.signer.is_ready() { self.draft_needs_focus = true; } else { @@ -947,6 +956,15 @@ impl GossipUi { app.set_page(Page::Feed(FeedKind::Person(person.pubkey))); } } + if GLOBALS.signer.is_ready() { + if ui.button("Send DM").clicked() { + app.replying_to = None; + app.draft_repost = None; + app.show_post_area = true; + app.draft_dm_channel = Some(DmChannel::new(&[person.pubkey])); + app.draft_needs_focus = true; + } + } }); if person.followed { diff --git a/src/ui/theme/mod.rs b/src/ui/theme/mod.rs index e1f9a44b..23deae10 100644 --- a/src/ui/theme/mod.rs +++ b/src/ui/theme/mod.rs @@ -73,12 +73,14 @@ macro_rules! theme_dispatch { } } + #[allow(dead_code)] pub fn highlight_color(&self) -> Color32 { match self.variant { $( $variant => $class::highlight_color(self.dark_mode), )+ } } + #[allow(dead_code)] pub fn accent_complementary_color(&self) -> Color32 { match self.variant { $( $variant => $class::accent_complementary_color(self.dark_mode), )+