mirror of
https://github.com/youwen5/iamb.git
synced 2025-06-19 21:29:52 -07:00
Support replying to messages (#3)
This commit is contained in:
parent
54ce042384
commit
d13d4b9f7f
7 changed files with 279 additions and 101 deletions
|
@ -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: |
|
||||
|
|
|
@ -61,7 +61,9 @@ pub enum VerifyAction {
|
|||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum MessageAction {
|
||||
Cancel,
|
||||
Download(Option<String>, bool),
|
||||
Reply,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
|
|
|
@ -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 });
|
||||
|
|
139
src/message.rs
139
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<RoomMessageEventContent>;
|
||||
pub type MessageFetchResult = IambResult<(Option<String>, Vec<MessageEvent>)>;
|
||||
pub type MessageFetchResult = IambResult<(Option<String>, Vec<RoomMessageEvent>)>;
|
||||
pub type MessageKey = (MessageTimeStamp, OwnedEventId);
|
||||
pub type Messages = BTreeMap<MessageKey, Message>;
|
||||
|
||||
|
@ -146,6 +148,13 @@ impl MessageTimeStamp {
|
|||
fn is_local_echo(&self) -> bool {
|
||||
matches!(self, MessageTimeStamp::LocalEcho)
|
||||
}
|
||||
|
||||
pub fn as_millis(&self) -> Option<MilliSecondsSinceUnixEpoch> {
|
||||
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<RoomMessageEventContent>),
|
||||
Redacted,
|
||||
pub enum MessageEvent {
|
||||
Original(Box<OriginalRoomMessageEvent>),
|
||||
Redacted(Box<RedactedRoomMessageEvent>),
|
||||
Local(Box<RoomMessageEventContent>),
|
||||
}
|
||||
|
||||
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<MessageEvent> for Message {
|
||||
fn from(event: MessageEvent) -> Self {
|
||||
match event {
|
||||
MessageLikeEvent::Original(ev) => {
|
||||
let content = MessageContent::Original(ev.content.into());
|
||||
impl From<OriginalRoomMessageEvent> 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<RedactedRoomMessageEvent> 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<RoomMessageEvent> 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
64
src/tests.rs
64
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 {
|
||||
|
|
|
@ -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<MessageKey>,
|
||||
}
|
||||
|
||||
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<IambInfo> for ChatState {
|
|||
|
||||
scrollback: self.scrollback.dup(store),
|
||||
focus: self.focus,
|
||||
|
||||
reply_to: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -432,7 +485,7 @@ impl PromptActions<ProgramContext, ProgramStore, IambInfo> 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);
|
||||
|
|
|
@ -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<MessageCursor>,
|
||||
|
||||
/// The jumplist of visited messages.
|
||||
jumped: HistoryList<MessageCursor>,
|
||||
|
||||
/// 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<MessageKey> {
|
||||
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<IambInfo> 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<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
|||
self.cursor = pos;
|
||||
}
|
||||
|
||||
self.show_full_on_redraw = true;
|
||||
|
||||
return Ok(None);
|
||||
},
|
||||
EditAction::Yank => {
|
||||
|
@ -667,7 +704,7 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> 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);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue