Author:
Hash:
Timestamp:
+77 -145 +/-15 browse
Kevin Schoon [me@kevinschoon.com]
335ba580261377adcc81fda44d3a9fb79f9ae670
Thu, 04 Jan 2024 11:57:58 +0000 (1.5 years ago)
1 | diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md |
2 | index c27faf8..0180dbb 100644 |
3 | --- a/ATTRIBUTIONS.md |
4 | +++ b/ATTRIBUTIONS.md |
5 | @@ -3,6 +3,8 @@ |
6 | Ayllu would not be possible without many free software projects. |
7 | |
8 | [Git](https://git-scm.com/) |
9 | + [Meli](https://meli-email.org/) |
10 | + [Mailpot](https://git.meli-email.org/meli/mailpot) |
11 | |
12 | ## Many [Rust](https://www.rust-lang.org/) Libraries |
13 | |
14 | diff --git a/src/config.rs b/src/config.rs |
15 | index 9cdd349..0895b9d 100644 |
16 | --- a/src/config.rs |
17 | +++ b/src/config.rs |
18 | @@ -175,11 +175,21 @@ impl Xmpp { |
19 | } |
20 | |
21 | #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
22 | + pub struct SubscriptionPolicy { |
23 | + pub send_confirmation: bool, |
24 | + pub kind: String, |
25 | + } |
26 | + |
27 | + #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
28 | pub struct MailingList { |
29 | pub id: String, |
30 | pub name: Option<String>, |
31 | pub address: String, |
32 | + pub request_address: String, |
33 | pub description: String, |
34 | + pub topics: Vec<String>, |
35 | + pub post_policy: String, |
36 | + pub subscription_policy: SubscriptionPolicy, |
37 | } |
38 | |
39 | #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
40 | @@ -280,8 +290,7 @@ impl Configurable for Config { |
41 | Err(e) => { |
42 | return Err(format!( |
43 | "failed to load themes from path: {} ({})", |
44 | - self.web.themes_path, |
45 | - e |
46 | + self.web.themes_path, e |
47 | ) |
48 | .into()) |
49 | } |
50 | @@ -378,9 +387,4 @@ Disallow: /*/*/chart/* |
51 | // opts.render.github_pre_lang = false; |
52 | opts |
53 | } |
54 | - |
55 | - // returns if any discussion plugins are enabled |
56 | - pub fn discuss_enabled(&self) -> bool { |
57 | - self.mail.is_some() || self.xmpp.is_some() |
58 | - } |
59 | } |
60 | diff --git a/src/web2/routes/about.rs b/src/web2/routes/about.rs |
61 | index cb9d015..71f1fd2 100644 |
62 | --- a/src/web2/routes/about.rs |
63 | +++ b/src/web2/routes/about.rs |
64 | @@ -16,7 +16,7 @@ pub async fn serve( |
65 | ctx.insert("title", "about"); |
66 | ctx.insert( |
67 | "nav_elements", |
68 | - &navigation::global("about", cfg.discuss_enabled()), |
69 | + &navigation::global("about", cfg.mail.is_some()), |
70 | ); |
71 | let options = ComrakOptions::default(); |
72 | let mut plugins = ComrakPlugins::default(); |
73 | diff --git a/src/web2/routes/config.rs b/src/web2/routes/config.rs |
74 | index 0b6f19b..859122a 100644 |
75 | --- a/src/web2/routes/config.rs |
76 | +++ b/src/web2/routes/config.rs |
77 | @@ -22,7 +22,7 @@ pub async fn serve( |
78 | ctx.insert("themes", &cfg.web.themes); |
79 | ctx.insert( |
80 | "nav_elements", |
81 | - &navigation::global("config", cfg.discuss_enabled()), |
82 | + &navigation::global("config", cfg.mail.is_some()), |
83 | ); |
84 | let body = templates.render("config.html", &ctx)?; |
85 | Ok(Html(body)) |
86 | diff --git a/src/web2/routes/discuss.rs b/src/web2/routes/discuss.rs |
87 | deleted file mode 100644 |
88 | index c723d3b..0000000 |
89 | --- a/src/web2/routes/discuss.rs |
90 | +++ /dev/null |
91 | @@ -1,32 +0,0 @@ |
92 | - use axum::{extract::Extension, response::Html}; |
93 | - |
94 | - use crate::config::Config; |
95 | - use crate::web2::error::Error; |
96 | - use crate::web2::middleware::rpc_initiator::{Initiator, Kind as InitiatorKind}; |
97 | - use crate::web2::middleware::template::Template; |
98 | - use crate::web2::util; |
99 | - use crate::web2::util::navigation; |
100 | - |
101 | - pub async fn serve( |
102 | - Extension(cfg): Extension<Config>, |
103 | - Extension(initiator): Extension<Initiator>, |
104 | - Extension((templates, mut ctx)): Extension<Template>, |
105 | - ) -> Result<Html<String>, Error> { |
106 | - ctx.insert("title", "discuss"); |
107 | - ctx.insert( |
108 | - "nav_elements", |
109 | - &navigation::global("", cfg.discuss_enabled()), |
110 | - ); |
111 | - ctx.insert("discnav", &util::navigation::discnav("overview")); |
112 | - match initiator.clone().client(InitiatorKind::Xmpp) { |
113 | - Some(_client) => {} |
114 | - None => {} |
115 | - }; |
116 | - match initiator.client(InitiatorKind::Mail) { |
117 | - Some(_client) => {} |
118 | - None => {} |
119 | - }; |
120 | - // ctx.insert("lists", &cfg.mail.unwrap().lists); |
121 | - let body = templates.render("discuss.html", &ctx)?; |
122 | - Ok(Html(body)) |
123 | - } |
124 | diff --git a/src/web2/routes/index.rs b/src/web2/routes/index.rs |
125 | index 3f6d7d3..77f109d 100644 |
126 | --- a/src/web2/routes/index.rs |
127 | +++ b/src/web2/routes/index.rs |
128 | @@ -103,10 +103,7 @@ pub async fn index( |
129 | } |
130 | ctx.insert("title", "ayllu"); |
131 | ctx.insert("collections", &collections); |
132 | - ctx.insert( |
133 | - "nav_elements", |
134 | - &navigation::global("", cfg.discuss_enabled()), |
135 | - ); |
136 | + ctx.insert("nav_elements", &navigation::global("", cfg.mail.is_some())); |
137 | let body = templates.render("index.html", &ctx)?; |
138 | Ok(Html(body)) |
139 | } |
140 | @@ -128,10 +125,7 @@ pub async fn collection( |
141 | let entry = entry.unwrap(); |
142 | let repositories = load_repositories(entry.path.as_str(), &db).await?; |
143 | ctx.insert("title", "ayllu"); |
144 | - ctx.insert( |
145 | - "nav_elements", |
146 | - &navigation::global("", cfg.discuss_enabled()), |
147 | - ); |
148 | + ctx.insert("nav_elements", &navigation::global("", cfg.mail.is_some())); |
149 | ctx.insert("collection", &entry.clone()); |
150 | ctx.insert("repositories", &repositories); |
151 | ctx.insert("is_hidden", &entry.hidden.is_some_and(|x| x)); |
152 | diff --git a/src/web2/routes/mail.rs b/src/web2/routes/mail.rs |
153 | index d835715..e08efa4 100644 |
154 | --- a/src/web2/routes/mail.rs |
155 | +++ b/src/web2/routes/mail.rs |
156 | @@ -9,13 +9,13 @@ use crate::config::Config; |
157 | use crate::web2::error::Error; |
158 | use crate::web2::middleware::rpc_initiator::{Initiator, Kind as InitiatorKind}; |
159 | use crate::web2::middleware::template::Template; |
160 | - use crate::web2::util; |
161 | use crate::web2::util::navigation; |
162 | use ayllu_api::mail_capnp::server::Client as MailClient; |
163 | |
164 | #[derive(Deserialize)] |
165 | pub struct Params { |
166 | pub list_id: String, |
167 | + pub thread_id: Option<String>, |
168 | pub message_id: Option<String>, |
169 | } |
170 | |
171 | @@ -44,8 +44,7 @@ pub async fn lists( |
172 | Extension((templates, mut ctx)): Extension<Template>, |
173 | ) -> Result<Html<String>, Error> { |
174 | ctx.insert("title", "lists"); |
175 | - ctx.insert("nav_elements", &navigation::global("dicsuss", true)); |
176 | - ctx.insert("discnav", &util::navigation::discnav("mail")); |
177 | + ctx.insert("nav_elements", &navigation::global("mail", true)); |
178 | // TODO: add stats method and display like xmpp |
179 | ctx.insert("lists", &cfg.mail.unwrap().lists.clone()); |
180 | // ctx.insert("lists", &cfg.mail.unwrap().lists); |
181 | @@ -70,9 +69,8 @@ pub async fn threads( |
182 | ))), |
183 | }?; |
184 | ctx.insert("title", &format!("list {}", list.address)); |
185 | - ctx.insert("nav_elements", &navigation::global("dicsuss", true)); |
186 | + ctx.insert("nav_elements", &navigation::global("mail", true)); |
187 | ctx.insert("list", list); |
188 | - ctx.insert("discnav", &util::navigation::discnav("mail")); |
189 | |
190 | let mail_client = initiator.client(InitiatorKind::Mail).unwrap(); |
191 | let mut threads = mail_client |
192 | @@ -99,6 +97,7 @@ pub async fn threads( |
193 | .await?; |
194 | threads.sort_by(|first, second| second.timestamp.cmp(&first.timestamp)); |
195 | ctx.insert("threads", &threads); |
196 | + ctx.insert("request_email", &list.request_address); |
197 | let body = templates.render("threads.html", &ctx)?; |
198 | Ok(Html(body)) |
199 | } |
200 | @@ -124,7 +123,7 @@ pub async fn thread( |
201 | let mut req = c.read_thread_request(); |
202 | req.get().set_id(params.list_id.as_str().into()); |
203 | req.get() |
204 | - .set_message_id(params.message_id.unwrap().as_str().into()); |
205 | + .set_message_id(params.thread_id.unwrap().as_str().into()); |
206 | let result = req.send().promise.await?; |
207 | for message in result.get()?.get_thread()? { |
208 | messages.push(Message { |
209 | @@ -140,8 +139,7 @@ pub async fn thread( |
210 | }) |
211 | .await?; |
212 | ctx.insert("title", &format!("list {}", list.address)); |
213 | - ctx.insert("nav_elements", &navigation::global("dicsuss", true)); |
214 | - ctx.insert("discnav", &util::navigation::discnav("mail")); |
215 | + ctx.insert("nav_elements", &navigation::global("mail", true)); |
216 | ctx.insert("list", list); |
217 | ctx.insert("list_id", &list.id); |
218 | ctx.insert("messages", &messages); |
219 | @@ -149,7 +147,7 @@ pub async fn thread( |
220 | Ok(Html(body)) |
221 | } |
222 | |
223 | - pub async fn post( |
224 | + pub async fn message( |
225 | Path(params): Path<Params>, |
226 | Extension(initiator): Extension<Initiator>, |
227 | Extension(cfg): Extension<Config>, |
228 | @@ -183,8 +181,7 @@ pub async fn post( |
229 | }) |
230 | .await?; |
231 | ctx.insert("title", &format!("list {}", list.address)); |
232 | - ctx.insert("nav_elements", &navigation::global("dicsuss", true)); |
233 | - ctx.insert("discnav", &util::navigation::discnav("mail")); |
234 | + ctx.insert("nav_elements", &navigation::global("mail", true)); |
235 | ctx.insert("list", list); |
236 | ctx.insert("list_id", &list.id); |
237 | ctx.insert("message", &message); |
238 | diff --git a/src/web2/routes/mod.rs b/src/web2/routes/mod.rs |
239 | index b3b25fc..ebb476d 100644 |
240 | --- a/src/web2/routes/mod.rs |
241 | +++ b/src/web2/routes/mod.rs |
242 | @@ -8,7 +8,6 @@ pub mod builds; |
243 | pub mod chart; |
244 | pub mod commit; |
245 | pub mod config; |
246 | - pub mod discuss; |
247 | pub mod finger; |
248 | pub mod index; |
249 | pub mod log; |
250 | diff --git a/src/web2/routes/xmpp.rs b/src/web2/routes/xmpp.rs |
251 | index 3187057..a2dac61 100644 |
252 | --- a/src/web2/routes/xmpp.rs |
253 | +++ b/src/web2/routes/xmpp.rs |
254 | @@ -9,7 +9,6 @@ use crate::web2::error::Error; |
255 | use crate::web2::extractors::config::ConfigReader; |
256 | use crate::web2::middleware::rpc_initiator::{Initiator, Kind as InitiatorKind}; |
257 | use crate::web2::middleware::template::Template; |
258 | - use crate::web2::util; |
259 | use crate::web2::util::navigation; |
260 | use ayllu_api::xmpp_capnp::server::Client as XmppClient; |
261 | |
262 | @@ -34,8 +33,7 @@ pub async fn channels( |
263 | Extension(initiator): Extension<Initiator>, |
264 | ) -> Result<Html<String>, Error> { |
265 | ctx.insert("title", "Discussions"); |
266 | - ctx.insert("nav_elements", &navigation::global("dicsuss", true)); |
267 | - ctx.insert("discnav", &util::navigation::discnav("xmpp")); |
268 | + ctx.insert("nav_elements", &navigation::global("xmpp", true)); |
269 | let xmpp_client = initiator.client(InitiatorKind::Xmpp).unwrap(); |
270 | let channels = xmpp_client |
271 | .invoke(move |c: XmppClient| async move { |
272 | @@ -72,8 +70,7 @@ pub async fn channel( |
273 | Extension(initiator): Extension<Initiator>, |
274 | ) -> Result<Html<String>, Error> { |
275 | ctx.insert("title", "lists"); |
276 | - ctx.insert("nav_elements", &navigation::global("dicsuss", true)); |
277 | - ctx.insert("discnav", &util::navigation::discnav("xmpp")); |
278 | + ctx.insert("nav_elements", &navigation::global("xmpp", true)); |
279 | ctx.insert("channel", ¶ms.channel); |
280 | let xmpp_client = initiator.client(InitiatorKind::Xmpp).unwrap(); |
281 | let messages = xmpp_client |
282 | diff --git a/src/web2/server.rs b/src/web2/server.rs |
283 | index 653173b..1ede61a 100644 |
284 | --- a/src/web2/server.rs |
285 | +++ b/src/web2/server.rs |
286 | @@ -30,7 +30,6 @@ use crate::web2::routes::builds; |
287 | use crate::web2::routes::chart; |
288 | use crate::web2::routes::commit; |
289 | use crate::web2::routes::config; |
290 | - use crate::web2::routes::discuss; |
291 | use crate::web2::routes::finger; |
292 | use crate::web2::routes::index; |
293 | use crate::web2::routes::log as log_route; |
294 | @@ -171,48 +170,35 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> { |
295 | )), |
296 | ) |
297 | .nest( |
298 | - "/discuss", |
299 | + "/mail", |
300 | Router::new() |
301 | - .route("/", routing::get(discuss::serve)) |
302 | + .route("/", routing::get(mail::lists)) |
303 | + .route("/:list_id", routing::get(mail::threads)) |
304 | + .route("/:list_id/thread/:thread_id", routing::get(mail::thread)) |
305 | + .route("/:list_id/message/:message_id", routing::get(mail::message)) |
306 | .layer(from_fn_with_state( |
307 | - Arc::new(cfg.clone()), |
308 | - rpc_initiator::optional, |
309 | + Arc::new((cfg.clone(), templates.clone(), mail_required_plugins)), |
310 | + rpc_initiator::required, |
311 | )) |
312 | .layer(from_fn_with_state( |
313 | Arc::new((cfg.clone(), templates.clone())), |
314 | template::middleware, |
315 | + )), |
316 | + ) |
317 | + .nest( |
318 | + "/xmpp", |
319 | + Router::new() |
320 | + .route("/", routing::get(xmpp::channels)) |
321 | + .route("/:channel", routing::get(xmpp::channel)) |
322 | + .route("/:channel/:last_message", routing::get(xmpp::channel)) |
323 | + .layer(from_fn_with_state( |
324 | + Arc::new((cfg.clone(), templates.clone(), xmpp_required_plugins)), |
325 | + rpc_initiator::required, |
326 | )) |
327 | - .nest( |
328 | - "/mail", |
329 | - Router::new() |
330 | - .route("/", routing::get(mail::lists)) |
331 | - .route("/:list_id", routing::get(mail::threads)) |
332 | - .route("/:list_id/:message_id", routing::get(mail::thread)) |
333 | - .route("/post/:list_id/:message_id", routing::get(mail::post)) |
334 | - .layer(from_fn_with_state( |
335 | - Arc::new((cfg.clone(), templates.clone(), mail_required_plugins)), |
336 | - rpc_initiator::required, |
337 | - )) |
338 | - .layer(from_fn_with_state( |
339 | - Arc::new((cfg.clone(), templates.clone())), |
340 | - template::middleware, |
341 | - )), |
342 | - ) |
343 | - .nest( |
344 | - "/xmpp", |
345 | - Router::new() |
346 | - .route("/", routing::get(xmpp::channels)) |
347 | - .route("/:channel", routing::get(xmpp::channel)) |
348 | - .route("/:channel/:last_message", routing::get(xmpp::channel)) |
349 | - .layer(from_fn_with_state( |
350 | - Arc::new((cfg.clone(), templates.clone(), xmpp_required_plugins)), |
351 | - rpc_initiator::required, |
352 | - )) |
353 | - .layer(from_fn_with_state( |
354 | - Arc::new((cfg.clone(), templates.clone())), |
355 | - template::middleware, |
356 | - )), |
357 | - ), |
358 | + .layer(from_fn_with_state( |
359 | + Arc::new((cfg.clone(), templates.clone())), |
360 | + template::middleware, |
361 | + )), |
362 | ) |
363 | .nest( |
364 | "/:collection/:name", |
365 | diff --git a/src/web2/util.rs b/src/web2/util.rs |
366 | index 59de08c..79b1f3e 100644 |
367 | --- a/src/web2/util.rs |
368 | +++ b/src/web2/util.rs |
369 | @@ -3,7 +3,6 @@ use std::path::PathBuf; |
370 | use axum::http::Uri; |
371 | use url::Url; |
372 | |
373 | - |
374 | // select a segment of the path from the given url between start and end. |
375 | // e.g. |
376 | // http://fuu.bar/baz/qux 0 1 -> Some(/baz) |
377 | @@ -60,7 +59,7 @@ pub mod navigation { |
378 | |
379 | pub type Items = Vec<(String, String, bool)>; |
380 | |
381 | - pub fn global(current_page: &str, discuss_visible: bool) -> Items { |
382 | + pub fn global(current_page: &str, mail_visible: bool) -> Items { |
383 | let mut nav: Items = vec![ |
384 | ( |
385 | String::from("about"), |
386 | @@ -73,11 +72,11 @@ pub mod navigation { |
387 | current_page == "config", |
388 | ), |
389 | ]; |
390 | - if discuss_visible { |
391 | + if mail_visible { |
392 | nav.push(( |
393 | - String::from("discuss"), |
394 | - String::from("/discuss"), |
395 | - current_page == "discuss", |
396 | + String::from("mail"), |
397 | + String::from("/mail"), |
398 | + current_page == "mail", |
399 | )) |
400 | } |
401 | nav |
402 | @@ -193,26 +192,6 @@ pub mod navigation { |
403 | ), |
404 | ] |
405 | } |
406 | - |
407 | - pub fn discnav(current_page: &str) -> Items { |
408 | - vec![ |
409 | - ( |
410 | - String::from("overview"), |
411 | - String::from("/discuss"), |
412 | - current_page == "overview", |
413 | - ), |
414 | - ( |
415 | - String::from("mail"), |
416 | - String::from("/discuss/mail"), |
417 | - current_page == "mail", |
418 | - ), |
419 | - ( |
420 | - String::from("xmpp"), |
421 | - String::from("/discuss/xmpp"), |
422 | - current_page == "xmpp", |
423 | - ), |
424 | - ] |
425 | - } |
426 | } |
427 | |
428 | const UNIT: f64 = 1024.0; |
429 | diff --git a/themes/default/templates/discuss.html b/themes/default/templates/discuss.html |
430 | deleted file mode 100644 |
431 | index ae080ac..0000000 |
432 | --- a/themes/default/templates/discuss.html |
433 | +++ /dev/null |
434 | @@ -1,11 +0,0 @@ |
435 | - {% import "macros.html" as macros %} |
436 | - {% extends "base.html" %} |
437 | - {% block content %} |
438 | - <section> |
439 | - <article> |
440 | - <header> |
441 | - {{ macros::navigation(items=discnav, title="Discussions") }} |
442 | - </header> |
443 | - </article> |
444 | - </section> |
445 | - {% endblock %} |
446 | diff --git a/themes/default/templates/lists.html b/themes/default/templates/lists.html |
447 | index bdb32ba..0196b96 100644 |
448 | --- a/themes/default/templates/lists.html |
449 | +++ b/themes/default/templates/lists.html |
450 | @@ -4,7 +4,7 @@ |
451 | <section> |
452 | <article> |
453 | <header> |
454 | - {{ macros::navigation(items=discnav, title="Mailing Lists") }} |
455 | + <h1> Mailing Lists </h1> |
456 | </header> |
457 | <table> |
458 | <thead> |
459 | @@ -16,9 +16,9 @@ |
460 | <tbody> |
461 | {% for list in lists %} |
462 | <tr> |
463 | - <td><a href="/discuss/mail/{{list.id}}">{{ list.id }}</a></td> |
464 | + <td>{{ list.id }}</td> |
465 | <td>{{ list.name }}</td> |
466 | - <td>{{ list.description }}</td> |
467 | + <td><a href="/mail/{{list.id}}">{{ list.description }}</a></td> |
468 | <td>{{ list.address }}</td> |
469 | </tr> |
470 | {% endfor %} |
471 | diff --git a/themes/default/templates/thread.html b/themes/default/templates/thread.html |
472 | index 764f9b7..d0005e8 100644 |
473 | --- a/themes/default/templates/thread.html |
474 | +++ b/themes/default/templates/thread.html |
475 | @@ -7,7 +7,7 @@ |
476 | <header> |
477 | <b>From: {{ reply.from_address }}</b></br> |
478 | <b>To: ???</b></br> |
479 | - <b><a href="/discuss/mail/post/{{list_id}}/{{reply.message_id}}">{{ reply.message_id }}</a></b> |
480 | + <b><a href="/mail/{{list_id}}/message/{{reply.message_id}}">{{ reply.message_id }}</a></b> |
481 | <span class="right">{{ reply.created_at | format_epoch }}</span> |
482 | </header> |
483 | <pre>{{ reply.text }}</pre> |
484 | diff --git a/themes/default/templates/threads.html b/themes/default/templates/threads.html |
485 | index d7c7431..c7f8cfb 100644 |
486 | --- a/themes/default/templates/threads.html |
487 | +++ b/themes/default/templates/threads.html |
488 | @@ -4,8 +4,25 @@ |
489 | <section> |
490 | <article> |
491 | <header> |
492 | - {{ macros::navigation(items=discnav, title=list.id) }} |
493 | + <h1> {{ list.name }} </h1> |
494 | </header> |
495 | + <div class="mailing-list-details"> |
496 | + <h4> {{ list.description }} </h4> |
497 | + </br> |
498 | + <span class="labels"> |
499 | + {% for topic in list.topics %} |
500 | + <span class="feature">{{topic}}</span> |
501 | + {% endfor %} |
502 | + </br><b>Post Policy = {{list.post_policy}} </b> |
503 | + </br><b>Subscription Policy = {{list.subscription_policy.kind}}</b> |
504 | + </br><b>Send Confirmation = {{list.subscription_policy.send_confirmation}}</b> |
505 | + </br> |
506 | + </span></br> |
507 | + <h4> Subscribe </h4> |
508 | + <p> Send an e-mail to <a href="mailto:{{ request_email }}?subject=subscribe">{{request_email}}</a> with the following subject: <code>subscribe</code> </p> |
509 | + <h4> Unsubscribe </h4> |
510 | + <p> Send an e-mail to <a href="mailto:{{ request_email }}?subject=unsubscribe">{{request_email}}</a> with the following subject: <code>unsubscribe</code> </p> |
511 | + </div> |
512 | <table> |
513 | <thead> |
514 | <th> from </th> |
515 | @@ -18,7 +35,7 @@ |
516 | <tr> |
517 | <td>{{ thread.from }}</a></td> |
518 | <td>{{thread.timestamp | format_epoch }}</td> |
519 | - <td><a href="/discuss/mail/{{list.id}}/{{thread.message_id}}">{{thread.subject}}</a></td> |
520 | + <td><a href="/mail/{{list.id}}/thread/{{thread.message_id}}">{{thread.subject}}</a></td> |
521 | <td>{{thread.n_replies}}</td> |
522 | </tr> |
523 | {% endfor %} |