mirror of https://git.meli.delivery/meli/meli
mail/listing.rs: move mail view to listing parent component
Instead of having a different widget to view mail in for each Listing (plain, threaded, compact, etc) use a single widget in the listing's parent type. This will help with making the listing logic more modular in future refactors to allow all combinations of listing/mail view/ thread view positions and layouts.pull/227/head
parent
5c9b3fb044
commit
575509f1ed
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
* meli
|
||||||
|
*
|
||||||
|
* Copyright 2017 Manos Pitsidianakis
|
||||||
|
*
|
||||||
|
* This file is part of meli.
|
||||||
|
*
|
||||||
|
* meli is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* meli is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use melib::{Envelope, Error, Mail, Result};
|
||||||
|
|
||||||
|
use super::{EnvelopeView, MailView, ViewSettings};
|
||||||
|
use crate::{jobs::JoinHandle, mailbox_settings, Component, Context, ShortcutMaps, UIEvent};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum PendingReplyAction {
|
||||||
|
Reply,
|
||||||
|
ReplyToAuthor,
|
||||||
|
ReplyToAll,
|
||||||
|
ForwardAttachment,
|
||||||
|
ForwardInline,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MailViewState {
|
||||||
|
Init {
|
||||||
|
pending_action: Option<PendingReplyAction>,
|
||||||
|
},
|
||||||
|
LoadingBody {
|
||||||
|
handle: JoinHandle<Result<Vec<u8>>>,
|
||||||
|
pending_action: Option<PendingReplyAction>,
|
||||||
|
},
|
||||||
|
Error {
|
||||||
|
err: Error,
|
||||||
|
},
|
||||||
|
Loaded {
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
env: Box<Envelope>,
|
||||||
|
env_view: Box<EnvelopeView>,
|
||||||
|
stack: Vec<Box<dyn Component>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MailViewState {
|
||||||
|
pub fn load_bytes(self_: &mut MailView, bytes: Vec<u8>, context: &mut Context) {
|
||||||
|
let Some(coordinates) = self_.coordinates else { return; };
|
||||||
|
let account = &mut context.accounts[&coordinates.0];
|
||||||
|
if account
|
||||||
|
.collection
|
||||||
|
.get_env(coordinates.2)
|
||||||
|
.other_headers()
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
let _ = account
|
||||||
|
.collection
|
||||||
|
.get_env_mut(coordinates.2)
|
||||||
|
.populate_headers(&bytes);
|
||||||
|
}
|
||||||
|
let env = Box::new(account.collection.get_env(coordinates.2).clone());
|
||||||
|
let env_view = Box::new(EnvelopeView::new(
|
||||||
|
Mail {
|
||||||
|
envelope: *env.clone(),
|
||||||
|
bytes: bytes.clone(),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(ViewSettings {
|
||||||
|
theme_default: crate::conf::value(context, "theme_default"),
|
||||||
|
body_theme: crate::conf::value(context, "mail.view.body"),
|
||||||
|
env_view_shortcuts: mailbox_settings!(
|
||||||
|
context[coordinates.0][&coordinates.1]
|
||||||
|
.shortcuts
|
||||||
|
.envelope_view
|
||||||
|
)
|
||||||
|
.key_values(),
|
||||||
|
pager_filter: mailbox_settings!(
|
||||||
|
context[coordinates.0][&coordinates.1].pager.filter
|
||||||
|
)
|
||||||
|
.clone(),
|
||||||
|
html_filter: mailbox_settings!(
|
||||||
|
context[coordinates.0][&coordinates.1].pager.html_filter
|
||||||
|
)
|
||||||
|
.clone(),
|
||||||
|
url_launcher: mailbox_settings!(
|
||||||
|
context[coordinates.0][&coordinates.1].pager.url_launcher
|
||||||
|
)
|
||||||
|
.clone(),
|
||||||
|
auto_choose_multipart_alternative: mailbox_settings!(
|
||||||
|
context[coordinates.0][&coordinates.1]
|
||||||
|
.pager
|
||||||
|
.auto_choose_multipart_alternative
|
||||||
|
)
|
||||||
|
.is_true(),
|
||||||
|
expand_headers: false,
|
||||||
|
sticky_headers: *mailbox_settings!(
|
||||||
|
context[coordinates.0][&coordinates.1].pager.sticky_headers
|
||||||
|
),
|
||||||
|
show_date_in_my_timezone: mailbox_settings!(
|
||||||
|
context[coordinates.0][&coordinates.1]
|
||||||
|
.pager
|
||||||
|
.show_date_in_my_timezone
|
||||||
|
)
|
||||||
|
.is_true(),
|
||||||
|
show_extra_headers: mailbox_settings!(
|
||||||
|
context[coordinates.0][&coordinates.1]
|
||||||
|
.pager
|
||||||
|
.show_extra_headers
|
||||||
|
)
|
||||||
|
.clone(),
|
||||||
|
auto_verify_signatures: *mailbox_settings!(
|
||||||
|
context[coordinates.0][&coordinates.1]
|
||||||
|
.pgp
|
||||||
|
.auto_verify_signatures
|
||||||
|
),
|
||||||
|
auto_decrypt: *mailbox_settings!(
|
||||||
|
context[coordinates.0][&coordinates.1].pgp.auto_decrypt
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
context.main_loop_handler.clone(),
|
||||||
|
));
|
||||||
|
self_.state = MailViewState::Loaded {
|
||||||
|
env,
|
||||||
|
bytes,
|
||||||
|
env_view,
|
||||||
|
stack: vec![],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dirty(&self) -> bool {
|
||||||
|
matches!(self, Self::Loaded { ref env_view, .. } if env_view.is_dirty())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_dirty(&mut self, dirty: bool) {
|
||||||
|
if let Self::Loaded {
|
||||||
|
ref mut env_view, ..
|
||||||
|
} = self
|
||||||
|
{
|
||||||
|
env_view.set_dirty(dirty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||||
|
if let Self::Loaded { ref env_view, .. } = self {
|
||||||
|
env_view.shortcuts(context)
|
||||||
|
} else {
|
||||||
|
ShortcutMaps::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||||
|
if let Self::Loaded {
|
||||||
|
ref mut env_view, ..
|
||||||
|
} = self
|
||||||
|
{
|
||||||
|
env_view.process_event(event, context)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MailViewState {
|
||||||
|
fn default() -> Self {
|
||||||
|
MailViewState::Init {
|
||||||
|
pending_action: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* meli
|
||||||
|
*
|
||||||
|
* Copyright 2017 Manos Pitsidianakis
|
||||||
|
*
|
||||||
|
* This file is part of meli.
|
||||||
|
*
|
||||||
|
* meli is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* meli is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use melib::{attachment_types::Charset, pgp::DecryptionMetadata, Attachment, Error, Result};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
conf::shortcuts::EnvelopeViewShortcuts,
|
||||||
|
jobs::{JobId, JoinHandle},
|
||||||
|
ShortcutMap, ThemeAttribute, UIDialog,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ViewSettings {
|
||||||
|
pub pager_filter: Option<String>,
|
||||||
|
pub html_filter: Option<String>,
|
||||||
|
pub url_launcher: Option<String>,
|
||||||
|
pub expand_headers: bool,
|
||||||
|
pub theme_default: ThemeAttribute,
|
||||||
|
pub env_view_shortcuts: ShortcutMap,
|
||||||
|
/// `"mail.view.body"`
|
||||||
|
pub body_theme: ThemeAttribute,
|
||||||
|
pub auto_choose_multipart_alternative: bool,
|
||||||
|
pub sticky_headers: bool,
|
||||||
|
pub show_date_in_my_timezone: bool,
|
||||||
|
pub show_extra_headers: Vec<String>,
|
||||||
|
pub auto_verify_signatures: bool,
|
||||||
|
pub auto_decrypt: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ViewSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
theme_default: Default::default(),
|
||||||
|
body_theme: Default::default(),
|
||||||
|
pager_filter: None,
|
||||||
|
html_filter: None,
|
||||||
|
url_launcher: None,
|
||||||
|
env_view_shortcuts: EnvelopeViewShortcuts::default().key_values(),
|
||||||
|
auto_choose_multipart_alternative: true,
|
||||||
|
expand_headers: false,
|
||||||
|
sticky_headers: false,
|
||||||
|
show_date_in_my_timezone: false,
|
||||||
|
show_extra_headers: vec![],
|
||||||
|
auto_verify_signatures: true,
|
||||||
|
auto_decrypt: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
|
||||||
|
pub enum LinkKind {
|
||||||
|
Url,
|
||||||
|
Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
|
||||||
|
pub struct Link {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub kind: self::LinkKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub enum ForceCharset {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Dialog(Box<UIDialog<Option<Charset>>>),
|
||||||
|
Forced(Charset),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Option<Charset>> for &ForceCharset {
|
||||||
|
fn into(self) -> Option<Charset> {
|
||||||
|
match self {
|
||||||
|
ForceCharset::Forced(val) => Some(*val),
|
||||||
|
ForceCharset::None | ForceCharset::Dialog(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||||
|
pub enum Source {
|
||||||
|
Decoded,
|
||||||
|
Raw,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Default)]
|
||||||
|
pub enum ViewMode {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
Url,
|
||||||
|
Attachment(usize),
|
||||||
|
Source(Source),
|
||||||
|
Subview,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! is_variant {
|
||||||
|
($n:ident, $($var:tt)+) => {
|
||||||
|
#[inline]
|
||||||
|
pub fn $n(&self) -> bool {
|
||||||
|
matches!(self, Self::$($var)*)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewMode {
|
||||||
|
is_variant! { is_normal, Normal }
|
||||||
|
is_variant! { is_url, Url }
|
||||||
|
is_variant! { is_attachment, Attachment(_) }
|
||||||
|
is_variant! { is_source, Source(_) }
|
||||||
|
is_variant! { is_subview, Subview }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AttachmentDisplay {
|
||||||
|
Alternative {
|
||||||
|
inner: Box<Attachment>,
|
||||||
|
shown_display: usize,
|
||||||
|
display: Vec<AttachmentDisplay>,
|
||||||
|
},
|
||||||
|
InlineText {
|
||||||
|
inner: Box<Attachment>,
|
||||||
|
comment: Option<String>,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
InlineOther {
|
||||||
|
inner: Box<Attachment>,
|
||||||
|
},
|
||||||
|
Attachment {
|
||||||
|
inner: Box<Attachment>,
|
||||||
|
},
|
||||||
|
SignedPending {
|
||||||
|
inner: Box<Attachment>,
|
||||||
|
display: Vec<AttachmentDisplay>,
|
||||||
|
handle: JoinHandle<Result<()>>,
|
||||||
|
job_id: JobId,
|
||||||
|
},
|
||||||
|
SignedFailed {
|
||||||
|
inner: Box<Attachment>,
|
||||||
|
display: Vec<AttachmentDisplay>,
|
||||||
|
error: Error,
|
||||||
|
},
|
||||||
|
SignedUnverified {
|
||||||
|
inner: Box<Attachment>,
|
||||||
|
display: Vec<AttachmentDisplay>,
|
||||||
|
},
|
||||||
|
SignedVerified {
|
||||||
|
inner: Box<Attachment>,
|
||||||
|
display: Vec<AttachmentDisplay>,
|
||||||
|
description: String,
|
||||||
|
},
|
||||||
|
EncryptedPending {
|
||||||
|
inner: Box<Attachment>,
|
||||||
|
handle: JoinHandle<Result<(DecryptionMetadata, Vec<u8>)>>,
|
||||||
|
},
|
||||||
|
EncryptedFailed {
|
||||||
|
inner: Box<Attachment>,
|
||||||
|
error: Error,
|
||||||
|
},
|
||||||
|
EncryptedSuccess {
|
||||||
|
inner: Box<Attachment>,
|
||||||
|
plaintext: Box<Attachment>,
|
||||||
|
plaintext_display: Vec<AttachmentDisplay>,
|
||||||
|
description: String,
|
||||||
|
},
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* meli
|
||||||
|
*
|
||||||
|
* Copyright 2017-2018 Manos Pitsidianakis
|
||||||
|
*
|
||||||
|
* This file is part of meli.
|
||||||
|
*
|
||||||
|
* meli is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* meli is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::{fs::File, io::Write, os::unix::fs::PermissionsExt, path::Path};
|
||||||
|
|
||||||
|
use melib::Result;
|
||||||
|
|
||||||
|
pub fn save_attachment(path: &Path, bytes: &[u8]) -> Result<()> {
|
||||||
|
let mut f = File::create(path)?;
|
||||||
|
let mut permissions = f.metadata()?.permissions();
|
||||||
|
permissions.set_mode(0o600); // Read/write for owner only.
|
||||||
|
f.set_permissions(permissions)?;
|
||||||
|
f.write_all(bytes)?;
|
||||||
|
f.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> String {
|
||||||
|
/* Purge unused field codes */
|
||||||
|
let command = command
|
||||||
|
.replace("%i", "")
|
||||||
|
.replace("%c", "")
|
||||||
|
.replace("%k", "");
|
||||||
|
if command.contains("%f") {
|
||||||
|
command.replacen("%f", &path.replace(' ', "\\ "), 1)
|
||||||
|
} else if command.contains("%F") {
|
||||||
|
command.replacen("%F", &path.replace(' ', "\\ "), 1)
|
||||||
|
} else if command.contains("%u") || command.contains("%U") {
|
||||||
|
let from_pattern = if command.contains("%u") { "%u" } else { "%U" };
|
||||||
|
if is_url {
|
||||||
|
command.replacen(from_pattern, &path, 1)
|
||||||
|
} else {
|
||||||
|
command.replacen(
|
||||||
|
from_pattern,
|
||||||
|
&format!("file://{}", path).replace(' ', "\\ "),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if is_url {
|
||||||
|
format!("{} {}", command, path)
|
||||||
|
} else {
|
||||||
|
format!("{} {}", command, path.replace(' ', "\\ "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_desktop_exec() {
|
||||||
|
assert_eq!(
|
||||||
|
"ristretto /tmp/file".to_string(),
|
||||||
|
desktop_exec_to_command("ristretto %F", "/tmp/file".to_string(), false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"/usr/lib/firefox-esr/firefox-esr file:///tmp/file".to_string(),
|
||||||
|
desktop_exec_to_command(
|
||||||
|
"/usr/lib/firefox-esr/firefox-esr %u",
|
||||||
|
"/tmp/file".to_string(),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"/usr/lib/firefox-esr/firefox-esr www.example.com".to_string(),
|
||||||
|
desktop_exec_to_command(
|
||||||
|
"/usr/lib/firefox-esr/firefox-esr %u",
|
||||||
|
"www.example.com".to_string(),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"/usr/bin/vlc --started-from-file www.example.com".to_string(),
|
||||||
|
desktop_exec_to_command(
|
||||||
|
"/usr/bin/vlc --started-from-file %U",
|
||||||
|
"www.example.com".to_string(),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"zathura --fork file:///tmp/file".to_string(),
|
||||||
|
desktop_exec_to_command("zathura --fork %U", "file:///tmp/file".to_string(), true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue