2023-01-10 19:59:30 -08:00
|
|
|
use std::borrow::Cow;
|
|
|
|
use std::ffi::OsStr;
|
|
|
|
use std::fs;
|
2023-01-19 16:05:02 -08:00
|
|
|
use std::ops::Deref;
|
2023-01-10 19:59:30 -08:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
2023-01-28 12:29:06 +00:00
|
|
|
use tokio;
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
use matrix_sdk::{
|
2023-01-10 19:59:30 -08:00
|
|
|
attachment::AttachmentConfig,
|
|
|
|
media::{MediaFormat, MediaRequest},
|
2023-02-09 17:53:33 -08:00
|
|
|
room::{Joined, Room as MatrixRoom},
|
2023-01-10 19:59:30 -08:00
|
|
|
ruma::{
|
2023-02-09 17:53:33 -08:00
|
|
|
events::reaction::{ReactionEventContent, Relation as Reaction},
|
2023-01-12 21:20:32 -08:00
|
|
|
events::room::message::{
|
|
|
|
MessageType,
|
|
|
|
OriginalRoomMessageEvent,
|
2023-01-19 16:05:02 -08:00
|
|
|
Relation,
|
|
|
|
Replacement,
|
2023-01-12 21:20:32 -08:00
|
|
|
RoomMessageEventContent,
|
|
|
|
TextMessageEventContent,
|
|
|
|
},
|
2023-02-09 17:53:33 -08:00
|
|
|
EventId,
|
2023-01-10 19:59:30 -08:00
|
|
|
OwnedRoomId,
|
|
|
|
RoomId,
|
|
|
|
},
|
2022-12-29 18:00:59 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
use modalkit::{
|
2023-01-12 21:20:32 -08:00
|
|
|
tui::{
|
|
|
|
buffer::Buffer,
|
|
|
|
layout::Rect,
|
|
|
|
text::{Span, Spans},
|
|
|
|
widgets::{Paragraph, StatefulWidget, Widget},
|
|
|
|
},
|
2022-12-29 18:00:59 -08:00
|
|
|
widgets::textbox::{TextBox, TextBoxState},
|
|
|
|
widgets::TerminalCursor,
|
|
|
|
widgets::{PromptActions, WindowOps},
|
|
|
|
};
|
|
|
|
|
|
|
|
use modalkit::editing::{
|
|
|
|
action::{
|
|
|
|
EditError,
|
|
|
|
EditInfo,
|
|
|
|
EditResult,
|
|
|
|
Editable,
|
|
|
|
EditorAction,
|
2023-01-10 19:59:30 -08:00
|
|
|
InfoMessage,
|
2022-12-29 18:00:59 -08:00
|
|
|
Jumpable,
|
|
|
|
PromptAction,
|
|
|
|
Promptable,
|
|
|
|
Scrollable,
|
2023-01-10 19:59:30 -08:00
|
|
|
UIError,
|
2022-12-29 18:00:59 -08:00
|
|
|
},
|
2023-03-01 18:46:33 -08:00
|
|
|
base::{CloseFlags, Count, MoveDir1D, PositionList, ScrollStyle, WordStyle, WriteFlags},
|
|
|
|
completion::CompletionList,
|
2022-12-29 18:00:59 -08:00
|
|
|
context::Resolve,
|
|
|
|
history::{self, HistoryList},
|
|
|
|
rope::EditRope,
|
|
|
|
};
|
|
|
|
|
|
|
|
use crate::base::{
|
2023-01-28 12:29:06 +00:00
|
|
|
DownloadFlags,
|
2022-12-29 18:00:59 -08:00
|
|
|
IambAction,
|
|
|
|
IambBufferId,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambError,
|
2022-12-29 18:00:59 -08:00
|
|
|
IambInfo,
|
|
|
|
IambResult,
|
2023-01-10 19:59:30 -08:00
|
|
|
MessageAction,
|
2022-12-29 18:00:59 -08:00
|
|
|
ProgramAction,
|
|
|
|
ProgramContext,
|
|
|
|
ProgramStore,
|
|
|
|
RoomFocus,
|
2023-01-12 21:20:32 -08:00
|
|
|
RoomInfo,
|
2023-01-10 19:59:30 -08:00
|
|
|
SendAction,
|
2022-12-29 18:00:59 -08:00
|
|
|
};
|
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
use crate::message::{Message, MessageEvent, MessageKey, MessageTimeStamp};
|
2023-02-09 17:53:33 -08:00
|
|
|
use crate::worker::Requester;
|
2023-01-10 19:59:30 -08:00
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
use super::scrollback::{Scrollback, ScrollbackState};
|
|
|
|
|
|
|
|
pub struct ChatState {
|
|
|
|
room_id: OwnedRoomId,
|
2023-01-05 18:12:25 -08:00
|
|
|
room: MatrixRoom,
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
tbox: TextBoxState<IambInfo>,
|
|
|
|
sent: HistoryList<EditRope>,
|
|
|
|
sent_scrollback: history::ScrollbackState,
|
|
|
|
|
|
|
|
scrollback: ScrollbackState,
|
|
|
|
focus: RoomFocus,
|
2023-01-12 21:20:32 -08:00
|
|
|
|
|
|
|
reply_to: Option<MessageKey>,
|
2023-01-19 16:05:02 -08:00
|
|
|
editing: Option<MessageKey>,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ChatState {
|
|
|
|
pub fn new(room: MatrixRoom, store: &mut ProgramStore) -> Self {
|
|
|
|
let room_id = room.room_id().to_owned();
|
|
|
|
let scrollback = ScrollbackState::new(room_id.clone());
|
|
|
|
let id = IambBufferId::Room(room_id.clone(), RoomFocus::MessageBar);
|
|
|
|
let ebuf = store.load_buffer(id);
|
|
|
|
let tbox = TextBoxState::new(ebuf);
|
|
|
|
|
|
|
|
ChatState {
|
|
|
|
room_id,
|
2023-01-05 18:12:25 -08:00
|
|
|
room,
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
tbox,
|
|
|
|
sent: HistoryList::new(EditRope::from(""), 100),
|
|
|
|
sent_scrollback: history::ScrollbackState::Pending,
|
|
|
|
|
|
|
|
scrollback,
|
|
|
|
focus: RoomFocus::MessageBar,
|
2023-01-12 21:20:32 -08:00
|
|
|
|
|
|
|
reply_to: None,
|
2023-01-19 16:05:02 -08:00
|
|
|
editing: None,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
fn get_joined(&self, worker: &Requester) -> Result<Joined, IambError> {
|
|
|
|
worker.client.get_joined_room(self.id()).ok_or(IambError::NotJoined)
|
|
|
|
}
|
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
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;
|
2023-01-19 16:05:02 -08:00
|
|
|
self.editing = None;
|
2023-01-12 21:20:32 -08:00
|
|
|
self.tbox.reset()
|
|
|
|
}
|
|
|
|
|
2023-01-11 17:54:49 -08:00
|
|
|
pub fn refresh_room(&mut self, store: &mut ProgramStore) {
|
|
|
|
if let Some(room) = store.application.worker.client.get_room(self.id()) {
|
|
|
|
self.room = room;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
pub async fn message_command(
|
|
|
|
&mut self,
|
|
|
|
act: MessageAction,
|
|
|
|
_: ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> IambResult<EditInfo> {
|
|
|
|
let client = &store.application.worker.client;
|
|
|
|
|
|
|
|
let settings = &store.application.settings;
|
2023-03-01 18:46:33 -08:00
|
|
|
let info = store.application.rooms.get_or_default(self.room_id.clone());
|
2023-01-10 19:59:30 -08:00
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
let msg = self
|
|
|
|
.scrollback
|
|
|
|
.get_mut(&mut info.messages)
|
|
|
|
.ok_or(IambError::NoSelectedMessage)?;
|
2023-01-10 19:59:30 -08:00
|
|
|
|
|
|
|
match act {
|
2023-01-12 21:20:32 -08:00
|
|
|
MessageAction::Cancel => {
|
|
|
|
self.reply_to = None;
|
2023-01-19 16:05:02 -08:00
|
|
|
self.editing = None;
|
2023-01-12 21:20:32 -08:00
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
},
|
2023-01-28 12:29:06 +00:00
|
|
|
MessageAction::Download(filename, flags) => {
|
2023-01-12 21:20:32 -08:00
|
|
|
if let MessageEvent::Original(ev) = &msg.event {
|
2023-01-10 19:59:30 -08:00
|
|
|
let media = client.media();
|
|
|
|
|
|
|
|
let mut filename = match filename {
|
|
|
|
Some(f) => PathBuf::from(f),
|
|
|
|
None => settings.dirs.downloads.clone(),
|
|
|
|
};
|
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
let source = match &ev.content.msgtype {
|
2023-01-10 19:59:30 -08:00
|
|
|
MessageType::Audio(c) => {
|
|
|
|
if filename.is_dir() {
|
|
|
|
filename.push(c.body.as_str());
|
|
|
|
}
|
|
|
|
|
|
|
|
c.source.clone()
|
|
|
|
},
|
|
|
|
MessageType::File(c) => {
|
|
|
|
if filename.is_dir() {
|
|
|
|
if let Some(name) = &c.filename {
|
|
|
|
filename.push(name);
|
|
|
|
} else {
|
|
|
|
filename.push(c.body.as_str());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
c.source.clone()
|
|
|
|
},
|
|
|
|
MessageType::Image(c) => {
|
|
|
|
if filename.is_dir() {
|
|
|
|
filename.push(c.body.as_str());
|
|
|
|
}
|
|
|
|
|
|
|
|
c.source.clone()
|
|
|
|
},
|
|
|
|
MessageType::Video(c) => {
|
|
|
|
if filename.is_dir() {
|
|
|
|
filename.push(c.body.as_str());
|
|
|
|
}
|
|
|
|
|
|
|
|
c.source.clone()
|
|
|
|
},
|
|
|
|
_ => {
|
|
|
|
return Err(IambError::NoAttachment.into());
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2023-01-31 12:04:10 +00:00
|
|
|
if !filename.exists() || flags.contains(DownloadFlags::FORCE) {
|
2023-01-28 12:29:06 +00:00
|
|
|
let req = MediaRequest { source, format: MediaFormat::File };
|
2023-01-10 19:59:30 -08:00
|
|
|
|
2023-01-28 12:29:06 +00:00
|
|
|
let bytes =
|
|
|
|
media.get_media_content(&req, true).await.map_err(IambError::from)?;
|
2023-01-10 19:59:30 -08:00
|
|
|
|
2023-01-28 12:29:06 +00:00
|
|
|
fs::write(filename.as_path(), bytes.as_slice())?;
|
2023-01-10 19:59:30 -08:00
|
|
|
|
2023-01-28 12:29:06 +00:00
|
|
|
msg.downloaded = true;
|
2023-01-31 12:04:10 +00:00
|
|
|
} else if !flags.contains(DownloadFlags::OPEN) {
|
|
|
|
let msg = format!(
|
|
|
|
"The file {} already exists; add ! to end of command to overwrite it.",
|
|
|
|
filename.display()
|
|
|
|
);
|
|
|
|
let err = UIError::Failure(msg);
|
|
|
|
|
|
|
|
return Err(err);
|
2023-01-28 12:29:06 +00:00
|
|
|
}
|
2023-01-10 19:59:30 -08:00
|
|
|
|
2023-01-28 12:29:06 +00:00
|
|
|
let info = if flags.contains(DownloadFlags::OPEN) {
|
|
|
|
// open::that may not return until the spawned program closes.
|
|
|
|
let target = filename.clone().into_os_string();
|
|
|
|
tokio::task::spawn_blocking(move || open::that(target));
|
2023-01-10 19:59:30 -08:00
|
|
|
|
2023-01-28 12:29:06 +00:00
|
|
|
InfoMessage::from(format!(
|
|
|
|
"Attachment downloaded to {} and opened",
|
|
|
|
filename.display()
|
|
|
|
))
|
|
|
|
} else {
|
|
|
|
InfoMessage::from(format!(
|
|
|
|
"Attachment downloaded to {}",
|
|
|
|
filename.display()
|
|
|
|
))
|
|
|
|
};
|
2023-01-10 19:59:30 -08:00
|
|
|
|
|
|
|
return Ok(info.into());
|
|
|
|
}
|
|
|
|
|
|
|
|
Err(IambError::NoAttachment.into())
|
|
|
|
},
|
2023-01-19 16:05:02 -08:00
|
|
|
MessageAction::Edit => {
|
|
|
|
if msg.sender != settings.profile.user_id {
|
|
|
|
let msg = "Cannot edit messages sent by someone else";
|
|
|
|
let err = UIError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
let ev = match &msg.event {
|
|
|
|
MessageEvent::Original(ev) => &ev.content,
|
2023-01-26 15:40:16 -08:00
|
|
|
MessageEvent::Local(_, ev) => ev.deref(),
|
2023-01-19 16:05:02 -08:00
|
|
|
_ => {
|
|
|
|
let msg = "Cannot edit a redacted message";
|
|
|
|
let err = UIError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
let text = match &ev.msgtype {
|
|
|
|
MessageType::Text(msg) => msg.body.as_str(),
|
|
|
|
_ => {
|
|
|
|
let msg = "Cannot edit a non-text message";
|
|
|
|
let err = UIError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
self.tbox.set_text(text);
|
|
|
|
self.editing = self.scrollback.get_key(info);
|
2023-01-25 17:00:50 -08:00
|
|
|
self.focus = RoomFocus::MessageBar;
|
2023-01-19 16:05:02 -08:00
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
},
|
2023-02-09 17:53:33 -08:00
|
|
|
MessageAction::React(emoji) => {
|
|
|
|
let room = self.get_joined(&store.application.worker)?;
|
|
|
|
let event_id = match &msg.event {
|
2023-03-13 16:43:04 -07:00
|
|
|
MessageEvent::EncryptedOriginal(ev) => ev.event_id.clone(),
|
|
|
|
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
2023-02-09 17:53:33 -08:00
|
|
|
MessageEvent::Original(ev) => ev.event_id.clone(),
|
|
|
|
MessageEvent::Local(event_id, _) => event_id.clone(),
|
|
|
|
MessageEvent::Redacted(_) => {
|
|
|
|
let msg = "Cannot react to a redacted message";
|
|
|
|
let err = UIError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
};
|
2023-01-13 17:53:54 -08:00
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
let reaction = Reaction::new(event_id, emoji);
|
|
|
|
let msg = ReactionEventContent::new(reaction);
|
|
|
|
let _ = room.send(msg, None).await.map_err(IambError::from)?;
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
},
|
|
|
|
MessageAction::Redact(reason) => {
|
|
|
|
let room = self.get_joined(&store.application.worker)?;
|
2023-01-13 17:53:54 -08:00
|
|
|
let event_id = match &msg.event {
|
2023-03-13 16:43:04 -07:00
|
|
|
MessageEvent::EncryptedOriginal(ev) => ev.event_id.clone(),
|
|
|
|
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
2023-01-13 17:53:54 -08:00
|
|
|
MessageEvent::Original(ev) => ev.event_id.clone(),
|
2023-01-26 15:40:16 -08:00
|
|
|
MessageEvent::Local(event_id, _) => event_id.clone(),
|
2023-01-13 17:53:54 -08:00
|
|
|
MessageEvent::Redacted(_) => {
|
2023-02-09 17:53:33 -08:00
|
|
|
let msg = "Cannot redact already redacted message";
|
2023-01-13 17:53:54 -08:00
|
|
|
let err = UIError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
let event_id = event_id.as_ref();
|
|
|
|
let reason = reason.as_deref();
|
|
|
|
let _ = room.redact(event_id, reason, None).await.map_err(IambError::from)?;
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
},
|
2023-01-12 21:20:32 -08:00
|
|
|
MessageAction::Reply => {
|
|
|
|
self.reply_to = self.scrollback.get_key(info);
|
|
|
|
self.focus = RoomFocus::MessageBar;
|
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
Ok(None)
|
|
|
|
},
|
|
|
|
MessageAction::Unreact(emoji) => {
|
|
|
|
let room = self.get_joined(&store.application.worker)?;
|
|
|
|
let event_id: &EventId = match &msg.event {
|
2023-03-13 16:43:04 -07:00
|
|
|
MessageEvent::EncryptedOriginal(ev) => ev.event_id.as_ref(),
|
|
|
|
MessageEvent::EncryptedRedacted(ev) => ev.event_id.as_ref(),
|
2023-02-09 17:53:33 -08:00
|
|
|
MessageEvent::Original(ev) => ev.event_id.as_ref(),
|
|
|
|
MessageEvent::Local(event_id, _) => event_id.as_ref(),
|
|
|
|
MessageEvent::Redacted(_) => {
|
|
|
|
let msg = "Cannot unreact to a redacted message";
|
|
|
|
let err = UIError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
let reactions = match info.reactions.get(event_id) {
|
|
|
|
Some(r) => r,
|
|
|
|
None => return Ok(None),
|
|
|
|
};
|
|
|
|
|
|
|
|
let reactions = reactions.iter().filter_map(|(event_id, (reaction, user_id))| {
|
|
|
|
if user_id != &settings.profile.user_id {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(emoji) = &emoji {
|
|
|
|
if emoji == reaction {
|
|
|
|
return Some(event_id);
|
|
|
|
} else {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return Some(event_id);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
for reaction in reactions {
|
|
|
|
let _ = room.redact(reaction, None, None).await.map_err(IambError::from)?;
|
|
|
|
}
|
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
Ok(None)
|
|
|
|
},
|
2023-01-10 19:59:30 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn send_command(
|
|
|
|
&mut self,
|
|
|
|
act: SendAction,
|
|
|
|
_: ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> IambResult<EditInfo> {
|
|
|
|
let room = store
|
|
|
|
.application
|
|
|
|
.worker
|
|
|
|
.client
|
|
|
|
.get_joined_room(self.id())
|
|
|
|
.ok_or(IambError::NotJoined)?;
|
2023-03-01 18:46:33 -08:00
|
|
|
let info = store.application.rooms.get_or_default(self.id().to_owned());
|
2023-01-19 16:05:02 -08:00
|
|
|
let mut show_echo = true;
|
2023-01-10 19:59:30 -08:00
|
|
|
|
|
|
|
let (event_id, msg) = match act {
|
|
|
|
SendAction::Submit => {
|
2023-03-13 09:19:10 -07:00
|
|
|
let msg = self.tbox.get();
|
2023-01-10 19:59:30 -08:00
|
|
|
|
2023-03-13 09:19:10 -07:00
|
|
|
if msg.is_blank() {
|
2023-01-10 19:59:30 -08:00
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
2023-03-13 09:19:10 -07:00
|
|
|
let msg = TextMessageEventContent::markdown(msg.to_string());
|
2023-01-10 19:59:30 -08:00
|
|
|
let msg = MessageType::Text(msg);
|
2023-01-12 21:20:32 -08:00
|
|
|
|
|
|
|
let mut msg = RoomMessageEventContent::new(msg);
|
|
|
|
|
2023-01-19 16:05:02 -08:00
|
|
|
if let Some((_, event_id)) = &self.editing {
|
|
|
|
msg.relates_to = Some(Relation::Replacement(Replacement::new(
|
|
|
|
event_id.clone(),
|
|
|
|
Box::new(msg.clone()),
|
|
|
|
)));
|
|
|
|
|
|
|
|
show_echo = false;
|
|
|
|
} else if let Some(m) = self.get_reply_to(info) {
|
2023-01-12 21:20:32 -08:00
|
|
|
// XXX: Switch to RoomMessageEventContent::reply() once it's stable?
|
|
|
|
msg = msg.make_reply_to(m);
|
|
|
|
}
|
2023-01-10 19:59:30 -08:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
// Reset message bar state now that it's been sent.
|
|
|
|
self.reset();
|
2023-01-10 19:59:30 -08:00
|
|
|
|
|
|
|
(event_id, msg)
|
|
|
|
},
|
|
|
|
SendAction::Upload(file) => {
|
|
|
|
let path = Path::new(file.as_str());
|
|
|
|
let mime = mime_guess::from_path(path).first_or(mime::APPLICATION_OCTET_STREAM);
|
|
|
|
|
|
|
|
let bytes = fs::read(path)?;
|
|
|
|
let name = path
|
|
|
|
.file_name()
|
|
|
|
.map(OsStr::to_string_lossy)
|
|
|
|
.unwrap_or_else(|| Cow::from("Attachment"));
|
|
|
|
let config = AttachmentConfig::new();
|
|
|
|
|
|
|
|
let resp = room
|
|
|
|
.send_attachment(name.as_ref(), &mime, bytes.as_ref(), config)
|
|
|
|
.await
|
|
|
|
.map_err(IambError::from)?;
|
|
|
|
|
|
|
|
// Mock up the local echo message for the scrollback.
|
2023-01-30 13:51:32 -08:00
|
|
|
let msg = TextMessageEventContent::plain(format!("[Attached File: {name}]"));
|
2023-01-10 19:59:30 -08:00
|
|
|
let msg = MessageType::Text(msg);
|
|
|
|
let msg = RoomMessageEventContent::new(msg);
|
|
|
|
|
|
|
|
(resp.event_id, msg)
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2023-01-19 16:05:02 -08:00
|
|
|
if show_echo {
|
|
|
|
let user = store.application.settings.profile.user_id.clone();
|
2023-01-26 15:40:16 -08:00
|
|
|
let key = (MessageTimeStamp::LocalEcho, event_id.clone());
|
|
|
|
let msg = MessageEvent::Local(event_id, msg.into());
|
2023-01-19 16:05:02 -08:00
|
|
|
let msg = Message::new(msg, user, MessageTimeStamp::LocalEcho);
|
|
|
|
info.messages.insert(key, msg);
|
|
|
|
}
|
2023-01-10 19:59:30 -08:00
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
// Jump to the end of the scrollback to show the message.
|
|
|
|
self.scrollback.goto_latest();
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
pub fn focus_toggle(&mut self) {
|
|
|
|
self.focus = match self.focus {
|
|
|
|
RoomFocus::Scrollback => RoomFocus::MessageBar,
|
|
|
|
RoomFocus::MessageBar => RoomFocus::Scrollback,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-01-05 18:12:25 -08:00
|
|
|
pub fn room(&self) -> &MatrixRoom {
|
|
|
|
&self.room
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
pub fn id(&self) -> &RoomId {
|
|
|
|
&self.room_id
|
|
|
|
}
|
2023-01-03 13:57:28 -08:00
|
|
|
|
|
|
|
pub fn typing_notice(
|
|
|
|
&self,
|
|
|
|
act: &EditorAction,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) {
|
|
|
|
if !self.focus.is_msgbar() || act.is_readonly(ctx) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-01-26 15:40:16 -08:00
|
|
|
if !store.application.settings.tunables.typing_notice_send {
|
2023-01-03 13:57:28 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
store.application.worker.typing_notice(self.room_id.clone());
|
|
|
|
}
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
macro_rules! delegate {
|
|
|
|
($s: expr, $id: ident => $e: expr) => {
|
|
|
|
match $s.focus {
|
|
|
|
RoomFocus::Scrollback => {
|
|
|
|
match $s {
|
|
|
|
ChatState { scrollback: $id, .. } => $e,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
RoomFocus::MessageBar => {
|
|
|
|
match $s {
|
|
|
|
ChatState { tbox: $id, .. } => $e,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
impl WindowOps<IambInfo> for ChatState {
|
|
|
|
fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) {
|
|
|
|
Chat::new(store).focus(focused).render(area, buf, self)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn dup(&self, store: &mut ProgramStore) -> Self {
|
|
|
|
// XXX: I want each WindowSlot to have its own shared buffer, instead of each Room; need to
|
|
|
|
// find a good way to pass that info here so that it can be part of the content id.
|
|
|
|
let id = IambBufferId::Room(self.room_id.clone(), RoomFocus::MessageBar);
|
|
|
|
let ebuf = store.load_buffer(id);
|
|
|
|
let tbox = TextBoxState::new(ebuf);
|
|
|
|
|
|
|
|
ChatState {
|
|
|
|
room_id: self.room_id.clone(),
|
2023-01-05 18:12:25 -08:00
|
|
|
room: self.room.clone(),
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
tbox,
|
|
|
|
sent: self.sent.clone(),
|
|
|
|
sent_scrollback: history::ScrollbackState::Pending,
|
|
|
|
|
|
|
|
scrollback: self.scrollback.dup(store),
|
|
|
|
focus: self.focus,
|
2023-01-12 21:20:32 -08:00
|
|
|
|
|
|
|
reply_to: None,
|
2023-01-19 16:05:02 -08:00
|
|
|
editing: None,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn close(&mut self, _: CloseFlags, _: &mut ProgramStore) -> bool {
|
|
|
|
// XXX: what's the right closing behaviour for a room?
|
|
|
|
// Should write send a message?
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
2023-03-01 18:46:33 -08:00
|
|
|
fn write(
|
|
|
|
&mut self,
|
|
|
|
_: Option<&str>,
|
|
|
|
_: WriteFlags,
|
|
|
|
_: &mut ProgramStore,
|
|
|
|
) -> IambResult<EditInfo> {
|
|
|
|
// XXX: what's the right writing behaviour for a room?
|
|
|
|
// Should write send a message?
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_completions(&self) -> Option<CompletionList> {
|
|
|
|
delegate!(self, w => w.get_completions())
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
fn get_cursor_word(&self, style: &WordStyle) -> Option<String> {
|
|
|
|
delegate!(self, w => w.get_cursor_word(style))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_selected_word(&self) -> Option<String> {
|
|
|
|
delegate!(self, w => w.get_selected_word())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Editable<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
|
|
|
fn editor_command(
|
|
|
|
&mut self,
|
|
|
|
act: &EditorAction,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
2023-01-03 13:57:28 -08:00
|
|
|
self.typing_notice(act, ctx, store);
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
match delegate!(self, w => w.editor_command(act, ctx, store)) {
|
|
|
|
res @ Ok(_) => res,
|
|
|
|
Err(EditError::WrongBuffer(IambBufferId::Room(room_id, focus)))
|
|
|
|
if room_id == self.room_id && act.is_switchable(ctx) =>
|
|
|
|
{
|
|
|
|
// Switch focus.
|
|
|
|
self.focus = focus;
|
|
|
|
|
|
|
|
// Run command again.
|
|
|
|
delegate!(self, w => w.editor_command(act, ctx, store))
|
|
|
|
},
|
|
|
|
res @ Err(_) => res,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TerminalCursor for ChatState {
|
|
|
|
fn get_term_cursor(&self) -> Option<(u16, u16)> {
|
|
|
|
delegate!(self, w => w.get_term_cursor())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Jumpable<ProgramContext, IambInfo> for ChatState {
|
|
|
|
fn jump(
|
|
|
|
&mut self,
|
|
|
|
list: PositionList,
|
|
|
|
dir: MoveDir1D,
|
|
|
|
count: usize,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
) -> IambResult<usize> {
|
|
|
|
delegate!(self, w => w.jump(list, dir, count, ctx))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Scrollable<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
|
|
|
fn scroll(
|
|
|
|
&mut self,
|
|
|
|
style: &ScrollStyle,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
|
|
|
// Send all scroll commands to the scrollback.
|
|
|
|
//
|
|
|
|
// If there's enough message text for scrolling to be necessary,
|
|
|
|
// navigating with movement keys should be enough to do the job.
|
|
|
|
self.scrollback.scroll(style, ctx, store)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PromptActions<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
|
|
|
fn submit(
|
|
|
|
&mut self,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
_: &mut ProgramStore,
|
|
|
|
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
2023-01-10 19:59:30 -08:00
|
|
|
let act = SendAction::Submit;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
Ok(vec![(IambAction::from(act).into(), ctx.clone())])
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
fn abort(
|
|
|
|
&mut self,
|
|
|
|
empty: bool,
|
|
|
|
_: &ProgramContext,
|
|
|
|
_: &mut ProgramStore,
|
|
|
|
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
|
|
|
let text = self.tbox.get();
|
|
|
|
|
|
|
|
if empty && text.is_blank() {
|
|
|
|
return Ok(vec![]);
|
|
|
|
}
|
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
let text = self.reset().trim();
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
if text.is_empty() {
|
|
|
|
let _ = self.sent.end();
|
|
|
|
} else {
|
|
|
|
self.sent.select(text);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Ok(vec![]);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn recall(
|
|
|
|
&mut self,
|
|
|
|
dir: &MoveDir1D,
|
|
|
|
count: &Count,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
_: &mut ProgramStore,
|
|
|
|
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
|
|
|
let count = ctx.resolve(count);
|
|
|
|
let rope = self.tbox.get();
|
|
|
|
|
|
|
|
let text = self.sent.recall(&rope, &mut self.sent_scrollback, *dir, count);
|
|
|
|
|
|
|
|
if let Some(text) = text {
|
|
|
|
self.tbox.set_text(text);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(vec![])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Promptable<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
|
|
|
fn prompt(
|
|
|
|
&mut self,
|
|
|
|
act: &PromptAction,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
|
|
|
if let RoomFocus::Scrollback = self.focus {
|
|
|
|
return Ok(vec![]);
|
|
|
|
}
|
|
|
|
|
|
|
|
match act {
|
|
|
|
PromptAction::Submit => self.submit(ctx, store),
|
|
|
|
PromptAction::Abort(empty) => self.abort(*empty, ctx, store),
|
|
|
|
PromptAction::Recall(dir, count) => self.recall(dir, count, ctx, store),
|
|
|
|
_ => Err(EditError::Unimplemented("unknown prompt action".to_string())),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Chat<'a> {
|
|
|
|
store: &'a mut ProgramStore,
|
|
|
|
focused: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Chat<'a> {
|
|
|
|
pub fn new(store: &'a mut ProgramStore) -> Chat<'a> {
|
|
|
|
Chat { store, focused: false }
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn focus(mut self, focused: bool) -> Self {
|
|
|
|
self.focused = focused;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> StatefulWidget for Chat<'a> {
|
|
|
|
type State = ChatState;
|
|
|
|
|
|
|
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
|
|
|
let lines = state.tbox.has_lines(5).max(1) as u16;
|
|
|
|
let drawh = area.height;
|
|
|
|
let texth = lines.min(drawh).clamp(1, 5);
|
2023-01-12 21:20:32 -08:00
|
|
|
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);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
let scrollarea = Rect::new(area.x, area.y, area.width, scrollh);
|
2023-01-12 21:20:32 -08:00
|
|
|
let descarea = Rect::new(area.x, scrollarea.y + scrollh, area.width, desch);
|
|
|
|
let textarea = Rect::new(area.x, descarea.y + desch, area.width, texth);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2023-01-19 16:05:02 -08:00
|
|
|
let desc_spans = match (&state.editing, &state.reply_to) {
|
|
|
|
(None, None) => None,
|
|
|
|
(Some(_), _) => Some(Spans::from("Editing message")),
|
|
|
|
(_, Some(_)) => {
|
|
|
|
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()
|
|
|
|
})
|
|
|
|
},
|
|
|
|
};
|
2023-01-12 21:20:32 -08:00
|
|
|
|
|
|
|
if let Some(desc_spans) = desc_spans {
|
|
|
|
Paragraph::new(desc_spans).render(descarea, buf);
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
let prompt = if self.focused { "> " } else { " " };
|
|
|
|
|
|
|
|
let tbox = TextBox::new().prompt(prompt);
|
|
|
|
tbox.render(textarea, buf, &mut state.tbox);
|
|
|
|
}
|
|
|
|
}
|