diff --git a/README.md b/README.md index 0b8735c..ceec578 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ two other TUI clients and Element Web: | Accepting Invites | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Typing Notification | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | E2E | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Replies | :x: ([#3]) | :heavy_check_mark: | :x: | :heavy_check_mark: | +| Replies | :heavy_check_mark: | :heavy_check_mark: | :x: | :heavy_check_mark: | | Attachment uploading | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | | Attachment downloading | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Send stickers | :x: | :x: | :x: | :heavy_check_mark: | diff --git a/src/base.rs b/src/base.rs index 13456d5..8435f02 100644 --- a/src/base.rs +++ b/src/base.rs @@ -61,7 +61,9 @@ pub enum VerifyAction { #[derive(Clone, Debug, Eq, PartialEq)] pub enum MessageAction { + Cancel, Download(Option, bool), + Reply, } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/src/commands.rs b/src/commands.rs index 3234015..988d49e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -133,6 +133,28 @@ fn iamb_members(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { return Ok(step); } +fn iamb_cancel(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + if !desc.arg.text.is_empty() { + return Result::Err(CommandError::InvalidArgument); + } + + let ract = IambAction::from(MessageAction::Cancel); + let step = CommandStep::Continue(ract.into(), ctx.context.take()); + + return Ok(step); +} + +fn iamb_reply(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + if !desc.arg.text.is_empty() { + return Result::Err(CommandError::InvalidArgument); + } + + let ract = IambAction::from(MessageAction::Reply); + let step = CommandStep::Continue(ract.into(), ctx.context.take()); + + return Ok(step); +} + fn iamb_rooms(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { if !desc.arg.text.is_empty() { return Result::Err(CommandError::InvalidArgument); @@ -231,11 +253,13 @@ fn iamb_download(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult } fn add_iamb_commands(cmds: &mut ProgramCommands) { + cmds.add_command(ProgramCommand { names: vec!["cancel".into()], f: iamb_cancel }); cmds.add_command(ProgramCommand { names: vec!["dms".into()], f: iamb_dms }); cmds.add_command(ProgramCommand { names: vec!["download".into()], f: iamb_download }); cmds.add_command(ProgramCommand { names: vec!["invite".into()], f: iamb_invite }); cmds.add_command(ProgramCommand { names: vec!["join".into()], f: iamb_join }); cmds.add_command(ProgramCommand { names: vec!["members".into()], f: iamb_members }); + cmds.add_command(ProgramCommand { names: vec!["reply".into()], f: iamb_reply }); cmds.add_command(ProgramCommand { names: vec!["rooms".into()], f: iamb_rooms }); cmds.add_command(ProgramCommand { names: vec!["set".into()], f: iamb_set }); cmds.add_command(ProgramCommand { names: vec!["spaces".into()], f: iamb_spaces }); diff --git a/src/message.rs b/src/message.rs index d0d5b02..240aa10 100644 --- a/src/message.rs +++ b/src/message.rs @@ -11,9 +11,12 @@ use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use matrix_sdk::ruma::{ - events::{ - room::message::{MessageType, RoomMessageEventContent}, - MessageLikeEvent, + events::room::message::{ + MessageType, + OriginalRoomMessageEvent, + RedactedRoomMessageEvent, + RoomMessageEvent, + RoomMessageEventContent, }, MilliSecondsSinceUnixEpoch, OwnedEventId, @@ -33,8 +36,7 @@ use crate::{ config::ApplicationSettings, }; -pub type MessageEvent = MessageLikeEvent; -pub type MessageFetchResult = IambResult<(Option, Vec)>; +pub type MessageFetchResult = IambResult<(Option, Vec)>; pub type MessageKey = (MessageTimeStamp, OwnedEventId); pub type Messages = BTreeMap; @@ -146,6 +148,13 @@ impl MessageTimeStamp { fn is_local_echo(&self) -> bool { matches!(self, MessageTimeStamp::LocalEcho) } + + pub fn as_millis(&self) -> Option { + match self { + MessageTimeStamp::OriginServer(ms) => MilliSecondsSinceUnixEpoch(*ms).into(), + MessageTimeStamp::LocalEcho => None, + } + } } impl Ord for MessageTimeStamp { @@ -304,61 +313,65 @@ impl PartialOrd for MessageCursor { } #[derive(Clone)] -pub enum MessageContent { - Original(Box), - Redacted, +pub enum MessageEvent { + Original(Box), + Redacted(Box), + Local(Box), } -impl MessageContent { +impl MessageEvent { pub fn show(&self) -> Cow<'_, str> { match self { - MessageContent::Original(ev) => { - let s = match &ev.msgtype { - MessageType::Text(content) => content.body.as_ref(), - MessageType::Emote(content) => content.body.as_ref(), - MessageType::Notice(content) => content.body.as_str(), - MessageType::ServerNotice(content) => content.body.as_str(), - - MessageType::VerificationRequest(_) => { - // XXX: implement - - return Cow::Owned("[verification request]".into()); - }, - MessageType::Audio(content) => { - return Cow::Owned(format!("[Attached Audio: {}]", content.body)); - }, - MessageType::File(content) => { - return Cow::Owned(format!("[Attached File: {}]", content.body)); - }, - MessageType::Image(content) => { - return Cow::Owned(format!("[Attached Image: {}]", content.body)); - }, - MessageType::Video(content) => { - return Cow::Owned(format!("[Attached Video: {}]", content.body)); - }, - _ => { - return Cow::Owned(format!("[Unknown message type: {:?}]", ev.msgtype())); - }, - }; - - Cow::Borrowed(s) - }, - MessageContent::Redacted => Cow::Borrowed("[redacted]"), + MessageEvent::Original(ev) => show_room_content(&ev.content), + MessageEvent::Redacted(_) => Cow::Borrowed("[redacted]"), + MessageEvent::Local(content) => show_room_content(content), } } } +fn show_room_content(content: &RoomMessageEventContent) -> Cow<'_, str> { + let s = match &content.msgtype { + MessageType::Text(content) => content.body.as_ref(), + MessageType::Emote(content) => content.body.as_ref(), + MessageType::Notice(content) => content.body.as_str(), + MessageType::ServerNotice(content) => content.body.as_str(), + + MessageType::VerificationRequest(_) => { + // XXX: implement + + return Cow::Owned("[verification request]".into()); + }, + MessageType::Audio(content) => { + return Cow::Owned(format!("[Attached Audio: {}]", content.body)); + }, + MessageType::File(content) => { + return Cow::Owned(format!("[Attached File: {}]", content.body)); + }, + MessageType::Image(content) => { + return Cow::Owned(format!("[Attached Image: {}]", content.body)); + }, + MessageType::Video(content) => { + return Cow::Owned(format!("[Attached Video: {}]", content.body)); + }, + _ => { + return Cow::Owned(format!("[Unknown message type: {:?}]", content.msgtype())); + }, + }; + + Cow::Borrowed(s) +} + #[derive(Clone)] pub struct Message { - pub content: MessageContent, + pub event: MessageEvent, pub sender: OwnedUserId, pub timestamp: MessageTimeStamp, pub downloaded: bool, } impl Message { - pub fn new(content: MessageContent, sender: OwnedUserId, timestamp: MessageTimeStamp) -> Self { - Message { content, sender, timestamp, downloaded: false } + pub fn new(event: MessageEvent, sender: OwnedUserId, timestamp: MessageTimeStamp) -> Self { + Message { event, sender, timestamp, downloaded: false } } pub fn show( @@ -369,7 +382,7 @@ impl Message { settings: &ApplicationSettings, ) -> Text { let width = vwctx.get_width(); - let mut msg = self.content.show(); + let mut msg = self.event.show(); if self.downloaded { msg.to_mut().push_str(" \u{2705}"); @@ -465,24 +478,38 @@ impl Message { } } -impl From for Message { - fn from(event: MessageEvent) -> Self { - match event { - MessageLikeEvent::Original(ev) => { - let content = MessageContent::Original(ev.content.into()); +impl From for Message { + fn from(event: OriginalRoomMessageEvent) -> Self { + let timestamp = event.origin_server_ts.into(); + let user_id = event.sender.clone(); + let content = MessageEvent::Original(event.into()); - Message::new(content, ev.sender, ev.origin_server_ts.into()) - }, - MessageLikeEvent::Redacted(ev) => { - Message::new(MessageContent::Redacted, ev.sender, ev.origin_server_ts.into()) - }, + Message::new(content, user_id, timestamp) + } +} + +impl From for Message { + fn from(event: RedactedRoomMessageEvent) -> Self { + let timestamp = event.origin_server_ts.into(); + let user_id = event.sender.clone(); + let content = MessageEvent::Redacted(event.into()); + + Message::new(content, user_id, timestamp) + } +} + +impl From for Message { + fn from(event: RoomMessageEvent) -> Self { + match event { + RoomMessageEvent::Original(ev) => ev.into(), + RoomMessageEvent::Redacted(ev) => ev.into(), } } } impl ToString for Message { fn to_string(&self) -> String { - self.content.show().into_owned() + self.event.show().into_owned() } } diff --git a/src/tests.rs b/src/tests.rs index 3b79b20..65e57b6 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -3,10 +3,11 @@ use std::path::PathBuf; use matrix_sdk::ruma::{ event_id, - events::room::message::RoomMessageEventContent, + events::room::message::{OriginalRoomMessageEvent, RoomMessageEventContent}, server_name, user_id, EventId, + OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, @@ -30,7 +31,7 @@ use crate::{ }, message::{ Message, - MessageContent, + MessageEvent, MessageKey, MessageTimeStamp::{LocalEcho, OriginServer}, Messages, @@ -45,54 +46,69 @@ lazy_static! { pub static ref TEST_USER3: OwnedUserId = user_id!("@user3:example.com").to_owned(); pub static ref TEST_USER4: OwnedUserId = user_id!("@user4:example.com").to_owned(); pub static ref TEST_USER5: OwnedUserId = user_id!("@user5:example.com").to_owned(); - pub static ref MSG1_KEY: MessageKey = (LocalEcho, EventId::new(server_name!("example.com"))); - pub static ref MSG2_KEY: MessageKey = - (OriginServer(UInt::new(1).unwrap()), EventId::new(server_name!("example.com"))); - pub static ref MSG3_KEY: MessageKey = ( - OriginServer(UInt::new(2).unwrap()), - event_id!("$5jRz3KfVhaUzXtVj7k:example.com").to_owned() - ); - pub static ref MSG4_KEY: MessageKey = ( - OriginServer(UInt::new(2).unwrap()), - event_id!("$JP6qFV7WyXk5ZnexM3:example.com").to_owned() - ); - pub static ref MSG5_KEY: MessageKey = - (OriginServer(UInt::new(8).unwrap()), EventId::new(server_name!("example.com"))); + pub static ref MSG1_EVID: OwnedEventId = EventId::new(server_name!("example.com")); + pub static ref MSG2_EVID: OwnedEventId = EventId::new(server_name!("example.com")); + pub static ref MSG3_EVID: OwnedEventId = + event_id!("$5jRz3KfVhaUzXtVj7k:example.com").to_owned(); + pub static ref MSG4_EVID: OwnedEventId = + event_id!("$JP6qFV7WyXk5ZnexM3:example.com").to_owned(); + pub static ref MSG5_EVID: OwnedEventId = EventId::new(server_name!("example.com")); + pub static ref MSG1_KEY: MessageKey = (LocalEcho, MSG1_EVID.clone()); + pub static ref MSG2_KEY: MessageKey = (OriginServer(UInt::new(1).unwrap()), MSG2_EVID.clone()); + pub static ref MSG3_KEY: MessageKey = (OriginServer(UInt::new(2).unwrap()), MSG3_EVID.clone()); + pub static ref MSG4_KEY: MessageKey = (OriginServer(UInt::new(2).unwrap()), MSG4_EVID.clone()); + pub static ref MSG5_KEY: MessageKey = (OriginServer(UInt::new(8).unwrap()), MSG5_EVID.clone()); +} + +pub fn mock_room1_message( + content: RoomMessageEventContent, + sender: OwnedUserId, + key: MessageKey, +) -> Message { + let origin_server_ts = key.0.as_millis().unwrap(); + let event_id = key.1; + + let event = OriginalRoomMessageEvent { + content, + event_id, + sender, + origin_server_ts, + room_id: TEST_ROOM1_ID.clone(), + unsigned: Default::default(), + }; + + event.into() } pub fn mock_message1() -> Message { let content = RoomMessageEventContent::text_plain("writhe"); - let content = MessageContent::Original(content.into()); + let content = MessageEvent::Local(content.into()); Message::new(content, TEST_USER1.clone(), MSG1_KEY.0) } pub fn mock_message2() -> Message { let content = RoomMessageEventContent::text_plain("helium"); - let content = MessageContent::Original(content.into()); - Message::new(content, TEST_USER2.clone(), MSG2_KEY.0) + mock_room1_message(content, TEST_USER2.clone(), MSG2_KEY.clone()) } pub fn mock_message3() -> Message { let content = RoomMessageEventContent::text_plain("this\nis\na\nmultiline\nmessage"); - let content = MessageContent::Original(content.into()); - Message::new(content, TEST_USER2.clone(), MSG3_KEY.0) + mock_room1_message(content, TEST_USER2.clone(), MSG3_KEY.clone()) } pub fn mock_message4() -> Message { let content = RoomMessageEventContent::text_plain("help"); - let content = MessageContent::Original(content.into()); - Message::new(content, TEST_USER1.clone(), MSG4_KEY.0) + mock_room1_message(content, TEST_USER1.clone(), MSG4_KEY.clone()) } pub fn mock_message5() -> Message { let content = RoomMessageEventContent::text_plain("character"); - let content = MessageContent::Original(content.into()); - Message::new(content, TEST_USER2.clone(), MSG5_KEY.0) + mock_room1_message(content, TEST_USER2.clone(), MSG4_KEY.clone()) } pub fn mock_messages() -> Messages { diff --git a/src/windows/room/chat.rs b/src/windows/room/chat.rs index 9368de7..7f131c6 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -8,14 +8,24 @@ use matrix_sdk::{ media::{MediaFormat, MediaRequest}, room::Room as MatrixRoom, ruma::{ - events::room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent}, + events::room::message::{ + MessageType, + OriginalRoomMessageEvent, + RoomMessageEventContent, + TextMessageEventContent, + }, OwnedRoomId, RoomId, }, }; use modalkit::{ - tui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget}, + tui::{ + buffer::Buffer, + layout::Rect, + text::{Span, Spans}, + widgets::{Paragraph, StatefulWidget, Widget}, + }, widgets::textbox::{TextBox, TextBoxState}, widgets::TerminalCursor, widgets::{PromptActions, WindowOps}, @@ -52,10 +62,11 @@ use crate::base::{ ProgramContext, ProgramStore, RoomFocus, + RoomInfo, SendAction, }; -use crate::message::{Message, MessageContent, MessageTimeStamp}; +use crate::message::{Message, MessageEvent, MessageKey, MessageTimeStamp}; use super::scrollback::{Scrollback, ScrollbackState}; @@ -69,6 +80,8 @@ pub struct ChatState { scrollback: ScrollbackState, focus: RoomFocus, + + reply_to: Option, } impl ChatState { @@ -89,9 +102,27 @@ impl ChatState { scrollback, focus: RoomFocus::MessageBar, + + reply_to: None, } } + fn get_reply_to<'a>(&self, info: &'a RoomInfo) -> Option<&'a OriginalRoomMessageEvent> { + let key = self.reply_to.as_ref()?; + let msg = info.messages.get(key)?; + + if let MessageEvent::Original(ev) = &msg.event { + Some(ev) + } else { + None + } + } + + fn reset(&mut self) -> EditRope { + self.reply_to = None; + self.tbox.reset() + } + pub fn refresh_room(&mut self, store: &mut ProgramStore) { if let Some(room) = store.application.worker.client.get_room(self.id()) { self.room = room; @@ -112,8 +143,13 @@ impl ChatState { let msg = self.scrollback.get_mut(info).ok_or(IambError::NoSelectedMessage)?; match act { + MessageAction::Cancel => { + self.reply_to = None; + + Ok(None) + }, MessageAction::Download(filename, force) => { - if let MessageContent::Original(ev) = &msg.content { + if let MessageEvent::Original(ev) = &msg.event { let media = client.media(); let mut filename = match filename { @@ -121,7 +157,7 @@ impl ChatState { None => settings.dirs.downloads.clone(), }; - let source = match &ev.msgtype { + let source = match &ev.content.msgtype { MessageType::Audio(c) => { if filename.is_dir() { filename.push(c.body.as_str()); @@ -188,6 +224,12 @@ impl ChatState { Err(IambError::NoAttachment.into()) }, + MessageAction::Reply => { + self.reply_to = self.scrollback.get_key(info); + self.focus = RoomFocus::MessageBar; + + Ok(None) + }, } } @@ -203,6 +245,7 @@ impl ChatState { .client .get_joined_room(self.id()) .ok_or(IambError::NotJoined)?; + let info = store.application.rooms.entry(self.id().to_owned()).or_default(); let (event_id, msg) = match act { SendAction::Submit => { @@ -214,15 +257,21 @@ impl ChatState { let msg = TextMessageEventContent::plain(msg); let msg = MessageType::Text(msg); - let msg = RoomMessageEventContent::new(msg); + + let mut msg = RoomMessageEventContent::new(msg); + + if let Some(m) = self.get_reply_to(info) { + // XXX: Switch to RoomMessageEventContent::reply() once it's stable? + msg = msg.make_reply_to(m); + } // XXX: second parameter can be a locally unique transaction id. // Useful for doing retries. let resp = room.send(msg.clone(), None).await.map_err(IambError::from)?; let event_id = resp.event_id; - // Clear the TextBoxState contents now that the message is sent. - self.tbox.reset(); + // Reset message bar state now that it's been sent. + self.reset(); (event_id, msg) }, @@ -252,12 +301,14 @@ impl ChatState { }; let user = store.application.settings.profile.user_id.clone(); - let info = store.application.get_room_info(self.id().to_owned()); let key = (MessageTimeStamp::LocalEcho, event_id); - let msg = MessageContent::Original(msg.into()); + let msg = MessageEvent::Local(msg.into()); let msg = Message::new(msg, user, MessageTimeStamp::LocalEcho); info.messages.insert(key, msg); + // Jump to the end of the scrollback to show the message. + self.scrollback.goto_latest(); + Ok(None) } @@ -333,6 +384,8 @@ impl WindowOps for ChatState { scrollback: self.scrollback.dup(store), focus: self.focus, + + reply_to: None, } } @@ -432,7 +485,7 @@ impl PromptActions for ChatState { return Ok(vec![]); } - let text = self.tbox.reset().trim(); + let text = self.reset().trim(); if text.is_empty() { let _ = self.sent.end(); @@ -506,15 +559,34 @@ impl<'a> StatefulWidget for Chat<'a> { let lines = state.tbox.has_lines(5).max(1) as u16; let drawh = area.height; let texth = lines.min(drawh).clamp(1, 5); - let scrollh = drawh.saturating_sub(texth); + let desch = if state.reply_to.is_some() { + drawh.saturating_sub(texth).min(1) + } else { + 0 + }; + let scrollh = drawh.saturating_sub(texth).saturating_sub(desch); let scrollarea = Rect::new(area.x, area.y, area.width, scrollh); - let textarea = Rect::new(scrollarea.x, scrollarea.y + scrollh, scrollarea.width, texth); + let descarea = Rect::new(area.x, scrollarea.y + scrollh, area.width, desch); + let textarea = Rect::new(area.x, descarea.y + desch, area.width, texth); let scrollback_focused = state.focus.is_scrollback() && self.focused; let scrollback = Scrollback::new(self.store).focus(scrollback_focused); scrollback.render(scrollarea, buf, &mut state.scrollback); + let desc_spans = state.reply_to.as_ref().and_then(|k| { + let room = self.store.application.rooms.get(state.id())?; + let msg = room.messages.get(k)?; + let user = self.store.application.settings.get_user_span(msg.sender.as_ref()); + let spans = Spans(vec![Span::from("Replying to "), user]); + + spans.into() + }); + + if let Some(desc_spans) = desc_spans { + Paragraph::new(desc_spans).render(descarea, buf); + } + let prompt = if self.focused { "> " } else { " " }; let tbox = TextBox::new().prompt(prompt); diff --git a/src/windows/room/scrollback.rs b/src/windows/room/scrollback.rs index 53f7fb0..5030715 100644 --- a/src/windows/room/scrollback.rs +++ b/src/windows/room/scrollback.rs @@ -104,11 +104,26 @@ fn nth_after(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageCursor { } pub struct ScrollbackState { + /// The room identifier. room_id: OwnedRoomId, + + /// The buffer identifier used for saving marks, etc. id: IambBufferId, + + /// The currently selected message in the scrollback. cursor: MessageCursor, + + /// Contextual info about the viewport used during rendering. viewctx: ViewportContext, + + /// The jumplist of visited messages. jumped: HistoryList, + + /// Whether the full message should be drawn during the next render() call. + /// + /// This is used to ensure that ^E/^Y work nicely when the cursor is currently + /// on a multiline message. + show_full_on_redraw: bool, } impl ScrollbackState { @@ -117,8 +132,20 @@ impl ScrollbackState { let cursor = MessageCursor::default(); let viewctx = ViewportContext::default(); let jumped = HistoryList::default(); + let show_full_on_redraw = false; - ScrollbackState { room_id, id, cursor, viewctx, jumped } + ScrollbackState { + room_id, + id, + cursor, + viewctx, + jumped, + show_full_on_redraw, + } + } + + pub fn goto_latest(&mut self) { + self.cursor = MessageCursor::latest(); } /// Set the dimensions and placement within the terminal window for this list. @@ -126,6 +153,13 @@ impl ScrollbackState { self.viewctx.dimensions = (area.width as usize, area.height as usize); } + pub fn get_key(&self, info: &mut RoomInfo) -> Option { + self.cursor + .timestamp + .clone() + .or_else(|| info.messages.last_key_value().map(|kv| kv.0.clone())) + } + pub fn get_mut<'a>(&mut self, info: &'a mut RoomInfo) -> Option<&'a mut Message> { if let Some(k) = &self.cursor.timestamp { info.messages.get_mut(k) @@ -397,7 +431,7 @@ impl ScrollbackState { continue; } - if needle.is_match(msg.content.show().as_ref()) { + if needle.is_match(msg.event.show().as_ref()) { mc = MessageCursor::from(key.clone()).into(); count -= 1; } @@ -421,7 +455,7 @@ impl ScrollbackState { break; } - if needle.is_match(msg.content.show().as_ref()) { + if needle.is_match(msg.event.show().as_ref()) { mc = MessageCursor::from(key.clone()).into(); count -= 1; } @@ -462,6 +496,7 @@ impl WindowOps for ScrollbackState { cursor: self.cursor.clone(), viewctx: self.viewctx.clone(), jumped: self.jumped.clone(), + show_full_on_redraw: false, } } @@ -582,6 +617,8 @@ impl EditorActions for ScrollbackState { self.cursor = pos; } + self.show_full_on_redraw = true; + return Ok(None); }, EditAction::Yank => { @@ -667,7 +704,7 @@ impl EditorActions for ScrollbackState { let mut yanked = EditRope::from(""); for (_, msg) in self.messages(range, info) { - yanked += EditRope::from(msg.content.show().into_owned()); + yanked += EditRope::from(msg.event.show().into_owned()); yanked += EditRope::from('\n'); } @@ -1173,15 +1210,15 @@ impl<'a> StatefulWidget for Scrollback<'a> { nth_key_before(cursor_key.clone(), height, info) }; - let full = cursor.timestamp.is_none(); - + let foc = self.focused || cursor.timestamp.is_some(); + let full = std::mem::take(&mut state.show_full_on_redraw) || cursor.timestamp.is_none(); let mut lines = vec![]; let mut sawit = false; let mut prev = None; for (key, item) in info.messages.range(&corner_key..) { let sel = key == cursor_key; - let txt = item.show(prev, self.focused && sel, &state.viewctx, settings); + let txt = item.show(prev, foc && sel, &state.viewctx, settings); prev = Some(item);