commit c0662798a2667cd887d0c022221dcd6e23837430
parent e7ada80876d4e69b5659e1865d6018e690236741
Author: kernelkind <kernelkind@gmail.com>
Date:   Sun,  2 Feb 2025 17:51:40 -0500
add PostView mentions UI
Signed-off-by: kernelkind <kernelkind@gmail.com>
Diffstat:
3 files changed, 129 insertions(+), 16 deletions(-)
diff --git a/crates/notedeck_columns/src/draft.rs b/crates/notedeck_columns/src/draft.rs
@@ -1,16 +1,23 @@
 use poll_promise::Promise;
 
-use crate::{media_upload::Nip94Event, ui::note::PostType, Error};
+use crate::{media_upload::Nip94Event, post::PostBuffer, ui::note::PostType, Error};
 use std::collections::HashMap;
 
 #[derive(Default)]
 pub struct Draft {
-    pub buffer: String,
+    pub buffer: PostBuffer,
+    pub cur_mention_hint: Option<MentionHint>,
     pub uploaded_media: Vec<Nip94Event>, // media uploads to include
     pub uploading_media: Vec<Promise<Result<Nip94Event, Error>>>, // promises that aren't ready yet
     pub upload_errors: Vec<String>,      // media upload errors to show the user
 }
 
