diff --git a/kit/src/layout/chatbar/mod.rs b/kit/src/layout/chatbar/mod.rs index af7e64072e1..23239be5e7d 100644 --- a/kit/src/layout/chatbar/mod.rs +++ b/kit/src/layout/chatbar/mod.rs @@ -13,7 +13,6 @@ use crate::{ }; use common::{icons, language::get_local_text, warp_runner::thumbnail_to_base64}; - pub type To = &'static str; pub enum SuggestionType { @@ -77,6 +76,7 @@ pub struct Props<'a> { suggestions: &'a SuggestionType, oncursor_update: Option>, on_suggestion_click: Option>, + onup_down_arrow: Option>, } #[derive(Props)] @@ -175,6 +175,12 @@ pub fn Chatbar<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { let selected_suggestion: &UseRef> = use_ref(cx, || None); let arrow_selected = use_ref(cx, || false); let is_suggestion_modal_closed: &UseRef = use_ref(cx, || false); + let has_value = cx + .props + .value + .as_ref() + .map(|s| !s.is_empty()) + .unwrap_or_default(); let eval = use_eval(cx); cx.render(rsx!( @@ -237,7 +243,7 @@ pub fn Chatbar<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { *cursor_position.write_silent() = Some(p) }, is_disabled: cx.props.is_disabled, - prevent_up_down_arrows: !cx.props.suggestions.is_empty(), + prevent_up_down_arrows: !cx.props.suggestions.is_empty() || !has_value, onup_down_arrow: move |code| { let amount = match cx.props.suggestions { @@ -247,6 +253,11 @@ pub fn Chatbar<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { }; if amount == 0 { *selected_suggestion.write_silent() = None; + if let SuggestionType::None = cx.props.suggestions { + if let Some(e) = &cx.props.onup_down_arrow { + e.call(code); + } + } return; } let current = &mut *selected_suggestion.write(); diff --git a/ui/src/layouts/chats/data/chat_data/active_chat/messages.rs b/ui/src/layouts/chats/data/chat_data/active_chat/messages.rs index 5d678ceb73f..efe5b05b7bc 100644 --- a/ui/src/layouts/chats/data/chat_data/active_chat/messages.rs +++ b/ui/src/layouts/chats/data/chat_data/active_chat/messages.rs @@ -2,6 +2,7 @@ use chrono::{DateTime, Utc}; use common::warp_runner::ui_adapter; use std::collections::{HashMap, HashSet, VecDeque}; use uuid::Uuid; +use warp::crypto::DID; use crate::layouts::chats::data::DEFAULT_MESSAGES_TO_TAKE; @@ -15,13 +16,19 @@ pub struct Messages { pub loaded: HashSet, // used for displayed_messages pub times: HashMap>, + pub last_user_msg: Option, } impl Messages { - pub fn new(mut m: VecDeque) -> Self { + pub fn new(own: DID, mut m: VecDeque) -> Self { let mut message_times = HashMap::new(); let mut messages = VecDeque::new(); let displayed = VecDeque::new(); + let last_user_msg = m + .iter() + .rev() + .find(|msg| msg.inner.sender().eq(&own)) + .map(|msg| msg.inner.id()); for msg in m.drain(..) { message_times.insert(msg.inner.id(), msg.inner.date()); messages.push_back(msg); @@ -32,6 +39,7 @@ impl Messages { displayed, loaded: HashSet::new(), times: message_times, + last_user_msg, } } @@ -46,7 +54,7 @@ impl Messages { self.displayed.clear(); } - pub fn insert_messages(&mut self, m: Vec) { + pub fn insert_messages(&mut self, own: DID, m: Vec) { if m.is_empty() { return; } @@ -69,19 +77,19 @@ impl Messages { if self.all.is_empty() { log::trace!("appending messages"); - return self.append_messages(m); + return self.append_messages(own, m); } // latest last if m.last().unwrap().inner.date() <= self.all.front().unwrap().inner.date() { log::trace!("appending messages"); - return self.prepend_messages(m); + return self.prepend_messages(own, m); } // earliest first if m.first().unwrap().inner.date() >= self.all.back().unwrap().inner.date() { log::trace!("appending messages"); - return self.append_messages(m); + return self.append_messages(own, m); } log::warn!("insert_messages: invalid insert"); @@ -189,7 +197,12 @@ impl Messages { } impl Messages { - fn append_messages(&mut self, mut m: Vec) { + fn append_messages(&mut self, own: DID, mut m: Vec) { + self.last_user_msg = m + .iter() + .rev() + .find(|msg| msg.inner.sender().eq(&own)) + .map(|msg| msg.inner.id()); m.retain(|x| !self.times.contains_key(&x.inner.id())); for msg in m.iter() { self.times.insert(msg.inner.id(), msg.inner.date()); @@ -205,7 +218,15 @@ impl Messages { } } - fn prepend_messages(&mut self, mut m: Vec) { + fn prepend_messages(&mut self, own: DID, mut m: Vec) { + // Calculate last user message + if self.last_user_msg.is_none() { + self.last_user_msg = m + .iter() + .rev() + .find(|msg| msg.inner.sender().eq(&own)) + .map(|msg| msg.inner.id()); + } m.retain(|x| !self.times.contains_key(&x.inner.id())); for msg in m.iter() { self.times.insert(msg.inner.id(), msg.inner.date()); diff --git a/ui/src/layouts/chats/data/chat_data/active_chat/mod.rs b/ui/src/layouts/chats/data/chat_data/active_chat/mod.rs index ca30b781c69..0349f15733c 100644 --- a/ui/src/layouts/chats/data/chat_data/active_chat/mod.rs +++ b/ui/src/layouts/chats/data/chat_data/active_chat/mod.rs @@ -36,7 +36,7 @@ impl ActiveChat { ) -> Self { Self { metadata: Metadata::new(s, chat), - messages: Messages::new(messages), + messages: Messages::new(s.did_key(), messages), is_initialized: false, key: Uuid::new_v4(), } diff --git a/ui/src/layouts/chats/data/chat_data/mod.rs b/ui/src/layouts/chats/data/chat_data/mod.rs index a912b9a8f2a..8191d299c3f 100644 --- a/ui/src/layouts/chats/data/chat_data/mod.rs +++ b/ui/src/layouts/chats/data/chat_data/mod.rs @@ -23,6 +23,11 @@ pub struct MessagesToSend { pub messages_to_send: Vec<(Option, Vec)>, } +#[derive(Clone, Default)] +pub struct MessagesToEdit { + pub edit: Option, +} + impl PartialEq for ChatData { fn eq(&self, _other: &Self) -> bool { false @@ -162,7 +167,9 @@ impl ChatData { return; } - self.active_chat.messages.insert_messages(messages); + self.active_chat + .messages + .insert_messages(self.active_chat.my_id().did_key(), messages); } pub fn is_loaded(&self, conv_id: Uuid) -> bool { @@ -191,7 +198,9 @@ impl ChatData { } if should_append_msg { - self.active_chat.messages.insert_messages(vec![msg]); + self.active_chat + .messages + .insert_messages(self.active_chat.my_id().did_key(), vec![msg]); true } else { if let Some(behavior) = behavior { diff --git a/ui/src/layouts/chats/presentation/chat/mod.rs b/ui/src/layouts/chats/presentation/chat/mod.rs index d93af9671a6..2e86d8f3476 100644 --- a/ui/src/layouts/chats/presentation/chat/mod.rs +++ b/ui/src/layouts/chats/presentation/chat/mod.rs @@ -16,7 +16,7 @@ use kit::{ use crate::{ components::media::calling::CallControl, layouts::chats::{ - data::{self, ChatData, MessagesToSend, ScrollBtn}, + data::{self, ChatData, MessagesToEdit, MessagesToSend, ScrollBtn}, presentation::{ chat::{edit_group::EditGroup, group_settings::GroupSettings, group_users::GroupUsers}, chatbar::get_chatbar, @@ -40,6 +40,7 @@ pub fn Compose(cx: Scope) -> Element { use_shared_state_provider(cx, ChatData::default); use_shared_state_provider(cx, ScrollBtn::new); use_shared_state_provider(cx, MessagesToSend::default); + use_shared_state_provider(cx, MessagesToEdit::default); let state = use_shared_state::(cx)?; let chat_data = use_shared_state::(cx)?; diff --git a/ui/src/layouts/chats/presentation/chatbar/mod.rs b/ui/src/layouts/chats/presentation/chatbar/mod.rs index 5bf97282b5f..d68fc1c6cbf 100644 --- a/ui/src/layouts/chats/presentation/chatbar/mod.rs +++ b/ui/src/layouts/chats/presentation/chatbar/mod.rs @@ -42,7 +42,10 @@ use crate::{ components::{files::attachments::Attachments, shortcuts}, layouts::{ chats::{ - data::{ChatData, ChatProps, MessagesToSend, MsgChInput, ScrollBtn, TypingIndicator}, + data::{ + ChatData, ChatProps, MessagesToEdit, MessagesToSend, MsgChInput, ScrollBtn, + TypingIndicator, + }, scripts::SHOW_CONTEXT, }, storage::send_files_layout::{modal::SendFilesLayoutModal, SendFilesStartLocation}, @@ -62,6 +65,7 @@ pub fn get_chatbar<'a>(cx: &'a Scoped<'a, ChatProps>) -> Element<'a> { let chat_data = use_shared_state::(cx)?; let scroll_btn = use_shared_state::(cx)?; let to_send = use_shared_state::(cx)?; + let edit_msg = use_shared_state::(cx)?; state.write_silent().scope_ids.chatbar = Some(cx.scope_id().0); let active_chat_id = chat_data.read().active_chat.id(); @@ -437,6 +441,13 @@ pub fn get_chatbar<'a>(cx: &'a Scoped<'a, ChatProps>) -> Element<'a> { suggestions.set(SuggestionType::None); } }, + onup_down_arrow: move |code|{ + if code == Code::ArrowUp && edit_msg.read().edit.is_none() { + if let Some(msg) = chat_data.read().active_chat.messages.last_user_msg { + edit_msg.write().edit = Some(msg); + } + } + }, controls: cx.render( rsx!( Button { diff --git a/ui/src/layouts/chats/presentation/messages/mod.rs b/ui/src/layouts/chats/presentation/messages/mod.rs index aa93c4caadc..683bda4d26f 100644 --- a/ui/src/layouts/chats/presentation/messages/mod.rs +++ b/ui/src/layouts/chats/presentation/messages/mod.rs @@ -52,7 +52,7 @@ use crate::{ components::emoji_group::EmojiGroup, layouts::{ chats::{ - data::{self, ChatData, MessagesToSend, ScrollBtn}, + data::{self, ChatData, MessagesToEdit, MessagesToSend, ScrollBtn}, scripts, }, storage::files_layout::file_preview::open_file_preview_modal, @@ -343,7 +343,7 @@ struct MessagesProps<'a> { } fn wrap_messages_in_context_menu<'a>(cx: Scope<'a, MessagesProps<'a>>) -> Element<'a> { let state = use_shared_state::(cx)?; - let edit_msg: &UseState> = use_state(cx, || None); + let edit_msg = use_shared_state::(cx)?; // see comment in ContextMenu about this variable. let reacting_to: &UseState> = use_state(cx, || None); @@ -362,7 +362,7 @@ fn wrap_messages_in_context_menu<'a>(cx: Scope<'a, MessagesProps<'a>>) -> Elemen // WARNING: these keys are required to prevent a bug with the context menu, which manifests when deleting messages. let is_editing = edit_msg - .get() + .read().edit .map(|id| !cx.props.is_remote && (id == message.inner.id())) .unwrap_or(false); let message_key = format!("{}-{:?}", &message.key, is_editing); @@ -376,7 +376,7 @@ fn wrap_messages_in_context_menu<'a>(cx: Scope<'a, MessagesProps<'a>>) -> Elemen message: grouped_message, is_remote: cx.props.is_remote, message_key: message_key, - edit_msg: edit_msg.clone(), + edit_msg: edit_msg, pending: cx.props.pending }); } @@ -390,7 +390,7 @@ fn wrap_messages_in_context_menu<'a>(cx: Scope<'a, MessagesProps<'a>>) -> Elemen message: grouped_message, is_remote: cx.props.is_remote, message_key: message_key, - edit_msg: edit_msg.clone(), + edit_msg: edit_msg, pending: cx.props.pending })), items: cx.render(rsx!( @@ -482,9 +482,9 @@ fn wrap_messages_in_context_menu<'a>(cx: Scope<'a, MessagesProps<'a>>) -> Elemen aria_label: "messages-edit".into(), text: get_local_text("messages.edit"), should_render: !cx.props.is_remote - && edit_msg.get().map(|id| id != msg_uuid).unwrap_or(true), + && edit_msg.read().edit.map(|id| id != msg_uuid).unwrap_or(true), onpress: move |_| { - edit_msg.set(Some(msg_uuid)); + edit_msg.write().edit = Some(msg_uuid); state.write().ui.ignore_focus = true; } }, @@ -493,9 +493,9 @@ fn wrap_messages_in_context_menu<'a>(cx: Scope<'a, MessagesProps<'a>>) -> Elemen aria_label: "messages-cancel-edit".into(), text: get_local_text("messages.cancel-edit"), should_render: !cx.props.is_remote - && edit_msg.get().map(|id| id == msg_uuid).unwrap_or(false), + && edit_msg.read().edit.map(|id| id == msg_uuid).unwrap_or(false), onpress: move |_| { - edit_msg.set(None); + edit_msg.write().edit = None; state.write().ui.ignore_focus = false; } }, @@ -522,7 +522,7 @@ struct MessageProps<'a> { message: &'a data::MessageGroupMsg, is_remote: bool, message_key: String, - edit_msg: UseState>, + edit_msg: &'a UseSharedState, pending: bool, } fn render_message<'a>(cx: Scope<'a, MessageProps<'a>>) -> Element<'a> { @@ -548,7 +548,8 @@ fn render_message<'a>(cx: Scope<'a, MessageProps<'a>>) -> Element<'a> { } = cx.props; let message = &grouped_message.message; let is_editing = edit_msg - .current() + .read() + .edit .map(|id| !cx.props.is_remote && (id == message.inner.id())) .unwrap_or(false); @@ -693,7 +694,7 @@ fn render_message<'a>(cx: Scope<'a, MessageProps<'a>>) -> Element<'a> { } }, on_edit: move |update: String| { - edit_msg.set(None); + edit_msg.write().edit = None; state.write().ui.ignore_focus = false; let msg = update.split('\n').map(|x| x.to_string()).collect::>(); if message.inner.lines() == msg || !msg.iter().any(|x| !x.trim().is_empty()) {