Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: edit messages #301

Merged
merged 13 commits into from
Jul 30, 2024

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.6.0-dev

## Added

- Edit messages ([#301])

[#301]: https://github.com/boxdot/gurk-rs/pull/301

## 0.5.0

New configuration which enables encryption of the signal keystore and
Expand Down
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ debug = true
dev = ["prost", "base64"]

[dependencies]
presage = { git = "https://github.com/whisperfish/presage", rev = "e2392c42a0392397b9db782607fdd7ab2ea91b5f" }
presage-store-sled = { git = "https://github.com/whisperfish/presage", rev = "e2392c42a0392397b9db782607fdd7ab2ea91b5f" }
presage = { git = "https://github.com/whisperfish/presage", rev = "67d98a3cb021dd365a671be067abc69deb71c736" }
presage-store-sled = { git = "https://github.com/whisperfish/presage", rev = "67d98a3cb021dd365a671be067abc69deb71c736" }

# dev feature dependencies
prost = { version = "0.12.0", optional = true }
Expand All @@ -49,7 +49,6 @@ hex = "0.4.3"
hostname = "0.4.0"
image = { version = "0.25.0", default-features = false, features = ["png"] }
itertools = "0.12.0"
# not used directly; brings sqlcipher capabilities to sqlite
libsqlite3-sys = { version = "0.27.0", features = ["bundled-sqlcipher-vendored-openssl"] }
log-panics = "2.1.0"
mime_guess = "2.0.4"
Expand Down Expand Up @@ -81,6 +80,10 @@ whoami = "1.2.3"
url = "2.5.0"
tempfile = "3.3.0"

[package.metadata.cargo-machete]
# not used directly; brings sqlcipher capabilities to sqlite
ignored = ["libsqlite3-sys"]

[dev-dependencies]
criterion = { version = "0.5", features = ["async_tokio", "html_reports"] }
hex-literal = "0.4.1"
Expand Down
99 changes: 80 additions & 19 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub struct App {
receipt_handler: ReceiptHandler,
pub input: Input,
pub is_multiline_input: bool,
editing: Option<MessageId>,
pub(crate) select_channel: SelectChannel,
clipboard: Option<Clipboard>,
event_tx: mpsc::UnboundedSender<Event>,
Expand Down Expand Up @@ -119,6 +120,7 @@ impl App {
receipt_handler: ReceiptHandler::new(),
input: Default::default(),
is_multiline_input: false,
editing: None,
select_channel: Default::default(),
clipboard,
event_tx,
Expand Down Expand Up @@ -256,7 +258,14 @@ impl App {
self.get_input().on_end();
}
KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.get_input().on_end();
if !self.select_channel.is_shown
&& self.channels.selected_item().is_some()
&& self.input.is_empty()
{
self.start_editing();
} else {
self.get_input().on_end();
}
}
KeyCode::Backspace => {
self.get_input().on_backspace();
Expand All @@ -270,7 +279,7 @@ impl App {
KeyCode::Esc => {
if self.select_channel.is_shown {
self.select_channel.is_shown = false;
} else {
} else if !self.reset_editing() {
self.reset_message_selection();
}
}
Expand Down Expand Up @@ -305,14 +314,18 @@ impl App {
Some(())
}

fn selected_message(&self) -> Option<Cow<Message>> {
fn selected_message_id(&self) -> Option<MessageId> {
// Messages are shown in reversed order => selected is reversed
let channel_id = self.channels.selected_item()?;
let messages = self.messages.get(channel_id)?;
let idx = messages.state.selected()?;
let idx = messages.items.len().checked_sub(idx + 1)?;
let arrived_at = messages.items.get(idx)?;
let message_id = MessageId::new(*channel_id, *arrived_at);
Some(MessageId::new(*channel_id, *arrived_at))
}

fn selected_message(&self) -> Option<Cow<Message>> {
let message_id = self.selected_message_id()?;
self.storage.message(message_id)
}

Expand Down Expand Up @@ -388,10 +401,15 @@ impl App {
.storage
.channel(channel_id)
.expect("non-existent channel");
let quote = self.selected_message();
let (sent_message, response) =
self.signal_manager
.send_text(&channel, input, quote.as_deref(), attachments);
let editing = self.editing.take();
let quote = editing.is_none().then(|| self.selected_message()).flatten();
let (sent_message, response) = self.signal_manager.send_text(
&channel,
input,
quote.as_deref(),
editing.map(|id| id.arrived_at),
attachments,
);

let message_id = MessageId::new(channel_id, sent_message.arrived_at);
let tx = self.event_tx.clone();
Expand All @@ -404,18 +422,20 @@ impl App {
}
});

let sent_message = self.storage.store_message(channel_id, sent_message);
self.messages
.get_mut(&channel_id)
.expect("non-existent channel")
.items
.push(sent_message.arrived_at);
if let Some(id) = editing {
self.storage
.store_edited_message(channel_id, id.arrived_at, sent_message);
} else {
let sent_message = self.storage.store_message(channel_id, sent_message);
self.messages
.get_mut(&channel_id)
.expect("non-existent channel")
.items
.push(sent_message.arrived_at);
};

let sent_with_quote = sent_message.quote.is_some();
self.reset_message_selection();
self.reset_unread_messages();
if sent_with_quote {
self.reset_message_selection();
}
self.bubble_up_channel(channel_idx);
}

Expand Down Expand Up @@ -460,7 +480,7 @@ impl App {
}

pub async fn on_message(&mut self, content: Content) -> anyhow::Result<()> {
// tracing::debug!("incoming: {:#?}", content);
tracing::info!(?content, "incoming");

#[cfg(feature = "dev")]
if self.config.developer.dump_raw_messages {
Expand Down Expand Up @@ -1435,6 +1455,47 @@ impl App {
}
Ok(())
}

pub(crate) fn is_editing(&self) -> bool {
self.editing.is_some()
}

/// Returns `true` if editing was reset, otherwise `false`
fn reset_editing(&mut self) -> bool {
let is_reset = self.editing.take().is_some();
if is_reset {
self.take_input();
}
is_reset
}

fn start_editing(&mut self) -> Option<()> {
if !self.input.is_empty() {
return None;
}

let message_id = self.selected_message_id()?;
let message = self.storage.message(message_id)?;

if message.from_id != self.user_id {
return None;
}

let target_sent_timestamp = self
.storage
.edits(message_id)
.last()
.map(|last_edit| last_edit.arrived_at)
.unwrap_or(message.arrived_at);
let message_id = MessageId::new(message_id.channel_id, target_sent_timestamp);
let text = message.message.clone()?;

self.editing.replace(message_id);
self.input.data = text;
self.input.on_end();

Some(())
}
}

#[derive(Debug, Default)]
Expand Down
2 changes: 2 additions & 0 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ impl App {
return Ok(());
};

tracing::info!(?sync_message, "#########");

// edit message
if let Some(Sent {
edit_message:
Expand Down
4 changes: 4 additions & 0 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ impl Input {
self.cursor.delete_suffix(&mut self.data);
}

pub fn is_empty(&self) -> bool {
self.data.is_empty()
}

pub fn take(&mut self) -> String {
self.cursor = Default::default();
std::mem::take(&mut self.data)
Expand Down
4 changes: 4 additions & 0 deletions src/shortcuts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ pub static SHORTCUTS: &[ShortCut] = &[
event: "ctrl+e / end",
description: "Move cursor the the end of the text.",
},
ShortCut {
event: "ctrl+e",
description: "Edit selected message",
},
ShortCut {
event: "Esc",
description: "Reset message selection / Close popup.",
Expand Down
Loading