+pub struct MentionHint {
+    pub index: usize,
+    pub pos: egui::Pos2,
+    pub text: String,
+}
+
 #[derive(Default)]
 pub struct Drafts {
     replies: HashMap<[u8; 32], Draft>,
@@ -46,7 +53,7 @@ impl Draft {
     }
 
     pub fn clear(&mut self) {
-        self.buffer = "".to_string();
+        self.buffer = PostBuffer::default();
         self.upload_errors = Vec::new();
         self.uploaded_media = Vec::new();
         self.uploading_media = Vec::new();
diff --git a/crates/notedeck_columns/src/post.rs b/crates/notedeck_columns/src/post.rs
@@ -13,6 +13,7 @@ pub struct NewPost {
     pub content: String,
     pub account: FullKeypair,
     pub media: Vec<Nip94Event>,
+    pub mentions: Vec<Pubkey>,
 }
 
 fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
@@ -23,11 +24,17 @@ fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
 }
 
 impl NewPost {
-    pub fn new(content: String, account: FullKeypair, media: Vec<Nip94Event>) -> Self {
+    pub fn new(
+        content: String,
+        account: enostr::FullKeypair,
+        media: Vec<Nip94Event>,
+        mentions: Vec<Pubkey>,
+    ) -> Self {
         NewPost {
             content,
             account,
             media,
+            mentions,
         }
     }
 
diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs
@@ -1,12 +1,16 @@
-use crate::draft::{Draft, Drafts};
+use crate::draft::{Draft, Drafts, MentionHint};
 use crate::images::fetch_img;
 use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
-use crate::post::NewPost;
+use crate::post::{MentionType, NewPost};
+use crate::profile::get_display_name;
+use crate::ui::search_results::SearchResultsView;
 use crate::ui::{self, Preview, PreviewConfig};
 use crate::Result;
+use egui::text::CCursorRange;
+use egui::text_edit::TextEditOutput;
 use egui::widgets::text_edit::TextEdit;
 use egui::{vec2, Frame, Layout, Margin, Pos2, ScrollArea, Sense};
-use enostr::{FilledKeypair, FullKeypair, NoteId, RelayPool};
+use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
 use nostrdb::{Ndb, Transaction};
 
 use notedeck::{ImageCache, NoteCache};
@@ -126,18 +130,85 @@ impl<'a> PostView<'a> {
             );
         }
 
-        let response = ui.add_sized(
-            ui.available_size(),
-            TextEdit::multiline(&mut self.draft.buffer)
-                .hint_text(egui::RichText::new("Write a banger note here...").weak())
-                .frame(false),
-        );
+        let textedit = TextEdit::multiline(&mut self.draft.buffer)
+            .hint_text(egui::RichText::new("Write a banger note here...").weak())
+            .frame(false)
+            .desired_width(ui.available_width());
 
-        let focused = response.has_focus();
+        let out = textedit.show(ui);
+
+        if let Some(cursor_index) = get_cursor_index(&out.state.cursor.char_range()) {
+            self.show_mention_hints(txn, ui, cursor_index, &out);
+        }
+
+        let focused = out.response.has_focus();
 
         ui.ctx().data_mut(|d| d.insert_temp(self.id(), focused));
 
-        response
+        out.response
+    }
+
+    fn show_mention_hints(
+        &mut self,
+        txn: &nostrdb::Transaction,
+        ui: &mut egui::Ui,
+        cursor_index: usize,
+        textedit_output: &TextEditOutput,
+    ) {
+        if let Some(mention) = &self.draft.buffer.get_mention(cursor_index) {
+            if mention.info.mention_type == MentionType::Pending {
+                let mention_str = self.draft.buffer.get_mention_string(mention);
+
+                if !mention_str.is_empty() {
+                    if let Some(mention_hint) = &mut self.draft.cur_mention_hint {
+                        if mention_hint.index != mention.index {
+                            mention_hint.index = mention.index;
+                            mention_hint.pos = calculate_mention_hints_pos(
+                                textedit_output,
+                                mention.info.start_index,
+                            );
+                        }
+                        mention_hint.text = mention_str.to_owned();
+                    } else {
+                        self.draft.cur_mention_hint = Some(MentionHint {
+                            index: mention.index,
+                            text: mention_str.to_owned(),
+                            pos: calculate_mention_hints_pos(
+                                textedit_output,
+                                mention.info.start_index,
+                            ),
+                        });
+                    }
+                }
+
+                if let Some(hint) = &self.draft.cur_mention_hint {
+                    let hint_rect = {
+                        let mut hint_rect = self.inner_rect;
+                        hint_rect.set_top(hint.pos.y);
+                        hint_rect
+                    };
+
+                    if let Ok(res) = self.ndb.search_profile(txn, mention_str, 10) {
+                        let hint_selection =
+                            SearchResultsView::new(self.img_cache, self.ndb, txn, &res)
+                                .show_in_rect(hint_rect, ui);
+
+                        if let Some(hint_index) = hint_selection {
+                            if let Some(pk) = res.get(hint_index) {
+                                let record = self.ndb.get_profile_by_pubkey(txn, pk);
+
+                                self.draft.buffer.select_mention_and_replace_name(
+                                    mention.index,
+                                    get_display_name(record.ok().as_ref()).name(),
+                                    Pubkey::new(**pk),
+                                );
+                                self.draft.cur_mention_hint = None;
+                            }
+                        }
+                    }
+                }
+            }
+        }
     }
 
     fn focused(&self, ui: &egui::Ui) -> bool {
@@ -237,10 +308,12 @@ impl<'a> PostView<'a> {
                                     )
                                     .clicked()
                                 {
+                                    let output = self.draft.buffer.output();
                                     let new_post = NewPost::new(
-                                        self.draft.buffer.clone(),
+                                        output.text,
                                         self.poster.to_full(),
                                         self.draft.uploaded_media.clone(),
+                                        output.mentions,
                                     );
                                     Some(PostAction::new(self.post_type.clone(), new_post))
                                 } else {
@@ -485,6 +558,32 @@ fn show_remove_upload_button(ui: &mut egui::Ui, desired_rect: egui::Rect) -> egu
     resp
 }
 
+fn get_cursor_index(cursor: &Option<CCursorRange>) -> Option<usize> {
+    let range = cursor.as_ref()?;
+
+    if range.primary.index == range.secondary.index {
+        Some(range.primary.index)
+    } else {
+        None
+    }
+}
+
+fn calculate_mention_hints_pos(out: &TextEditOutput, char_pos: usize) -> egui::Pos2 {
+    let mut cur_pos = 0;
+
+    for row in &out.galley.rows {
+        if cur_pos + row.glyphs.len() <= char_pos {
+            cur_pos += row.glyphs.len();
+        } else if let Some(glyph) = row.glyphs.get(char_pos - cur_pos) {
+            let mut pos = glyph.pos + out.galley_pos.to_vec2();
+            pos.y += row.rect.height();
+            return pos;
+        }
+    }
+
+    out.text_clip_rect.left_bottom()
+}
+
 mod preview {
 
     use crate::media_upload::Nip94Event;