web: add thread replies to post view
parent
3b3665b40c
commit
0e333af4e5
|
@ -79,6 +79,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
}
|
||||
|
||||
compressed.write_all(b"&[")?;
|
||||
for (name, template_path) in templates {
|
||||
let mut templ = OpenOptions::new()
|
||||
.write(false)
|
||||
|
@ -98,6 +99,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
compressed.write_all(format!("{:?}", compressed_bytes).as_bytes())?;
|
||||
compressed.write_all(b"),")?;
|
||||
}
|
||||
compressed.write_all(b"]")?;
|
||||
|
||||
commit_sha();
|
||||
Ok(())
|
||||
|
|
100
web/src/lists.rs
100
web/src/lists.rs
|
@ -47,13 +47,31 @@ pub async fn list(
|
|||
.map(|user| db.list_subscription_by_address(list.pk, &user.address).ok());
|
||||
|
||||
let posts = db.list_posts(list.pk, None)?;
|
||||
let post_map = posts
|
||||
.iter()
|
||||
.map(|p| (p.message_id.as_str(), p))
|
||||
.collect::<IndexMap<&str, &mailpot::models::DbVal<mailpot::models::Post>>>();
|
||||
let mut hist = months
|
||||
.iter()
|
||||
.map(|m| (m.to_string(), [0usize; 31]))
|
||||
.collect::<HashMap<String, [usize; 31]>>();
|
||||
let posts_ctx = posts
|
||||
.iter()
|
||||
.map(|post| {
|
||||
let envelopes: Arc<std::sync::RwLock<HashMap<melib::EnvelopeHash, melib::Envelope>>> =
|
||||
Default::default();
|
||||
let mut env_lock = envelopes.write().unwrap();
|
||||
|
||||
for post in &posts {
|
||||
let envelope = melib::Envelope::from_bytes(post.message.as_slice(), None)
|
||||
.expect("Could not parse mail");
|
||||
env_lock.insert(envelope.hash(), envelope);
|
||||
}
|
||||
let mut threads: melib::Threads = melib::Threads::new(posts.len());
|
||||
drop(env_lock);
|
||||
threads.amend(&envelopes);
|
||||
let roots = thread_roots(&envelopes, &mut threads);
|
||||
let posts_ctx = roots
|
||||
.into_iter()
|
||||
.map(|(thread, length, _timestamp)| {
|
||||
let post = &post_map[&thread.message_id.as_str()];
|
||||
//2019-07-14T14:21:02
|
||||
if let Some(day) = post.datetime.get(8..10).and_then(|d| d.parse::<u64>().ok()) {
|
||||
hist.get_mut(&post.month_year).unwrap()[day.saturating_sub(1) as usize] += 1;
|
||||
|
@ -70,7 +88,7 @@ pub async fn list(
|
|||
{
|
||||
subject_ref = subject_ref[2 + list.id.len()..].trim();
|
||||
}
|
||||
minijinja::context! {
|
||||
let ret = minijinja::context! {
|
||||
pk => post.pk,
|
||||
list => post.list,
|
||||
subject => subject_ref,
|
||||
|
@ -79,8 +97,10 @@ pub async fn list(
|
|||
message => post.message,
|
||||
timestamp => post.timestamp,
|
||||
datetime => post.datetime,
|
||||
root_url_prefix => &state.root_url_prefix,
|
||||
}
|
||||
replies => length.saturating_sub(1),
|
||||
last_active => thread.datetime,
|
||||
};
|
||||
ret
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let crumbs = vec![
|
||||
|
@ -123,7 +143,7 @@ pub async fn list_post(
|
|||
auth: AuthContext,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Html<String>, ResponseError> {
|
||||
let db = Connection::open_db(state.conf.clone())?;
|
||||
let db = Connection::open_db(state.conf.clone())?.trusted();
|
||||
let Some(list) = (match id {
|
||||
ListPathIdentifier::Pk(id) => db.list(id)?,
|
||||
ListPathIdentifier::Id(id) => db.list_by_id(id)?,
|
||||
|
@ -146,6 +166,7 @@ pub async fn list_post(
|
|||
StatusCode::NOT_FOUND,
|
||||
));
|
||||
};
|
||||
let thread = super::utils::thread_db(&db, list.pk, &post.message_id);
|
||||
let envelope = melib::Envelope::from_bytes(post.message.as_slice(), None)
|
||||
.with_status(StatusCode::BAD_REQUEST)?;
|
||||
let body = envelope.body_bytes(post.message.as_slice());
|
||||
|
@ -190,6 +211,7 @@ pub async fn list_post(
|
|||
message => post.message,
|
||||
timestamp => post.timestamp,
|
||||
datetime => post.datetime,
|
||||
thread => thread,
|
||||
root_url_prefix => &state.root_url_prefix,
|
||||
current_user => auth.current_user,
|
||||
user_context => user_context,
|
||||
|
@ -485,3 +507,67 @@ pub enum SubscriptionPolicySettings {
|
|||
Request,
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// Raw post page.
|
||||
pub async fn post_raw(
|
||||
ListPostRawPath(id, msg_id): ListPostRawPath,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<String, ResponseError> {
|
||||
let db = Connection::open_db(state.conf.clone())?.trusted();
|
||||
let Some(list) = (match id {
|
||||
ListPathIdentifier::Pk(id) => db.list(id)?,
|
||||
ListPathIdentifier::Id(id) => db.list_by_id(id)?,
|
||||
}) else {
|
||||
return Err(ResponseError::new(
|
||||
"List not found".to_string(),
|
||||
StatusCode::NOT_FOUND,
|
||||
));
|
||||
};
|
||||
|
||||
let post = if let Some(post) = db.list_post_by_message_id(list.pk, &msg_id)? {
|
||||
post
|
||||
} else {
|
||||
return Err(ResponseError::new(
|
||||
format!("Post with Message-ID {} not found", msg_id),
|
||||
StatusCode::NOT_FOUND,
|
||||
));
|
||||
};
|
||||
Ok(String::from_utf8_lossy(&post.message).to_string())
|
||||
}
|
||||
|
||||
/// .eml post page.
|
||||
pub async fn post_eml(
|
||||
ListPostEmlPath(id, msg_id): ListPostEmlPath,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<impl IntoResponse, ResponseError> {
|
||||
let db = Connection::open_db(state.conf.clone())?.trusted();
|
||||
let Some(list) = (match id {
|
||||
ListPathIdentifier::Pk(id) => db.list(id)?,
|
||||
ListPathIdentifier::Id(id) => db.list_by_id(id)?,
|
||||
}) else {
|
||||
return Err(ResponseError::new(
|
||||
"List not found".to_string(),
|
||||
StatusCode::NOT_FOUND,
|
||||
));
|
||||
};
|
||||
|
||||
let post = if let Some(post) = db.list_post_by_message_id(list.pk, &msg_id)? {
|
||||
post
|
||||
} else {
|
||||
return Err(ResponseError::new(
|
||||
format!("Post with Message-ID {} not found", msg_id),
|
||||
StatusCode::NOT_FOUND,
|
||||
));
|
||||
};
|
||||
let mut response = post.into_inner().message.into_response();
|
||||
response.headers_mut().insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
http::HeaderValue::from_static("application/octet-stream"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
http::header::CONTENT_DISPOSITION,
|
||||
http::HeaderValue::try_from(format!("attachment; filename=\"{}.eml\"", msg_id)).unwrap(),
|
||||
);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
|
|
@ -62,6 +62,8 @@ async fn main() {
|
|||
.route("/", get(root))
|
||||
.typed_get(list)
|
||||
.typed_get(list_post)
|
||||
.typed_get(post_raw)
|
||||
.typed_get(post_eml)
|
||||
.typed_get(list_edit.layer(RequireAuth::login_with_role_or_redirect(
|
||||
Role::User..,
|
||||
Arc::clone(&login_url),
|
||||
|
|
|
@ -37,6 +37,7 @@ lazy_static::lazy_static! {
|
|||
}
|
||||
}
|
||||
add!(function calendarize,
|
||||
strip_carets,
|
||||
login_path,
|
||||
logout_path,
|
||||
settings_path,
|
||||
|
@ -44,7 +45,9 @@ lazy_static::lazy_static! {
|
|||
list_path,
|
||||
list_settings_path,
|
||||
list_edit_path,
|
||||
list_post_path
|
||||
list_post_path,
|
||||
post_raw_path,
|
||||
post_eml_path
|
||||
);
|
||||
add!(filter pluralize);
|
||||
#[cfg(not(feature = "zstd"))]
|
||||
|
@ -381,3 +384,16 @@ pub fn pluralize(
|
|||
(true, _, Some(suffix)) => suffix.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn strip_carets(_state: &minijinja::State, arg: Value) -> std::result::Result<Value, Error> {
|
||||
Ok(Value::from(
|
||||
arg.as_str()
|
||||
.ok_or_else(|| {
|
||||
minijinja::Error::new(
|
||||
minijinja::ErrorKind::InvalidOperation,
|
||||
format!("argument to strip_carets() is of type {}", arg.kind()),
|
||||
)
|
||||
})?
|
||||
.strip_carets(),
|
||||
))
|
||||
}
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub const COMPRESSED: &[(&str, &[u8])] = &[include!("compressed.data")];
|
||||
pub const COMPRESSED: &[(&str, &[u8])] = include!("compressed.data");
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
text-rendering:optimizeLegibility;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
-moz-osx-font-smoothing:grayscale;
|
||||
font-family:-apple-system,BlinkMacSystemFont,Roboto,Roboto Slab,Droid Serif,Segoe UI,system-ui,Arial,sans-serif;
|
||||
font-family:var(--sans-serif-system-stack);
|
||||
font-size:100%;
|
||||
}
|
||||
|
||||
|
@ -149,9 +149,12 @@
|
|||
}
|
||||
|
||||
:root {
|
||||
--emoji-system-stack: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--monospace-system-stack: /* apple */ ui-monospace, SFMono-Regular, Menlo, Monaco,
|
||||
/* windows */ "Cascadia Mono", "Segoe UI Mono", Consolas,
|
||||
/* free unixes */ "Liberation Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
||||
/* free unixes */ "DejaVu Sans Mono", "Liberation Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace, var(--emoji-system-stack);
|
||||
--sans-serif-system-stack:-apple-system,BlinkMacSystemFont,Roboto,Roboto Slab,Droid Serif,Segoe UI,system-ui,Arial,sans-serif, var(--emoji-system-stack);
|
||||
--grotesque-system-stack: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif, var(--emoji-system-stack);
|
||||
--text-primary: CanvasText;
|
||||
--text-faded: GrayText;
|
||||
--horizontal-rule: #88929d;
|
||||
|
@ -167,7 +170,7 @@
|
|||
--text-link: #0069c2;
|
||||
--text-invert: #fff;
|
||||
--background-primary: #fff;
|
||||
--background-secondary: #f9f9fb;
|
||||
--background-secondary: #ebebeb;
|
||||
--background-tertiary: #fff;
|
||||
--background-toc-active: #ebeaea;
|
||||
--background-mark-yellow: #c7b70066;
|
||||
|
@ -344,6 +347,7 @@
|
|||
}
|
||||
main.layout>.rightside { grid-area: rightside; }
|
||||
main.layout>footer {
|
||||
font-family: var(--grotesque-system-stack);
|
||||
grid-area: footer;
|
||||
border-top: 2px inset;
|
||||
margin-block-start: 1rem;
|
||||
|
@ -370,6 +374,8 @@
|
|||
|
||||
main.layout>div.header>h1 {
|
||||
margin: 1rem;
|
||||
font-family: var(--grotesque-system-stack);
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
main.layout>div.header>div.page-header {
|
||||
|
@ -416,6 +422,7 @@
|
|||
|
||||
main.layout>div.header h2.page-title {
|
||||
margin: 1rem 0px;
|
||||
font-family: var(--grotesque-system-stack);
|
||||
}
|
||||
|
||||
nav.breadcrumbs {
|
||||
|
@ -649,6 +656,10 @@
|
|||
margin-inline-end: 1rem;
|
||||
}
|
||||
|
||||
table.headers {
|
||||
margin-left: -3vw;
|
||||
}
|
||||
|
||||
table.headers tr>th {
|
||||
text-align: right;
|
||||
color: var(--text-faded);
|
||||
|
@ -657,29 +668,44 @@
|
|||
table.headers th[scope="row"] {
|
||||
padding-right: .5rem;
|
||||
vertical-align: top;
|
||||
font-family: var(--grotesque-system-stack);
|
||||
}
|
||||
|
||||
table.headers tr>td {
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
word-wrap: anywhere;
|
||||
word-break: break-all;
|
||||
width: 50ch;
|
||||
}
|
||||
|
||||
div.post-body {
|
||||
margin: 1rem;
|
||||
margin: 1rem 0px;
|
||||
}
|
||||
|
||||
div.post-body>pre {
|
||||
max-width: 69vw;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
margin-left: min(5rem, 14vw);
|
||||
hyphens: auto;
|
||||
background-color: var(--background-secondary);
|
||||
outline: 1rem solid var(--background-secondary);
|
||||
margin: 2rem 0;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
div.post-body:not(:last-child) {
|
||||
padding-bottom: .5rem;
|
||||
border-bottom: 1px solid var(--horizontal-rule);
|
||||
}
|
||||
|
||||
td.message-id,
|
||||
span.message-id{
|
||||
color: var(--text-faded);
|
||||
}
|
||||
.message-id>a {
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
td.message-id:before,
|
||||
span.message-id:before{
|
||||
content: '<';
|
||||
|
@ -705,6 +731,13 @@
|
|||
span.faded:is(:focus, :hover, :focus-visible, :focus-within) {
|
||||
color: revert;
|
||||
}
|
||||
details.reply-details button {
|
||||
padding: 0.3rem;
|
||||
font-size: medium;
|
||||
min-width: 0;
|
||||
margin-block-start: 0.2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul.lists {
|
||||
padding: 1rem 2rem;
|
||||
|
|
|
@ -94,6 +94,7 @@
|
|||
<div class="entry" role="listitem" aria-labelledby="post_link_{{ loop.index }}">
|
||||
<span class="subject"><a id="post_link_{{ loop.index }}" href="{{ root_url_prefix }}{{ list_post_path(list.id, post.message_id) }}">{{ post.subject }}</a> <span class="metadata replies" title="reply count">{{ post.replies }} repl{{ post.replies|pluralize("y","ies") }}</span></span>
|
||||
<span class="metadata"><span aria-hidden="true">👤 </span><span class="from" title="post author">{{ post.address }}</span><span aria-hidden="true"> 📆 </span><span class="date" title="post date">{{ post.datetime }}</span></span>
|
||||
{% if post.replies > 0 %}<span class="metadata"><span aria-hidden="true">💓 </span><span class="last-active" title="latest thread activity">{{ post.last_active }}</span></span>{% endif %}
|
||||
<span class="metadata"><span aria-hidden="true">🪪 </span><span class="message-id" title="e-mail Message-ID">{{ post.message_id }}</span></span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -38,5 +38,29 @@
|
|||
<div class="post-body">
|
||||
<pre title="E-mail text content">{{ body }}</pre>
|
||||
</div>
|
||||
{% for (depth, post, body, date) in thread %}
|
||||
<table class="headers" title="E-mail headers">
|
||||
<caption class="screen-reader-only">E-mail headers</caption>
|
||||
<tr>
|
||||
<th scope="row">From:</th>
|
||||
<td>{{ post.address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Date:</th>
|
||||
<td class="faded">{{ date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Message-ID:</th>
|
||||
<td class="faded message-id"><a href="{{ root_url_prefix }}{{ list_post_path(list.id, post.message_id) }}">{{ strip_carets(post.message_id) }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><details class="reply-details"><summary>more …</summary><a href="{{ root_url_prefix }}{{ post_raw_path(list.id, post.message_id) }}">View raw</a> <a href="{{ root_url_prefix }}{{ post_eml_path(list.id, post.message_id) }}">Download as <code>eml</code> (RFC 5322 format)</a></details></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="post-body">
|
||||
<pre title="E-mail text content">{{ body }}</pre>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% include "footer.html" %}
|
||||
|
|
|
@ -90,6 +90,14 @@ impl From<&DbVal<mailpot::models::MailingList>> for ListPath {
|
|||
#[typed_path("/list/:id/posts/:msgid/")]
|
||||
pub struct ListPostPath(pub ListPathIdentifier, pub String);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
|
||||
#[typed_path("/list/:id/posts/:msgid/raw/")]
|
||||
pub struct ListPostRawPath(pub ListPathIdentifier, pub String);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
|
||||
#[typed_path("/list/:id/posts/:msgid/eml/")]
|
||||
pub struct ListPostEmlPath(pub ListPathIdentifier, pub String);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
|
||||
#[typed_path("/list/:id/edit/")]
|
||||
pub struct ListEditPath(pub ListPathIdentifier);
|
||||
|
@ -159,31 +167,39 @@ list_id_impl!(list_edit_path, ListEditPath);
|
|||
list_id_impl!(list_subscribers_path, ListEditSubscribersPath);
|
||||
list_id_impl!(list_candidates_path, ListEditCandidatesPath);
|
||||
|
||||
pub fn list_post_path(id: Value, msg_id: Value) -> std::result::Result<Value, Error> {
|
||||
let Some(msg_id) = msg_id.as_str().map(|s| if s.starts_with('<') && s.ends_with('>') { s.to_string() } else {
|
||||
format!("<{s}>")
|
||||
}) else {
|
||||
return Err(Error::new(
|
||||
minijinja::ErrorKind::UnknownMethod,
|
||||
"Second argument of list_post_path must be a string."
|
||||
));
|
||||
};
|
||||
macro_rules! list_post_impl {
|
||||
($ident:ident, $ty:tt) => {
|
||||
pub fn $ident(id: Value, msg_id: Value) -> std::result::Result<Value, Error> {
|
||||
let Some(msg_id) = msg_id.as_str().map(|s| if s.starts_with('<') && s.ends_with('>') { s.to_string() } else {
|
||||
format!("<{s}>")
|
||||
}) else {
|
||||
return Err(Error::new(
|
||||
minijinja::ErrorKind::UnknownMethod,
|
||||
"Second argument of list_post_path must be a string."
|
||||
));
|
||||
};
|
||||
|
||||
if let Some(id) = id.as_str() {
|
||||
return Ok(Value::from_safe_string(
|
||||
ListPostPath(ListPathIdentifier::Id(id.to_string()), msg_id)
|
||||
.to_crumb()
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let pk = id.try_into()?;
|
||||
Ok(Value::from_safe_string(
|
||||
ListPostPath(ListPathIdentifier::Pk(pk), msg_id)
|
||||
.to_crumb()
|
||||
.to_string(),
|
||||
))
|
||||
if let Some(id) = id.as_str() {
|
||||
return Ok(Value::from_safe_string(
|
||||
$ty(ListPathIdentifier::Id(id.to_string()), msg_id)
|
||||
.to_crumb()
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let pk = id.try_into()?;
|
||||
Ok(Value::from_safe_string(
|
||||
$ty(ListPathIdentifier::Pk(pk), msg_id)
|
||||
.to_crumb()
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
list_post_impl!(list_post_path, ListPostPath);
|
||||
list_post_impl!(post_raw_path, ListPostRawPath);
|
||||
list_post_impl!(post_eml_path, ListPostEmlPath);
|
||||
|
||||
pub mod tsr {
|
||||
use std::{borrow::Cow, convert::Infallible};
|
||||
|
||||
|
|
199
web/src/utils.rs
199
web/src/utils.rs
|
@ -212,3 +212,202 @@ where
|
|||
.map(|s| Value::from_safe_string(s.to_string()))
|
||||
.serialize(ser)
|
||||
}
|
||||
|
||||
pub struct ThreadEntry {
|
||||
pub hash: melib::EnvelopeHash,
|
||||
pub depth: usize,
|
||||
pub thread_node: melib::ThreadNodeHash,
|
||||
pub thread: melib::ThreadHash,
|
||||
pub from: String,
|
||||
pub message_id: String,
|
||||
pub timestamp: u64,
|
||||
pub datetime: String,
|
||||
}
|
||||
|
||||
pub fn thread_db(
|
||||
db: &mailpot::Connection,
|
||||
list: i64,
|
||||
root: &str,
|
||||
) -> Vec<(i64, DbVal<mailpot::models::Post>, String, String)> {
|
||||
let mut stmt = db
|
||||
.connection
|
||||
.prepare(
|
||||
"WITH RECURSIVE cte_replies AS MATERIALIZED
|
||||
(
|
||||
SELECT
|
||||
pk,
|
||||
message_id,
|
||||
REPLACE(
|
||||
TRIM(
|
||||
SUBSTR(
|
||||
CAST(message AS TEXT),
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
)
|
||||
+
|
||||
LENGTH('in-reply-to: '),
|
||||
INSTR(
|
||||
SUBSTR(
|
||||
CAST(message AS TEXT),
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: ')
|
||||
+
|
||||
LENGTH('in-reply-to: ')
|
||||
),
|
||||
'>'
|
||||
)
|
||||
)
|
||||
),
|
||||
' ',
|
||||
''
|
||||
) AS in_reply_to,
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
) AS offset
|
||||
FROM post
|
||||
WHERE
|
||||
offset > 0
|
||||
UNION
|
||||
SELECT
|
||||
pk,
|
||||
message_id,
|
||||
NULL AS in_reply_to,
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
) AS offset
|
||||
FROM post
|
||||
WHERE
|
||||
offset = 0
|
||||
),
|
||||
cte_thread(parent, root, depth) AS (
|
||||
SELECT DISTINCT
|
||||
message_id AS parent,
|
||||
message_id AS root,
|
||||
0 AS depth
|
||||
FROM cte_replies
|
||||
WHERE
|
||||
in_reply_to IS NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
t.message_id AS parent,
|
||||
cte_thread.root AS root,
|
||||
(cte_thread.depth + 1) AS depth
|
||||
FROM cte_replies
|
||||
AS t
|
||||
JOIN
|
||||
cte_thread
|
||||
ON cte_thread.parent = t.in_reply_to
|
||||
WHERE t.in_reply_to IS NOT NULL
|
||||
)
|
||||
SELECT * FROM cte_thread WHERE root = ? ORDER BY root, depth;",
|
||||
)
|
||||
.unwrap();
|
||||
let iter = stmt
|
||||
.query_map(rusqlite::params![root], |row| {
|
||||
let parent: String = row.get("parent")?;
|
||||
let root: String = row.get("root")?;
|
||||
let depth: i64 = row.get("depth")?;
|
||||
Ok((parent, root, depth))
|
||||
})
|
||||
.unwrap();
|
||||
let mut ret = vec![];
|
||||
for post in iter {
|
||||
let post = post.unwrap();
|
||||
ret.push(post);
|
||||
}
|
||||
let posts = db.list_posts(list, None).unwrap();
|
||||
ret.into_iter()
|
||||
.filter_map(|(m, _, depth)| {
|
||||
posts
|
||||
.iter()
|
||||
.find(|p| m.as_str().strip_carets() == p.message_id.as_str().strip_carets())
|
||||
.map(|p| {
|
||||
let envelope = melib::Envelope::from_bytes(p.message.as_slice(), None).unwrap();
|
||||
let body = envelope.body_bytes(p.message.as_slice());
|
||||
let body_text = body.text();
|
||||
let date = envelope.date_as_str().to_string();
|
||||
(depth, p.clone(), body_text, date)
|
||||
})
|
||||
})
|
||||
.skip(1)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn thread(
|
||||
envelopes: &Arc<std::sync::RwLock<HashMap<melib::EnvelopeHash, melib::Envelope>>>,
|
||||
threads: &melib::Threads,
|
||||
root_env_hash: melib::EnvelopeHash,
|
||||
) -> Vec<ThreadEntry> {
|
||||
let env_lock = envelopes.read().unwrap();
|
||||
let thread = threads.envelope_to_thread[&root_env_hash];
|
||||
let mut ret = vec![];
|
||||
for (depth, t) in threads.thread_group_iter(thread) {
|
||||
let hash = threads.thread_nodes[&t].message.unwrap();
|
||||
ret.push(ThreadEntry {
|
||||
hash,
|
||||
depth,
|
||||
thread_node: t,
|
||||
thread,
|
||||
message_id: env_lock[&hash].message_id().to_string(),
|
||||
from: env_lock[&hash].field_from_to_string(),
|
||||
datetime: env_lock[&hash].date_as_str().to_string(),
|
||||
timestamp: env_lock[&hash].timestamp,
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn thread_roots(
|
||||
envelopes: &Arc<std::sync::RwLock<HashMap<melib::EnvelopeHash, melib::Envelope>>>,
|
||||
threads: &mut melib::Threads,
|
||||
) -> Vec<(ThreadEntry, usize, u64)> {
|
||||
let items = threads.roots();
|
||||
let env_lock = envelopes.read().unwrap();
|
||||
let mut ret = vec![];
|
||||
'items_for_loop: for thread in items {
|
||||
let mut iter_ptr = threads.thread_ref(thread).root();
|
||||
let thread_node = &threads.thread_nodes()[&iter_ptr];
|
||||
let root_env_hash = if let Some(h) = thread_node.message().or_else(|| {
|
||||
if thread_node.children().is_empty() {
|
||||
return None;
|
||||
}
|
||||
iter_ptr = thread_node.children()[0];
|
||||
while threads.thread_nodes()[&iter_ptr].message().is_none() {
|
||||
if threads.thread_nodes()[&iter_ptr].children().is_empty() {
|
||||
return None;
|
||||
}
|
||||
iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
|
||||
}
|
||||
threads.thread_nodes()[&iter_ptr].message()
|
||||
}) {
|
||||
h
|
||||
} else {
|
||||
continue 'items_for_loop;
|
||||
};
|
||||
if !env_lock.contains_key(&root_env_hash) {
|
||||
panic!("key = {}", root_env_hash);
|
||||
}
|
||||
let envelope: &melib::Envelope = &env_lock[&root_env_hash];
|
||||
let tref = threads.thread_ref(thread);
|
||||
ret.push((
|
||||
ThreadEntry {
|
||||
hash: root_env_hash,
|
||||
depth: 0,
|
||||
thread_node: iter_ptr,
|
||||
thread,
|
||||
message_id: envelope.message_id().to_string(),
|
||||
from: envelope.field_from_to_string(),
|
||||
datetime: envelope.date_as_str().to_string(),
|
||||
timestamp: envelope.timestamp,
|
||||
},
|
||||
tref.len,
|
||||
tref.date,
|
||||
));
|
||||
}
|
||||
ret.sort_by_key(|(_, _, key)| std::cmp::Reverse(*key));
|
||||
ret
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue