Use Ructe (#327)

All the template are now compiled at compile-time with the `ructe` crate.

I preferred to use it instead of askama because it allows more complex Rust expressions, where askama only supports a small subset of expressions and doesn't allow them everywhere (for instance, `{{ macro!() | filter }}` would result in a parsing error).

The diff is quite huge, but there is normally no changes in functionality.

Fixes #161 and unblocks #110 and #273
This commit is contained in:
Baptiste Gelez 2018-12-06 18:54:16 +01:00 committed by GitHub
parent 5f059c3e98
commit 70af57c6e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
121 changed files with 3132 additions and 3260 deletions

View File

@ -5,10 +5,10 @@ end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.{js,rs,css,tera}] [*.{js,rs,css,tera,html}]
charset = utf-8 charset = utf-8
[*.{rs,tera,css}] [*.{rs,tera,css,html}]
indent_style = space indent_style = space
indent_size = 4 indent_size = 4

3
.gitignore vendored
View File

@ -1,4 +1,3 @@
rls rls
/target /target
**/*.rs.bk **/*.rs.bk
@ -14,4 +13,6 @@ docker-compose.yml
*.sqlite *.sqlite
*.sqlite3 *.sqlite3
*.swp *.swp
tags.*
!tags.rs
search_index search_index

View File

@ -1,6 +1,6 @@
language: rust language: rust
rust: rust:
- nightly-2018-07-17 - nightly-2018-10-06
cache: cache:
cargo: true cargo: true
directories: directories:

588
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,25 +2,28 @@
authors = ["Bat' <baptiste@gelez.xyz>"] authors = ["Bat' <baptiste@gelez.xyz>"]
name = "plume" name = "plume"
version = "0.2.0" version = "0.2.0"
[dependencies] [dependencies]
activitypub = "0.1.3" activitypub = "0.1.3"
askama_escape = "0.1"
atom_syndication = "0.6" atom_syndication = "0.6"
canapi = "0.1" canapi = "0.1"
colored = "1.6" colored = "1.6"
dotenv = "0.13" dotenv = "0.13"
failure = "0.1" failure = "0.1"
gettext-rs = "0.4"
guid-create = "0.1" guid-create = "0.1"
heck = "0.3.0" heck = "0.3.0"
num_cpus = "1.0" num_cpus = "1.0"
rocket = "0.4.0-rc.1"
rocket_contrib = { version = "0.4.0-rc.1", features = ["json"] }
rocket_i18n = "0.3.1"
rpassword = "2.0" rpassword = "2.0"
scheduled-thread-pool = "0.2.0" scheduled-thread-pool = "0.2.0"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_qs = "0.4" serde_qs = "0.4"
tera = "0.11" validator = "0.8"
validator = "0.7"
validator_derive = "0.7" validator_derive = "0.7"
webfinger = "0.3.1" webfinger = "0.3.1"
@ -54,26 +57,13 @@ path = "plume-common"
[dependencies.plume-models] [dependencies.plume-models]
path = "plume-models" path = "plume-models"
[dependencies.rocket]
git = "https://github.com/SergioBenitez/Rocket"
rev = "55459db7732b9a240826a5c120c650f87e3372ce"
[dependencies.rocket_codegen]
git = "https://github.com/SergioBenitez/Rocket"
rev = "55459db7732b9a240826a5c120c650f87e3372ce"
[dependencies.rocket_contrib]
features = ["tera_templates", "json"]
git = "https://github.com/SergioBenitez/Rocket"
rev = "55459db7732b9a240826a5c120c650f87e3372ce"
[dependencies.rocket_csrf] [dependencies.rocket_csrf]
git = "https://github.com/fdb-hiroshima/rocket_csrf" git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "0dfb822d5cbf65a5eee698099368b7c0f4c61fa4" rev = "717fad53cfd2ee5cbee5b4571f6190644f9dddd7"
[dependencies.rocket_i18n] [build-dependencies]
git = "https://github.com/BaptisteGelez/rocket_i18n" ructe = "0.5.2"
rev = "75a3bfd7b847324c078a355a7f101f8241a9f59b" rocket_i18n = { version = "0.3.1", features = ["build"] }
[features] [features]
default = ["postgres"] default = ["postgres"]

15
build.rs Normal file
View File

@ -0,0 +1,15 @@
extern crate ructe;
extern crate rocket_i18n;
use ructe::*;
use std::{env, path::PathBuf};
fn main() {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
.join("templates");
compile_templates(&in_dir, &out_dir).expect("compile templates");
println!("cargo:rerun-if-changed=po");
rocket_i18n::update_po("plume", &["de", "en", "fr", "gl", "it", "ja", "nb", "pl", "ru"]);
rocket_i18n::compile_po("plume", &["de", "en", "fr", "gl", "it", "ja", "nb", "pl", "ru"]);
}

View File

@ -41,23 +41,26 @@ Now, make any changes to the code you want. After committing your changes, push
The project maintainers may suggest further changes to improve the pull request even more. After implementing this locally, you can push to your upstream fork again and the changes will immediately show up in the pull request after pushing. Once all the suggested changes are made, the pull request may be accepted. Thanks for contributing. The project maintainers may suggest further changes to improve the pull request even more. After implementing this locally, you can push to your upstream fork again and the changes will immediately show up in the pull request after pushing. Once all the suggested changes are made, the pull request may be accepted. Thanks for contributing.
## When working with Tera templates ## When working with Ructe templates
When working with the interface, or any message that will be displayed to the final user, keep in mind that Plume is an internationalized software. To make sure that the parts of the interface you are changing are translatable, you should: When working with the interface, or any message that will be displayed to the final user,
keep in mind that Plume is an internationalized software.
To make sure that the parts of the interface you are changing are translatable, you should:
- Use the `_` and `_n` filters instead of directly writing strings in your HTML markup - Wrap strings to translate in the `i18n!` macro (see [rocket_i18n docs](https://docs.rs/rocket_i18n/)
for more details about its arguments).The `Catalog` argument is usually `ctx.1`.
- Add the strings to translate to the `po/plume.pot` file - Add the strings to translate to the `po/plume.pot` file
Here is an example: let's say we want to add two strings, a simple one and one that may deal with plurals. The first step is to add them to whatever template we want to display them in: Here is an example: let's say we want to add two strings, a simple one and one
that may deal with plurals. The first step is to add them to whatever
template we want to display them in:
```jinja ```html
<p>{{ "Hello, world!" | _ }}</p> <p>@i18n!(ctx.1, "Hello, world!")</p>
<p>{{ "You have {{ count }} new notifications" | _n(singular="You have one new notification", count=n_notifications) }}</p> <p>@i18n!(ctx.1, "You have one new notification", "You have {0} new notifications", n_notifications)</p>
``` ```
As you can see, the `_` doesn't need any special argument to work, but `_n` requires `singular` (the singular form, in English) and `count` (the number of items, to determine which form to use) to be present. Note that any parameters given to these filters can be used as regular Tera variables inside of the translated strings, like we are doing with the `count` variable in the second string above.
The second step is to add them to POT file. To add a simple message, just do: The second step is to add them to POT file. To add a simple message, just do:
```po ```po
@ -69,7 +72,7 @@ For plural forms, the syntax is a bit different:
```po ```po
msgid "You have one new notification" # The singular form msgid "You have one new notification" # The singular form
msgid_plural "You have {{ count }} new notifications" # The plural one msgid_plural "You have {0} new notifications" # The plural one
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
``` ```
@ -84,4 +87,4 @@ For CSS, the only rule is to use One True Brace Style.
For JavaScript, we use [the JavaScript Standard Style](https://standardjs.com/). For JavaScript, we use [the JavaScript Standard Style](https://standardjs.com/).
For HTML/Tera templates, we use HTML5 syntax. For HTML/Ructe templates, we use HTML5 syntax.

View File

@ -23,16 +23,21 @@ Sometimes, strings may change depending on a number (for instance, a post counte
``` ```
msgid "One post" msgid "One post"
msgid_plural "{{ count }} posts" msgid_plural "{0} posts"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
``` ```
Then you should fill the two `msgstr` field, one with the singular form, the second with the plural one. If your language as more than two forms, you can add another one by following the same pattern (`msgstr[n] ""`). Then you should fill the two `msgstr` field, one with the singular form,
the second with the plural one. If your language as more than two forms,
you can add another one by following the same pattern (`msgstr[n] ""`).
## Interpolation ## Interpolation
Strings you translate may contain data from Plume (a username for instance). To tell Plume where to put these data, surround their identifier by `{{` and `}}`. The identifier is also present in this form in the English string to translate (this what you can see above, with the `{{ count }} posts` message). Strings you translate may contain data from Plume (a username for instance).
To tell Plume where to put these data, surround the number that identifies
them by `{` and `}`. The identifier is also present in this form in the English
string to translate (this what you can see above, with the `{0} posts` message).
## Note ## Note

View File

@ -11,11 +11,11 @@ array_tool = "1.0"
base64 = "0.9" base64 = "0.9"
failure = "0.1" failure = "0.1"
failure_derive = "0.1" failure_derive = "0.1"
gettext-rs = "0.4"
heck = "0.3.0" heck = "0.3.0"
hex = "0.3" hex = "0.3"
hyper = "0.11.27" hyper = "0.11.27"
openssl = "0.10.11" openssl = "0.10.11"
rocket = "0.4.0-rc.1"
reqwest = "0.9" reqwest = "0.9"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
@ -28,7 +28,3 @@ version = "0.4"
[dependencies.pulldown-cmark] [dependencies.pulldown-cmark]
default-features = false default-features = false
version = "0.1.2" version = "0.1.2"
[dependencies.rocket]
git = "https://github.com/SergioBenitez/Rocket"
rev = "55459db7732b9a240826a5c120c650f87e3372ce"

View File

@ -1,4 +1,4 @@
#![feature(custom_attribute, iterator_flatten)] #![feature(custom_attribute)]
extern crate activitypub; extern crate activitypub;
#[macro_use] #[macro_use]
@ -10,7 +10,6 @@ extern crate chrono;
extern crate failure; extern crate failure;
#[macro_use] #[macro_use]
extern crate failure_derive; extern crate failure_derive;
extern crate gettextrs;
extern crate hex; extern crate hex;
extern crate heck; extern crate heck;
extern crate hyper; extern crate hyper;

View File

@ -1,4 +1,3 @@
use gettextrs::gettext;
use heck::CamelCase; use heck::CamelCase;
use openssl::rand::rand_bytes; use openssl::rand::rand_bytes;
use pulldown_cmark::{Event, Parser, Options, Tag, html}; use pulldown_cmark::{Event, Parser, Options, Tag, html};
@ -23,8 +22,13 @@ pub fn make_actor_id(name: &str) -> String {
.collect() .collect()
} }
/**
* Redirects to the login page with a given message.
*
* Note that the message should be translated before passed to this function.
*/
pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> { pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
Flash::new(Redirect::to(format!("/login?m={}", gettext(message.to_string()))), "callback", url.into().to_string()) Flash::new(Redirect::to(format!("/login?m={}", message)), "callback", url.into().to_string())
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -6,6 +6,7 @@ authors = ["Baptiste Gelez <baptiste@gelez.xyz>"]
[dependencies] [dependencies]
activitypub = "0.1.1" activitypub = "0.1.1"
ammonia = "1.2.0" ammonia = "1.2.0"
askama_escape = "0.1"
bcrypt = "0.2" bcrypt = "0.2"
canapi = "0.1" canapi = "0.1"
guid-create = "0.1" guid-create = "0.1"
@ -13,6 +14,7 @@ heck = "0.3.0"
itertools = "0.7.8" itertools = "0.7.8"
lazy_static = "*" lazy_static = "*"
openssl = "0.10.11" openssl = "0.10.11"
rocket = "0.4.0-rc.1"
reqwest = "0.9" reqwest = "0.9"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
@ -36,10 +38,6 @@ path = "../plume-api"
[dependencies.plume-common] [dependencies.plume-common]
path = "../plume-common" path = "../plume-common"
[dependencies.rocket]
git = "https://github.com/SergioBenitez/Rocket"
rev = "55459db7732b9a240826a5c120c650f87e3372ce"
[dev-dependencies] [dev-dependencies]
diesel_migrations = "1.3.0" diesel_migrations = "1.3.0"

View File

@ -68,26 +68,10 @@ impl Comment {
.len() // TODO count in database? .len() // TODO count in database?
} }
pub fn to_json(&self, conn: &Connection, others: &[Comment]) -> serde_json::Value { pub fn get_responses(&self, conn: &Connection) -> Vec<Comment> {
let mut json = serde_json::to_value(self).expect("Comment::to_json: serialization error"); comments::table.filter(comments::in_response_to_id.eq(self.id))
json["author"] = self.get_author(conn).to_json(conn); .load::<Comment>(conn)
let mentions = Mention::list_for_comment(conn, self.id) .expect("Comment::get_responses: loading error")
.into_iter()
.map(|m| {
m.get_mentioned(conn)
.map(|u| u.get_fqn(conn))
.unwrap_or_default()
})
.collect::<Vec<String>>();
json["mentions"] = serde_json::to_value(mentions).expect("Comment::to_json: mention error");
json["responses"] = json!(
others
.into_iter()
.filter(|c| c.in_response_to_id.map(|id| id == self.id).unwrap_or(false))
.map(|c| c.to_json(conn, others))
.collect::<Vec<_>>()
);
json
} }
pub fn update_ap_url(&self, conn: &Connection) -> Comment { pub fn update_ap_url(&self, conn: &Connection) -> Comment {

View File

@ -20,9 +20,9 @@ pub struct Instance {
pub open_registrations: bool, pub open_registrations: bool,
pub short_description: SafeString, pub short_description: SafeString,
pub long_description: SafeString, pub long_description: SafeString,
pub default_license: String, pub default_license : String,
pub long_description_html: String, pub long_description_html: SafeString,
pub short_description_html: String, pub short_description_html: SafeString,
} }
#[derive(Clone, Insertable)] #[derive(Clone, Insertable)]
@ -244,14 +244,15 @@ pub(crate) mod tests {
default_license, default_license,
local, local,
long_description, long_description,
long_description_html,
short_description, short_description,
short_description_html,
name, name,
open_registrations, open_registrations,
public_domain public_domain
] ]
); );
assert_eq!(res.long_description_html.get(), &inserted.long_description_html);
assert_eq!(res.short_description_html.get(), &inserted.short_description_html);
assert_eq!(Instance::local_id(conn), res.id); assert_eq!(Instance::local_id(conn), res.id);
Ok(()) Ok(())
}); });
@ -282,14 +283,14 @@ pub(crate) mod tests {
default_license, default_license,
local, local,
long_description, long_description,
long_description_html,
short_description, short_description,
short_description_html,
name, name,
open_registrations, open_registrations,
public_domain public_domain
] ]
) );
assert_eq!(&newinst.long_description_html, inst.long_description_html.get());
assert_eq!(&newinst.short_description_html, inst.short_description_html.get());
}); });
let page = Instance::page(conn, (0, 2)); let page = Instance::page(conn, (0, 2));
@ -391,12 +392,12 @@ pub(crate) mod tests {
); );
assert_eq!( assert_eq!(
inst.long_description_html, inst.long_description_html,
"<p><a href=\"/with_link\">long_description</a></p>\n" SafeString::new("<p><a href=\"/with_link\">long_description</a></p>\n")
); );
assert_eq!(inst.short_description.get(), "[short](#link)"); assert_eq!(inst.short_description.get(), "[short](#link)");
assert_eq!( assert_eq!(
inst.short_description_html, inst.short_description_html,
"<p><a href=\"#link\">short</a></p>\n" SafeString::new("<p><a href=\"#link\">short</a></p>\n")
); );
Ok(()) Ok(())

View File

@ -1,8 +1,8 @@
#![allow(proc_macro_derive_resolution_fallback)] // This can be removed after diesel-1.4 #![allow(proc_macro_derive_resolution_fallback)] // This can be removed after diesel-1.4
#![feature(crate_in_paths)]
extern crate activitypub; extern crate activitypub;
extern crate ammonia; extern crate ammonia;
extern crate askama_escape;
extern crate bcrypt; extern crate bcrypt;
extern crate canapi; extern crate canapi;
extern crate chrono; extern crate chrono;

View File

@ -1,13 +1,14 @@
use activitypub::object::Image; use activitypub::object::Image;
use askama_escape::escape;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID; use guid_create::GUID;
use reqwest; use reqwest;
use serde_json;
use std::{fs, path::Path}; use std::{fs, path::Path};
use plume_common::activity_pub::Id; use plume_common::activity_pub::Id;
use instance::Instance; use instance::Instance;
use safe_string::SafeString;
use schema::medias; use schema::medias;
use users::User; use users::User;
use {ap_url, Connection}; use {ap_url, Connection};
@ -36,6 +37,14 @@ pub struct NewMedia {
pub owner_id: i32, pub owner_id: i32,
} }
#[derive(PartialEq)]
pub enum MediaCategory {
Image,
Audio,
Video,
Unknown,
}
impl Media { impl Media {
insert!(medias, NewMedia); insert!(medias, NewMedia);
get!(medias); get!(medias);
@ -47,65 +56,65 @@ impl Media {
.expect("Media::list_all_medias: loading error") .expect("Media::list_all_medias: loading error")
} }
pub fn to_json(&self, conn: &Connection) -> serde_json::Value { pub fn category(&self) -> MediaCategory {
let mut json = serde_json::to_value(self).expect("Media::to_json: serialization error"); match self
let url = self.url(conn);
let (cat, preview, html, md) = match self
.file_path .file_path
.rsplitn(2, '.') .rsplitn(2, '.')
.next() .next()
.expect("Media::to_json: extension error") .expect("Media::category: extension error")
{ {
"png" | "jpg" | "jpeg" | "gif" | "svg" => ( "png" | "jpg" | "jpeg" | "gif" | "svg" => MediaCategory::Image,
"image", "mp3" | "wav" | "flac" => MediaCategory::Audio,
format!( "mp4" | "avi" | "webm" | "mov" => MediaCategory::Video,
"<img src=\"{}\" alt=\"{}\" title=\"{}\" class=\"preview\">", _ => MediaCategory::Unknown,
url, self.alt_text, self.alt_text }
), }
format!(
"<img src=\"{}\" alt=\"{}\" title=\"{}\">", pub fn preview_html(&self, conn: &Connection) -> SafeString {
url, self.alt_text, self.alt_text let url = self.url(conn);
), match self.category() {
format!("![{}]({})", self.alt_text, url), MediaCategory::Image => SafeString::new(&format!(
), r#"<img src="{}" alt="{}" title="{}" class=\"preview\">"#,
"mp3" | "wav" | "flac" => ( url, escape(&self.alt_text), escape(&self.alt_text)
"audio", )),
format!( MediaCategory::Audio => SafeString::new(&format!(
"<audio src=\"{}\" title=\"{}\" class=\"preview\"></audio>", r#"<audio src="{}" title="{}" class="preview"></audio>"#,
url, self.alt_text url, escape(&self.alt_text)
), )),
format!( MediaCategory::Video => SafeString::new(&format!(
"<audio src=\"{}\" title=\"{}\"></audio>", r#"<video src="{}" title="{}" class="preview"></video>"#,
url, self.alt_text url, escape(&self.alt_text)
), )),
format!( MediaCategory::Unknown => SafeString::new(""),
"<audio src=\"{}\" title=\"{}\"></audio>", }
url, self.alt_text }
),
), pub fn html(&self, conn: &Connection) -> SafeString {
"mp4" | "avi" | "webm" | "mov" => ( let url = self.url(conn);
"video", match self.category() {
format!( MediaCategory::Image => SafeString::new(&format!(
"<video src=\"{}\" title=\"{}\" class=\"preview\"></video>", r#"<img src="{}" alt="{}" title="{}">"#,
url, self.alt_text url, escape(&self.alt_text), escape(&self.alt_text)
), )),
format!( MediaCategory::Audio => SafeString::new(&format!(
"<video src=\"{}\" title=\"{}\"></video>", r#"<audio src="{}" title="{}"></audio>"#,
url, self.alt_text url, escape(&self.alt_text)
), )),
format!( MediaCategory::Video => SafeString::new(&format!(
"<video src=\"{}\" title=\"{}\"></video>", r#"<video src="{}" title="{}"></video>"#,
url, self.alt_text url, escape(&self.alt_text)
), )),
), MediaCategory::Unknown => SafeString::new(""),
_ => ("unknown", String::new(), String::new(), String::new()), }
}; }
json["html_preview"] = json!(preview);
json["html"] = json!(html); pub fn markdown(&self, conn: &Connection) -> SafeString {
json["url"] = json!(url); let url = self.url(conn);
json["md"] = json!(md); match self.category() {
json["category"] = json!(cat); MediaCategory::Image => SafeString::new(&format!("![{}]({})", escape(&self.alt_text), url)),
json MediaCategory::Audio | MediaCategory::Video => self.html(conn),
MediaCategory::Unknown => SafeString::new(""),
}
} }
pub fn url(&self, conn: &Connection) -> String { pub fn url(&self, conn: &Connection) -> String {

View File

@ -1,6 +1,5 @@
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use serde_json;
use comments::Comment; use comments::Comment;
use follows::Follow; use follows::Follow;
@ -71,45 +70,65 @@ impl Notification {
.ok() .ok()
} }
pub fn to_json(&self, conn: &Connection) -> serde_json::Value { pub fn get_message(&self) -> &'static str {
let mut json = json!(self); match self.kind.as_ref() {
json["object"] = json!(match self.kind.as_ref() { notification_kind::COMMENT => "{0} commented your article.",
notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment| json!({ notification_kind::FOLLOW => "{0} is now following you.",
"post": comment.get_post(conn).to_json(conn), notification_kind::LIKE => "{0} liked your article.",
"user": comment.get_author(conn).to_json(conn), notification_kind::MENTION => "{0} mentioned you.",
"id": comment.id notification_kind::RESHARE => "{0} boosted your article.",
})), _ => unreachable!("Notification::get_message: Unknow type"),
notification_kind::FOLLOW => Follow::get(conn, self.object_id).map(|follow| { }
json!({ }
"follower": User::get(conn, follow.follower_id).map(|u| u.to_json(conn))
}) pub fn get_url(&self, conn: &Connection) -> Option<String> {
}), match self.kind.as_ref() {
notification_kind::LIKE => Like::get(conn, self.object_id).map(|like| { notification_kind::COMMENT => self.get_post(conn).map(|p| format!("{}#comment-{}", p.url(conn), self.object_id)),
json!({ notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).get_fqn(conn))),
"post": Post::get(conn, like.post_id).map(|p| p.to_json(conn)), notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention|
"user": User::get(conn, like.user_id).map(|u| u.to_json(conn)) mention.get_post(conn).map(|p| p.url(conn))
}) .unwrap_or_else(|| {
}), let comment = mention.get_comment(conn).expect("Notification::get_url: comment not found error");
notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention| { format!("{}#comment-{}", comment.get_post(conn).url(conn), comment.id)
json!({ })
"user": mention.get_user(conn).map(|u| u.to_json(conn)), ),
"url": mention.get_post(conn).map(|p| p.to_json(conn)["url"].clone()) _ => None,
.unwrap_or_else(|| { }
let comment = mention.get_comment(conn).expect("Notification::to_json: comment not found error"); }
let post = comment.get_post(conn).to_json(conn);
json!(format!("{}#comment-{}", post["url"].as_str().expect("Notification::to_json: post url error"), comment.id)) pub fn get_post(&self, conn: &Connection) -> Option<Post> {
}) match self.kind.as_ref() {
}) notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment| comment.get_post(conn)),
}), notification_kind::LIKE => Like::get(conn, self.object_id).and_then(|like| Post::get(conn, like.post_id)),
notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare| { notification_kind::RESHARE => Reshare::get(conn, self.object_id).and_then(|reshare| reshare.get_post(conn)),
json!({ _ => None,
"post": reshare.get_post(conn).map(|p| p.to_json(conn)), }
"user": reshare.get_user(conn).map(|u| u.to_json(conn)) }
})
}), pub fn get_actor(&self, conn: &Connection) -> User {
_ => Some(json!({})), match self.kind.as_ref() {
}); notification_kind::COMMENT => Comment::get(conn, self.object_id).expect("Notification::get_actor: comment error").get_author(conn),
json notification_kind::FOLLOW => User::get(conn, Follow::get(conn, self.object_id).expect("Notification::get_actor: follow error").follower_id)
.expect("Notification::get_actor: follower error"),
notification_kind::LIKE => User::get(conn, Like::get(conn, self.object_id).expect("Notification::get_actor: like error").user_id)
.expect("Notification::get_actor: liker error"),
notification_kind::MENTION => Mention::get(conn, self.object_id).expect("Notification::get_actor: mention error").get_user(conn)
.expect("Notification::get_actor: mentioner error"),
notification_kind::RESHARE => Reshare::get(conn, self.object_id).expect("Notification::get_actor: reshare error").get_user(conn)
.expect("Notification::get_actor: resharer error"),
_ => unreachable!("Notification::get_actor: Unknow type"),
}
}
pub fn icon_class(&self) -> &'static str {
match self.kind.as_ref() {
notification_kind::COMMENT => "icon-message-circle",
notification_kind::FOLLOW => "icon-user-plus",
notification_kind::LIKE => "icon-heart",
notification_kind::MENTION => "icon-at-sign",
notification_kind::RESHARE => "icon-repeat",
_ => unreachable!("Notification::get_actor: Unknow type"),
}
} }
pub fn delete(&self, conn: &Connection) { pub fn delete(&self, conn: &Connection) {

View File

@ -763,17 +763,9 @@ impl Post {
} }
} }
pub fn to_json(&self, conn: &Connection) -> serde_json::Value { pub fn url(&self, conn: &Connection) -> String {
let blog = self.get_blog(conn); let blog = self.get_blog(conn);
json!({ format!("/~/{}/{}", blog.get_fqn(conn), self.slug)
"post": self,
"author": self.get_authors(conn)[0].to_json(conn),
"url": format!("/~/{}/{}/", blog.get_fqn(conn), self.slug),
"date": self.creation_date.timestamp(),
"blog": blog.to_json(conn),
"tags": Tag::for_post(&*conn, self.id),
"cover": self.cover_id.and_then(|i| Media::get(conn, i).map(|m| m.to_json(conn))),
})
} }
pub fn compute_id(&self, conn: &Connection) -> String { pub fn compute_id(&self, conn: &Connection) -> String {
@ -784,6 +776,10 @@ impl Post {
self.slug self.slug
)) ))
} }
pub fn cover_url(&self, conn: &Connection) -> Option<String> {
self.cover_id.and_then(|i| Media::get(conn, i)).map(|c| c.url(conn))
}
} }
impl<'a> FromActivity<Article, (&'a Connection, &'a Searcher)> for Post { impl<'a> FromActivity<Article, (&'a Connection, &'a Searcher)> for Post {

View File

@ -26,6 +26,7 @@ use rocket::{
request::{self, FromRequest, Request}, request::{self, FromRequest, Request},
}; };
use serde_json; use serde_json;
use std::cmp::PartialEq;
use url::Url; use url::Url;
use webfinger::*; use webfinger::*;
@ -797,20 +798,8 @@ impl User {
CustomPerson::new(actor, ap_signature) CustomPerson::new(actor, ap_signature)
} }
pub fn to_json(&self, conn: &Connection) -> serde_json::Value { pub fn avatar_url(&self, conn: &Connection) -> String {
let mut json = serde_json::to_value(self).expect("User::to_json: serializing error"); self.avatar_id.and_then(|id| Media::get(conn, id).map(|m| m.url(conn))).unwrap_or("/static/default-avatar.png".to_string())
json["fqn"] = serde_json::Value::String(self.get_fqn(conn));
json["name"] = if !self.display_name.is_empty() {
json!(self.display_name)
} else {
json!(self.get_fqn(conn))
};
json["avatar"] = json!(
self.avatar_id
.and_then(|id| Media::get(conn, id).map(|m| m.url(conn)))
.unwrap_or_else(|| String::from("/static/default-avatar.png"))
);
json
} }
pub fn webfinger(&self, conn: &Connection) -> Webfinger { pub fn webfinger(&self, conn: &Connection) -> Webfinger {
@ -874,6 +863,14 @@ impl User {
pub fn needs_update(&self) -> bool { pub fn needs_update(&self) -> bool {
(Utc::now().naive_utc() - self.last_fetched_date).num_days() > 1 (Utc::now().naive_utc() - self.last_fetched_date).num_days() > 1
} }
pub fn name(&self, conn: &Connection) -> String {
if !self.display_name.is_empty() {
self.display_name.clone()
} else {
self.get_fqn(conn)
}
}
} }
impl<'a, 'r> FromRequest<'a, 'r> for User { impl<'a, 'r> FromRequest<'a, 'r> for User {
@ -946,6 +943,12 @@ impl Signer for User {
} }
} }
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl NewUser { impl NewUser {
/// Creates a new local user /// Creates a new local user
pub fn new_local( pub fn new_local(

View File

@ -1,9 +0,0 @@
en
fr
pl
de
nb
gl
it
ru
ja

156
po/de.po
View File

@ -34,8 +34,8 @@ msgstr "Titel"
msgid "Create blog" msgid "Create blog"
msgstr "Blog erstellen" msgstr "Blog erstellen"
msgid "Comment \"{{ post }}\"" msgid "Comment \"{0}\""
msgstr "Kommentar \"{{ post }}\"" msgstr "Kommentar \"{0}\""
msgid "Content" msgid "Content"
msgstr "Inhalt" msgstr "Inhalt"
@ -63,25 +63,22 @@ msgstr "Name"
msgid "Let&#x27;s go!" msgid "Let&#x27;s go!"
msgstr "Los geht's!" msgstr "Los geht's!"
msgid "Welcome to {{ instance_name | escape }}" msgid "Welcome to {0}"
msgstr "Willkommen auf {{ instance_name | escape }}" msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "Benachrichtigungen" msgstr "Benachrichtigungen"
msgid "" msgid "Written by {0}"
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgstr "" msgstr ""
"Geschrieben von {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}"
"{{ link_3 }}"
msgid "This article is under the {{ license }} license." msgid "This article is under the {0} license."
msgstr "Dieser Artikel steht unter der {{ license }} Lizenz." msgstr "Dieser Artikel steht unter der {0} Lizenz."
msgid "One like" msgid "One like"
msgid_plural "{{ count }} likes" msgid_plural "{0} likes"
msgstr[0] "Ein Like" msgstr[0] "Ein Like"
msgstr[1] "{{ count }} Likes" msgstr[1] "{0} Likes"
msgid "I don&#x27;t like this anymore" msgid "I don&#x27;t like this anymore"
msgstr "Nicht mehr Liken" msgstr "Nicht mehr Liken"
@ -90,9 +87,9 @@ msgid "Add yours"
msgstr "Like" msgstr "Like"
msgid "One Boost" msgid "One Boost"
msgid_plural "{{ count }} Boosts" msgid_plural "{0} Boosts"
msgstr[0] "Ein Boost" msgstr[0] "Ein Boost"
msgstr[1] "{{ count }} Boosts" msgstr[1] "{0} Boosts"
msgid "I don&#x27;t want to boost this anymore" msgid "I don&#x27;t want to boost this anymore"
msgstr "Nicht mehr boosten" msgstr "Nicht mehr boosten"
@ -153,8 +150,8 @@ msgstr "Das bist du"
msgid "Edit your profile" msgid "Edit your profile"
msgstr "Ändere dein Profil" msgstr "Ändere dein Profil"
msgid "Open on {{ instance_url }}" msgid "Open on {0}"
msgstr "Öffnen auf {{ instance_url }}" msgstr ""
msgid "Follow" msgid "Follow"
msgstr "Folgen" msgstr "Folgen"
@ -166,9 +163,9 @@ msgid "Recently boosted"
msgstr "Kürzlich geboostet" msgstr "Kürzlich geboostet"
msgid "One follower" msgid "One follower"
msgid_plural "{{ count }} followers" msgid_plural "{0} followers"
msgstr[0] "Ein Follower" msgstr[0] "Ein Follower"
msgstr[1] "{{ count }} Followers" msgstr[1] "{0} Followers"
msgid "Edit your account" msgid "Edit your account"
msgstr "Ändere deinen Account" msgstr "Ändere deinen Account"
@ -188,8 +185,8 @@ msgstr "Zusammenfassung"
msgid "Update account" msgid "Update account"
msgstr "Account aktualisieren" msgstr "Account aktualisieren"
msgid "{{ name | escape }}'s followers" msgid "{0}'s followers"
msgstr "{{ name | escape }}s Follower" msgstr "{0}s Follower"
msgid "Followers" msgid "Followers"
msgstr "Follower" msgstr "Follower"
@ -257,21 +254,20 @@ msgstr "Du musst eingeloggt sein, um jemandem zu folgen"
msgid "You need to be logged in order to edit your profile" msgid "You need to be logged in order to edit your profile"
msgstr "Du musst eingeloggt sein, um dein Profil zu editieren" msgstr "Du musst eingeloggt sein, um dein Profil zu editieren"
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgid "By {0}"
msgstr "" msgstr ""
"Von {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgid "{{ data }} boosted your article" msgid "{0} boosted your article"
msgstr "{{ data }} hat deinen Artikel geboostet" msgstr "{0} hat deinen Artikel geboostet"
msgid "{{ data }} started following you" msgid "{0} started following you"
msgstr "{{ data }} folgt dir nun" msgstr "{0} folgt dir nun"
msgid "{{ data }} liked your article" msgid "{0} liked your article"
msgstr "{{ data }} hat deinen Artikel geliked" msgstr "{0} hat deinen Artikel geliked"
msgid "{{ data }} commented your article" msgid "{0} commented your article"
msgstr "{{ data }} hat deinen Artikel kommentiert" msgstr "{0} hat deinen Artikel kommentiert"
msgid "We couldn&#x27;t find this page." msgid "We couldn&#x27;t find this page."
msgstr "Wir konnten diese Seite nicht finden." msgstr "Wir konnten diese Seite nicht finden."
@ -285,8 +281,8 @@ msgstr "Nicht berechtigt."
msgid "You are not author in this blog." msgid "You are not author in this blog."
msgstr "Du bist kein Autor in diesem Blog." msgstr "Du bist kein Autor in diesem Blog."
msgid "{{ data }} mentioned you." msgid "{0} mentioned you."
msgstr "{{ data }} hat dich erwähnt." msgstr "{0} hat dich erwähnt."
msgid "Your comment" msgid "Your comment"
msgstr "Dein Kommentar" msgstr "Dein Kommentar"
@ -326,9 +322,9 @@ msgid "Password should be at least 8 characters long"
msgstr "Passwort sollte mindestens 8 Zeichen lang sein" msgstr "Passwort sollte mindestens 8 Zeichen lang sein"
msgid "One author in this blog: " msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: " msgid_plural "{0} authors in this blog: "
msgstr[0] "Ein Autor in diesem Blog: " msgstr[0] "Ein Autor in diesem Blog: "
msgstr[1] "{{ count }} Autoren in diesem Blog: " msgstr[1] "{0} Autoren in diesem Blog: "
msgid "Login or use your Fediverse account to interact with this article" msgid "Login or use your Fediverse account to interact with this article"
msgstr "" msgstr ""
@ -339,9 +335,9 @@ msgid "Optional"
msgstr "Optional" msgstr "Optional"
msgid "One article in this blog" msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog" msgid_plural "{0} articles in this blog"
msgstr[0] "Ein Artikel in diesem Blog" msgstr[0] "Ein Artikel in diesem Blog"
msgstr[1] "{{ count }} Artikel in diesem Blog" msgstr[1] "{0} Artikel in diesem Blog"
msgid "Previous page" msgid "Previous page"
msgstr "Vorherige Seite" msgstr "Vorherige Seite"
@ -349,21 +345,6 @@ msgstr "Vorherige Seite"
msgid "Next page" msgid "Next page"
msgstr "Nächste Seite" msgstr "Nächste Seite"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} hat dich erwähnt."
msgid "{{ user }} commented your article."
msgstr "{{ user }} hat deinen Artikel kommentiert."
msgid "{{ user }} is now following you."
msgstr "{{ user }} folgt dir nun."
msgid "{{ user }} liked your article."
msgstr "{{ user }} hat deinen Artikel geliked."
msgid "{{ user }} boosted your article."
msgstr "{{ user }} hat deinen Artikel geboostet."
msgid "Source code" msgid "Source code"
msgstr "Quellcode" msgstr "Quellcode"
@ -419,20 +400,17 @@ msgstr ""
msgid "Create your account" msgid "Create your account"
msgstr "Eigenen Account erstellen" msgstr "Eigenen Account erstellen"
msgid "About {{ instance_name }}" msgid "About {0}"
msgstr "Über {{ instance_name }}" msgstr ""
msgid "Home to" msgid "Home to <em>{0}</em> users"
msgstr "Heimat von" msgstr ""
msgid "people" msgid "Who wrote <em>{0}</em> articles"
msgstr "Menschen" msgstr ""
msgid "Who wrote" msgid "And connected to <em>{0}</em> other instances"
msgstr "Wer schrieb" msgstr ""
msgid "articles"
msgstr "Artikel"
msgid "Read the detailed rules" msgid "Read the detailed rules"
msgstr "Lies die detailierten Regeln" msgstr "Lies die detailierten Regeln"
@ -444,17 +422,11 @@ msgstr "Artikel löschen"
msgid "Delete this blog" msgid "Delete this blog"
msgstr "Artikel löschen" msgstr "Artikel löschen"
msgid "And connected to"
msgstr "Und verbunden mit"
msgid "other instances"
msgstr "anderen Instanzen"
msgid "Administred by" msgid "Administred by"
msgstr "Administriert von" msgstr "Administriert von"
msgid "Runs Plume {{ version }}" msgid "Runs Plume {0}"
msgstr "Verwendet Plume {{ version }}" msgstr "Verwendet Plume {0}"
#, fuzzy #, fuzzy
msgid "Your media" msgid "Your media"
@ -463,8 +435,8 @@ msgstr "Deine Mediendateien"
msgid "Go to your gallery" msgid "Go to your gallery"
msgstr "Zu deiner Gallerie" msgstr "Zu deiner Gallerie"
msgid "{{ name}}'s avatar'" msgid "{0}'s avatar'"
msgstr "{{ name}}'s Avatar'" msgstr "{0}'s Avatar'"
msgid "Media details" msgid "Media details"
msgstr "Mediendetails" msgstr "Mediendetails"
@ -472,7 +444,6 @@ msgstr "Mediendetails"
msgid "Go back to the gallery" msgid "Go back to the gallery"
msgstr "Zurück zur Gallerie" msgstr "Zurück zur Gallerie"
#, fuzzy
msgid "Markdown code" msgid "Markdown code"
msgstr "Markdown Code" msgstr "Markdown Code"
@ -497,7 +468,6 @@ msgstr "Hochladen von Mediendateien"
msgid "Description" msgid "Description"
msgstr "Beschreibung" msgstr "Beschreibung"
#, fuzzy
msgid "Content warning" msgid "Content warning"
msgstr "Warnhinweis zum Inhalt" msgstr "Warnhinweis zum Inhalt"
@ -522,7 +492,6 @@ msgstr "Um zu liken, musst du eingeloggt sein"
msgid "Login to boost" msgid "Login to boost"
msgstr "Um zu boosten, musst du eingeloggt sein" msgstr "Um zu boosten, musst du eingeloggt sein"
#, fuzzy
msgid "Your feed" msgid "Your feed"
msgstr "Dein Feed" msgstr "Dein Feed"
@ -541,20 +510,21 @@ msgstr "Artikel"
msgid "All the articles of the Fediverse" msgid "All the articles of the Fediverse"
msgstr "Alle Artikel des Fediverse" msgstr "Alle Artikel des Fediverse"
msgid "Articles from {{ instance.name }}" msgid "Articles from {0}"
msgstr "Artikel von {{ instance.name }}" msgstr "Artikel von {0}"
msgid "View all" msgid "View all"
msgstr "Alles anzeigen" msgstr "Alles anzeigen"
msgid "Articles tagged \"{{ tag }}\"" msgid "Articles tagged \"{0}\""
msgstr "Mit \"{{ tag }}\" markierte Artikel" msgstr "Mit \"{0}\" markierte Artikel"
msgid "Edit" msgid "Edit"
msgstr "Bearbeiten" msgstr "Bearbeiten"
msgid "Edit {{ post }}" #, fuzzy
msgstr "{{ post }} bearbeiten" msgid "Edit {0}"
msgstr "Bearbeiten"
msgid "Update" msgid "Update"
msgstr "Aktualisieren" msgstr "Aktualisieren"
@ -574,8 +544,9 @@ msgstr ""
"Cookies in deinem Browser aktiviert sind und versuche diese Seite neu zu " "Cookies in deinem Browser aktiviert sind und versuche diese Seite neu zu "
"laden. Bitte melde diesen Fehler, falls er erneut auftritt." "laden. Bitte melde diesen Fehler, falls er erneut auftritt."
msgid "Administration of {{ instance.name }}" #, fuzzy
msgstr "Administration von {{ instance.name }}" msgid "Administration of {0}"
msgstr "Administration"
msgid "Instances" msgid "Instances"
msgstr "Instanzen" msgstr "Instanzen"
@ -638,5 +609,20 @@ msgstr "Administration"
msgid "None" msgid "None"
msgstr "" msgstr ""
#~ msgid "Your password should be at least 8 characters long" #~ msgid "Home to"
#~ msgstr "Das Passwort sollte mindestens 8 Zeichen lang sein" #~ msgstr "Heimat von"
#~ msgid "people"
#~ msgstr "Menschen"
#~ msgid "Who wrote"
#~ msgstr "Wer schrieb"
#~ msgid "articles"
#~ msgstr "Artikel"
#~ msgid "And connected to"
#~ msgstr "Und verbunden mit"
#~ msgid "other instances"
#~ msgstr "anderen Instanzen"

View File

@ -33,7 +33,7 @@ msgstr ""
msgid "Create blog" msgid "Create blog"
msgstr "" msgstr ""
msgid "Comment \"{{ post }}\"" msgid "Comment \"{0}\""
msgstr "" msgstr ""
msgid "Content" msgid "Content"
@ -60,23 +60,21 @@ msgstr ""
msgid "Let&#x27;s go!" msgid "Let&#x27;s go!"
msgstr "" msgstr ""
#, fuzzy msgid "Welcome to {0}"
msgid "Welcome to {{ instance_name | escape }}" msgstr ""
msgstr "Welcome to {{ instance_name }}"
msgid "Notifications" msgid "Notifications"
msgstr "" msgstr ""
msgid "" msgid "Written by {0}"
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgstr "" msgstr ""
msgid "This article is under the {{ license }} license." msgid "This article is under the {0} license."
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "One like" msgid "One like"
msgid_plural "{{ count }} likes" msgid_plural "{0} likes"
msgstr[0] "One follower" msgstr[0] "One follower"
msgstr[1] "{{ count }} followers" msgstr[1] "{{ count }} followers"
@ -87,7 +85,7 @@ msgid "Add yours"
msgstr "" msgstr ""
msgid "One Boost" msgid "One Boost"
msgid_plural "{{ count }} Boosts" msgid_plural "{0} Boosts"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
@ -148,9 +146,8 @@ msgstr ""
msgid "Edit your profile" msgid "Edit your profile"
msgstr "" msgstr ""
#, fuzzy msgid "Open on {0}"
msgid "Open on {{ instance_url }}" msgstr ""
msgstr "Welcome to {{ instance_name }}"
msgid "Follow" msgid "Follow"
msgstr "" msgstr ""
@ -162,8 +159,9 @@ msgstr "One follower"
msgid "Recently boosted" msgid "Recently boosted"
msgstr "" msgstr ""
#, fuzzy
msgid "One follower" msgid "One follower"
msgid_plural "{{ count }} followers" msgid_plural "{0} followers"
msgstr[0] "One follower" msgstr[0] "One follower"
msgstr[1] "{{ count }} followers" msgstr[1] "{{ count }} followers"
@ -186,7 +184,7 @@ msgid "Update account"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "{{ name | escape }}'s followers" msgid "{0}'s followers"
msgstr "One follower" msgstr "One follower"
#, fuzzy #, fuzzy
@ -256,19 +254,19 @@ msgstr ""
msgid "You need to be logged in order to edit your profile" msgid "You need to be logged in order to edit your profile"
msgstr "" msgstr ""
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgid "By {0}"
msgstr "" msgstr ""
msgid "{{ data }} boosted your article" msgid "{0} boosted your article"
msgstr "" msgstr ""
msgid "{{ data }} started following you" msgid "{0} started following you"
msgstr "" msgstr ""
msgid "{{ data }} liked your article" msgid "{0} liked your article"
msgstr "" msgstr ""
msgid "{{ data }} commented your article" msgid "{0} commented your article"
msgstr "" msgstr ""
msgid "We couldn&#x27;t find this page." msgid "We couldn&#x27;t find this page."
@ -283,7 +281,7 @@ msgstr ""
msgid "You are not author in this blog." msgid "You are not author in this blog."
msgstr "" msgstr ""
msgid "{{ data }} mentioned you." msgid "{0} mentioned you."
msgstr "" msgstr ""
msgid "Your comment" msgid "Your comment"
@ -323,7 +321,7 @@ msgid "Password should be at least 8 characters long"
msgstr "" msgstr ""
msgid "One author in this blog: " msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: " msgid_plural "{0} authors in this blog: "
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
@ -334,7 +332,7 @@ msgid "Optional"
msgstr "" msgstr ""
msgid "One article in this blog" msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog" msgid_plural "{0} articles in this blog"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
@ -344,21 +342,6 @@ msgstr ""
msgid "Next page" msgid "Next page"
msgstr "" msgstr ""
msgid "{{ user }} mentioned you."
msgstr ""
msgid "{{ user }} commented your article."
msgstr ""
msgid "{{ user }} is now following you."
msgstr ""
msgid "{{ user }} liked your article."
msgstr ""
msgid "{{ user }} boosted your article."
msgstr ""
msgid "Source code" msgid "Source code"
msgstr "" msgstr ""
@ -412,20 +395,16 @@ msgstr ""
msgid "Create your account" msgid "Create your account"
msgstr "" msgstr ""
#, fuzzy msgid "About {0}"
msgid "About {{ instance_name }}"
msgstr "Welcome to {{ instance_name }}"
msgid "Home to"
msgstr "" msgstr ""
msgid "people" msgid "Home to <em>{0}</em> users"
msgstr "" msgstr ""
msgid "Who wrote" msgid "Who wrote <em>{0}</em> articles"
msgstr "" msgstr ""
msgid "articles" msgid "And connected to <em>{0}</em> other instances"
msgstr "" msgstr ""
msgid "Read the detailed rules" msgid "Read the detailed rules"
@ -437,16 +416,10 @@ msgstr ""
msgid "Delete this blog" msgid "Delete this blog"
msgstr "" msgstr ""
msgid "And connected to"
msgstr ""
msgid "other instances"
msgstr ""
msgid "Administred by" msgid "Administred by"
msgstr "" msgstr ""
msgid "Runs Plume {{ version }}" msgid "Runs Plume {0}"
msgstr "" msgstr ""
msgid "Your media" msgid "Your media"
@ -455,7 +428,7 @@ msgstr ""
msgid "Go to your gallery" msgid "Go to your gallery"
msgstr "" msgstr ""
msgid "{{ name}}'s avatar'" msgid "{0}'s avatar'"
msgstr "" msgstr ""
msgid "Media details" msgid "Media details"
@ -529,20 +502,20 @@ msgid "All the articles of the Fediverse"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "Articles from {{ instance.name }}" msgid "Articles from {0}"
msgstr "Welcome to {{ instance_name }}" msgstr "Welcome to {{ instance_name }}"
msgid "View all" msgid "View all"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "Articles tagged \"{{ tag }}\"" msgid "Articles tagged \"{0}\""
msgstr "Welcome to {{ instance_name }}" msgstr "Welcome to {{ instance_name }}"
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
msgid "Edit {{ post }}" msgid "Edit {0}"
msgstr "" msgstr ""
msgid "Update" msgid "Update"
@ -561,7 +534,7 @@ msgid ""
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "Administration of {{ instance.name }}" msgid "Administration of {0}"
msgstr "Welcome to {{ instance_name }}" msgstr "Welcome to {{ instance_name }}"
msgid "Instances" msgid "Instances"

152
po/fr.po
View File

@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Gtranslator 2.91.7\n" "X-Generator: Gtranslator 2.91.7\n"
msgid "Latest articles" msgid "Latest articles"
@ -37,8 +37,8 @@ msgstr "Titre"
msgid "Create blog" msgid "Create blog"
msgstr "Créer le blog" msgstr "Créer le blog"
msgid "Comment \"{{ post }}\"" msgid "Comment \"{0}\""
msgstr "Commenter « {{ post }} »" msgstr "Commenter « {0} »"
msgid "Content" msgid "Content"
msgstr "Contenu" msgstr "Contenu"
@ -66,24 +66,22 @@ msgstr "Nom"
msgid "Let&#x27;s go!" msgid "Let&#x27;s go!"
msgstr "Cest parti !" msgstr "Cest parti !"
msgid "Welcome to {{ instance_name | escape }}" msgid "Welcome to {0}"
msgstr "Bienvenue sur {{ instance_name | escape }}" msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "Notifications" msgstr "Notifications"
msgid "" msgid "Written by {0}"
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}" msgstr "Écrit par {0}"
msgstr ""
"Écrit par {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgid "This article is under the {{ license }} license." msgid "This article is under the {0} license."
msgstr "Cet article est placé sous la licence {{ license }}" msgstr "Cet article est placé sous la licence {0}"
msgid "One like" msgid "One like"
msgid_plural "{{ count }} likes" msgid_plural "{0} likes"
msgstr[0] "{{ count }} personne aime cet article" msgstr[0] "{0} personne aime cet article"
msgstr[1] "{{ count }} personnes aiment cet article" msgstr[1] "{0} personnes aiment cet article"
msgid "I don&#x27;t like this anymore" msgid "I don&#x27;t like this anymore"
msgstr "Je naime plus" msgstr "Je naime plus"
@ -92,9 +90,9 @@ msgid "Add yours"
msgstr "Jaime" msgstr "Jaime"
msgid "One Boost" msgid "One Boost"
msgid_plural "{{ count }} Boosts" msgid_plural "{0} Boosts"
msgstr[0] "{{ count }} partage" msgstr[0] "{0} partage"
msgstr[1] "{{ count }} partages" msgstr[1] "{0} partages"
msgid "I don&#x27;t want to boost this anymore" msgid "I don&#x27;t want to boost this anymore"
msgstr "Je ne veux plus repartager ceci" msgstr "Je ne veux plus repartager ceci"
@ -155,8 +153,8 @@ msgstr "Cest vous"
msgid "Edit your profile" msgid "Edit your profile"
msgstr "Modifier votre profil" msgstr "Modifier votre profil"
msgid "Open on {{ instance_url }}" msgid "Open on {0}"
msgstr "Ouvrir sur {{ instance_url }}" msgstr "Ouvrir sur {0}"
msgid "Follow" msgid "Follow"
msgstr "Sabonner" msgstr "Sabonner"
@ -168,9 +166,9 @@ msgid "Recently boosted"
msgstr "Récemment partagé" msgstr "Récemment partagé"
msgid "One follower" msgid "One follower"
msgid_plural "{{ count }} followers" msgid_plural "{0} followers"
msgstr[0] "{{ count }} abonné⋅e" msgstr[0] "{0} abonné⋅e"
msgstr[1] "{{ count }} abonné⋅e⋅s" msgstr[1] "{0} abonné⋅e⋅s"
msgid "Edit your account" msgid "Edit your account"
msgstr "Modifier votre compte" msgstr "Modifier votre compte"
@ -190,8 +188,8 @@ msgstr "Description"
msgid "Update account" msgid "Update account"
msgstr "Mettre à jour mes informations" msgstr "Mettre à jour mes informations"
msgid "{{ name | escape }}'s followers" msgid "{0}'s followers"
msgstr "Les abonné⋅e⋅s de {{ name | escape }}" msgstr "Abonné⋅e⋅s de {0}"
msgid "Followers" msgid "Followers"
msgstr "Abonné⋅e⋅s" msgstr "Abonné⋅e⋅s"
@ -259,21 +257,20 @@ msgstr "Vous devez vous connecter pour suivre quelquun"
msgid "You need to be logged in order to edit your profile" msgid "You need to be logged in order to edit your profile"
msgstr "Vous devez vous connecter pour modifier votre profil" msgstr "Vous devez vous connecter pour modifier votre profil"
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgid "By {0}"
msgstr "" msgstr "Par {0}"
"Par {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgid "{{ data }} boosted your article" msgid "{0} boosted your article"
msgstr "{{ data }} a partagé votre article" msgstr "{0} a partagé votre article"
msgid "{{ data }} started following you" msgid "{0} started following you"
msgstr "{{ data }} vous suit" msgstr "{0} vous suit"
msgid "{{ data }} liked your article" msgid "{0} liked your article"
msgstr "{{ data }} a aimé votre article" msgstr "{0} a aimé votre article"
msgid "{{ data }} commented your article" msgid "{0} commented your article"
msgstr "{{ data }} a commenté votre article" msgstr "{0} a commenté votre article"
msgid "We couldn&#x27;t find this page." msgid "We couldn&#x27;t find this page."
msgstr "Page introuvable." msgstr "Page introuvable."
@ -287,8 +284,8 @@ msgstr "Vous navez pas les droits."
msgid "You are not author in this blog." msgid "You are not author in this blog."
msgstr "Vous nêtes pas auteur⋅ice dans ce blog." msgstr "Vous nêtes pas auteur⋅ice dans ce blog."
msgid "{{ data }} mentioned you." msgid "{0} mentioned you."
msgstr "{{ data }} vous a mentionné." msgstr "{0} vous a mentionné."
msgid "Your comment" msgid "Your comment"
msgstr "Votre commentaire" msgstr "Votre commentaire"
@ -329,9 +326,9 @@ msgid "Password should be at least 8 characters long"
msgstr "Le mot de passe doit faire au moins 8 caractères." msgstr "Le mot de passe doit faire au moins 8 caractères."
msgid "One author in this blog: " msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: " msgid_plural "{0} authors in this blog: "
msgstr[0] "{{ count }} auteur⋅ice dans ce blog : " msgstr[0] "{0} auteur⋅ice dans ce blog : "
msgstr[1] "{{ count }} auteur⋅ice⋅s dans ce blog : " msgstr[1] "{0} auteur⋅ice⋅s dans ce blog : "
msgid "Login or use your Fediverse account to interact with this article" msgid "Login or use your Fediverse account to interact with this article"
msgstr "" msgstr ""
@ -342,9 +339,9 @@ msgid "Optional"
msgstr "Optionnel" msgstr "Optionnel"
msgid "One article in this blog" msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog" msgid_plural "{0} articles in this blog"
msgstr[0] "{{ count }} article dans ce blog" msgstr[0] "{0} article dans ce blog"
msgstr[1] "{{ count }} articles dans ce blog" msgstr[1] "{0} articles dans ce blog"
msgid "Previous page" msgid "Previous page"
msgstr "Page précédente" msgstr "Page précédente"
@ -352,21 +349,6 @@ msgstr "Page précédente"
msgid "Next page" msgid "Next page"
msgstr "Page suivante" msgstr "Page suivante"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} vous a mentionné."
msgid "{{ user }} commented your article."
msgstr "{{ user }} a commenté votre article."
msgid "{{ user }} is now following you."
msgstr "{{ user }} vous suit."
msgid "{{ user }} liked your article."
msgstr "{{ user }} a aimé votre article."
msgid "{{ user }} boosted your article."
msgstr "{{ user }} a partagé votre article."
msgid "Source code" msgid "Source code"
msgstr "Code source" msgstr "Code source"
@ -424,20 +406,17 @@ msgstr ""
msgid "Create your account" msgid "Create your account"
msgstr "Créer votre compte" msgstr "Créer votre compte"
msgid "About {{ instance_name }}" msgid "About {0}"
msgstr "À propos de {{ instance_name }}" msgstr "À propos de {0}"
msgid "Home to" msgid "Home to <em>{0}</em> users"
msgstr "Accueille" msgstr "Accueille <em>{0} personnes"
msgid "people" msgid "Who wrote <em>{0}</em> articles"
msgstr "personnes" msgstr "Qui ont écrit <em>{0}</em> articles"
msgid "Who wrote" msgid "And connected to <em>{0}</em> other instances"
msgstr "Ayant écrit" msgstr "Et connecté à <em>{0}</em> autres instances"
msgid "articles"
msgstr "articles"
msgid "Read the detailed rules" msgid "Read the detailed rules"
msgstr "Lire les règles détaillées" msgstr "Lire les règles détaillées"
@ -448,17 +427,11 @@ msgstr "Supprimer cet article"
msgid "Delete this blog" msgid "Delete this blog"
msgstr "Supprimer ce blog" msgstr "Supprimer ce blog"
msgid "And connected to"
msgstr "Et connectée à"
msgid "other instances"
msgstr "autres instances"
msgid "Administred by" msgid "Administred by"
msgstr "Administré par" msgstr "Administré par"
msgid "Runs Plume {{ version }}" msgid "Runs Plume {0}"
msgstr "Propulsé par Plume {{ version }}" msgstr "Propulsé par Plume {0}"
msgid "Your media" msgid "Your media"
msgstr "Vos médias" msgstr "Vos médias"
@ -466,8 +439,8 @@ msgstr "Vos médias"
msgid "Go to your gallery" msgid "Go to your gallery"
msgstr "Aller à votre galerie" msgstr "Aller à votre galerie"
msgid "{{ name}}'s avatar'" msgid "{0}'s avatar'"
msgstr "Avatar de {{ name }}" msgstr "Avatar de {0}"
msgid "Media details" msgid "Media details"
msgstr "Détails du média" msgstr "Détails du média"
@ -541,20 +514,20 @@ msgstr "Articles"
msgid "All the articles of the Fediverse" msgid "All the articles of the Fediverse"
msgstr "Tous les articles de la Fédiverse" msgstr "Tous les articles de la Fédiverse"
msgid "Articles from {{ instance.name }}" msgid "Articles from {0}"
msgstr "Articles de {{ instance.name }}" msgstr "Articles de {0}"
msgid "View all" msgid "View all"
msgstr "Tout afficher" msgstr "Tout afficher"
msgid "Articles tagged \"{{ tag }}\"" msgid "Articles tagged \"{0}\""
msgstr "Articles taggués « {{ instance.name }} »" msgstr "Articles taggués « {0} »"
msgid "Edit" msgid "Edit"
msgstr "Modifier" msgstr "Modifier"
msgid "Edit {{ post }}" msgid "Edit {0}"
msgstr "Modifier « {{ post }} »" msgstr "Modifier {0}"
msgid "Update" msgid "Update"
msgstr "Mettre à jour" msgstr "Mettre à jour"
@ -574,8 +547,8 @@ msgstr ""
"sont activés dans votre navigateur, et essayez de recharger cette page. Si " "sont activés dans votre navigateur, et essayez de recharger cette page. Si "
"vous continuez à voir cette erreur, merci de la signaler." "vous continuez à voir cette erreur, merci de la signaler."
msgid "Administration of {{ instance.name }}" msgid "Administration of {0}"
msgstr "Administration de {{ instance.name }}" msgstr "Administration de {0}"
msgid "Instances" msgid "Instances"
msgstr "Instances" msgstr "Instances"
@ -631,9 +604,8 @@ msgstr "Cet article nest pas encore publié."
msgid "There is currently no article with that tag" msgid "There is currently no article with that tag"
msgstr "Il n'y a pas encore d'article avec ce tag" msgstr "Il n'y a pas encore d'article avec ce tag"
#, fuzzy
msgid "Illustration" msgid "Illustration"
msgstr "Administration" msgstr "Illustration"
msgid "None" msgid "None"
msgstr "" msgstr "Aucun"

153
po/gl.po
View File

@ -10,7 +10,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" "Plural-Forms: nplurals=1; plural=n > 1;\n"
msgid "Latest articles" msgid "Latest articles"
msgstr "Últimos artigos" msgstr "Últimos artigos"
@ -33,8 +33,8 @@ msgstr "Título"
msgid "Create blog" msgid "Create blog"
msgstr "Crear blog" msgstr "Crear blog"
msgid "Comment \"{{ post }}\"" msgid "Comment \"{0}\""
msgstr "Comentar \"{{ post }}\"" msgstr "Comentar \"{0}\""
msgid "Content" msgid "Content"
msgstr "Contido" msgstr "Contido"
@ -60,24 +60,22 @@ msgstr "Nome"
msgid "Let&#x27;s go!" msgid "Let&#x27;s go!"
msgstr "Imos!" msgstr "Imos!"
msgid "Welcome to {{ instance_name | escape }}" msgid "Welcome to {0}"
msgstr "Ben vida a {{ instance_name | escape }}" msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "Notificacións" msgstr "Notificacións"
msgid "" msgid "Written by {0}"
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgstr "" msgstr ""
"Escrito por {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgid "This article is under the {{ license }} license." msgid "This article is under the {0} license."
msgstr "Este artigo ten licenza {{ license }}" msgstr "Este artigo ten licenza {0}"
msgid "One like" msgid "One like"
msgid_plural "{{ count }} likes" msgid_plural "{0} likes"
msgstr[0] "Un gústame" msgstr[0] "Un gústame"
msgstr[1] "{{ count }} gústame" msgstr[1] "{0} gústame"
msgid "I don&#x27;t like this anymore" msgid "I don&#x27;t like this anymore"
msgstr "Xa non me gusta" msgstr "Xa non me gusta"
@ -86,9 +84,9 @@ msgid "Add yours"
msgstr "Engada os seus" msgstr "Engada os seus"
msgid "One Boost" msgid "One Boost"
msgid_plural "{{ count }} Boosts" msgid_plural "{0} Boosts"
msgstr[0] "Unha promoción" msgstr[0] "Unha promoción"
msgstr[1] "{{ count }} promocións" msgstr[1] "{0} promocións"
msgid "I don&#x27;t want to boost this anymore" msgid "I don&#x27;t want to boost this anymore"
msgstr "Quero retirar a pomoción realizada" msgstr "Quero retirar a pomoción realizada"
@ -147,8 +145,8 @@ msgstr "É vostede"
msgid "Edit your profile" msgid "Edit your profile"
msgstr "Edite o seu perfil" msgstr "Edite o seu perfil"
msgid "Open on {{ instance_url }}" msgid "Open on {0}"
msgstr "Abrir en {{ instance_url }}" msgstr ""
msgid "Follow" msgid "Follow"
msgstr "Seguir" msgstr "Seguir"
@ -160,9 +158,9 @@ msgid "Recently boosted"
msgstr "Promocionada recentemente" msgstr "Promocionada recentemente"
msgid "One follower" msgid "One follower"
msgid_plural "{{ count }} followers" msgid_plural "{0} followers"
msgstr[0] "Unha seguidora" msgstr[0] "Unha seguidora"
msgstr[1] "{{ count }} seguidoras" msgstr[1] "{0} seguidoras"
msgid "Edit your account" msgid "Edit your account"
msgstr "Edite a súa conta" msgstr "Edite a súa conta"
@ -182,8 +180,9 @@ msgstr "Resumen"
msgid "Update account" msgid "Update account"
msgstr "Actualizar conta" msgstr "Actualizar conta"
msgid "{{ name | escape }}'s followers" #, fuzzy
msgstr "Seguidoras de {{ name | escape }}" msgid "{0}'s followers"
msgstr "Unha seguidora"
msgid "Followers" msgid "Followers"
msgstr "Seguidoras" msgstr "Seguidoras"
@ -251,21 +250,20 @@ msgstr "Debe estar conectada para seguir a alguén"
msgid "You need to be logged in order to edit your profile" msgid "You need to be logged in order to edit your profile"
msgstr "Debe estar conectada para editar o seu perfil" msgstr "Debe estar conectada para editar o seu perfil"
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgid "By {0}"
msgstr "" msgstr ""
"Por {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgid "{{ data }} boosted your article" msgid "{0} boosted your article"
msgstr "{{ data }} promoveron o seu artigo" msgstr "{0} promoveron o seu artigo"
msgid "{{ data }} started following you" msgid "{0} started following you"
msgstr "{{ data }} comezou a seguila" msgstr "{0} comezou a seguila"
msgid "{{ data }} liked your article" msgid "{0} liked your article"
msgstr "{{ data }} gustou do seu artigo" msgstr "{0} gustou do seu artigo"
msgid "{{ data }} commented your article" msgid "{0} commented your article"
msgstr "{{ data }} comentou o seu artigo" msgstr "{0} comentou o seu artigo"
msgid "We couldn&#x27;t find this page." msgid "We couldn&#x27;t find this page."
msgstr "Non atopamos esta páxina" msgstr "Non atopamos esta páxina"
@ -279,8 +277,8 @@ msgstr "Non ten permiso."
msgid "You are not author in this blog." msgid "You are not author in this blog."
msgstr "Vostede non é autora en este blog." msgstr "Vostede non é autora en este blog."
msgid "{{ data }} mentioned you." msgid "{0} mentioned you."
msgstr "{{ data }} mencionouna." msgstr "{0} mencionouna."
msgid "Your comment" msgid "Your comment"
msgstr "O seu comentario" msgstr "O seu comentario"
@ -319,9 +317,9 @@ msgid "Password should be at least 8 characters long"
msgstr "O contrasinal debe ter ao menos 8 caracteres" msgstr "O contrasinal debe ter ao menos 8 caracteres"
msgid "One author in this blog: " msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: " msgid_plural "{0} authors in this blog: "
msgstr[0] "Unha autora en este blog: " msgstr[0] "Unha autora en este blog: "
msgstr[1] "{{ count }} autoras en este blog: " msgstr[1] "{0} autoras en este blog: "
msgid "Login or use your Fediverse account to interact with this article" msgid "Login or use your Fediverse account to interact with this article"
msgstr "" msgstr ""
@ -332,9 +330,9 @@ msgid "Optional"
msgstr "Opcional" msgstr "Opcional"
msgid "One article in this blog" msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog" msgid_plural "{0} articles in this blog"
msgstr[0] "Un artigo en este blog" msgstr[0] "Un artigo en este blog"
msgstr[1] "{{ count }} artigos en este blog" msgstr[1] "{0} artigos en este blog"
msgid "Previous page" msgid "Previous page"
msgstr "Páxina anterior" msgstr "Páxina anterior"
@ -342,21 +340,6 @@ msgstr "Páxina anterior"
msgid "Next page" msgid "Next page"
msgstr "Páxina seguinte" msgstr "Páxina seguinte"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} mencionouna."
msgid "{{ user }} commented your article."
msgstr "{{ user }} comentou o artigo."
msgid "{{ user }} is now following you."
msgstr "{{ user }} está a seguila."
msgid "{{ user }} liked your article."
msgstr "{{ user }} gustou do seu artigo."
msgid "{{ user }} boosted your article."
msgstr "{{ user }} promoveu o seu artigo."
msgid "Source code" msgid "Source code"
msgstr "Código fonte" msgstr "Código fonte"
@ -412,20 +395,17 @@ msgstr ""
msgid "Create your account" msgid "Create your account"
msgstr "Cree a súa conta" msgstr "Cree a súa conta"
msgid "About {{ instance_name }}" msgid "About {0}"
msgstr "Acerca de {{ instance_name }}" msgstr ""
msgid "Home to" msgid "Home to <em>{0}</em> users"
msgstr "Fogar de" msgstr ""
msgid "people" msgid "Who wrote <em>{0}</em> articles"
msgstr "persoas" msgstr ""
msgid "Who wrote" msgid "And connected to <em>{0}</em> other instances"
msgstr "Que escribiron" msgstr ""
msgid "articles"
msgstr "artigos"
msgid "Read the detailed rules" msgid "Read the detailed rules"
msgstr "Lea o detalle das normas" msgstr "Lea o detalle das normas"
@ -436,17 +416,11 @@ msgstr "Borrar este artigo"
msgid "Delete this blog" msgid "Delete this blog"
msgstr "Borrar este blog" msgstr "Borrar este blog"
msgid "And connected to"
msgstr "E conectada a"
msgid "other instances"
msgstr "outras instancias"
msgid "Administred by" msgid "Administred by"
msgstr "Administrada por" msgstr "Administrada por"
msgid "Runs Plume {{ version }}" msgid "Runs Plume {0}"
msgstr "Versión Plume {{ version }}" msgstr "Versión Plume {0}"
msgid "Your media" msgid "Your media"
msgstr "Os seus medios" msgstr "Os seus medios"
@ -454,7 +428,7 @@ msgstr "Os seus medios"
msgid "Go to your gallery" msgid "Go to your gallery"
msgstr "Ir a súa galería" msgstr "Ir a súa galería"
msgid "{{ name}}'s avatar'" msgid "{0}'s avatar'"
msgstr "Avatar de {{ name}}" msgstr "Avatar de {{ name}}"
msgid "Media details" msgid "Media details"
@ -529,20 +503,21 @@ msgstr "Artigos"
msgid "All the articles of the Fediverse" msgid "All the articles of the Fediverse"
msgstr "Todos os artigos do Fediverso" msgstr "Todos os artigos do Fediverso"
msgid "Articles from {{ instance.name }}" msgid "Articles from {0}"
msgstr "Artigos desde {{ instance_name }}" msgstr "Artigos desde {0}"
msgid "View all" msgid "View all"
msgstr "Ver todos" msgstr "Ver todos"
msgid "Articles tagged \"{{ tag }}\"" msgid "Articles tagged \"{0}\""
msgstr "Artigos etiquetados con {{ instance_name }}" msgstr "Artigos etiquetados con {0}"
msgid "Edit" msgid "Edit"
msgstr "Editar" msgstr "Editar"
msgid "Edit {{ post }}" #, fuzzy
msgstr "Editar \"{{ post }}\"" msgid "Edit {0}"
msgstr "Editar"
msgid "Update" msgid "Update"
msgstr "Actualizar" msgstr "Actualizar"
@ -562,8 +537,9 @@ msgstr ""
"no navegador, e recargue a páxina. Si persiste o aviso de este fallo, " "no navegador, e recargue a páxina. Si persiste o aviso de este fallo, "
"informe por favor." "informe por favor."
msgid "Administration of {{ instance.name }}" #, fuzzy
msgstr "Administración de {{ instance_name }}" msgid "Administration of {0}"
msgstr "Administración"
msgid "Instances" msgid "Instances"
msgstr "Instancias" msgstr "Instancias"
@ -611,7 +587,6 @@ msgstr ""
msgid "Users" msgid "Users"
msgstr "Usuarias" msgstr "Usuarias"
#, fuzzy
msgid "This post isn't published yet." msgid "This post isn't published yet."
msgstr "Esto é un borrador, non publicar por agora." msgstr "Esto é un borrador, non publicar por agora."
@ -624,3 +599,21 @@ msgstr "Administración"
msgid "None" msgid "None"
msgstr "" msgstr ""
#~ msgid "Home to"
#~ msgstr "Fogar de"
#~ msgid "people"
#~ msgstr "persoas"
#~ msgid "Who wrote"
#~ msgstr "Que escribiron"
#~ msgid "articles"
#~ msgstr "artigos"
#~ msgid "And connected to"
#~ msgstr "E conectada a"
#~ msgid "other instances"
#~ msgstr "outras instancias"

152
po/it.po
View File

@ -33,8 +33,8 @@ msgstr "Titolo"
msgid "Create blog" msgid "Create blog"
msgstr "Crea blog" msgstr "Crea blog"
msgid "Comment \"{{ post }}\"" msgid "Comment \"{0}\""
msgstr "Commenta \"{{ post }}\"" msgstr "Commenta \"{0}\""
msgid "Content" msgid "Content"
msgstr "Contenuto" msgstr "Contenuto"
@ -60,24 +60,22 @@ msgstr "Nome"
msgid "Let&#x27;s go!" msgid "Let&#x27;s go!"
msgstr "Andiamo!" msgstr "Andiamo!"
msgid "Welcome to {{ instance_name | escape }}" msgid "Welcome to {0}"
msgstr "Benvenuto su {{ instance_name | escape }}" msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "Notifiche" msgstr "Notifiche"
msgid "" msgid "Written by {0}"
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgstr "" msgstr ""
"Scritto da {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgid "This article is under the {{ license }} license." msgid "This article is under the {0} license."
msgstr "Questo articolo è rilasciato con licenza {{ license }} ." msgstr "Questo articolo è rilasciato con licenza {0} ."
msgid "One like" msgid "One like"
msgid_plural "{{ count }} likes" msgid_plural "{0} likes"
msgstr[0] "Un mi piace" msgstr[0] "Un mi piace"
msgstr[1] "{{ count }} mi piace" msgstr[1] "{0} mi piace"
msgid "I don&#x27;t like this anymore" msgid "I don&#x27;t like this anymore"
msgstr "Non mi piace più" msgstr "Non mi piace più"
@ -86,9 +84,9 @@ msgid "Add yours"
msgstr "Metti mi piace" msgstr "Metti mi piace"
msgid "One Boost" msgid "One Boost"
msgid_plural "{{ count }} Boosts" msgid_plural "{0} Boosts"
msgstr[0] "Un Boost" msgstr[0] "Un Boost"
msgstr[1] "{{ count }} Boost" msgstr[1] "{0} Boost"
msgid "I don&#x27;t want to boost this anymore" msgid "I don&#x27;t want to boost this anymore"
msgstr "Annulla boost" msgstr "Annulla boost"
@ -149,8 +147,8 @@ msgstr "Sei tu"
msgid "Edit your profile" msgid "Edit your profile"
msgstr "Modifica il tuo profilo" msgstr "Modifica il tuo profilo"
msgid "Open on {{ instance_url }}" msgid "Open on {0}"
msgstr "Apri su {{ instance_url }}" msgstr ""
msgid "Follow" msgid "Follow"
msgstr "Segui" msgstr "Segui"
@ -162,9 +160,9 @@ msgid "Recently boosted"
msgstr "Boostato recentemente" msgstr "Boostato recentemente"
msgid "One follower" msgid "One follower"
msgid_plural "{{ count }} followers" msgid_plural "{0} followers"
msgstr[0] "Uno ti segue" msgstr[0] "Uno ti segue"
msgstr[1] "{{ count }} ti seguono" msgstr[1] "{0} ti seguono"
msgid "Edit your account" msgid "Edit your account"
msgstr "Modifica il tuo account" msgstr "Modifica il tuo account"
@ -184,8 +182,9 @@ msgstr "Riepilogo"
msgid "Update account" msgid "Update account"
msgstr "Aggiorna account" msgstr "Aggiorna account"
msgid "{{ name | escape }}'s followers" #, fuzzy
msgstr "Persone che seguono {{ name | escape }}" msgid "{0}'s followers"
msgstr "Uno ti segue"
msgid "Followers" msgid "Followers"
msgstr "Seguaci" msgstr "Seguaci"
@ -253,21 +252,20 @@ msgstr "Devi effettuare l'accesso per seguire qualcuno"
msgid "You need to be logged in order to edit your profile" msgid "You need to be logged in order to edit your profile"
msgstr "Devi effettuare l'accesso per modificare il tuo profilo" msgstr "Devi effettuare l'accesso per modificare il tuo profilo"
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgid "By {0}"
msgstr "" msgstr ""
"Per {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgid "{{ data }} boosted your article" msgid "{0} boosted your article"
msgstr "{{ data }} ha boostato il tuo articolo" msgstr "{0} ha boostato il tuo articolo"
msgid "{{ data }} started following you" msgid "{0} started following you"
msgstr "{{ data }} ha iniziato a seguirti" msgstr "{0} ha iniziato a seguirti"
msgid "{{ data }} liked your article" msgid "{0} liked your article"
msgstr "{{ data }} ha messo mi piace al tuo articolo" msgstr "{0} ha messo mi piace al tuo articolo"
msgid "{{ data }} commented your article" msgid "{0} commented your article"
msgstr "{{ data }} ha commentato il tuo articolo" msgstr "{0} ha commentato il tuo articolo"
msgid "We couldn&#x27;t find this page." msgid "We couldn&#x27;t find this page."
msgstr "Non riusciamo a trovare questa pagina." msgstr "Non riusciamo a trovare questa pagina."
@ -281,8 +279,8 @@ msgstr "Non sei autorizzato."
msgid "You are not author in this blog." msgid "You are not author in this blog."
msgstr "Non sei l'autore di questo blog." msgstr "Non sei l'autore di questo blog."
msgid "{{ data }} mentioned you." msgid "{0} mentioned you."
msgstr "{{ data }} ti ha menzionato." msgstr "{0} ti ha menzionato."
msgid "Your comment" msgid "Your comment"
msgstr "Il tuo commento" msgstr "Il tuo commento"
@ -321,9 +319,9 @@ msgid "Password should be at least 8 characters long"
msgstr "Le password devono essere lunghe almeno 8 caratteri" msgstr "Le password devono essere lunghe almeno 8 caratteri"
msgid "One author in this blog: " msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: " msgid_plural "{0} authors in this blog: "
msgstr[0] "Un autore in questo blog: " msgstr[0] "Un autore in questo blog: "
msgstr[1] "{{ count }} autori in questo blog: " msgstr[1] "{0} autori in questo blog: "
msgid "Login or use your Fediverse account to interact with this article" msgid "Login or use your Fediverse account to interact with this article"
msgstr "" msgstr ""
@ -334,9 +332,9 @@ msgid "Optional"
msgstr "Opzionale" msgstr "Opzionale"
msgid "One article in this blog" msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog" msgid_plural "{0} articles in this blog"
msgstr[0] "Un articolo in questo blog" msgstr[0] "Un articolo in questo blog"
msgstr[1] "{{ count }} articoli in questo blog" msgstr[1] "{0} articoli in questo blog"
msgid "Previous page" msgid "Previous page"
msgstr "Pagina precedente" msgstr "Pagina precedente"
@ -344,21 +342,6 @@ msgstr "Pagina precedente"
msgid "Next page" msgid "Next page"
msgstr "Prossima pagina" msgstr "Prossima pagina"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} ti ha menzionato."
msgid "{{ user }} commented your article."
msgstr "{{ user }} ha commentato il tuo articolo."
msgid "{{ user }} is now following you."
msgstr "{{ user }} ora ti segue."
msgid "{{ user }} liked your article."
msgstr "{{ user }} ha messo mi piace al tuo articolo."
msgid "{{ user }} boosted your article."
msgstr "{{ user }} ha boostato il tuo articolo."
msgid "Source code" msgid "Source code"
msgstr "Codice sorgente" msgstr "Codice sorgente"
@ -414,20 +397,17 @@ msgstr ""
msgid "Create your account" msgid "Create your account"
msgstr "Crea il tuo account" msgstr "Crea il tuo account"
msgid "About {{ instance_name }}" msgid "About {0}"
msgstr "A proposito di {{ instance_name }}" msgstr ""
msgid "Home to" msgid "Home to <em>{0}</em> users"
msgstr "Casa di" msgstr ""
msgid "people" msgid "Who wrote <em>{0}</em> articles"
msgstr "persone" msgstr ""
msgid "Who wrote" msgid "And connected to <em>{0}</em> other instances"
msgstr "Che hanno scritto" msgstr ""
msgid "articles"
msgstr "articoli"
msgid "Read the detailed rules" msgid "Read the detailed rules"
msgstr "Leggi le regole dettagliate" msgstr "Leggi le regole dettagliate"
@ -438,17 +418,11 @@ msgstr "Elimina questo articolo"
msgid "Delete this blog" msgid "Delete this blog"
msgstr "Elimina questo blog" msgstr "Elimina questo blog"
msgid "And connected to"
msgstr "E connesso a"
msgid "other instances"
msgstr "altre istanze"
msgid "Administred by" msgid "Administred by"
msgstr "Amministrata da" msgstr "Amministrata da"
msgid "Runs Plume {{ version }}" msgid "Runs Plume {0}"
msgstr "Utilizza Plume {{ version }}" msgstr "Utilizza Plume {0}"
msgid "Your media" msgid "Your media"
msgstr "I tuoi media" msgstr "I tuoi media"
@ -456,8 +430,8 @@ msgstr "I tuoi media"
msgid "Go to your gallery" msgid "Go to your gallery"
msgstr "Vai alla tua galleria" msgstr "Vai alla tua galleria"
msgid "{{ name}}'s avatar'" msgid "{0}'s avatar'"
msgstr "Avatar di {{ name}}" msgstr "Avatar di {0}"
msgid "Media details" msgid "Media details"
msgstr "Dettagli del media" msgstr "Dettagli del media"
@ -531,20 +505,21 @@ msgstr "Articoli"
msgid "All the articles of the Fediverse" msgid "All the articles of the Fediverse"
msgstr "Tutti gli articoli del Fediverso" msgstr "Tutti gli articoli del Fediverso"
msgid "Articles from {{ instance.name }}" msgid "Articles from {0}"
msgstr "Articoli da {{ instance.name }}" msgstr "Articoli da {0}}"
msgid "View all" msgid "View all"
msgstr "Vedi tutto" msgstr "Vedi tutto"
msgid "Articles tagged \"{{ tag }}\"" msgid "Articles tagged \"{0}\""
msgstr "Articoli etichettati \"{{ tag }}\"" msgstr "Articoli etichettati \"{0}\""
msgid "Edit" msgid "Edit"
msgstr "Modifica" msgstr "Modifica"
msgid "Edit {{ post }}" #, fuzzy
msgstr "Modifica {{ post }}" msgid "Edit {0}"
msgstr "Modifica"
msgid "Update" msgid "Update"
msgstr "Aggiorna" msgstr "Aggiorna"
@ -564,8 +539,9 @@ msgstr ""
"i cookies nel tuo browser, e prova a ricaricare questa pagina. Se l'errore " "i cookies nel tuo browser, e prova a ricaricare questa pagina. Se l'errore "
"si dovesse ripresentare, per favore segnalacelo." "si dovesse ripresentare, per favore segnalacelo."
msgid "Administration of {{ instance.name }}" #, fuzzy
msgstr "Amministrazione di {{ instance.name }}" msgid "Administration of {0}"
msgstr "Amministrazione"
msgid "Instances" msgid "Instances"
msgstr "Istanze" msgstr "Istanze"
@ -626,3 +602,21 @@ msgstr "Amministrazione"
msgid "None" msgid "None"
msgstr "" msgstr ""
#~ msgid "Home to"
#~ msgstr "Casa di"
#~ msgid "people"
#~ msgstr "persone"
#~ msgid "Who wrote"
#~ msgstr "Che hanno scritto"
#~ msgid "articles"
#~ msgstr "articoli"
#~ msgid "And connected to"
#~ msgstr "E connesso a"
#~ msgid "other instances"
#~ msgstr "altre istanze"

225
po/ja.po
View File

@ -4,13 +4,13 @@ msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2018-12-03 21:52+0900\n" "PO-Revision-Date: 2018-12-03 21:52+0900\n"
"Last-Translator: Ryo Nakano <ryonakaknock3@gmail.com>\n"
"Language-Team: \n"
"Language: ja\n" "Language: ja\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"Last-Translator: Ryo Nakano <ryonakaknock3@gmail.com>\n"
"Language-Team: \n"
"X-Generator: Poedit 2.2\n" "X-Generator: Poedit 2.2\n"
msgid "Latest articles" msgid "Latest articles"
@ -34,7 +34,8 @@ msgstr "タイトル"
msgid "Create blog" msgid "Create blog"
msgstr "ブログを作成" msgstr "ブログを作成"
msgid "Comment \"{{ post }}\"" #, fuzzy
msgid "Comment \"{0}\""
msgstr "\"{{ post }}\" にコメント" msgstr "\"{{ post }}\" にコメント"
msgid "Content" msgid "Content"
@ -47,7 +48,8 @@ msgid "Something broke on our side."
msgstr "サーバー側で何らかの問題が発生しました。" msgstr "サーバー側で何らかの問題が発生しました。"
msgid "Sorry about that. If you think this is a bug, please report it." msgid "Sorry about that. If you think this is a bug, please report it."
msgstr "申し訳ありません。これがバグだと思われる場合は、問題を報告してください。" msgstr ""
"申し訳ありません。これがバグだと思われる場合は、問題を報告してください。"
msgid "Configuration" msgid "Configuration"
msgstr "設定" msgstr "設定"
@ -61,20 +63,22 @@ msgstr "名前"
msgid "Let&#x27;s go!" msgid "Let&#x27;s go!"
msgstr "開始しましょう!" msgstr "開始しましょう!"
msgid "Welcome to {{ instance_name | escape }}" msgid "Welcome to {0}"
msgstr "{{ instance_name | escape }} へようこそ" msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "通知" msgstr "通知"
msgid "Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}" msgid "Written by {0}"
msgstr "{{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }} さんが投稿" msgstr ""
msgid "This article is under the {{ license }} license." #, fuzzy
msgid "This article is under the {0} license."
msgstr "この記事は {{ license }} ライセンスの元で公開されています。" msgstr "この記事は {{ license }} ライセンスの元で公開されています。"
#, fuzzy
msgid "One like" msgid "One like"
msgid_plural "{{ count }} likes" msgid_plural "{0} likes"
msgstr[0] "{{ count }} いいね" msgstr[0] "{{ count }} いいね"
msgid "I don&#x27;t like this anymore" msgid "I don&#x27;t like this anymore"
@ -83,8 +87,9 @@ msgstr "もうこれにいいねしません"
msgid "Add yours" msgid "Add yours"
msgstr "いいねする" msgstr "いいねする"
#, fuzzy
msgid "One Boost" msgid "One Boost"
msgid_plural "{{ count }} Boosts" msgid_plural "{0} Boosts"
msgstr[0] "{{ count }} ブースト" msgstr[0] "{{ count }} ブースト"
msgid "I don&#x27;t want to boost this anymore" msgid "I don&#x27;t want to boost this anymore"
@ -130,7 +135,9 @@ msgid "Your Blogs"
msgstr "自分のブログ" msgstr "自分のブログ"
msgid "You don&#x27;t have any blog yet. Create your own, or ask to join one." msgid "You don&#x27;t have any blog yet. Create your own, or ask to join one."
msgstr "まだブログを開設していません。ご自身のブログを開設するか、他のブログに参加するようにお願いしてください。" msgstr ""
"まだブログを開設していません。ご自身のブログを開設するか、他のブログに参加す"
"るようにお願いしてください。"
msgid "Start a new blog" msgid "Start a new blog"
msgstr "新しいブログを開始" msgstr "新しいブログを開始"
@ -144,8 +151,8 @@ msgstr "自分"
msgid "Edit your profile" msgid "Edit your profile"
msgstr "自分のプロフィールを編集" msgstr "自分のプロフィールを編集"
msgid "Open on {{ instance_url }}" msgid "Open on {0}"
msgstr "{{ instance_url }} で開く" msgstr ""
msgid "Follow" msgid "Follow"
msgstr "フォロー" msgstr "フォロー"
@ -156,8 +163,9 @@ msgstr "フォロー解除"
msgid "Recently boosted" msgid "Recently boosted"
msgstr "最近ブーストしたもの" msgstr "最近ブーストしたもの"
#, fuzzy
msgid "One follower" msgid "One follower"
msgid_plural "{{ count }} followers" msgid_plural "{0} followers"
msgstr[0] "{{ count }} フォロワー" msgstr[0] "{{ count }} フォロワー"
msgid "Edit your account" msgid "Edit your account"
@ -178,8 +186,9 @@ msgstr "要約"
msgid "Update account" msgid "Update account"
msgstr "アカウントをアップデート" msgstr "アカウントをアップデート"
msgid "{{ name | escape }}'s followers" #, fuzzy
msgstr "{{ name | escape }} のフォロワー" msgid "{0}'s followers"
msgstr "{{ count }} フォロワー"
msgid "Followers" msgid "Followers"
msgstr "フォロワー" msgstr "フォロワー"
@ -247,19 +256,23 @@ msgstr "他の人をフォローするにはログインする必要がありま
msgid "You need to be logged in order to edit your profile" msgid "You need to be logged in order to edit your profile"
msgstr "自分のプロフィールを編集するにはログインする必要があります" msgstr "自分のプロフィールを編集するにはログインする必要があります"
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgid "By {0}"
msgstr "{{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }} が投稿" msgstr ""
msgid "{{ data }} boosted your article" #, fuzzy
msgid "{0} boosted your article"
msgstr "{{ data }} があなたの記事をブーストしました" msgstr "{{ data }} があなたの記事をブーストしました"
msgid "{{ data }} started following you" #, fuzzy
msgid "{0} started following you"
msgstr "{{ data }} があなたのフォローを開始しました" msgstr "{{ data }} があなたのフォローを開始しました"
msgid "{{ data }} liked your article" #, fuzzy
msgid "{0} liked your article"
msgstr "{{ data }} があなたの記事をいいねしました" msgstr "{{ data }} があなたの記事をいいねしました"
msgid "{{ data }} commented your article" #, fuzzy
msgid "{0} commented your article"
msgstr "{{ data }} があなたの記事にコメントしました" msgstr "{{ data }} があなたの記事にコメントしました"
msgid "We couldn&#x27;t find this page." msgid "We couldn&#x27;t find this page."
@ -274,7 +287,8 @@ msgstr "認証されていません。"
msgid "You are not author in this blog." msgid "You are not author in this blog."
msgstr "あなたはこのブログの作者ではありません。" msgstr "あなたはこのブログの作者ではありません。"
msgid "{{ data }} mentioned you." #, fuzzy
msgid "{0} mentioned you."
msgstr "{{ data }} があなたをメンションしました。" msgstr "{{ data }} があなたをメンションしました。"
msgid "Your comment" msgid "Your comment"
@ -313,18 +327,21 @@ msgstr "無効なメールアドレス"
msgid "Password should be at least 8 characters long" msgid "Password should be at least 8 characters long"
msgstr "パスワードは最低 8 文字にするべきです" msgstr "パスワードは最低 8 文字にするべきです"
#, fuzzy
msgid "One author in this blog: " msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: " msgid_plural "{0} authors in this blog: "
msgstr[0] "ブログに {{ count }} 人の作成者がいます: " msgstr[0] "ブログに {{ count }} 人の作成者がいます: "
msgid "Login or use your Fediverse account to interact with this article" msgid "Login or use your Fediverse account to interact with this article"
msgstr "この記事と関わるにはログインするか Fediverse アカウントを使用してください" msgstr ""
"この記事と関わるにはログインするか Fediverse アカウントを使用してください"
msgid "Optional" msgid "Optional"
msgstr "省略可" msgstr "省略可"
#, fuzzy
msgid "One article in this blog" msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog" msgid_plural "{0} articles in this blog"
msgstr[0] "ブログ内に {{ count }} 件の記事" msgstr[0] "ブログ内に {{ count }} 件の記事"
msgid "Previous page" msgid "Previous page"
@ -333,21 +350,6 @@ msgstr "前のページ"
msgid "Next page" msgid "Next page"
msgstr "次のページ" msgstr "次のページ"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} があなたをメンションしました。"
msgid "{{ user }} commented your article."
msgstr "{{ user }} があなたの記事にコメントしました。"
msgid "{{ user }} is now following you."
msgstr "{{ user }} はあなたをフォローしています。"
msgid "{{ user }} liked your article."
msgstr "{{ user }} があなたの記事にいいねしました。"
msgid "{{ user }} boosted your article."
msgstr "{{ user }} があなたの記事をブーストしました。"
msgid "Source code" msgid "Source code"
msgstr "ソースコード" msgstr "ソースコード"
@ -393,26 +395,27 @@ msgstr "Plume は分散型ブログエンジンです。"
msgid "Authors can manage various blogs from an unique website." msgid "Authors can manage various blogs from an unique website."
msgstr "作成者は、ある固有の Web サイトから、さまざまなブログを管理できます。" msgstr "作成者は、ある固有の Web サイトから、さまざまなブログを管理できます。"
msgid "Articles are also visible on other Plume websites, and you can interact with them directly from other platforms like Mastodon." msgid ""
msgstr "記事は他の Plume Web サイトからも閲覧可能であり、Mastdon のように他のプラットフォームから直接記事と関わることができます。" "Articles are also visible on other Plume websites, and you can interact with "
"them directly from other platforms like Mastodon."
msgstr ""
"記事は他の Plume Web サイトからも閲覧可能であり、Mastdon のように他のプラット"
"フォームから直接記事と関わることができます。"
msgid "Create your account" msgid "Create your account"
msgstr "アカウントを作成" msgstr "アカウントを作成"
msgid "About {{ instance_name }}" msgid "About {0}"
msgstr "{{ instance_name }} について" msgstr ""
msgid "Home to" msgid "Home to <em>{0}</em> users"
msgstr "登録者数" msgstr ""
msgid "people" msgid "Who wrote <em>{0}</em> articles"
msgstr "" msgstr ""
msgid "Who wrote" msgid "And connected to <em>{0}</em> other instances"
msgstr "投稿記事数" msgstr ""
msgid "articles"
msgstr "件"
msgid "Read the detailed rules" msgid "Read the detailed rules"
msgstr "詳細な規則を読む" msgstr "詳細な規則を読む"
@ -423,16 +426,11 @@ msgstr "この記事を削除"
msgid "Delete this blog" msgid "Delete this blog"
msgstr "このブログを削除" msgstr "このブログを削除"
msgid "And connected to"
msgstr "他のインスタンスからの接続数"
msgid "other instances"
msgstr "件"
msgid "Administred by" msgid "Administred by"
msgstr "管理者" msgstr "管理者"
msgid "Runs Plume {{ version }}" #, fuzzy
msgid "Runs Plume {0}"
msgstr "Plume {{ version }} を実行中" msgstr "Plume {{ version }} を実行中"
msgid "Your media" msgid "Your media"
@ -441,7 +439,8 @@ msgstr "メディア"
msgid "Go to your gallery" msgid "Go to your gallery"
msgstr "ギャラリーを参照" msgstr "ギャラリーを参照"
msgid "{{ name}}'s avatar'" #, fuzzy
msgid "{0}'s avatar'"
msgstr "{{ name}} のアバター" msgstr "{{ name}} のアバター"
msgid "Media details" msgid "Media details"
@ -483,8 +482,11 @@ msgstr "ファイル"
msgid "Send" msgid "Send"
msgstr "送信" msgstr "送信"
msgid "Sorry, but registrations are closed on this instance. Try to find another one" msgid ""
msgstr "申し訳ありませんが、このインスタンスでは登録者は限定されています。別のインスタンスをお探しください" "Sorry, but registrations are closed on this instance. Try to find another one"
msgstr ""
"申し訳ありませんが、このインスタンスでは登録者は限定されています。別のインス"
"タンスをお探しください"
msgid "Subtitle" msgid "Subtitle"
msgstr "サブタイトル" msgstr "サブタイトル"
@ -505,7 +507,9 @@ msgid "Local feed"
msgstr "このインスタンスのフィード" msgstr "このインスタンスのフィード"
msgid "Nothing to see here yet. Try to follow more people." msgid "Nothing to see here yet. Try to follow more people."
msgstr "ここにはまだ表示できるものがありません。他の人をもっとフォローしてみてください。" msgstr ""
"ここにはまだ表示できるものがありません。他の人をもっとフォローしてみてくださ"
"い。"
msgid "Articles" msgid "Articles"
msgstr "記事" msgstr "記事"
@ -513,20 +517,23 @@ msgstr "記事"
msgid "All the articles of the Fediverse" msgid "All the articles of the Fediverse"
msgstr "Fediverse のすべての記事" msgstr "Fediverse のすべての記事"
msgid "Articles from {{ instance.name }}" #, fuzzy
msgid "Articles from {0}"
msgstr "{{ instance.name }} の記事" msgstr "{{ instance.name }} の記事"
msgid "View all" msgid "View all"
msgstr "すべて表示" msgstr "すべて表示"
msgid "Articles tagged \"{{ tag }}\"" #, fuzzy
msgid "Articles tagged \"{0}\""
msgstr "\"{{ tag }}\" タグの記事" msgstr "\"{{ tag }}\" タグの記事"
msgid "Edit" msgid "Edit"
msgstr "編集" msgstr "編集"
msgid "Edit {{ post }}" #, fuzzy
msgstr "{{ post }} を編集" msgid "Edit {0}"
msgstr "編集"
msgid "Update" msgid "Update"
msgstr "アップデート" msgstr "アップデート"
@ -537,11 +544,18 @@ msgstr "このページは見つかりませんでした。"
msgid "Invalid CSRF token." msgid "Invalid CSRF token."
msgstr "無効な CSRF トークンです。" msgstr "無効な CSRF トークンです。"
msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." msgid ""
msgstr "ご自身の CSRF トークンにおいて問題が発生しました。お使いのブラウザーで Cookie が有効になっていることを確認して、このページを再読み込みしてみてください。このエラーメッセージが表示され続けた場合、問題を報告してください。" "Something is wrong with your CSRF token. Make sure cookies are enabled in "
"you browser, and try reloading this page. If you continue to see this error "
"message, please report it."
msgstr ""
"ご自身の CSRF トークンにおいて問題が発生しました。お使いのブラウザーで "
"Cookie が有効になっていることを確認して、このページを再読み込みしてみてくだ"
"さい。このエラーメッセージが表示され続けた場合、問題を報告してください。"
msgid "Administration of {{ instance.name }}" #, fuzzy
msgstr "{{ instance.name }} の管理" msgid "Administration of {0}"
msgstr "管理"
msgid "Instances" msgid "Instances"
msgstr "インスタンス" msgstr "インスタンス"
@ -599,3 +613,66 @@ msgstr "図"
msgid "None" msgid "None"
msgstr "なし" msgstr "なし"
#~ msgid "Welcome to {{ instance_name | escape }}"
#~ msgstr "{{ instance_name | escape }} へようこそ"
#~ msgid ""
#~ "Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}"
#~ "{{ link_3 }}"
#~ msgstr ""
#~ "{{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }} さんが投"
#~ "稿"
#~ msgid "Open on {{ instance_url }}"
#~ msgstr "{{ instance_url }} で開く"
#~ msgid "{{ name | escape }}'s followers"
#~ msgstr "{{ name | escape }} のフォロワー"
#~ msgid ""
#~ "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
#~ msgstr ""
#~ "{{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }} が投稿"
#~ msgid "{{ user }} mentioned you."
#~ msgstr "{{ user }} があなたをメンションしました。"
#~ msgid "{{ user }} commented your article."
#~ msgstr "{{ user }} があなたの記事にコメントしました。"
#~ msgid "{{ user }} is now following you."
#~ msgstr "{{ user }} はあなたをフォローしています。"
#~ msgid "{{ user }} liked your article."
#~ msgstr "{{ user }} があなたの記事にいいねしました。"
#~ msgid "{{ user }} boosted your article."
#~ msgstr "{{ user }} があなたの記事をブーストしました。"
#~ msgid "About {{ instance_name }}"
#~ msgstr "{{ instance_name }} について"
#~ msgid "Home to"
#~ msgstr "登録者数"
#~ msgid "people"
#~ msgstr "人"
#~ msgid "Who wrote"
#~ msgstr "投稿記事数"
#~ msgid "articles"
#~ msgstr "件"
#~ msgid "And connected to"
#~ msgstr "他のインスタンスからの接続数"
#~ msgid "other instances"
#~ msgstr "件"
#~ msgid "Edit {{ post }}"
#~ msgstr "{{ post }} を編集"
#~ msgid "Administration of {{ instance.name }}"
#~ msgstr "{{ instance.name }} の管理"

159
po/nb.po
View File

@ -33,8 +33,8 @@ msgstr "Tittel"
msgid "Create blog" msgid "Create blog"
msgstr "Opprett blogg" msgstr "Opprett blogg"
msgid "Comment \"{{ post }}\"" msgid "Comment \"{0}\""
msgstr "Kommentér \"{{ post }}\"" msgstr "Kommentér \"{0}\""
msgid "Content" msgid "Content"
msgstr "Innhold" msgstr "Innhold"
@ -62,24 +62,22 @@ msgstr "Navn"
msgid "Let&#x27;s go!" msgid "Let&#x27;s go!"
msgstr "Kjør på!" msgstr "Kjør på!"
msgid "Welcome to {{ instance_name | escape }}" msgid "Welcome to {0}"
msgstr "Velkommen til {{ instance_name | escape }}" msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "Meldinger" msgstr "Meldinger"
msgid "" msgid "Written by {0}"
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgstr "" msgstr ""
"Skrevet av {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape}}{{ link_3 }}"
msgid "This article is under the {{ license }} license." msgid "This article is under the {0} license."
msgstr "Denne artikkelen er publisert med lisensen {{ license }}" msgstr "Denne artikkelen er publisert med lisensen {0}"
msgid "One like" msgid "One like"
msgid_plural "{{ count }} likes" msgid_plural "{0} likes"
msgstr[0] "Ett hjerte" msgstr[0] "Ett hjerte"
msgstr[1] "{{ count }} hjerter" msgstr[1] "{0} hjerter"
msgid "I don&#x27;t like this anymore" msgid "I don&#x27;t like this anymore"
msgstr "Jeg liker ikke dette lengre" msgstr "Jeg liker ikke dette lengre"
@ -88,11 +86,10 @@ msgid "Add yours"
msgstr "Legg til din" msgstr "Legg til din"
msgid "One Boost" msgid "One Boost"
msgid_plural "{{ count }} Boosts" msgid_plural "{0} Boosts"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#, fuzzy
msgid "I don&#x27;t want to boost this anymore" msgid "I don&#x27;t want to boost this anymore"
msgstr "Jeg ønsker ikke å dele dette lengre" msgstr "Jeg ønsker ikke å dele dette lengre"
@ -152,8 +149,8 @@ msgstr "Dette er deg"
msgid "Edit your profile" msgid "Edit your profile"
msgstr "Rediger profilen din" msgstr "Rediger profilen din"
msgid "Open on {{ instance_url }}" msgid "Open on {0}"
msgstr "Åpne hos {{ instance_url }}" msgstr ""
msgid "Follow" msgid "Follow"
msgstr "Følg" msgstr "Følg"
@ -161,14 +158,13 @@ msgstr "Følg"
msgid "Unfollow" msgid "Unfollow"
msgstr "Slutt å følge" msgstr "Slutt å følge"
#, fuzzy
msgid "Recently boosted" msgid "Recently boosted"
msgstr "Nylig delt" msgstr "Nylig delt"
msgid "One follower" msgid "One follower"
msgid_plural "{{ count }} followers" msgid_plural "{0} followers"
msgstr[0] "Én følger" msgstr[0] "Én følger"
msgstr[1] "{{ count }} følgere" msgstr[1] "{0} følgere"
msgid "Edit your account" msgid "Edit your account"
msgstr "Rediger kontoen din" msgstr "Rediger kontoen din"
@ -188,8 +184,9 @@ msgstr "Sammendrag"
msgid "Update account" msgid "Update account"
msgstr "Oppdater konto" msgstr "Oppdater konto"
msgid "{{ name | escape }}'s followers" #, fuzzy
msgstr "{{ name | escape}} sine følgere" msgid "{0}'s followers"
msgstr "Én følger"
msgid "Followers" msgid "Followers"
msgstr "Følgere" msgstr "Følgere"
@ -258,21 +255,20 @@ msgstr "Du må være logget inn for å følge noen"
msgid "You need to be logged in order to edit your profile" msgid "You need to be logged in order to edit your profile"
msgstr "Du må være logget inn for å redigere profilen din" msgstr "Du må være logget inn for å redigere profilen din"
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgid "By {0}"
msgstr "Av {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgstr ""
#, fuzzy msgid "{0} boosted your article"
msgid "{{ data }} boosted your article" msgstr "{0} la inn en kommentar til artikkelen din"
msgstr "{{ data }} la inn en kommentar til artikkelen din"
msgid "{{ data }} started following you" msgid "{0} started following you"
msgstr "{{ data }} har begynt å følge deg" msgstr "{0} har begynt å følge deg"
msgid "{{ data }} liked your article" msgid "{0} liked your article"
msgstr "{{ data }} likte artikkelen din" msgstr "{0} likte artikkelen din"
msgid "{{ data }} commented your article" msgid "{0} commented your article"
msgstr "{{ data }} la inn en kommentar til artikkelen din" msgstr "{0} la inn en kommentar til artikkelen din"
msgid "We couldn&#x27;t find this page." msgid "We couldn&#x27;t find this page."
msgstr "Den siden fant vi ikke." msgstr "Den siden fant vi ikke."
@ -286,8 +282,8 @@ msgstr "Det har du har ikke tilgang til."
msgid "You are not author in this blog." msgid "You are not author in this blog."
msgstr "Du er ikke denne bloggens forfatter." msgstr "Du er ikke denne bloggens forfatter."
msgid "{{ data }} mentioned you." msgid "{0} mentioned you."
msgstr "{{ data }} nevnte deg." msgstr "{0} nevnte deg."
msgid "Your comment" msgid "Your comment"
msgstr "Din kommentar" msgstr "Din kommentar"
@ -310,7 +306,6 @@ msgstr "Et innlegg med samme navn finnes allerede."
msgid "We need an email or a username to identify you" msgid "We need an email or a username to identify you"
msgstr "Vi trenger en epost eller et brukernavn for å identifisere deg" msgstr "Vi trenger en epost eller et brukernavn for å identifisere deg"
#, fuzzy
msgid "Your password can't be empty" msgid "Your password can't be empty"
msgstr "Kommentaren din kan ikke være tom" msgstr "Kommentaren din kan ikke være tom"
@ -327,9 +322,9 @@ msgid "Password should be at least 8 characters long"
msgstr "Passord må bestå av minst åtte tegn" msgstr "Passord må bestå av minst åtte tegn"
msgid "One author in this blog: " msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: " msgid_plural "{0} authors in this blog: "
msgstr[0] "Én forfatter av denne bloggen: " msgstr[0] "Én forfatter av denne bloggen: "
msgstr[1] "{{ count }} forfattere av denne bloggen: " msgstr[1] "{0} forfattere av denne bloggen: "
msgid "Login or use your Fediverse account to interact with this article" msgid "Login or use your Fediverse account to interact with this article"
msgstr "" msgstr ""
@ -339,9 +334,9 @@ msgid "Optional"
msgstr "Valgfritt" msgstr "Valgfritt"
msgid "One article in this blog" msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog" msgid_plural "{0} articles in this blog"
msgstr[0] "Én artikkel i denne bloggen" msgstr[0] "Én artikkel i denne bloggen"
msgstr[1] "{{ count }} artikler i denne bloggen" msgstr[1] "{0} artikler i denne bloggen"
msgid "Previous page" msgid "Previous page"
msgstr "Forrige side" msgstr "Forrige side"
@ -349,22 +344,6 @@ msgstr "Forrige side"
msgid "Next page" msgid "Next page"
msgstr "Neste side" msgstr "Neste side"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} nevnte deg."
msgid "{{ user }} commented your article."
msgstr "{{ user }} la igjen en kommentar til artikkelen din."
msgid "{{ user }} is now following you."
msgstr "{{ user }} har nå begynt å følge deg."
msgid "{{ user }} liked your article."
msgstr "{{ user }} likte artikkelen din."
#, fuzzy
msgid "{{ user }} boosted your article."
msgstr "{{ user }} la igjen en kommentar til artikkelen din."
msgid "Source code" msgid "Source code"
msgstr "Kildekode" msgstr "Kildekode"
@ -420,20 +399,17 @@ msgstr ""
msgid "Create your account" msgid "Create your account"
msgstr "Opprett din konto" msgstr "Opprett din konto"
msgid "About {{ instance_name }}" msgid "About {0}"
msgstr "Om {{ instance_name }}" msgstr ""
msgid "Home to" msgid "Home to <em>{0}</em> users"
msgstr "Hjem for" msgstr ""
msgid "people" msgid "Who wrote <em>{0}</em> articles"
msgstr "personer" msgstr ""
msgid "Who wrote" msgid "And connected to <em>{0}</em> other instances"
msgstr "Som har skrevet" msgstr ""
msgid "articles"
msgstr "artikler"
msgid "Read the detailed rules" msgid "Read the detailed rules"
msgstr "Les reglene" msgstr "Les reglene"
@ -445,18 +421,11 @@ msgstr "Siste artikler"
msgid "Delete this blog" msgid "Delete this blog"
msgstr "" msgstr ""
msgid "And connected to"
msgstr ""
#, fuzzy
msgid "other instances"
msgstr "Om denne instansen"
#, fuzzy #, fuzzy
msgid "Administred by" msgid "Administred by"
msgstr "Administrasjon" msgstr "Administrasjon"
msgid "Runs Plume {{ version }}" msgid "Runs Plume {0}"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
@ -466,7 +435,7 @@ msgstr "Din kommentar"
msgid "Go to your gallery" msgid "Go to your gallery"
msgstr "" msgstr ""
msgid "{{ name}}'s avatar'" msgid "{0}'s avatar'"
msgstr "" msgstr ""
msgid "Media details" msgid "Media details"
@ -546,22 +515,22 @@ msgid "All the articles of the Fediverse"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "Articles from {{ instance.name }}" msgid "Articles from {0}"
msgstr "Om {{ instance_name }}" msgstr "Om {0}"
msgid "View all" msgid "View all"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "Articles tagged \"{{ tag }}\"" msgid "Articles tagged \"{0}\""
msgstr "Om {{ instance_name }}" msgstr "Om {0}"
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "Edit {{ post }}" msgid "Edit {0}"
msgstr "Kommentér \"{{ post }}\"" msgstr "Kommentér \"{0}\""
#, fuzzy #, fuzzy
msgid "Update" msgid "Update"
@ -581,8 +550,8 @@ msgid ""
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "Administration of {{ instance.name }}" msgid "Administration of {0}"
msgstr "Om {{ instance_name }}" msgstr "Administrasjon"
#, fuzzy #, fuzzy
msgid "Instances" msgid "Instances"
@ -646,22 +615,18 @@ msgstr "Administrasjon"
msgid "None" msgid "None"
msgstr "" msgstr ""
#~ msgid "One reshare" #~ msgid "Home to"
#~ msgid_plural "{{ count }} reshares" #~ msgstr "Hjem for"
#~ msgstr[0] "Én deling"
#~ msgstr[1] "{{ count }} delinger"
#~ msgid "Reshare" #~ msgid "people"
#~ msgstr "Del" #~ msgstr "personer"
#~ msgid "You need to be logged in order to reshare a post" #~ msgid "Who wrote"
#~ msgstr "Du må være logget inn for å dele et innlegg" #~ msgstr "Som har skrevet"
#~ msgid "{{ data }} reshared your article" #~ msgid "articles"
#~ msgstr "{{ data }} delte din artikkel" #~ msgstr "artikler"
#~ msgid "{{ user }} reshared your article." #, fuzzy
#~ msgstr "{{ user }} delte artikkelen din med sine følgere." #~ msgid "other instances"
#~ msgstr "Om denne instansen"
#~ msgid "Your password should be at least 8 characters long"
#~ msgstr "Passordet ditt må bestå av minst åtte tegn"

176
po/pl.po
View File

@ -35,8 +35,8 @@ msgstr "Tytuł"
msgid "Create blog" msgid "Create blog"
msgstr "Utwórz blog" msgstr "Utwórz blog"
msgid "Comment \"{{ post }}\"" msgid "Comment \"{0}\""
msgstr "Komentarz „{{ post }}”" msgstr "Komentarz „{0}”"
msgid "Content" msgid "Content"
msgstr "Zawartość" msgstr "Zawartość"
@ -63,26 +63,23 @@ msgstr "Nazwa"
msgid "Let&#x27;s go!" msgid "Let&#x27;s go!"
msgstr "Przejdźmy dalej!" msgstr "Przejdźmy dalej!"
msgid "Welcome to {{ instance_name | escape }}" msgid "Welcome to {0}"
msgstr "Witamy na {{ instance_name | escape }}" msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "Powiadomienia" msgstr "Powiadomienia"
msgid "" msgid "Written by {0}"
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgstr "" msgstr ""
"Napisano przez {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}"
"{{ link_3 }}"
msgid "This article is under the {{ license }} license." msgid "This article is under the {0} license."
msgstr "Ten artykuł został opublikowany na licencji {{ license }}." msgstr "Ten artykuł został opublikowany na licencji {0}."
msgid "One like" msgid "One like"
msgid_plural "{{ count }} likes" msgid_plural "{0} likes"
msgstr[0] "Jedno polubienie" msgstr[0] "Jedno polubienie"
msgstr[1] "{{ count }} polubienia" msgstr[1] "{0} polubienia"
msgstr[2] "{{ count }} polubień" msgstr[2] "{0} polubień"
msgid "I don&#x27;t like this anymore" msgid "I don&#x27;t like this anymore"
msgstr "Już tego nie lubię" msgstr "Już tego nie lubię"
@ -91,10 +88,10 @@ msgid "Add yours"
msgstr "Dodaj swoje" msgstr "Dodaj swoje"
msgid "One Boost" msgid "One Boost"
msgid_plural "{{ count }} Boosts" msgid_plural "{0} Boosts"
msgstr[0] "Jedno podbicie" msgstr[0] "Jedno podbicie"
msgstr[1] "{{ count }} podbicia" msgstr[1] "{0} podbicia"
msgstr[2] "{{ count }} podbić" msgstr[2] "{0} podbić"
msgid "I don&#x27;t want to boost this anymore" msgid "I don&#x27;t want to boost this anymore"
msgstr "Cofnij podbicie" msgstr "Cofnij podbicie"
@ -155,8 +152,8 @@ msgstr "To Ty"
msgid "Edit your profile" msgid "Edit your profile"
msgstr "Edytuj swój profil" msgstr "Edytuj swój profil"
msgid "Open on {{ instance_url }}" msgid "Open on {0}"
msgstr "Otwórz na {{ instance_url }}" msgstr ""
msgid "Follow" msgid "Follow"
msgstr "Obserwuj" msgstr "Obserwuj"
@ -168,10 +165,10 @@ msgid "Recently boosted"
msgstr "Ostatnio podbite" msgstr "Ostatnio podbite"
msgid "One follower" msgid "One follower"
msgid_plural "{{ count }} followers" msgid_plural "{0} followers"
msgstr[0] "Jeden obserwujący" msgstr[0] "Jeden obserwujący"
msgstr[1] "{{ count }} obserwujących" msgstr[1] "{0} obserwujących"
msgstr[2] "{{ count }} obserwujących" msgstr[2] "{0} obserwujących"
msgid "Edit your account" msgid "Edit your account"
msgstr "Edytuj swoje konto" msgstr "Edytuj swoje konto"
@ -191,8 +188,9 @@ msgstr "Opis"
msgid "Update account" msgid "Update account"
msgstr "Aktualizuj konto" msgstr "Aktualizuj konto"
msgid "{{ name | escape }}'s followers" #, fuzzy
msgstr "Osoby śledzące {{ name | escape }}" msgid "{0}'s followers"
msgstr "Jeden obserwujący"
msgid "Followers" msgid "Followers"
msgstr "Śledzący" msgstr "Śledzący"
@ -260,22 +258,20 @@ msgstr "Musisz się zalogować, aby zacząć obserwować innych"
msgid "You need to be logged in order to edit your profile" msgid "You need to be logged in order to edit your profile"
msgstr "Musisz się zalogować , aby móc edytować swój profil" msgstr "Musisz się zalogować , aby móc edytować swój profil"
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgid "By {0}"
msgstr "" msgstr ""
"Napisano przez {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}"
"{{ link_3 }}"
msgid "{{ data }} boosted your article" msgid "{0} boosted your article"
msgstr "{{ data }} podbił(a) Twój artykuł" msgstr "{0} podbił(a) Twój artykuł"
msgid "{{ data }} started following you" msgid "{0} started following you"
msgstr "{{ data }} zaczął(-ęła) Cię obserwować" msgstr "{0} zaczął(-ęła) Cię obserwować"
msgid "{{ data }} liked your article" msgid "{0} liked your article"
msgstr "{{ data }} polubił(a) Twój artykuł" msgstr "{0} polubił(a) Twój artykuł"
msgid "{{ data }} commented your article" msgid "{0} commented your article"
msgstr "{{ data }} skomentował(a) Twój artykuł" msgstr "{0} skomentował(a) Twój artykuł"
msgid "We couldn&#x27;t find this page." msgid "We couldn&#x27;t find this page."
msgstr "Nie udało się odnaleźć tej strony." msgstr "Nie udało się odnaleźć tej strony."
@ -289,8 +285,8 @@ msgstr "Nie jesteś zalogowany."
msgid "You are not author in this blog." msgid "You are not author in this blog."
msgstr "Nie jesteś autorem tego bloga." msgstr "Nie jesteś autorem tego bloga."
msgid "{{ data }} mentioned you." msgid "{0} mentioned you."
msgstr "{{ data }} wspomniał(a) o Tobie." msgstr "{0} wspomniał(a) o Tobie."
msgid "Your comment" msgid "Your comment"
msgstr "Twój komentarz" msgstr "Twój komentarz"
@ -330,10 +326,10 @@ msgid "Password should be at least 8 characters long"
msgstr "Hasło musi składać się z przynajmniej 8 znaków" msgstr "Hasło musi składać się z przynajmniej 8 znaków"
msgid "One author in this blog: " msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: " msgid_plural "{0} authors in this blog: "
msgstr[0] "Ten blog ma jednego autora: " msgstr[0] "Ten blog ma jednego autora: "
msgstr[1] "Ten blog ma {{ count }} autorów: " msgstr[1] "Ten blog ma {0} autorów: "
msgstr[2] "Ten blog ma {{ count }} autorów: " msgstr[2] "Ten blog ma {0} autorów: "
msgid "Login or use your Fediverse account to interact with this article" msgid "Login or use your Fediverse account to interact with this article"
msgstr "" msgstr ""
@ -344,10 +340,10 @@ msgid "Optional"
msgstr "Nieobowiązkowe" msgstr "Nieobowiązkowe"
msgid "One article in this blog" msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog" msgid_plural "{0} articles in this blog"
msgstr[0] "Jeden artykuł na tym blogu" msgstr[0] "Jeden artykuł na tym blogu"
msgstr[1] "{{ count }} artykuły na tym blogu" msgstr[1] "{0} artykuły na tym blogu"
msgstr[2] "{{ count }} artykułów na tym blogu" msgstr[2] "{0} artykułów na tym blogu"
msgid "Previous page" msgid "Previous page"
msgstr "Poprzednia strona" msgstr "Poprzednia strona"
@ -355,21 +351,6 @@ msgstr "Poprzednia strona"
msgid "Next page" msgid "Next page"
msgstr "Następna strona" msgstr "Następna strona"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} wspomniał(a) o Tobie."
msgid "{{ user }} commented your article."
msgstr "{{ user }} skomentował(a) Twój artykuł."
msgid "{{ user }} is now following you."
msgstr "{{ user }} zaczął(-ęła) Cię obserwować."
msgid "{{ user }} liked your article."
msgstr "{{ user }} polubił(a) Twój artykuł."
msgid "{{ user }} boosted your article."
msgstr "{{ user }} podbił(a) Twój artykuł."
msgid "Source code" msgid "Source code"
msgstr "Kod źródłowy" msgstr "Kod źródłowy"
@ -425,20 +406,17 @@ msgstr ""
msgid "Create your account" msgid "Create your account"
msgstr "Utwórz konto" msgstr "Utwórz konto"
msgid "About {{ instance_name }}" msgid "About {0}"
msgstr "O {{ instance_name }}" msgstr ""
msgid "Home to" msgid "Home to <em>{0}</em> users"
msgstr "Dom dla" msgstr ""
msgid "people" msgid "Who wrote <em>{0}</em> articles"
msgstr "osób" msgstr ""
msgid "Who wrote" msgid "And connected to <em>{0}</em> other instances"
msgstr "Które napisały" msgstr ""
msgid "articles"
msgstr "artykuły"
msgid "Read the detailed rules" msgid "Read the detailed rules"
msgstr "Przeczytaj szczegółowe zasady" msgstr "Przeczytaj szczegółowe zasady"
@ -449,17 +427,11 @@ msgstr "Usuń ten artykuł"
msgid "Delete this blog" msgid "Delete this blog"
msgstr "Usuń ten blog" msgstr "Usuń ten blog"
msgid "And connected to"
msgstr "Połączony z"
msgid "other instances"
msgstr "innych instancji"
msgid "Administred by" msgid "Administred by"
msgstr "Administrowany przez" msgstr "Administrowany przez"
msgid "Runs Plume {{ version }}" msgid "Runs Plume {0}"
msgstr "Działa na Plume {{ version }}" msgstr "Działa na Plume {0}"
msgid "Your media" msgid "Your media"
msgstr "Twoja zawartość multimedialna" msgstr "Twoja zawartość multimedialna"
@ -467,8 +439,8 @@ msgstr "Twoja zawartość multimedialna"
msgid "Go to your gallery" msgid "Go to your gallery"
msgstr "Przejdź do swojej galerii" msgstr "Przejdź do swojej galerii"
msgid "{{ name}}'s avatar'" msgid "{0}'s avatar'"
msgstr "Awatar {{name}}" msgstr "Awatar {0}"
msgid "Media details" msgid "Media details"
msgstr "Szczegóły zawartości multimedialnej" msgstr "Szczegóły zawartości multimedialnej"
@ -542,20 +514,21 @@ msgstr "Artykuły"
msgid "All the articles of the Fediverse" msgid "All the articles of the Fediverse"
msgstr "Wszystkie artykuły w Fediwersum" msgstr "Wszystkie artykuły w Fediwersum"
msgid "Articles from {{ instance.name }}" msgid "Articles from {0}"
msgstr "Artykuły z {{ instance.name }}" msgstr "Artykuły z {0}"
msgid "View all" msgid "View all"
msgstr "Zobacz wszystko" msgstr "Zobacz wszystko"
msgid "Articles tagged \"{{ tag }}\"" msgid "Articles tagged \"{0}\""
msgstr "Artykuły oznaczone „{{ tag }}”" msgstr "Artykuły oznaczone „{0}”"
msgid "Edit" msgid "Edit"
msgstr "Edytuj" msgstr "Edytuj"
msgid "Edit {{ post }}" #, fuzzy
msgstr "Edytuj {{ post }}" msgid "Edit {0}"
msgstr "Edytuj"
msgid "Update" msgid "Update"
msgstr "Aktualizuj" msgstr "Aktualizuj"
@ -575,8 +548,9 @@ msgstr ""
"włączone pliki cookies i spróbuj odświeżyć stronę. Jeżeli wciąż widzisz tę " "włączone pliki cookies i spróbuj odświeżyć stronę. Jeżeli wciąż widzisz tę "
"wiadomość, zgłoś to." "wiadomość, zgłoś to."
msgid "Administration of {{ instance.name }}" #, fuzzy
msgstr "Administracja {{ instance.name }}" msgid "Administration of {0}"
msgstr "Administracja"
msgid "Instances" msgid "Instances"
msgstr "Instancje" msgstr "Instancje"
@ -635,26 +609,20 @@ msgstr "Ilustracja"
msgid "None" msgid "None"
msgstr "Brak" msgstr "Brak"
#~ msgid "One reshare" #~ msgid "Home to"
#~ msgid_plural "{{ count }} reshares" #~ msgstr "Dom dla"
#~ msgstr[0] "Jedno udostępnienie"
#~ msgstr[1] "{{ count }} udostępnienia"
#~ msgstr[2] "{{ count }} udostępnień"
#~ msgid "Reshare" #~ msgid "people"
#~ msgstr "Udostępnij" #~ msgstr "osób"
#~ msgid "You need to be logged in order to reshare a post" #~ msgid "Who wrote"
#~ msgstr "Musisz się zalogować, aby udostępnić wpis" #~ msgstr "Które napisały"
#~ msgid "{{ data }} reshared your article" #~ msgid "articles"
#~ msgstr "{{ data }} udostępnił Twój artykuł" #~ msgstr "artykuły"
#~ msgid "{{ user }} reshared your article." #~ msgid "And connected to"
#~ msgstr "{{ user }} udostępnił Twój artykuł." #~ msgstr "Połączony z"
#~ msgid "Your password should be at least 8 characters long" #~ msgid "other instances"
#~ msgstr "Twoje hasło musi składać się przynajmniej z 8 znaków" #~ msgstr "innych instancji"
#~ msgid "Logowanie"
#~ msgstr "Zaloguj się"

View File

@ -33,7 +33,7 @@ msgstr ""
msgid "Create blog" msgid "Create blog"
msgstr "" msgstr ""
msgid "Comment \"{{ post }}\"" msgid "Comment \"{0}\""
msgstr "" msgstr ""
msgid "Content" msgid "Content"
@ -60,20 +60,20 @@ msgstr ""
msgid "Let&#x27;s go!" msgid "Let&#x27;s go!"
msgstr "" msgstr ""
msgid "Welcome to {{ instance_name | escape }}" msgid "Welcome to {0}"
msgstr "" msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "" msgstr ""
msgid "Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}" msgid "Written by {0}"
msgstr "" msgstr ""
msgid "This article is under the {{ license }} license." msgid "This article is under the {0} license."
msgstr "" msgstr ""
msgid "One like" msgid "One like"
msgid_plural "{{ count }} likes" msgid_plural "{0} likes"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
@ -84,7 +84,7 @@ msgid "Add yours"
msgstr "" msgstr ""
msgid "One Boost" msgid "One Boost"
msgid_plural "{{ count }} Boosts" msgid_plural "{0} Boosts"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
@ -145,7 +145,7 @@ msgstr ""
msgid "Edit your profile" msgid "Edit your profile"
msgstr "" msgstr ""
msgid "Open on {{ instance_url }}" msgid "Open on {0}"
msgstr "" msgstr ""
msgid "Follow" msgid "Follow"
@ -158,7 +158,7 @@ msgid "Recently boosted"
msgstr "" msgstr ""
msgid "One follower" msgid "One follower"
msgid_plural "{{ count }} followers" msgid_plural "{0} followers"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
@ -180,7 +180,7 @@ msgstr ""
msgid "Update account" msgid "Update account"
msgstr "" msgstr ""
msgid "{{ name | escape }}'s followers" msgid "{0}'s followers"
msgstr "" msgstr ""
msgid "Followers" msgid "Followers"
@ -249,19 +249,19 @@ msgstr ""
msgid "You need to be logged in order to edit your profile" msgid "You need to be logged in order to edit your profile"
msgstr "" msgstr ""
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgid "By {0}"
msgstr "" msgstr ""
msgid "{{ data }} boosted your article" msgid "{0} boosted your article"
msgstr "" msgstr ""
msgid "{{ data }} started following you" msgid "{0} started following you"
msgstr "" msgstr ""
msgid "{{ data }} liked your article" msgid "{0} liked your article"
msgstr "" msgstr ""
msgid "{{ data }} commented your article" msgid "{0} commented your article"
msgstr "" msgstr ""
msgid "We couldn&#x27;t find this page." msgid "We couldn&#x27;t find this page."
@ -276,7 +276,7 @@ msgstr ""
msgid "You are not author in this blog." msgid "You are not author in this blog."
msgstr "" msgstr ""
msgid "{{ data }} mentioned you." msgid "{0} mentioned you."
msgstr "" msgstr ""
msgid "Your comment" msgid "Your comment"
@ -316,7 +316,7 @@ msgid "Password should be at least 8 characters long"
msgstr "" msgstr ""
msgid "One author in this blog: " msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: " msgid_plural "{0} authors in this blog: "
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
@ -327,7 +327,7 @@ msgid "Optional"
msgstr "" msgstr ""
msgid "One article in this blog" msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog" msgid_plural "{0} articles in this blog"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
@ -337,21 +337,6 @@ msgstr ""
msgid "Next page" msgid "Next page"
msgstr "" msgstr ""
msgid "{{ user }} mentioned you."
msgstr ""
msgid "{{ user }} commented your article."
msgstr ""
msgid "{{ user }} is now following you."
msgstr ""
msgid "{{ user }} liked your article."
msgstr ""
msgid "{{ user }} boosted your article."
msgstr ""
msgid "Source code" msgid "Source code"
msgstr "" msgstr ""
@ -403,19 +388,16 @@ msgstr ""
msgid "Create your account" msgid "Create your account"
msgstr "" msgstr ""
msgid "About {{ instance_name }}" msgid "About {0}"
msgstr "" msgstr ""
msgid "Home to" msgid "Home to <em>{0}</em> users"
msgstr "" msgstr ""
msgid "people" msgid "Who wrote <em>{0}</em> articles"
msgstr "" msgstr ""
msgid "Who wrote" msgid "And connected to <em>{0}</em> other instances"
msgstr ""
msgid "articles"
msgstr "" msgstr ""
msgid "Read the detailed rules" msgid "Read the detailed rules"
@ -427,16 +409,10 @@ msgstr ""
msgid "Delete this blog" msgid "Delete this blog"
msgstr "" msgstr ""
msgid "And connected to"
msgstr ""
msgid "other instances"
msgstr ""
msgid "Administred by" msgid "Administred by"
msgstr "" msgstr ""
msgid "Runs Plume {{ version }}" msgid "Runs Plume {0}"
msgstr "" msgstr ""
msgid "Your media" msgid "Your media"
@ -445,7 +421,7 @@ msgstr ""
msgid "Go to your gallery" msgid "Go to your gallery"
msgstr "" msgstr ""
msgid "{{ name}}'s avatar'" msgid "{0}'s avatar'"
msgstr "" msgstr ""
msgid "Media details" msgid "Media details"
@ -517,19 +493,19 @@ msgstr ""
msgid "All the articles of the Fediverse" msgid "All the articles of the Fediverse"
msgstr "" msgstr ""
msgid "Articles from {{ instance.name }}" msgid "Articles from {0}"
msgstr "" msgstr ""
msgid "View all" msgid "View all"
msgstr "" msgstr ""
msgid "Articles tagged \"{{ tag }}\"" msgid "Articles tagged \"{0}\""
msgstr "" msgstr ""
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
msgid "Edit {{ post }}" msgid "Edit {0}"
msgstr "" msgstr ""
msgid "Update" msgid "Update"
@ -544,7 +520,7 @@ msgstr ""
msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it."
msgstr "" msgstr ""
msgid "Administration of {{ instance.name }}" msgid "Administration of {0}"
msgstr "" msgstr ""
msgid "Instances" msgid "Instances"

164
po/ru.po
View File

@ -35,8 +35,8 @@ msgstr "Заголовок"
msgid "Create blog" msgid "Create blog"
msgstr "Создать блог" msgstr "Создать блог"
msgid "Comment \"{{ post }}\"" msgid "Comment \"{0}\""
msgstr "Комментарий \"{{ post }}\"" msgstr "Комментарий \"{0}\""
msgid "Content" msgid "Content"
msgstr "Содержимое" msgstr "Содержимое"
@ -64,26 +64,23 @@ msgstr "Имя"
msgid "Let&#x27;s go!" msgid "Let&#x27;s go!"
msgstr "Поехали!" msgstr "Поехали!"
msgid "Welcome to {{ instance_name | escape }}" msgid "Welcome to {0}"
msgstr "Добро пожаловать на {{ instance_name | escape }}" msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "Уведомления" msgstr "Уведомления"
msgid "" msgid "Written by {0}"
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgstr "" msgstr ""
"Написано {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
#, fuzzy msgid "This article is under the {0} license."
msgid "This article is under the {{ license }} license." msgstr "Эта статья распространяется под лицензией {0}"
msgstr "Эта статья распространяется под лицензией {{ license }}"
msgid "One like" msgid "One like"
msgid_plural "{{ count }} likes" msgid_plural "{0} likes"
msgstr[0] "Один лайк" msgstr[0] "Один лайк"
msgstr[1] "{{count}} лайка" msgstr[1] "{0} лайка"
msgstr[2] "{{ count }} лайков" msgstr[2] "{0} лайков"
msgid "I don&#x27;t like this anymore" msgid "I don&#x27;t like this anymore"
msgstr "Мне больше не нравится это" msgstr "Мне больше не нравится это"
@ -92,10 +89,10 @@ msgid "Add yours"
msgstr "Добавить свой" msgstr "Добавить свой"
msgid "One Boost" msgid "One Boost"
msgid_plural "{{ count }} Boosts" msgid_plural "{0} Boosts"
msgstr[0] "Одно продвижение" msgstr[0] "Одно продвижение"
msgstr[1] "{{ count }} продвижения" msgstr[1] "{0} продвижения"
msgstr[2] "{{ count }} продвижений" msgstr[2] "{0} продвижений"
msgid "I don&#x27;t want to boost this anymore" msgid "I don&#x27;t want to boost this anymore"
msgstr "Я не хочу больше продвигать это" msgstr "Я не хочу больше продвигать это"
@ -157,8 +154,8 @@ msgstr "Это вы"
msgid "Edit your profile" msgid "Edit your profile"
msgstr "Редактировать ваш профиль" msgstr "Редактировать ваш профиль"
msgid "Open on {{ instance_url }}" msgid "Open on {0}"
msgstr "Открыть на {{ instance_url }}" msgstr ""
msgid "Follow" msgid "Follow"
msgstr "Подписаться" msgstr "Подписаться"
@ -170,10 +167,10 @@ msgid "Recently boosted"
msgstr "Недавно продвинутые" msgstr "Недавно продвинутые"
msgid "One follower" msgid "One follower"
msgid_plural "{{ count }} followers" msgid_plural "{0} followers"
msgstr[0] "Один подписчик" msgstr[0] "Один подписчик"
msgstr[1] "{{ count }} подписчика" msgstr[1] "{0} подписчика"
msgstr[2] "{{ count }} подписчиков" msgstr[2] "{0} подписчиков"
msgid "Edit your account" msgid "Edit your account"
msgstr "Редактировать ваш аккаунт" msgstr "Редактировать ваш аккаунт"
@ -194,8 +191,9 @@ msgstr "Резюме"
msgid "Update account" msgid "Update account"
msgstr "Обновить аккаунт" msgstr "Обновить аккаунт"
msgid "{{ name | escape }}'s followers" #, fuzzy
msgstr "Подписчики {{ name | escape }}" msgid "{0}'s followers"
msgstr "Один подписчик"
msgid "Followers" msgid "Followers"
msgstr "Подписчики" msgstr "Подписчики"
@ -263,20 +261,20 @@ msgstr "Вы должны войти чтобы подписаться на ко
msgid "You need to be logged in order to edit your profile" msgid "You need to be logged in order to edit your profile"
msgstr "Вы должны войти чтобы редактировать ваш профиль" msgstr "Вы должны войти чтобы редактировать ваш профиль"
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgid "By {0}"
msgstr "От {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" msgstr ""
msgid "{{ data }} boosted your article" msgid "{0} boosted your article"
msgstr "{{ data }} продвинул(а) вашу статью" msgstr "{0} продвинул(а) вашу статью"
msgid "{{ data }} started following you" msgid "{0} started following you"
msgstr "{{ data }} подписался на вас" msgstr "{0} подписался на вас"
msgid "{{ data }} liked your article" msgid "{0} liked your article"
msgstr "{{ data }} понравилась ваша статья" msgstr "{0} понравилась ваша статья"
msgid "{{ data }} commented your article" msgid "{0} commented your article"
msgstr "{{ data }} прокомментировал(а) вашу статью" msgstr "{0} прокомментировал(а) вашу статью"
msgid "We couldn&#x27;t find this page." msgid "We couldn&#x27;t find this page."
msgstr "Мы не можем найти эту страницу." msgstr "Мы не можем найти эту страницу."
@ -291,8 +289,8 @@ msgstr "Вы не авторизованы."
msgid "You are not author in this blog." msgid "You are not author in this blog."
msgstr "Вы не автор этого блога." msgstr "Вы не автор этого блога."
msgid "{{ data }} mentioned you." msgid "{0} mentioned you."
msgstr "{{ data }} упомянул(а) вас." msgstr "{0} упомянул(а) вас."
msgid "Your comment" msgid "Your comment"
msgstr "Ваш комментарий" msgstr "Ваш комментарий"
@ -333,10 +331,10 @@ msgid "Password should be at least 8 characters long"
msgstr "Пароль должен быть не короче 8 символов" msgstr "Пароль должен быть не короче 8 символов"
msgid "One author in this blog: " msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: " msgid_plural "{0} authors in this blog: "
msgstr[0] "Один автор в этом блоге: " msgstr[0] "Один автор в этом блоге: "
msgstr[1] "{{ count }} автора в этом блоге: " msgstr[1] "{0} автора в этом блоге: "
msgstr[2] "{{ count }} авторов в этом блоге: " msgstr[2] "{0} авторов в этом блоге: "
msgid "Login or use your Fediverse account to interact with this article" msgid "Login or use your Fediverse account to interact with this article"
msgstr "" msgstr ""
@ -347,10 +345,10 @@ msgid "Optional"
msgstr "Не обязательно" msgstr "Не обязательно"
msgid "One article in this blog" msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog" msgid_plural "{0} articles in this blog"
msgstr[0] "Один пост в этом блоге" msgstr[0] "Один пост в этом блоге"
msgstr[1] "{{count}} поста в этом блоге" msgstr[1] "{0} поста в этом блоге"
msgstr[2] "{{ count }} постов в этом блоге" msgstr[2] "{0} постов в этом блоге"
msgid "Previous page" msgid "Previous page"
msgstr "Предыдущая страница" msgstr "Предыдущая страница"
@ -358,21 +356,6 @@ msgstr "Предыдущая страница"
msgid "Next page" msgid "Next page"
msgstr "Следующая страница" msgstr "Следующая страница"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} упомянул вас."
msgid "{{ user }} commented your article."
msgstr "{{ user }} прокомментировал вашу статью."
msgid "{{ user }} is now following you."
msgstr "{{ user }} не подписан на вас."
msgid "{{ user }} liked your article."
msgstr "{{ user }} понравилась ваша статья."
msgid "{{ user }} boosted your article."
msgstr "{{ user }} продвинул(а) вашу статью."
msgid "Source code" msgid "Source code"
msgstr "Исходный код" msgstr "Исходный код"
@ -428,20 +411,17 @@ msgstr ""
msgid "Create your account" msgid "Create your account"
msgstr "Создать аккаунт" msgstr "Создать аккаунт"
msgid "About {{ instance_name }}" msgid "About {0}"
msgstr "О {{ instance_name }}" msgstr ""
msgid "Home to" msgid "Home to <em>{0}</em> users"
msgstr "Дом для" msgstr ""
msgid "people" msgid "Who wrote <em>{0}</em> articles"
msgstr "человек" msgstr ""
msgid "Who wrote" msgid "And connected to <em>{0}</em> other instances"
msgstr "Которые написали" msgstr ""
msgid "articles"
msgstr "статей"
msgid "Read the detailed rules" msgid "Read the detailed rules"
msgstr "Прочитать подробные правила" msgstr "Прочитать подробные правила"
@ -452,17 +432,11 @@ msgstr "Удалить эту статью"
msgid "Delete this blog" msgid "Delete this blog"
msgstr "Удалить этот блог" msgstr "Удалить этот блог"
msgid "And connected to"
msgstr "И подключен к"
msgid "other instances"
msgstr "другим узлам"
msgid "Administred by" msgid "Administred by"
msgstr "Администрируется" msgstr "Администрируется"
msgid "Runs Plume {{ version }}" msgid "Runs Plume {0}"
msgstr "Работает на Plume {{ version }}" msgstr "Работает на Plume {0}"
msgid "Your media" msgid "Your media"
msgstr "Ваши медиафайлы" msgstr "Ваши медиафайлы"
@ -470,8 +444,8 @@ msgstr "Ваши медиафайлы"
msgid "Go to your gallery" msgid "Go to your gallery"
msgstr "Перейти в вашу галерею" msgstr "Перейти в вашу галерею"
msgid "{{ name}}'s avatar'" msgid "{0}'s avatar'"
msgstr "Аватар {{ name }}" msgstr "Аватар {0}"
msgid "Media details" msgid "Media details"
msgstr "Детали медиафайла" msgstr "Детали медиафайла"
@ -548,20 +522,21 @@ msgstr "Статьи"
msgid "All the articles of the Fediverse" msgid "All the articles of the Fediverse"
msgstr "Все статьи из Fediverse" msgstr "Все статьи из Fediverse"
msgid "Articles from {{ instance.name }}" msgid "Articles from {0}"
msgstr "Статьи с {{ instance.name }}" msgstr "Статьи с {0}"
msgid "View all" msgid "View all"
msgstr "Показать все" msgstr "Показать все"
msgid "Articles tagged \"{{ tag }}\"" msgid "Articles tagged \"{0}\""
msgstr "Статьи, отмеченные тегом «{{ tag }}»" msgstr "Статьи, отмеченные тегом «{0}»"
msgid "Edit" msgid "Edit"
msgstr "Редактировать" msgstr "Редактировать"
msgid "Edit {{ post }}" #, fuzzy
msgstr "Редактрировать {{ post }}" msgid "Edit {0}"
msgstr "Редактировать"
msgid "Update" msgid "Update"
msgstr "Обновить" msgstr "Обновить"
@ -581,8 +556,9 @@ msgstr ""
"cookies и попробуйте перезагрузить страницу. Если вы продолжите видеть это " "cookies и попробуйте перезагрузить страницу. Если вы продолжите видеть это "
"сообщение об ошибке, сообщите об этом." "сообщение об ошибке, сообщите об этом."
msgid "Administration of {{ instance.name }}" #, fuzzy
msgstr "Администрация {{ instance.name }}" msgid "Administration of {0}"
msgstr "Администрирование"
msgid "Instances" msgid "Instances"
msgstr "Узлы" msgstr "Узлы"
@ -643,3 +619,21 @@ msgstr "Иллюстрация"
msgid "None" msgid "None"
msgstr "Нет" msgstr "Нет"
#~ msgid "Home to"
#~ msgstr "Дом для"
#~ msgid "people"
#~ msgstr "человек"
#~ msgid "Who wrote"
#~ msgstr "Которые написали"
#~ msgid "articles"
#~ msgstr "статей"
#~ msgid "And connected to"
#~ msgstr "И подключен к"
#~ msgid "other instances"
#~ msgstr "другим узлам"

View File

@ -1 +1 @@
nightly-2018-07-17 nightly-2018-10-06

View File

@ -1,5 +1,5 @@
use canapi::Provider; use canapi::Provider;
use rocket_contrib::Json; use rocket_contrib::json::Json;
use serde_json; use serde_json;
use plume_api::apps::AppEndpoint; use plume_api::apps::AppEndpoint;
@ -10,7 +10,7 @@ use plume_models::{
}; };
#[post("/apps", data = "<data>")] #[post("/apps", data = "<data>")]
fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> { pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> {
let post = <App as Provider<Connection>>::create(&*conn, (*data).clone()).ok(); let post = <App as Provider<Connection>>::create(&*conn, (*data).clone()).ok();
Json(json!(post)) Json(json!(post))
} }

View File

@ -1,4 +1,5 @@
use rocket_contrib::Json; use rocket::request::Form;
use rocket_contrib::json::Json;
use serde_json; use serde_json;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
@ -10,7 +11,7 @@ use plume_models::{
}; };
#[derive(FromForm)] #[derive(FromForm)]
struct OAuthRequest { pub struct OAuthRequest {
client_id: String, client_id: String,
client_secret: String, client_secret: String,
password: String, password: String,
@ -18,8 +19,8 @@ struct OAuthRequest {
scopes: String, scopes: String,
} }
#[get("/oauth2?<query>")] #[get("/oauth2?<query..>")]
fn oauth(query: OAuthRequest, conn: DbConn) -> Json<serde_json::Value> { pub fn oauth(query: Form<OAuthRequest>, conn: DbConn) -> Json<serde_json::Value> {
let app = App::find_by_client_id(&*conn, &query.client_id).expect("OAuth request from unknown client"); let app = App::find_by_client_id(&*conn, &query.client_id).expect("OAuth request from unknown client");
if app.client_secret == query.client_secret { if app.client_secret == query.client_secret {
if let Some(user) = User::find_local(&*conn, &query.username) { if let Some(user) = User::find_local(&*conn, &query.username) {
@ -28,7 +29,7 @@ fn oauth(query: OAuthRequest, conn: DbConn) -> Json<serde_json::Value> {
app_id: app.id, app_id: app.id,
user_id: user.id, user_id: user.id,
value: random_hex(), value: random_hex(),
scopes: query.scopes, scopes: query.scopes.clone(),
}); });
Json(json!({ Json(json!({
"token": token.value "token": token.value

View File

@ -1,6 +1,6 @@
use canapi::Provider; use canapi::Provider;
use rocket::http::uri::Origin; use rocket::http::uri::Origin;
use rocket_contrib::Json; use rocket_contrib::json::Json;
use serde_json; use serde_json;
use serde_qs; use serde_qs;
@ -15,13 +15,13 @@ use api::authorization::*;
use Searcher; use Searcher;
#[get("/posts/<id>")] #[get("/posts/<id>")]
fn get(id: i32, conn: DbConn, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> { pub fn get(id: i32, conn: DbConn, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> {
let post = <Post as Provider<(&Connection, &UnmanagedSearcher, Option<i32>)>>::get(&(&*conn, &search, auth.map(|a| a.0.user_id)), id).ok(); let post = <Post as Provider<(&Connection, &UnmanagedSearcher, Option<i32>)>>::get(&(&*conn, &search, auth.map(|a| a.0.user_id)), id).ok();
Json(json!(post)) Json(json!(post))
} }
#[get("/posts")] #[get("/posts")]
fn list(conn: DbConn, uri: &Origin, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> { pub fn list(conn: DbConn, uri: &Origin, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> {
let query: PostEndpoint = serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error"); let query: PostEndpoint = serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error");
let post = <Post as Provider<(&Connection, &UnmanagedSearcher, Option<i32>)>>::list(&(&*conn, &search, auth.map(|a| a.0.user_id)), query); let post = <Post as Provider<(&Connection, &UnmanagedSearcher, Option<i32>)>>::list(&(&*conn, &search, auth.map(|a| a.0.user_id)), query);
Json(json!(post)) Json(json!(post))

View File

@ -1,7 +1,7 @@
#![feature(custom_derive, plugin, decl_macro)] #![feature(custom_derive, plugin, decl_macro, proc_macro_hygiene)]
#![plugin(rocket_codegen)]
extern crate activitypub; extern crate activitypub;
extern crate askama_escape;
extern crate atom_syndication; extern crate atom_syndication;
extern crate canapi; extern crate canapi;
extern crate chrono; extern crate chrono;
@ -10,7 +10,6 @@ extern crate ctrlc;
extern crate diesel; extern crate diesel;
extern crate dotenv; extern crate dotenv;
extern crate failure; extern crate failure;
extern crate gettextrs;
extern crate guid_create; extern crate guid_create;
extern crate heck; extern crate heck;
extern crate multipart; extern crate multipart;
@ -22,6 +21,7 @@ extern crate plume_models;
extern crate rocket; extern crate rocket;
extern crate rocket_contrib; extern crate rocket_contrib;
extern crate rocket_csrf; extern crate rocket_csrf;
#[macro_use]
extern crate rocket_i18n; extern crate rocket_i18n;
extern crate rpassword; extern crate rpassword;
extern crate scheduled_thread_pool; extern crate scheduled_thread_pool;
@ -38,7 +38,6 @@ extern crate webfinger;
use diesel::r2d2::ConnectionManager; use diesel::r2d2::ConnectionManager;
use rocket::State; use rocket::State;
use rocket_contrib::Template;
use rocket_csrf::CsrfFairingBuilder; use rocket_csrf::CsrfFairingBuilder;
use plume_models::{DATABASE_URL, Connection, use plume_models::{DATABASE_URL, Connection,
db_conn::DbPool, search::Searcher as UnmanagedSearcher}; db_conn::DbPool, search::Searcher as UnmanagedSearcher};
@ -49,6 +48,8 @@ use std::time::Duration;
mod api; mod api;
mod inbox; mod inbox;
#[macro_use]
mod template_utils;
mod routes; mod routes;
type Worker<'a> = State<'a, ScheduledThreadPool>; type Worker<'a> = State<'a, ScheduledThreadPool>;
@ -77,7 +78,6 @@ fn main() {
exit(0); exit(0);
}).expect("Error setting Ctrl-c handler"); }).expect("Error setting Ctrl-c handler");
rocket::ignite() rocket::ignite()
.mount("/", routes![ .mount("/", routes![
routes::blogs::paginated_details, routes::blogs::paginated_details,
@ -140,8 +140,7 @@ fn main() {
routes::reshares::create, routes::reshares::create,
routes::reshares::create_auth, routes::reshares::create_auth,
routes::search::index, routes::search::search,
routes::search::query,
routes::session::new, routes::session::new,
routes::session::new_message, routes::session::new_message,
@ -187,17 +186,14 @@ fn main() {
api::posts::get, api::posts::get,
api::posts::list, api::posts::list,
]) ])
.catch(catchers![ .register(catchers![
routes::errors::not_found, routes::errors::not_found,
routes::errors::server_error routes::errors::server_error
]) ])
.manage(dbpool) .manage(dbpool)
.manage(workpool) .manage(workpool)
.manage(searcher) .manage(searcher)
.attach(Template::custom(|engines| { .manage(include_i18n!("plume", [ "de", "en", "fr", "gl", "it", "ja", "nb", "pl", "ru" ]))
rocket_i18n::tera(&mut engines.tera);
}))
.attach(rocket_i18n::I18n::new("plume"))
.attach(CsrfFairingBuilder::new() .attach(CsrfFairingBuilder::new()
.set_default_target("/csrf-violation?target=<uri>".to_owned(), rocket::http::Method::Post) .set_default_target("/csrf-violation?target=<uri>".to_owned(), rocket::http::Method::Post)
.add_exceptions(vec![ .add_exceptions(vec![
@ -210,3 +206,5 @@ fn main() {
.finalize().expect("main: csrf fairing creation error")) .finalize().expect("main: csrf fairing creation error"))
.launch(); .launch();
} }
include!(concat!(env!("OUT_DIR"), "/templates.rs"));

View File

@ -5,8 +5,7 @@ use rocket::{
request::LenientForm, request::LenientForm,
response::{Redirect, Flash, content::Content} response::{Redirect, Flash, content::Content}
}; };
use rocket_contrib::Template; use rocket_i18n::I18n;
use serde_json;
use std::{collections::HashMap, borrow::Cow}; use std::{collections::HashMap, borrow::Cow};
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
@ -21,61 +20,62 @@ use plume_models::{
users::User users::User
}; };
use routes::Page; use routes::Page;
use template_utils::Ructe;
use Searcher; use Searcher;
#[get("/~/<name>?<page>", rank = 2)] #[get("/~/<name>?<page>", rank = 2)]
fn paginated_details(name: String, conn: DbConn, user: Option<User>, page: Page) -> Template { pub fn paginated_details(intl: I18n, name: String, conn: DbConn, user: Option<User>, page: Page) -> Result<Ructe, Ructe> {
may_fail!(user.map(|u| u.to_json(&*conn)), Blog::find_by_fqn(&*conn, &name), "Requested blog couldn't be found", |blog| { let blog = Blog::find_by_fqn(&*conn, &name)
let posts = Post::blog_page(&*conn, &blog, page.limits()); .ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?;
let articles = Post::get_for_blog(&*conn, &blog); let posts = Post::blog_page(&*conn, &blog, page.limits());
let authors = &blog.list_authors(&*conn); let articles = Post::get_for_blog(&*conn, &blog); // TODO only count them in DB
let authors = &blog.list_authors(&*conn);
Template::render("blogs/details", json!({ Ok(render!(blogs::details(
"blog": &blog.to_json(&*conn), &(&*conn, &intl.catalog, user.clone()),
"account": user.clone().map(|u| u.to_json(&*conn)), blog.clone(),
"is_author": user.map(|x| x.is_author_in(&*conn, &blog)), blog.get_fqn(&*conn),
"posts": posts.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), authors,
"authors": authors.into_iter().map(|u| u.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), articles.len(),
"n_authors": authors.len(), page.0,
"n_articles": articles.len(), Page::total(articles.len() as i32),
"page": page.page, user.map(|x| x.is_author_in(&*conn, &blog)).unwrap_or(false),
"n_pages": Page::total(articles.len() as i32) posts
})) )))
})
} }
#[get("/~/<name>", rank = 3)] #[get("/~/<name>", rank = 3)]
fn details(name: String, conn: DbConn, user: Option<User>) -> Template { pub fn details(intl: I18n, name: String, conn: DbConn, user: Option<User>) -> Result<Ructe, Ructe> {
paginated_details(name, conn, user, Page::first()) paginated_details(intl, name, conn, user, Page::first())
} }
#[get("/~/<name>", rank = 1)] #[get("/~/<name>", rank = 1)]
fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<CustomGroup>> { pub fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<CustomGroup>> {
let blog = Blog::find_local(&*conn, &name)?; let blog = Blog::find_local(&*conn, &name)?;
Some(ActivityStream::new(blog.to_activity(&*conn))) Some(ActivityStream::new(blog.to_activity(&*conn)))
} }
#[get("/blogs/new")] #[get("/blogs/new")]
fn new(user: User, conn: DbConn) -> Template { pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
Template::render("blogs/new", json!({ render!(blogs::new(
"account": user.to_json(&*conn), &(&*conn, &intl.catalog, Some(user)),
"errors": null, &NewBlogForm::default(),
"form": null ValidationErrors::default()
})) ))
} }
#[get("/blogs/new", rank = 2)] #[get("/blogs/new", rank = 2)]
fn new_auth() -> Flash<Redirect>{ pub fn new_auth(i18n: I18n) -> Flash<Redirect>{
utils::requires_login( utils::requires_login(
"You need to be logged in order to create a new blog", i18n!(i18n.catalog, "You need to be logged in order to create a new blog"),
uri!(new) uri!(new)
) )
} }
#[derive(FromForm, Validate, Serialize)] #[derive(Default, FromForm, Validate, Serialize)]
struct NewBlogForm { pub struct NewBlogForm {
#[validate(custom(function = "valid_slug", message = "Invalid name"))] #[validate(custom(function = "valid_slug", message = "Invalid name"))]
pub title: String pub title: String,
} }
fn valid_slug(title: &str) -> Result<(), ValidationError> { fn valid_slug(title: &str) -> Result<(), ValidationError> {
@ -87,9 +87,8 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
} }
} }
#[post("/blogs/new", data = "<data>")] #[post("/blogs/new", data = "<form>")]
fn create(conn: DbConn, data: LenientForm<NewBlogForm>, user: User) -> Result<Redirect, Template> { pub fn create(conn: DbConn, form: LenientForm<NewBlogForm>, user: User, intl: I18n) -> Result<Redirect, Ructe> {
let form = data.get();
let slug = utils::make_actor_id(&form.title); let slug = utils::make_actor_id(&form.title);
let mut errors = match form.validate() { let mut errors = match form.validate() {
@ -121,36 +120,37 @@ fn create(conn: DbConn, data: LenientForm<NewBlogForm>, user: User) -> Result<Re
Ok(Redirect::to(uri!(details: name = slug.clone()))) Ok(Redirect::to(uri!(details: name = slug.clone())))
} else { } else {
println!("{:?}", errors); Err(render!(blogs::new(
Err(Template::render("blogs/new", json!({ &(&*conn, &intl.catalog, Some(user)),
"account": user.to_json(&*conn), &*form,
"errors": errors.inner(), errors
"form": form )))
})))
} }
} }
#[post("/~/<name>/delete")] #[post("/~/<name>/delete")]
fn delete(conn: DbConn, name: String, user: Option<User>, searcher: Searcher) -> Result<Redirect, Option<Template>>{ pub fn delete(conn: DbConn, name: String, user: Option<User>, intl: I18n, searcher: Searcher) -> Result<Redirect, Option<Ructe>>{
let blog = Blog::find_local(&*conn, &name).ok_or(None)?; let blog = Blog::find_local(&*conn, &name).ok_or(None)?;
if user.map(|u| u.is_author_in(&*conn, &blog)).unwrap_or(false) { if user.clone().map(|u| u.is_author_in(&*conn, &blog)).unwrap_or(false) {
blog.delete(&conn, &searcher); blog.delete(&conn, &searcher);
Ok(Redirect::to(uri!(super::instance::index))) Ok(Redirect::to(uri!(super::instance::index)))
} else { } else {
Err(Some(Template::render("errors/403", json!({// TODO actually return 403 error code // TODO actually return 403 error code
"error_message": "You are not allowed to delete this blog." Err(Some(render!(errors::not_authorized(
})))) &(&*conn, &intl.catalog, user),
"You are not allowed to delete this blog."
))))
} }
} }
#[get("/~/<name>/outbox")] #[get("/~/<name>/outbox")]
fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> { pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
let blog = Blog::find_local(&*conn, &name)?; let blog = Blog::find_local(&*conn, &name)?;
Some(blog.outbox(&*conn)) Some(blog.outbox(&*conn))
} }
#[get("/~/<name>/atom.xml")] #[get("/~/<name>/atom.xml")]
fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> { pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
let blog = Blog::find_by_fqn(&*conn, &name)?; let blog = Blog::find_by_fqn(&*conn, &name)?;
let feed = FeedBuilder::default() let feed = FeedBuilder::default()
.title(blog.title.clone()) .title(blog.title.clone())

View File

@ -3,9 +3,9 @@ use rocket::{
request::LenientForm, request::LenientForm,
response::Redirect response::Redirect
}; };
use rocket_contrib::Template; use rocket_i18n::I18n;
use serde_json;
use validator::Validate; use validator::Validate;
use template_utils::Ructe;
use plume_common::{utils, activity_pub::{broadcast, ApRequest, ActivityStream}}; use plume_common::{utils, activity_pub::{broadcast, ApRequest, ActivityStream}};
use plume_models::{ use plume_models::{
@ -15,24 +15,24 @@ use plume_models::{
mentions::Mention, mentions::Mention,
posts::Post, posts::Post,
safe_string::SafeString, safe_string::SafeString,
tags::Tag,
users::User users::User
}; };
use Worker; use Worker;
#[derive(FromForm, Debug, Validate, Serialize)] #[derive(Default, FromForm, Debug, Validate, Serialize)]
struct NewCommentForm { pub struct NewCommentForm {
pub responding_to: Option<i32>, pub responding_to: Option<i32>,
#[validate(length(min = "1", message = "Your comment can't be empty"))] #[validate(length(min = "1", message = "Your comment can't be empty"))]
pub content: String, pub content: String,
pub warning: String, pub warning: String,
} }
#[post("/~/<blog_name>/<slug>/comment", data = "<data>")] #[post("/~/<blog_name>/<slug>/comment", data = "<form>")]
fn create(blog_name: String, slug: String, data: LenientForm<NewCommentForm>, user: User, conn: DbConn, worker: Worker) pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>, user: User, conn: DbConn, worker: Worker, intl: I18n)
-> Result<Redirect, Option<Template>> { -> Result<Redirect, Option<Ructe>> {
let blog = Blog::find_by_fqn(&*conn, &blog_name).ok_or(None)?; let blog = Blog::find_by_fqn(&*conn, &blog_name).ok_or(None)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?; let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?;
let form = data.get();
form.validate() form.validate()
.map(|_| { .map(|_| {
let (html, mentions, _hashtags) = utils::md_to_html(form.content.as_ref()); let (html, mentions, _hashtags) = utils::md_to_html(form.content.as_ref());
@ -62,28 +62,30 @@ fn create(blog_name: String, slug: String, data: LenientForm<NewCommentForm>, us
.map_err(|errors| { .map_err(|errors| {
// TODO: de-duplicate this code // TODO: de-duplicate this code
let comments = Comment::list_by_post(&*conn, post.id); let comments = Comment::list_by_post(&*conn, post.id);
let comms = comments.clone();
Some(Template::render("posts/details", json!({ let previous = form.responding_to.map(|r| Comment::get(&*conn, r)
"author": post.get_authors(&*conn)[0].to_json(&*conn), .expect("posts::details_reponse: Error retrieving previous comment"));
"post": post,
"blog": blog, Some(render!(posts::details(
"comments": &comments.into_iter().map(|c| c.to_json(&*conn, &comms)).collect::<Vec<serde_json::Value>>(), &(&*conn, &intl.catalog, Some(user.clone())),
"n_likes": post.get_likes(&*conn).len(), post.clone(),
"has_liked": user.has_liked(&*conn, &post), blog,
"n_reshares": post.get_reshares(&*conn).len(), &*form,
"has_reshared": user.has_reshared(&*conn, &post), errors,
"account": user.to_json(&*conn), Tag::for_post(&*conn, post.id),
"date": &post.creation_date.timestamp(), comments.into_iter().filter(|c| c.in_response_to_id.is_none()).collect::<Vec<Comment>>(),
"previous": form.responding_to.and_then(|r| Comment::get(&*conn, r)).map(|r| r.to_json(&*conn, &[])), previous,
"user_fqn": user.get_fqn(&*conn), post.get_likes(&*conn).len(),
"comment_form": form, post.get_reshares(&*conn).len(),
"comment_errors": errors, user.has_liked(&*conn, &post),
}))) user.has_reshared(&*conn, &post),
user.is_following(&*conn, post.get_authors(&*conn)[0].id),
post.get_authors(&*conn)[0].clone()
)))
}) })
} }
#[get("/~/<_blog>/<_slug>/comment/<id>")] #[get("/~/<_blog>/<_slug>/comment/<id>")]
fn activity_pub(_blog: String, _slug: String, id: i32, _ap: ApRequest, conn: DbConn) -> Option<ActivityStream<Note>> { pub fn activity_pub(_blog: String, _slug: String, id: i32, _ap: ApRequest, conn: DbConn) -> Option<ActivityStream<Note>> {
Comment::get(&*conn, id).map(|c| ActivityStream::new(c.to_activity(&*conn))) Comment::get(&*conn, id).map(|c| ActivityStream::new(c.to_activity(&*conn)))
} }

View File

@ -1,40 +1,36 @@
use rocket_contrib::Template;
use rocket::Request; use rocket::Request;
use rocket::request::FromRequest; use rocket::request::FromRequest;
use rocket_i18n::I18n;
use plume_models::db_conn::DbConn; use plume_models::db_conn::DbConn;
use plume_models::users::User; use plume_models::users::User;
use template_utils::Ructe;
#[catch(404)] #[catch(404)]
fn not_found(req: &Request) -> Template { pub fn not_found(req: &Request) -> Ructe {
let conn = req.guard::<DbConn>().succeeded(); let conn = req.guard::<DbConn>().succeeded();
let intl = req.guard::<I18n>().succeeded();
let user = User::from_request(req).succeeded(); let user = User::from_request(req).succeeded();
Template::render("errors/404", json!({ render!(errors::not_found(
"error_message": "Page not found", &(&*conn.unwrap(), &intl.unwrap().catalog, user)
"account": user.and_then(|u| conn.map(|conn| u.to_json(&*conn))) ))
}))
} }
#[catch(500)] #[catch(500)]
fn server_error(req: &Request) -> Template { pub fn server_error(req: &Request) -> Ructe {
let conn = req.guard::<DbConn>().succeeded(); let conn = req.guard::<DbConn>().succeeded();
let intl = req.guard::<I18n>().succeeded();
let user = User::from_request(req).succeeded(); let user = User::from_request(req).succeeded();
Template::render("errors/500", json!({ render!(errors::server_error(
"error_message": "Server error", &(&*conn.unwrap(), &intl.unwrap().catalog, user)
"account": user.and_then(|u| conn.map(|conn| u.to_json(&*conn))) ))
}))
} }
#[derive(FromForm)] #[post("/csrf-violation?<target>")]
pub struct Uri { pub fn csrf_violation(target: Option<String>, conn: DbConn, intl: I18n, user: Option<User>) -> Ructe {
target: String, if let Some(uri) = target {
} eprintln!("Csrf violation while acceding \"{}\"", uri)
#[post("/csrf-violation?<uri>")]
fn csrf_violation(uri: Option<Uri>) -> Template {
if let Some(uri) = uri {
eprintln!("Csrf violation while acceding \"{}\"", uri.target)
} }
Template::render("errors/csrf", json!({ render!(errors::csrf(
"error_message":"" &(&*conn, &intl.catalog, user)
})) ))
} }

View File

@ -1,8 +1,8 @@
use gettextrs::gettext;
use rocket::{request::LenientForm, response::{status, Redirect}}; use rocket::{request::LenientForm, response::{status, Redirect}};
use rocket_contrib::{Json, Template}; use rocket_contrib::json::Json;
use rocket_i18n::I18n;
use serde_json; use serde_json;
use validator::{Validate}; use validator::{Validate, ValidationErrors};
use plume_common::activity_pub::sign::{Signable, use plume_common::activity_pub::sign::{Signable,
verify_http_headers}; verify_http_headers};
@ -18,10 +18,11 @@ use plume_models::{
}; };
use inbox::Inbox; use inbox::Inbox;
use routes::Page; use routes::Page;
use template_utils::Ructe;
use Searcher; use Searcher;
#[get("/")] #[get("/")]
fn index(conn: DbConn, user: Option<User>) -> Template { pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Ructe {
match Instance::get_local(&*conn) { match Instance::get_local(&*conn) {
Some(inst) => { Some(inst) => {
let federated = Post::get_recents_page(&*conn, Page::first().limits()); let federated = Post::get_recents_page(&*conn, Page::first().limits());
@ -33,101 +34,107 @@ fn index(conn: DbConn, user: Option<User>) -> Template {
Post::user_feed_page(&*conn, in_feed, Page::first().limits()) Post::user_feed_page(&*conn, in_feed, Page::first().limits())
}); });
Template::render("instance/index", json!({ render!(instance::index(
"instance": inst, &(&*conn, &intl.catalog, user),
"account": user.map(|u| u.to_json(&*conn)), inst,
"federated": federated.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), User::count_local(&*conn) as i32,
"local": local.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), Post::count_local(&*conn) as i32,
"user_feed": user_feed.map(|f| f.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>()), local,
"n_users": User::count_local(&*conn), federated,
"n_articles": Post::count_local(&*conn) user_feed
})) ))
} }
None => { None => {
Template::render("errors/500", json!({ render!(errors::server_error(
"error_message": gettext("You need to configure your instance before using it.".to_string()) &(&*conn, &intl.catalog, user)
})) ))
} }
} }
} }
#[get("/local?<page>")] #[get("/local?<page>")]
fn paginated_local(conn: DbConn, user: Option<User>, page: Page) -> Template { pub fn paginated_local(conn: DbConn, user: Option<User>, page: Page, intl: I18n) -> Ructe {
let instance = Instance::get_local(&*conn).expect("instance::paginated_local: local instance not found error"); let instance = Instance::get_local(&*conn).expect("instance::paginated_local: local instance not found error");
let articles = Post::get_instance_page(&*conn, instance.id, page.limits()); let articles = Post::get_instance_page(&*conn, instance.id, page.limits());
Template::render("instance/local", json!({ render!(instance::local(
"account": user.map(|u| u.to_json(&*conn)), &(&*conn, &intl.catalog, user),
"instance": instance, instance,
"page": page.page, articles,
"n_pages": Page::total(Post::count_local(&*conn) as i32), page.0,
"articles": articles.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>() Page::total(Post::count_local(&*conn) as i32)
})) ))
} }
#[get("/local")] #[get("/local")]
fn local(conn: DbConn, user: Option<User>) -> Template { pub fn local(conn: DbConn, user: Option<User>, intl: I18n) -> Ructe {
paginated_local(conn, user, Page::first()) paginated_local(conn, user, Page::first(), intl)
} }
#[get("/feed")] #[get("/feed")]
fn feed(conn: DbConn, user: User) -> Template { pub fn feed(conn: DbConn, user: User, intl: I18n) -> Ructe {
paginated_feed(conn, user, Page::first()) paginated_feed(conn, user, Page::first(), intl)
} }
#[get("/feed?<page>")] #[get("/feed?<page>")]
fn paginated_feed(conn: DbConn, user: User, page: Page) -> Template { pub fn paginated_feed(conn: DbConn, user: User, page: Page, intl: I18n) -> Ructe {
let followed = user.get_following(&*conn); let followed = user.get_following(&*conn);
let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>(); let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>();
in_feed.push(user.id); in_feed.push(user.id);
let articles = Post::user_feed_page(&*conn, in_feed, page.limits()); let articles = Post::user_feed_page(&*conn, in_feed, page.limits());
Template::render("instance/feed", json!({ render!(instance::feed(
"account": user.to_json(&*conn), &(&*conn, &intl.catalog, Some(user)),
"page": page.page, articles,
"n_pages": Page::total(Post::count_local(&*conn) as i32), page.0,
"articles": articles.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>() Page::total(Post::count_local(&*conn) as i32)
})) ))
} }
#[get("/federated")] #[get("/federated")]
fn federated(conn: DbConn, user: Option<User>) -> Template { pub fn federated(conn: DbConn, user: Option<User>, intl: I18n) -> Ructe {
paginated_federated(conn, user, Page::first()) paginated_federated(conn, user, Page::first(), intl)
} }
#[get("/federated?<page>")] #[get("/federated?<page>")]
fn paginated_federated(conn: DbConn, user: Option<User>, page: Page) -> Template { pub fn paginated_federated(conn: DbConn, user: Option<User>, page: Page, intl: I18n) -> Ructe {
let articles = Post::get_recents_page(&*conn, page.limits()); let articles = Post::get_recents_page(&*conn, page.limits());
Template::render("instance/federated", json!({ render!(instance::federated(
"account": user.map(|u| u.to_json(&*conn)), &(&*conn, &intl.catalog, user),
"page": page.page, articles,
"n_pages": Page::total(Post::count_local(&*conn) as i32), page.0,
"articles": articles.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>() Page::total(Post::count_local(&*conn) as i32)
})) ))
} }
#[get("/admin")] #[get("/admin")]
fn admin(conn: DbConn, admin: Admin) -> Template { pub fn admin(conn: DbConn, admin: Admin, intl: I18n) -> Ructe {
Template::render("instance/admin", json!({ let local_inst = Instance::get_local(&*conn).expect("instance::admin: local instance not found");
"account": admin.0.to_json(&*conn), render!(instance::admin(
"instance": Instance::get_local(&*conn), &(&*conn, &intl.catalog, Some(admin.0)),
"errors": null, local_inst.clone(),
"form": null InstanceSettingsForm {
})) name: local_inst.name.clone(),
open_registrations: local_inst.open_registrations,
short_description: local_inst.short_description,
long_description: local_inst.long_description,
default_license: local_inst.default_license,
},
ValidationErrors::default()
))
} }
#[derive(FromForm, Validate, Serialize)] #[derive(Clone, FromForm, Validate, Serialize)]
struct InstanceSettingsForm { pub struct InstanceSettingsForm {
#[validate(length(min = "1"))] #[validate(length(min = "1"))]
name: String, pub name: String,
open_registrations: bool, pub open_registrations: bool,
short_description: SafeString, pub short_description: SafeString,
long_description: SafeString, pub long_description: SafeString,
#[validate(length(min = "1"))] #[validate(length(min = "1"))]
default_license: String pub default_license: String
} }
#[post("/admin", data = "<form>")] #[post("/admin", data = "<form>")]
fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSettingsForm>) -> Result<Redirect, Template> { pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSettingsForm>, intl: I18n) -> Result<Redirect, Ructe> {
let form = form.get();
form.validate() form.validate()
.map(|_| { .map(|_| {
let instance = Instance::get_local(&*conn).expect("instance::update_settings: local instance not found error"); let instance = Instance::get_local(&*conn).expect("instance::update_settings: local instance not found error");
@ -138,33 +145,36 @@ fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSetting
form.long_description.clone()); form.long_description.clone());
Redirect::to(uri!(admin)) Redirect::to(uri!(admin))
}) })
.map_err(|e| Template::render("instance/admin", json!({ .map_err(|e| {
"account": admin.0.to_json(&*conn), let local_inst = Instance::get_local(&*conn).expect("instance::update_settings: local instance not found");
"instance": Instance::get_local(&*conn), render!(instance::admin(
"errors": e.inner(), &(&*conn, &intl.catalog, Some(admin.0)),
"form": form local_inst,
}))) form.clone(),
e
))
})
} }
#[get("/admin/instances")] #[get("/admin/instances")]
fn admin_instances(admin: Admin, conn: DbConn) -> Template { pub fn admin_instances(admin: Admin, conn: DbConn, intl: I18n) -> Ructe {
admin_instances_paginated(admin, conn, Page::first()) admin_instances_paginated(admin, conn, Page::first(), intl)
} }
#[get("/admin/instances?<page>")] #[get("/admin/instances?<page>")]
fn admin_instances_paginated(admin: Admin, conn: DbConn, page: Page) -> Template { pub fn admin_instances_paginated(admin: Admin, conn: DbConn, page: Page, intl: I18n) -> Ructe {
let instances = Instance::page(&*conn, page.limits()); let instances = Instance::page(&*conn, page.limits());
Template::render("instance/list", json!({ render!(instance::list(
"account": admin.0.to_json(&*conn), &(&*conn, &intl.catalog, Some(admin.0)),
"instances": instances, Instance::get_local(&*conn).expect("admin_instances: local instance error"),
"instance": Instance::get_local(&*conn), instances,
"page": page.page, page.0,
"n_pages": Page::total(Instance::count(&*conn) as i32), Page::total(Instance::count(&*conn) as i32)
})) ))
} }
#[post("/admin/instances/<id>/block")] #[post("/admin/instances/<id>/block")]
fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Redirect { pub fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Redirect {
if let Some(inst) = Instance::get(&*conn, id) { if let Some(inst) = Instance::get(&*conn, id) {
inst.toggle_block(&*conn); inst.toggle_block(&*conn);
} }
@ -173,25 +183,22 @@ fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Redirect {
} }
#[get("/admin/users")] #[get("/admin/users")]
fn admin_users(admin: Admin, conn: DbConn) -> Template { pub fn admin_users(admin: Admin, conn: DbConn, intl: I18n) -> Ructe {
admin_users_paginated(admin, conn, Page::first()) admin_users_paginated(admin, conn, Page::first(), intl)
} }
#[get("/admin/users?<page>")] #[get("/admin/users?<page>")]
fn admin_users_paginated(admin: Admin, conn: DbConn, page: Page) -> Template { pub fn admin_users_paginated(admin: Admin, conn: DbConn, page: Page, intl: I18n) -> Ructe {
let users = User::get_local_page(&*conn, page.limits()).into_iter() render!(instance::users(
.map(|u| u.to_json(&*conn)).collect::<Vec<serde_json::Value>>(); &(&*conn, &intl.catalog, Some(admin.0)),
User::get_local_page(&*conn, page.limits()),
Template::render("instance/users", json!({ page.0,
"account": admin.0.to_json(&*conn), Page::total(User::count_local(&*conn) as i32)
"users": users, ))
"page": page.page,
"n_pages": Page::total(User::count_local(&*conn) as i32)
}))
} }
#[post("/admin/users/<id>/ban")] #[post("/admin/users/<id>/ban")]
fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Redirect { pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Redirect {
if let Some(u) = User::get(&*conn, id) { if let Some(u) = User::get(&*conn, id) {
u.delete(&*conn, &searcher); u.delete(&*conn, &searcher);
} }
@ -199,7 +206,7 @@ fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Redirect {
} }
#[post("/inbox", data = "<data>")] #[post("/inbox", data = "<data>")]
fn shared_inbox(conn: DbConn, data: String, headers: Headers, searcher: Searcher) -> Result<String, status::BadRequest<&'static str>> { pub fn shared_inbox(conn: DbConn, data: String, headers: Headers, searcher: Searcher) -> Result<String, status::BadRequest<&'static str>> {
let act: serde_json::Value = serde_json::from_str(&data[..]).expect("instance::shared_inbox: deserialization error"); let act: serde_json::Value = serde_json::from_str(&data[..]).expect("instance::shared_inbox: deserialization error");
let activity = act.clone(); let activity = act.clone();
@ -227,7 +234,7 @@ fn shared_inbox(conn: DbConn, data: String, headers: Headers, searcher: Searcher
} }
#[get("/nodeinfo")] #[get("/nodeinfo")]
fn nodeinfo(conn: DbConn) -> Json<serde_json::Value> { pub fn nodeinfo(conn: DbConn) -> Json<serde_json::Value> {
Json(json!({ Json(json!({
"version": "2.0", "version": "2.0",
"software": { "software": {
@ -252,20 +259,19 @@ fn nodeinfo(conn: DbConn) -> Json<serde_json::Value> {
} }
#[get("/about")] #[get("/about")]
fn about(user: Option<User>, conn: DbConn) -> Template { pub fn about(user: Option<User>, conn: DbConn, intl: I18n) -> Ructe {
Template::render("instance/about", json!({ render!(instance::about(
"account": user.map(|u| u.to_json(&*conn)), &(&*conn, &intl.catalog, user),
"instance": Instance::get_local(&*conn), Instance::get_local(&*conn).expect("Local instance not found"),
"admin": Instance::get_local(&*conn).map(|i| i.main_admin(&*conn).to_json(&*conn)), Instance::get_local(&*conn).expect("Local instance not found").main_admin(&*conn),
"version": env!("CARGO_PKG_VERSION"), User::count_local(&*conn),
"n_users": User::count_local(&*conn), Post::count_local(&*conn),
"n_articles": Post::count_local(&*conn), Instance::count(&*conn) - 1
"n_instances": Instance::count(&*conn) - 1 ))
}))
} }
#[get("/manifest.json")] #[get("/manifest.json")]
fn web_manifest(conn: DbConn) -> Json<serde_json::Value> { pub fn web_manifest(conn: DbConn) -> Json<serde_json::Value> {
let instance = Instance::get_local(&*conn).expect("instance::web_manifest: local instance not found error"); let instance = Instance::get_local(&*conn).expect("instance::web_manifest: local instance not found error");
Json(json!({ Json(json!({
"name": &instance.name, "name": &instance.name,

View File

@ -1,4 +1,5 @@
use rocket::{response::{Redirect, Flash}}; use rocket::response::{Redirect, Flash};
use rocket_i18n::I18n;
use plume_common::activity_pub::{broadcast, inbox::{Notify, Deletable}}; use plume_common::activity_pub::{broadcast, inbox::{Notify, Deletable}};
use plume_common::utils; use plume_common::utils;
@ -12,7 +13,7 @@ use plume_models::{
use Worker; use Worker;
#[post("/~/<blog>/<slug>/like")] #[post("/~/<blog>/<slug>/like")]
fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> { pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> {
let b = Blog::find_by_fqn(&*conn, &blog)?; let b = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?;
@ -39,9 +40,9 @@ fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker)
} }
#[post("/~/<blog>/<slug>/like", rank = 2)] #[post("/~/<blog>/<slug>/like", rank = 2)]
fn create_auth(blog: String, slug: String) -> Flash<Redirect>{ pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect>{
utils::requires_login( utils::requires_login(
"You need to be logged in order to like a post", i18n!(i18n.catalog, "You need to be logged in order to like a post"),
uri!(create: blog = blog, slug = slug) uri!(create: blog = blog, slug = slug)
) )
} }

View File

@ -1,31 +1,29 @@
use guid_create::GUID; use guid_create::GUID;
use multipart::server::{Multipart, save::{SavedData, SaveResult}}; use multipart::server::{Multipart, save::{SavedData, SaveResult}};
use rocket::{Data, http::ContentType, response::{Redirect, status}}; use rocket::{Data, http::ContentType, response::{Redirect, status}};
use rocket_contrib::Template; use rocket_i18n::I18n;
use serde_json;
use std::fs; use std::fs;
use plume_models::{db_conn::DbConn, medias::*, users::User}; use plume_models::{db_conn::DbConn, medias::*, users::User};
use template_utils::Ructe;
#[get("/medias")] #[get("/medias")]
fn list(user: User, conn: DbConn) -> Template { pub fn list(user: User, conn: DbConn, intl: I18n) -> Ructe {
let medias = Media::for_user(&*conn, user.id); let medias = Media::for_user(&*conn, user.id);
Template::render("medias/index", json!({ render!(medias::index(
"account": user.to_json(&*conn), &(&*conn, &intl.catalog, Some(user)),
"medias": medias.into_iter().map(|m| m.to_json(&*conn)).collect::<Vec<serde_json::Value>>() medias
})) ))
} }
#[get("/medias/new")] #[get("/medias/new")]
fn new(user: User, conn: DbConn) -> Template { pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
Template::render("medias/new", json!({ render!(medias::new(
"account": user.to_json(&*conn), &(&*conn, &intl.catalog, Some(user))
"form": {}, ))
"errors": {}
}))
} }
#[post("/medias/new", data = "<data>")] #[post("/medias/new", data = "<data>")]
fn upload(user: User, data: Data, ct: &ContentType, conn: DbConn) -> Result<Redirect, status::BadRequest<&'static str>> { pub fn upload(user: User, data: Data, ct: &ContentType, conn: DbConn) -> Result<Redirect, status::BadRequest<&'static str>> {
if ct.is_form_data() { if ct.is_form_data() {
let (_, boundary) = ct.params().find(|&(k, _)| k == "boundary").ok_or_else(|| status::BadRequest(Some("No boundary")))?; let (_, boundary) = ct.params().find(|&(k, _)| k == "boundary").ok_or_else(|| status::BadRequest(Some("No boundary")))?;
@ -86,23 +84,23 @@ fn read(data: &SavedData) -> String {
} }
#[get("/medias/<id>")] #[get("/medias/<id>")]
fn details(id: i32, user: User, conn: DbConn) -> Template { pub fn details(id: i32, user: User, conn: DbConn, intl: I18n) -> Ructe {
let media = Media::get(&*conn, id); let media = Media::get(&*conn, id).expect("Media::details: media not found");
Template::render("medias/details", json!({ render!(medias::details(
"account": user.to_json(&*conn), &(&*conn, &intl.catalog, Some(user)),
"media": media.map(|m| m.to_json(&*conn)) media
})) ))
} }
#[post("/medias/<id>/delete")] #[post("/medias/<id>/delete")]
fn delete(id: i32, _user: User, conn: DbConn) -> Option<Redirect> { pub fn delete(id: i32, _user: User, conn: DbConn) -> Option<Redirect> {
let media = Media::get(&*conn, id)?; let media = Media::get(&*conn, id)?;
media.delete(&*conn); media.delete(&*conn);
Some(Redirect::to(uri!(list))) Some(Redirect::to(uri!(list)))
} }
#[post("/medias/<id>/avatar")] #[post("/medias/<id>/avatar")]
fn set_avatar(id: i32, user: User, conn: DbConn) -> Option<Redirect> { pub fn set_avatar(id: i32, user: User, conn: DbConn) -> Option<Redirect> {
let media = Media::get(&*conn, id)?; let media = Media::get(&*conn, id)?;
user.set_avatar(&*conn, media.id); user.set_avatar(&*conn, media.id);
Some(Redirect::to(uri!(details: id = id))) Some(Redirect::to(uri!(details: id = id)))

View File

@ -1,70 +1,23 @@
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder}; use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
use rocket::{ use rocket::{
http::{RawStr, http::RawStr,
uri::{FromUriParam, UriDisplay}},
request::FromFormValue, request::FromFormValue,
response::NamedFile response::NamedFile,
};
use std::{
fmt,
path::{Path, PathBuf}
}; };
use std::path::{Path, PathBuf};
use plume_models::{Connection, posts::Post}; use plume_models::{Connection, posts::Post};
macro_rules! may_fail {
($account:expr, $expr:expr, $template:expr, $msg:expr, | $res:ident | $block:block) => {
{
let res = $expr;
if res.is_some() {
let $res = res.unwrap();
$block
} else {
Template::render(concat!("errors/", $template), json!({
"error_message": $msg,
"account": $account
}))
}
}
};
($account:expr, $expr:expr, $msg:expr, | $res:ident | $block:block) => {
may_fail!($account, $expr, "404", $msg, |$res| {
$block
})
};
($account:expr, $expr:expr, | $res:ident | $block:block) => {
may_fail!($account, $expr, "", |$res| {
$block
})
};
}
const ITEMS_PER_PAGE: i32 = 12; const ITEMS_PER_PAGE: i32 = 12;
#[derive(FromForm)] #[derive(Copy, Clone)]
pub struct Page { pub struct Page(i32);
page: i32
}
impl UriDisplay for Page {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "page={}", &self.page as &UriDisplay)
}
}
impl FromUriParam<i32> for Page {
type Target = Page;
fn from_uri_param(num: i32) -> Page {
Page { page: num }
}
}
impl<'v> FromFormValue<'v> for Page { impl<'v> FromFormValue<'v> for Page {
type Error = &'v RawStr; type Error = &'v RawStr;
fn from_form_value(form_value: &'v RawStr) -> Result<Page, &'v RawStr> { fn from_form_value(form_value: &'v RawStr) -> Result<Page, &'v RawStr> {
match form_value.parse::<i32>() { match form_value.parse::<i32>() {
Ok(page) => Ok(Page{page}), Ok(page) => Ok(Page(page)),
_ => Err(form_value), _ => Err(form_value),
} }
} }
@ -72,9 +25,7 @@ impl<'v> FromFormValue<'v> for Page {
impl Page { impl Page {
pub fn first() -> Page { pub fn first() -> Page {
Page { Page(1)
page: 1
}
} }
/// Computes the total number of pages needed to display n_items /// Computes the total number of pages needed to display n_items
@ -87,7 +38,7 @@ impl Page {
} }
pub fn limits(&self) -> (i32, i32) { pub fn limits(&self) -> (i32, i32) {
((self.page - 1) * ITEMS_PER_PAGE, self.page * ITEMS_PER_PAGE) ((self.0 - 1) * ITEMS_PER_PAGE, self.0 * ITEMS_PER_PAGE)
} }
} }
@ -126,6 +77,6 @@ pub mod search;
pub mod well_known; pub mod well_known;
#[get("/static/<file..>", rank = 2)] #[get("/static/<file..>", rank = 2)]
fn static_files(file: PathBuf) -> Option<NamedFile> { pub fn static_files(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new("static/").join(file)).ok() NamedFile::open(Path::new("static/").join(file)).ok()
} }

View File

@ -1,29 +1,30 @@
use rocket::response::{Redirect, Flash}; use rocket::response::{Redirect, Flash};
use rocket_contrib::Template; use rocket_i18n::I18n;
use plume_common::utils; use plume_common::utils;
use plume_models::{db_conn::DbConn, notifications::Notification, users::User}; use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
use routes::Page; use routes::Page;
use template_utils::Ructe;
#[get("/notifications?<page>")] #[get("/notifications?<page>")]
fn paginated_notifications(conn: DbConn, user: User, page: Page) -> Template { pub fn paginated_notifications(conn: DbConn, user: User, page: Page, intl: I18n) -> Ructe {
Template::render("notifications/index", json!({ render!(notifications::index(
"account": user.to_json(&*conn), &(&*conn, &intl.catalog, Some(user.clone())),
"notifications": Notification::page_for_user(&*conn, &user, page.limits()).into_iter().map(|n| n.to_json(&*conn)).collect::<Vec<_>>(), Notification::page_for_user(&*conn, &user, page.limits()),
"page": page.page, page.0,
"n_pages": Page::total(Notification::find_for_user(&*conn, &user).len() as i32) Page::total(Notification::find_for_user(&*conn, &user).len() as i32)
})) ))
} }
#[get("/notifications")] #[get("/notifications")]
fn notifications(conn: DbConn, user: User) -> Template { pub fn notifications(conn: DbConn, user: User, intl: I18n) -> Ructe {
paginated_notifications(conn, user, Page::first()) paginated_notifications(conn, user, Page::first(), intl)
} }
#[get("/notifications", rank = 2)] #[get("/notifications", rank = 2)]
fn notifications_auth() -> Flash<Redirect>{ pub fn notifications_auth(i18n: I18n) -> Flash<Redirect>{
utils::requires_login( utils::requires_login(
"You need to be logged in order to see your notifications", i18n!(i18n.catalog, "You need to be logged in order to see your notifications"),
uri!(notifications) uri!(notifications)
) )
} }

View File

@ -1,10 +1,9 @@
use activitypub::object::Article; use activitypub::object::Article;
use chrono::Utc; use chrono::Utc;
use heck::{CamelCase, KebabCase}; use heck::{CamelCase, KebabCase};
use rocket::{request::LenientForm}; use rocket::request::LenientForm;
use rocket::response::{Redirect, Flash}; use rocket::response::{Redirect, Flash};
use rocket_contrib::Template; use rocket_i18n::I18n;
use serde_json;
use std::{collections::{HashMap, HashSet}, borrow::Cow}; use std::{collections::{HashMap, HashSet}, borrow::Cow};
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
@ -23,66 +22,74 @@ use plume_models::{
tags::*, tags::*,
users::User users::User
}; };
use routes::comments::NewCommentForm;
use template_utils::Ructe;
use Worker; use Worker;
use Searcher; use Searcher;
#[derive(FromForm)]
struct CommentQuery {
responding_to: Option<i32>
}
// See: https://github.com/SergioBenitez/Rocket/pull/454 // See: https://github.com/SergioBenitez/Rocket/pull/454
#[get("/~/<blog>/<slug>", rank = 4)] #[get("/~/<blog>/<slug>", rank = 5)]
fn details(blog: String, slug: String, conn: DbConn, user: Option<User>) -> Template { pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, Ructe> {
details_response(blog, slug, conn, user, None) details_response(blog, slug, conn, user, None, intl)
} }
#[get("/~/<blog>/<slug>?<query>")] #[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
fn details_response(blog: String, slug: String, conn: DbConn, user: Option<User>, query: Option<CommentQuery>) -> Template { pub fn details_response(blog: String, slug: String, conn: DbConn, user: Option<User>, responding_to: Option<i32>, intl: I18n) -> Result<Ructe, Ructe> {
may_fail!(user.map(|u| u.to_json(&*conn)), Blog::find_by_fqn(&*conn, &blog), "Couldn't find this blog", |blog| { let blog = Blog::find_by_fqn(&*conn, &blog).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?;
may_fail!(user.map(|u| u.to_json(&*conn)), Post::find_by_slug(&*conn, &slug, blog.id), "Couldn't find this post", |post| { let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?;
if post.published || post.get_authors(&*conn).into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) { if post.published || post.get_authors(&*conn).into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) {
let comments = Comment::list_by_post(&*conn, post.id); let comments = Comment::list_by_post(&*conn, post.id);
let comms = comments.clone();
let previous = query.and_then(|q| q.responding_to.map(|r| Comment::get(&*conn, r) let previous = responding_to.map(|r| Comment::get(&*conn, r)
.expect("posts::details_reponse: Error retrieving previous comment").to_json(&*conn, &[]))); .expect("posts::details_reponse: Error retrieving previous comment"));
Template::render("posts/details", json!({
"author": post.get_authors(&*conn)[0].to_json(&*conn), Ok(render!(posts::details(
"article": post.to_json(&*conn), &(&*conn, &intl.catalog, user.clone()),
"blog": blog.to_json(&*conn), post.clone(),
"comments": &comments.into_iter().filter_map(|c| if c.in_response_to_id.is_none() { blog,
Some(c.to_json(&*conn, &comms)) &NewCommentForm {
} else { warning: previous.clone().map(|p| p.spoiler_text).unwrap_or_default(),
None content: previous.clone().map(|p| format!(
}).collect::<Vec<serde_json::Value>>(), "@{} {}",
"n_likes": post.get_likes(&*conn).len(), p.get_author(&*conn).get_fqn(&*conn),
"has_liked": user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false), Mention::list_for_comment(&*conn, p.id)
"n_reshares": post.get_reshares(&*conn).len(), .into_iter()
"has_reshared": user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false), .filter_map(|m| {
"account": &user.clone().map(|u| u.to_json(&*conn)), let user = user.clone();
"date": &post.creation_date.timestamp(), if let Some(mentioned) = m.get_mentioned(&*conn) {
"previous": previous, if user.is_none() || mentioned.id != user.expect("posts::details_response: user error while listing mentions").id {
"default": { Some(format!("@{}", mentioned.get_fqn(&*conn)))
"warning": previous.map(|p| p["spoiler_text"].clone()) } else {
}, None
"user_fqn": user.clone().map(|u| u.get_fqn(&*conn)).unwrap_or_default(), }
"is_author": user.clone().map(|u| post.get_authors(&*conn).into_iter().any(|a| u.id == a.id)).unwrap_or(false), } else {
"is_following": user.map(|u| u.is_following(&*conn, post.get_authors(&*conn)[0].id)).unwrap_or(false), None
"comment_form": null, }
"comment_errors": null, }).collect::<Vec<String>>().join(" "))
})) ).unwrap_or_default(),
} else { ..NewCommentForm::default()
Template::render("errors/403", json!({ },
"error_message": "This post isn't published yet." ValidationErrors::default(),
})) Tag::for_post(&*conn, post.id),
} comments.into_iter().filter(|c| c.in_response_to_id.is_none()).collect::<Vec<Comment>>(),
}) previous,
}) post.get_likes(&*conn).len(),
post.get_reshares(&*conn).len(),
user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false),
user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false),
user.map(|u| u.is_following(&*conn, post.get_authors(&*conn)[0].id)).unwrap_or(false),
post.get_authors(&*conn)[0].clone()
)))
} else {
Err(render!(errors::not_authorized(
&(&*conn, &intl.catalog, user.clone()),
"This post isn't published yet."
)))
}
} }
#[get("/~/<blog>/<slug>", rank = 3)] #[get("/~/<blog>/<slug>", rank = 3)]
fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result<ActivityStream<Article>, Option<String>> { pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result<ActivityStream<Article>, Option<String>> {
let blog = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?; let blog = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?; let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?;
if post.published { if post.published {
@ -93,44 +100,47 @@ fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) ->
} }
#[get("/~/<blog>/new", rank = 2)] #[get("/~/<blog>/new", rank = 2)]
fn new_auth(blog: String) -> Flash<Redirect> { pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
"You need to be logged in order to write a new post", i18n!(i18n.catalog, "You need to be logged in order to write a new post"),
uri!(new: blog = blog) uri!(new: blog = blog)
) )
} }
#[get("/~/<blog>/new", rank = 1)] #[get("/~/<blog>/new", rank = 1)]
fn new(blog: String, user: User, conn: DbConn) -> Option<Template> { pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> {
let b = Blog::find_by_fqn(&*conn, &blog)?; let b = Blog::find_by_fqn(&*conn, &blog)?;
if !user.is_author_in(&*conn, &b) { if !user.is_author_in(&*conn, &b) {
Some(Template::render("errors/403", json!({// TODO actually return 403 error code // TODO actually return 403 error code
"error_message": "You are not author in this blog." Some(render!(errors::not_authorized(
}))) &(&*conn, &intl.catalog, Some(user)),
"You are not author in this blog."
)))
} else { } else {
let medias = Media::for_user(&*conn, user.id); let medias = Media::for_user(&*conn, user.id);
Some(Template::render("posts/new", json!({ Some(render!(posts::new(
"account": user.to_json(&*conn), &(&*conn, &intl.catalog, Some(user)),
"instance": Instance::get_local(&*conn), false,
"editing": false, &NewPostForm::default(),
"errors": null, ValidationErrors::default(),
"form": null, Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
"is_draft": true, medias,
"medias": medias.into_iter().map(|m| m.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), true
}))) )))
} }
} }
#[get("/~/<blog>/<slug>/edit")] #[get("/~/<blog>/<slug>/edit")]
fn edit(blog: String, slug: String, user: User, conn: DbConn) -> Option<Template> { pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> {
let b = Blog::find_by_fqn(&*conn, &blog)?; let b = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?;
if !user.is_author_in(&*conn, &b) { if !user.is_author_in(&*conn, &b) {
Some(Template::render("errors/403", json!({// TODO actually return 403 error code Some(render!(errors::not_authorized(
"error_message": "You are not author in this blog." &(&*conn, &intl.catalog, Some(user)),
}))) "You are not author in this blog."
)))
} else { } else {
let source = if !post.source.is_empty() { let source = if !post.source.is_empty() {
post.source post.source
@ -139,12 +149,10 @@ fn edit(blog: String, slug: String, user: User, conn: DbConn) -> Option<Template
}; };
let medias = Media::for_user(&*conn, user.id); let medias = Media::for_user(&*conn, user.id);
Some(Template::render("posts/new", json!({ Some(render!(posts::new(
"account": user.to_json(&*conn), &(&*conn, &intl.catalog, Some(user)),
"instance": Instance::get_local(&*conn), true,
"editing": true, &NewPostForm {
"errors": null,
"form": NewPostForm {
title: post.title.clone(), title: post.title.clone(),
subtitle: post.subtitle.clone(), subtitle: post.subtitle.clone(),
content: source, content: source,
@ -157,19 +165,20 @@ fn edit(blog: String, slug: String, user: User, conn: DbConn) -> Option<Template
draft: true, draft: true,
cover: post.cover_id, cover: post.cover_id,
}, },
"is_draft": !post.published, ValidationErrors::default(),
"medias": medias.into_iter().map(|m| m.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
}))) medias,
!post.published
)))
} }
} }
#[post("/~/<blog>/<slug>/edit", data = "<data>")] #[post("/~/<blog>/<slug>/edit", data = "<form>")]
fn update(blog: String, slug: String, user: User, conn: DbConn, data: LenientForm<NewPostForm>, worker: Worker, searcher: Searcher) pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: LenientForm<NewPostForm>, worker: Worker, intl: I18n, searcher: Searcher)
-> Result<Redirect, Option<Template>> { -> Result<Redirect, Option<Ructe>> {
let b = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?; let b = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?;
let mut post = Post::find_by_slug(&*conn, &slug, b.id).ok_or(None)?; let mut post = Post::find_by_slug(&*conn, &slug, b.id).ok_or(None)?;
let form = data.get();
let new_slug = if !post.published { let new_slug = if !post.published {
form.title.to_string().to_kebab_case() form.title.to_string().to_kebab_case()
} else { } else {
@ -249,20 +258,21 @@ fn update(blog: String, slug: String, user: User, conn: DbConn, data: LenientFor
} }
} else { } else {
let medias = Media::for_user(&*conn, user.id); let medias = Media::for_user(&*conn, user.id);
Err(Some(Template::render("posts/new", json!({ let temp = render!(posts::new(
"account": user.to_json(&*conn), &(&*conn, &intl.catalog, Some(user)),
"instance": Instance::get_local(&*conn), true,
"editing": true, &*form,
"errors": errors.inner(), errors.clone(),
"form": form, Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
"is_draft": form.draft, medias.clone(),
"medias": medias.into_iter().map(|m| m.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), form.draft.clone()
})))) ));
Err(Some(temp))
} }
} }
#[derive(FromForm, Validate, Serialize)] #[derive(Default, FromForm, Validate, Serialize)]
struct NewPostForm { pub struct NewPostForm {
#[validate(custom(function = "valid_slug", message = "Invalid title"))] #[validate(custom(function = "valid_slug", message = "Invalid title"))]
pub title: String, pub title: String,
pub subtitle: String, pub subtitle: String,
@ -273,7 +283,7 @@ struct NewPostForm {
pub cover: Option<i32>, pub cover: Option<i32>,
} }
fn valid_slug(title: &str) -> Result<(), ValidationError> { pub fn valid_slug(title: &str) -> Result<(), ValidationError> {
let slug = title.to_string().to_kebab_case(); let slug = title.to_string().to_kebab_case();
if slug.is_empty() { if slug.is_empty() {
Err(ValidationError::new("empty_slug")) Err(ValidationError::new("empty_slug"))
@ -284,10 +294,9 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
} }
} }
#[post("/~/<blog_name>/new", data = "<data>")] #[post("/~/<blog_name>/new", data = "<form>")]
fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: DbConn, worker: Worker, searcher: Searcher) -> Result<Redirect, Option<Template>> { pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result<Redirect, Option<Ructe>> {
let blog = Blog::find_by_fqn(&*conn, &blog_name).ok_or(None)?; let blog = Blog::find_by_fqn(&*conn, &blog_name).ok_or(None)?;
let form = data.get();
let slug = form.title.to_string().to_kebab_case(); let slug = form.title.to_string().to_kebab_case();
let mut errors = match form.validate() { let mut errors = match form.validate() {
@ -367,20 +376,20 @@ fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: D
} }
} else { } else {
let medias = Media::for_user(&*conn, user.id); let medias = Media::for_user(&*conn, user.id);
Err(Some(Template::render("posts/new", json!({ Err(Some(render!(posts::new(
"account": user.to_json(&*conn), &(&*conn, &intl.catalog, Some(user)),
"instance": Instance::get_local(&*conn), false,
"editing": false, &*form,
"errors": errors.inner(), errors.clone(),
"form": form, Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
"is_draft": form.draft, medias,
"medias": medias.into_iter().map(|m| m.to_json(&*conn)).collect::<Vec<serde_json::Value>>() form.draft
})))) ))))
} }
} }
#[post("/~/<blog_name>/<slug>/delete")] #[post("/~/<blog_name>/<slug>/delete")]
fn delete(blog_name: String, slug: String, conn: DbConn, user: User, worker: Worker, searcher: Searcher) -> Redirect { pub fn delete(blog_name: String, slug: String, conn: DbConn, user: User, worker: Worker, searcher: Searcher) -> Redirect {
let post = Blog::find_by_fqn(&*conn, &blog_name) let post = Blog::find_by_fqn(&*conn, &blog_name)
.and_then(|blog| Post::find_by_slug(&*conn, &slug, blog.id)); .and_then(|blog| Post::find_by_slug(&*conn, &slug, blog.id));

View File

@ -1,4 +1,5 @@
use rocket::{response::{Redirect, Flash}}; use rocket::response::{Redirect, Flash};
use rocket_i18n::I18n;
use plume_common::activity_pub::{broadcast, inbox::{Deletable, Notify}}; use plume_common::activity_pub::{broadcast, inbox::{Deletable, Notify}};
use plume_common::utils; use plume_common::utils;
@ -12,7 +13,7 @@ use plume_models::{
use Worker; use Worker;
#[post("/~/<blog>/<slug>/reshare")] #[post("/~/<blog>/<slug>/reshare")]
fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> { pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> {
let b = Blog::find_by_fqn(&*conn, &blog)?; let b = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?;
@ -40,9 +41,9 @@ fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker)
} }
#[post("/~/<blog>/<slug>/reshare", rank=1)] #[post("/~/<blog>/<slug>/reshare", rank=1)]
fn create_auth(blog: String, slug: String) -> Flash<Redirect> { pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
"You need to be logged in order to reshare a post", i18n!(i18n.catalog, "You need to be logged in order to reshare a post"),
uri!(create: blog = blog, slug = slug) uri!(create: blog = blog, slug = slug)
) )
} }

View File

@ -1,23 +1,16 @@
use chrono::offset::Utc; use chrono::offset::Utc;
use rocket_contrib::Template; use rocket::request::Form;
use serde_json; use rocket_i18n::I18n;
use plume_models::{ use plume_models::{
db_conn::DbConn, users::User, db_conn::DbConn, users::User,
search::Query}; search::Query};
use routes::Page; use routes::Page;
use template_utils::Ructe;
use Searcher; use Searcher;
#[get("/search")] #[derive(Default, FromForm)]
fn index(conn: DbConn, user: Option<User>) -> Template { pub struct SearchQuery {
Template::render("search/index", json!({
"account": user.map(|u| u.to_json(&*conn)),
"now": format!("{}", Utc::today().format("%Y-%m-d")),
}))
}
#[derive(FromForm)]
struct SearchQuery {
q: Option<String>, q: Option<String>,
title: Option<String>, title: Option<String>,
subtitle: Option<String>, subtitle: Option<String>,
@ -36,7 +29,7 @@ struct SearchQuery {
macro_rules! param_to_query { macro_rules! param_to_query {
( $query:ident, $parsed_query:ident; normal: $($field:ident),*; date: $($date:ident),*) => { ( $query:ident, $parsed_query:ident; normal: $($field:ident),*; date: $($date:ident),*) => {
$( $(
if let Some(field) = $query.$field { if let Some(ref field) = $query.$field {
let mut rest = field.as_str(); let mut rest = field.as_str();
while !rest.is_empty() { while !rest.is_empty() {
let (token, r) = Query::get_first_token(rest); let (token, r) = Query::get_first_token(rest);
@ -46,7 +39,7 @@ macro_rules! param_to_query {
} }
)* )*
$( $(
if let Some(field) = $query.$date { if let Some(ref field) = $query.$date {
let mut rest = field.as_str(); let mut rest = field.as_str();
while !rest.is_empty() { while !rest.is_empty() {
use chrono::naive::NaiveDate; use chrono::naive::NaiveDate;
@ -62,23 +55,31 @@ macro_rules! param_to_query {
} }
#[get("/search?<query>")] #[get("/search?<query..>")]
fn query(query: SearchQuery, conn: DbConn, searcher: Searcher, user: Option<User>) -> Template { pub fn search(query: Form<SearchQuery>, conn: DbConn, searcher: Searcher, user: Option<User>, intl: I18n) -> Ructe {
let page = query.page.unwrap_or(Page::first()); let page = query.page.unwrap_or(Page::first());
let mut parsed_query = Query::from_str(&query.q.unwrap_or_default()); let mut parsed_query = Query::from_str(&query.q.as_ref().map(|q| q.as_str()).unwrap_or_default());
param_to_query!(query, parsed_query; normal: title, subtitle, content, tag, param_to_query!(query, parsed_query; normal: title, subtitle, content, tag,
instance, author, blog, lang, license; instance, author, blog, lang, license;
date: before, after); date: before, after);
let str_q = parsed_query.to_string(); let str_query = parsed_query.to_string();
let res = searcher.search_document(&conn, parsed_query, page.limits());
Template::render("search/result", json!({ if str_query.is_empty() {
"query":str_q, render!(search::index(
"account": user.map(|u| u.to_json(&*conn)), &(&*conn, &intl.catalog, user),
"next_page": if res.is_empty() { 0 } else { page.page+1 }, &format!("{}", Utc::today().format("%Y-%m-d"))
"posts": res.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), ))
"page": page.page, } else {
})) let res = searcher.search_document(&conn, parsed_query, page.limits());
let next_page = if res.is_empty() { 0 } else { page.0+1 };
render!(search::result(
&(&*conn, &intl.catalog, user),
&str_query,
res,
page.0,
next_page
))
}
} }

View File

@ -3,10 +3,11 @@ use rocket::{
response::Redirect, response::Redirect,
request::{LenientForm,FlashMessage} request::{LenientForm,FlashMessage}
}; };
use rocket_contrib::Template;
use rocket::http::ext::IntoOwned; use rocket::http::ext::IntoOwned;
use rocket_i18n::I18n;
use std::borrow::Cow; use std::borrow::Cow;
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
use template_utils::Ructe;
use plume_models::{ use plume_models::{
db_conn::DbConn, db_conn::DbConn,
@ -14,48 +15,43 @@ use plume_models::{
}; };
#[get("/login")] #[get("/login")]
fn new(user: Option<User>, conn: DbConn) -> Template { pub fn new(user: Option<User>, conn: DbConn, intl: I18n) -> Ructe {
Template::render("session/login", json!({ render!(session::login(
"account": user.map(|u| u.to_json(&*conn)), &(&*conn, &intl.catalog, user),
"errors": null, None,
"form": null &LoginForm::default(),
})) ValidationErrors::default()
))
} }
#[derive(FromForm)] #[get("/login?<m>")]
struct Message { pub fn new_message(user: Option<User>, m: String, conn: DbConn, intl: I18n) -> Ructe {
m: String render!(session::login(
} &(&*conn, &intl.catalog, user),
Some(i18n!(intl.catalog, &m).to_string()),
#[get("/login?<message>")] &LoginForm::default(),
fn new_message(user: Option<User>, message: Message, conn: DbConn) -> Template { ValidationErrors::default()
Template::render("session/login", json!({ ))
"account": user.map(|u| u.to_json(&*conn)),
"message": message.m,
"errors": null,
"form": null
}))
} }
#[derive(FromForm, Validate, Serialize)] #[derive(Default, FromForm, Validate, Serialize)]
struct LoginForm { pub struct LoginForm {
#[validate(length(min = "1", message = "We need an email or a username to identify you"))] #[validate(length(min = "1", message = "We need an email or a username to identify you"))]
email_or_name: String, pub email_or_name: String,
#[validate(length(min = "1", message = "Your password can't be empty"))] #[validate(length(min = "1", message = "Your password can't be empty"))]
password: String pub password: String
} }
#[post("/login", data = "<data>")] #[post("/login", data = "<form>")]
fn create(conn: DbConn, data: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies) -> Result<Redirect, Template> { pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies, intl: I18n) -> Result<Redirect, Ructe> {
let form = data.get();
let user = User::find_by_email(&*conn, &form.email_or_name) let user = User::find_by_email(&*conn, &form.email_or_name)
.or_else(|| User::find_local(&*conn, &form.email_or_name)); .or_else(|| User::find_local(&*conn, &form.email_or_name));
let mut errors = match form.validate() { let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e Err(e) => e
}; };
if let Some(user) = user.clone() { if let Some(user) = user.clone() {
if !user.auth(&form.password) { if !user.auth(&form.password) {
let mut err = ValidationError::new("invalid_login"); let mut err = ValidationError::new("invalid_login");
@ -87,27 +83,26 @@ fn create(conn: DbConn, data: LenientForm<LoginForm>, flash: Option<FlashMessage
let uri = Uri::parse(&destination) let uri = Uri::parse(&destination)
.map(|x| x.into_owned()) .map(|x| x.into_owned())
.map_err(|_| { .map_err(|_| render!(session::login(
Template::render("session/login", json!({ &(&*conn, &intl.catalog, None),
"account": null, None,
"errors": errors.inner(), &*form,
"form": form errors
})) )))?;
})?;
Ok(Redirect::to(uri)) Ok(Redirect::to(uri))
} else { } else {
println!("{:?}", errors); Err(render!(session::login(
Err(Template::render("session/login", json!({ &(&*conn, &intl.catalog, None),
"account": null, None,
"errors": errors.inner(), &*form,
"form": form errors
}))) )))
} }
} }
#[get("/logout")] #[get("/logout")]
fn delete(mut cookies: Cookies) -> Redirect { pub fn delete(mut cookies: Cookies) -> Redirect {
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) { if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
cookies.remove_private(cookie); cookies.remove_private(cookie);
} }

View File

@ -1,5 +1,4 @@
use rocket_contrib::Template; use rocket_i18n::I18n;
use serde_json;
use plume_models::{ use plume_models::{
db_conn::DbConn, db_conn::DbConn,
@ -7,20 +6,21 @@ use plume_models::{
users::User, users::User,
}; };
use routes::Page; use routes::Page;
use template_utils::Ructe;
#[get("/tag/<name>")] #[get("/tag/<name>")]
fn tag(user: Option<User>, conn: DbConn, name: String) -> Template { pub fn tag(user: Option<User>, conn: DbConn, name: String, intl: I18n) -> Ructe {
paginated_tag(user, conn, name, Page::first()) paginated_tag(user, conn, name, Page::first(), intl)
} }
#[get("/tag/<name>?<page>")] #[get("/tag/<name>?<page>")]
fn paginated_tag(user: Option<User>, conn: DbConn, name: String, page: Page) -> Template { pub fn paginated_tag(user: Option<User>, conn: DbConn, name: String, page: Page, intl: I18n) -> Ructe {
let posts = Post::list_by_tag(&*conn, name.clone(), page.limits()); let posts = Post::list_by_tag(&*conn, name.clone(), page.limits());
Template::render("tags/index", json!({ render!(tags::index(
"tag": name.clone(), &(&*conn, &intl.catalog, user),
"account": user.map(|u| u.to_json(&*conn)), name.clone(),
"articles": posts.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), posts,
"page": page.page, page.0,
"n_pages": Page::total(Post::count_for_tag(&*conn, name) as i32) Page::total(Post::count_for_tag(&*conn, name) as i32)
})) ))
} }

View File

@ -5,9 +5,9 @@ use rocket::{
request::LenientForm, request::LenientForm,
response::{status, Content, Flash, Redirect}, response::{status, Content, Flash, Redirect},
}; };
use rocket_contrib::Template; use rocket_i18n::I18n;
use serde_json; use serde_json;
use validator::{Validate, ValidationError}; use validator::{Validate, ValidationError, ValidationErrors};
use inbox::Inbox; use inbox::Inbox;
use plume_common::activity_pub::{ use plume_common::activity_pub::{
@ -22,11 +22,12 @@ use plume_models::{
reshares::Reshare, users::*, reshares::Reshare, users::*,
}; };
use routes::Page; use routes::Page;
use template_utils::Ructe;
use Worker; use Worker;
use Searcher; use Searcher;
#[get("/me")] #[get("/me")]
fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> { pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
match user { match user {
Some(user) => Ok(Redirect::to(uri!(details: name = user.username))), Some(user) => Ok(Redirect::to(uri!(details: name = user.username))),
None => Err(utils::requires_login("", uri!(me))), None => Err(utils::requires_login("", uri!(me))),
@ -34,7 +35,7 @@ fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
} }
#[get("/@/<name>", rank = 2)] #[get("/@/<name>", rank = 2)]
fn details( pub fn details(
name: String, name: String,
conn: DbConn, conn: DbConn,
account: Option<User>, account: Option<User>,
@ -42,111 +43,96 @@ fn details(
fetch_articles_conn: DbConn, fetch_articles_conn: DbConn,
fetch_followers_conn: DbConn, fetch_followers_conn: DbConn,
update_conn: DbConn, update_conn: DbConn,
intl: I18n,
searcher: Searcher, searcher: Searcher,
) -> Template { ) -> Result<Ructe, Ructe> {
may_fail!( let user = User::find_by_fqn(&*conn, &name).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, account.clone()))))?;
account.map(|a| a.to_json(&*conn)), let recents = Post::get_recents_for_author(&*conn, &user, 6);
User::find_by_fqn(&*conn, &name), let reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
"Couldn't find requested user",
|user| {
let recents = Post::get_recents_for_author(&*conn, &user, 6);
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
let user_id = user.id;
let n_followers = user.get_followers(&*conn).len();
if !user.get_instance(&*conn).local { if !user.get_instance(&*conn).local {
// Fetch new articles // Fetch new articles
let user_clone = user.clone(); let user_clone = user.clone();
let searcher = searcher.clone(); let searcher = searcher.clone();
worker.execute(move || { worker.execute(move || {
for create_act in user_clone.fetch_outbox::<Create>() { for create_act in user_clone.fetch_outbox::<Create>() {
match create_act.create_props.object_object::<Article>() { match create_act.create_props.object_object::<Article>() {
Ok(article) => { Ok(article) => {
Post::from_activity( Post::from_activity(
&(&fetch_articles_conn, &searcher), &(&*fetch_articles_conn, &searcher),
article, article,
user_clone.clone().into_id(), user_clone.clone().into_id(),
);
println!("Fetched article from remote user");
}
Err(e) => {
println!("Error while fetching articles in background: {:?}", e)
}
}
}
});
// Fetch followers
let user_clone = user.clone();
worker.execute(move || {
for user_id in user_clone.fetch_followers_ids() {
let follower =
User::find_by_ap_url(&*fetch_followers_conn, &user_id)
.unwrap_or_else(|| {
User::fetch_from_url(&*fetch_followers_conn, &user_id)
.expect("user::details: Couldn't fetch follower")
});
follows::Follow::insert(
&*fetch_followers_conn,
follows::NewFollow {
follower_id: follower.id,
following_id: user_clone.id,
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
},
); );
println!("Fetched article from remote user");
}
Err(e) => {
println!("Error while fetching articles in background: {:?}", e)
} }
});
// Update profile information if needed
let user_clone = user.clone();
if user.needs_update() {
worker.execute(move || {
user_clone.refetch(&*update_conn);
});
} }
} }
});
Template::render( // Fetch followers
"users/details", let user_clone = user.clone();
json!({ worker.execute(move || {
"user": user.to_json(&*conn), for user_id in user_clone.fetch_followers_ids() {
"instance_url": user.get_instance(&*conn).public_domain, let follower =
"is_remote": user.instance_id != Instance::local_id(&*conn), User::find_by_ap_url(&*fetch_followers_conn, &user_id)
"follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), .unwrap_or_else(|| {
"account": account.clone().map(|a| a.to_json(&*conn)), User::fetch_from_url(&*fetch_followers_conn, &user_id)
"recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), .expect("user::details: Couldn't fetch follower")
"reshares": reshares.into_iter().map(|r| r.get_post(&*conn).unwrap().to_json(&*conn)).collect::<Vec<serde_json::Value>>(), });
"is_self": account.map(|a| a.id == user_id).unwrap_or(false), follows::Follow::insert(
"n_followers": n_followers &*fetch_followers_conn,
}), follows::NewFollow {
) follower_id: follower.id,
following_id: user_clone.id,
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
},
);
}
});
// Update profile information if needed
let user_clone = user.clone();
if user.needs_update() {
worker.execute(move || {
user_clone.refetch(&*update_conn);
});
} }
) }
Ok(render!(users::details(
&(&*conn, &intl.catalog, account.clone()),
user.clone(),
account.map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
user.instance_id != Instance::local_id(&*conn),
user.get_instance(&*conn).public_domain,
recents,
reshares.into_iter().map(|r| r.get_post(&*conn).expect("user::details: Reshared post error")).collect()
)))
} }
#[get("/dashboard")] #[get("/dashboard")]
fn dashboard(user: User, conn: DbConn) -> Template { pub fn dashboard(user: User, conn: DbConn, intl: I18n) -> Ructe {
let blogs = Blog::find_for_author(&*conn, &user); let blogs = Blog::find_for_author(&*conn, &user);
Template::render( render!(users::dashboard(
"users/dashboard", &(&*conn, &intl.catalog, Some(user.clone())),
json!({ blogs,
"account": user.to_json(&*conn), Post::drafts_by_author(&*conn, &user)
"blogs": blogs, ))
"drafts": Post::drafts_by_author(&*conn, &user).into_iter().map(|a| a.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
}),
)
} }
#[get("/dashboard", rank = 2)] #[get("/dashboard", rank = 2)]
fn dashboard_auth() -> Flash<Redirect> { pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
"You need to be logged in order to access your dashboard", i18n!(i18n.catalog, "You need to be logged in order to access your dashboard"),
uri!(dashboard), uri!(dashboard),
) )
} }
#[post("/@/<name>/follow")] #[post("/@/<name>/follow")]
fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redirect> { pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redirect> {
let target = User::find_by_fqn(&*conn, &name)?; let target = User::find_by_fqn(&*conn, &name)?;
if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) { if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) {
let delete_act = follow.delete(&*conn); let delete_act = follow.delete(&*conn);
@ -171,49 +157,37 @@ fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redi
} }
#[post("/@/<name>/follow", rank = 2)] #[post("/@/<name>/follow", rank = 2)]
fn follow_auth(name: String) -> Flash<Redirect> { pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
"You need to be logged in order to follow someone", i18n!(i18n.catalog, "You need to be logged in order to follow someone"),
uri!(follow: name = name), uri!(follow: name = name),
) )
} }
#[get("/@/<name>/followers?<page>")] #[get("/@/<name>/followers?<page>")]
fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page: Page) -> Template { pub fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page: Page, intl: I18n) -> Result<Ructe, Ructe> {
may_fail!( let user = User::find_by_fqn(&*conn, &name).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, account.clone()))))?;
account.map(|a| a.to_json(&*conn)), let followers_count = user.get_followers(&*conn).len(); // TODO: count in DB
User::find_by_fqn(&*conn, &name),
"Couldn't find requested user",
|user| {
let user_id = user.id;
let followers_count = user.get_followers(&*conn).len();
Template::render( Ok(render!(users::followers(
"users/followers", &(&*conn, &intl.catalog, account.clone()),
json!({ user.clone(),
"user": user.to_json(&*conn), account.map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
"instance_url": user.get_instance(&*conn).public_domain, user.instance_id != Instance::local_id(&*conn),
"is_remote": user.instance_id != Instance::local_id(&*conn), user.get_instance(&*conn).public_domain,
"follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), user.get_followers_page(&*conn, page.limits()),
"followers": user.get_followers_page(&*conn, page.limits()).into_iter().map(|f| f.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), page.0,
"account": account.clone().map(|a| a.to_json(&*conn)), Page::total(followers_count as i32)
"is_self": account.map(|a| a.id == user_id).unwrap_or(false), )))
"n_followers": followers_count,
"page": page.page,
"n_pages": Page::total(followers_count as i32)
}),
)
}
)
} }
#[get("/@/<name>/followers", rank = 2)] #[get("/@/<name>/followers", rank = 2)]
fn followers(name: String, conn: DbConn, account: Option<User>) -> Template { pub fn followers(name: String, conn: DbConn, account: Option<User>, intl: I18n) -> Result<Ructe, Ructe> {
followers_paginated(name, conn, account, Page::first()) followers_paginated(name, conn, account, Page::first(), intl)
} }
#[get("/@/<name>", rank = 1)] #[get("/@/<name>", rank = 1)]
fn activity_details( pub fn activity_details(
name: String, name: String,
conn: DbConn, conn: DbConn,
_ap: ApRequest, _ap: ApRequest,
@ -223,71 +197,60 @@ fn activity_details(
} }
#[get("/users/new")] #[get("/users/new")]
fn new(user: Option<User>, conn: DbConn) -> Template { pub fn new(user: Option<User>, conn: DbConn, intl: I18n) -> Ructe {
Template::render( render!(users::new(
"users/new", &(&*conn, &intl.catalog, user),
json!({ Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true), &NewUserForm::default(),
"account": user.map(|u| u.to_json(&*conn)), ValidationErrors::default()
"errors": null, ))
"form": null
}),
)
} }
#[get("/@/<name>/edit")] #[get("/@/<name>/edit")]
fn edit(name: String, user: User, conn: DbConn) -> Option<Template> { pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> {
if user.username == name && !name.contains('@') { if user.username == name && !name.contains('@') {
Some(Template::render( Some(render!(users::edit(
"users/edit", &(&*conn, &intl.catalog, Some(user.clone())),
json!({ UpdateUserForm {
"account": user.to_json(&*conn) display_name: user.display_name.clone(),
}), email: user.email.clone().unwrap_or_default(),
)) summary: user.summary.to_string(),
},
ValidationErrors::default()
)))
} else { } else {
None None
} }
} }
#[get("/@/<name>/edit", rank = 2)] #[get("/@/<name>/edit", rank = 2)]
fn edit_auth(name: String) -> Flash<Redirect> { pub fn edit_auth(name: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
"You need to be logged in order to edit your profile", i18n!(i18n.catalog, "You need to be logged in order to edit your profile"),
uri!(edit: name = name), uri!(edit: name = name),
) )
} }
#[derive(FromForm)] #[derive(FromForm)]
struct UpdateUserForm { pub struct UpdateUserForm {
display_name: Option<String>, pub display_name: String,
email: Option<String>, pub email: String,
summary: Option<String>, pub summary: String,
} }
#[put("/@/<_name>/edit", data = "<data>")] #[put("/@/<_name>/edit", data = "<form>")]
fn update(_name: String, conn: DbConn, user: User, data: LenientForm<UpdateUserForm>) -> Redirect { pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm<UpdateUserForm>) -> Redirect {
user.update( user.update(
&*conn, &*conn,
data.get() if !form.display_name.is_empty() { form.display_name.clone() } else { user.display_name.clone() },
.display_name if !form.email.is_empty() { form.email.clone() } else { user.email.clone().unwrap_or_default() },
.clone() if !form.summary.is_empty() { form.summary.clone() } else { user.summary.to_string() },
.unwrap_or_else(|| user.display_name.to_string())
.to_string(),
data.get()
.email
.clone()
.unwrap_or_else(|| user.email.clone().unwrap())
.to_string(),
data.get()
.summary
.clone()
.unwrap_or_else(|| user.summary.to_string()),
); );
Redirect::to(uri!(me)) Redirect::to(uri!(me))
} }
#[post("/@/<name>/delete")] #[post("/@/<name>/delete")]
fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Option<Redirect> { pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Option<Redirect> {
let account = User::find_by_fqn(&*conn, &name)?; let account = User::find_by_fqn(&*conn, &name)?;
if user.id == account.id { if user.id == account.id {
account.delete(&*conn, &searcher); account.delete(&*conn, &searcher);
@ -302,7 +265,7 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher
} }
} }
#[derive(FromForm, Serialize, Validate)] #[derive(Default, FromForm, Serialize, Validate)]
#[validate( #[validate(
schema( schema(
function = "passwords_match", function = "passwords_match",
@ -310,29 +273,29 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher
message = "Passwords are not matching" message = "Passwords are not matching"
) )
)] )]
struct NewUserForm { pub struct NewUserForm {
#[validate(length(min = "1", message = "Username can't be empty"), #[validate(length(min = "1", message = "Username can't be empty"),
custom( function = "validate_username", message = "User name is not allowed to contain any of < > & @ ' or \""))] custom( function = "validate_username", message = "User name is not allowed to contain any of < > & @ ' or \""))]
username: String, pub username: String,
#[validate(email(message = "Invalid email"))] #[validate(email(message = "Invalid email"))]
email: String, pub email: String,
#[validate( #[validate(
length( length(
min = "8", min = "8",
message = "Password should be at least 8 characters long" message = "Password should be at least 8 characters long"
) )
)] )]
password: String, pub password: String,
#[validate( #[validate(
length( length(
min = "8", min = "8",
message = "Password should be at least 8 characters long" message = "Password should be at least 8 characters long"
) )
)] )]
password_confirmation: String, pub password_confirmation: String,
} }
fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> { pub fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
if form.password != form.password_confirmation { if form.password != form.password_confirmation {
Err(ValidationError::new("password_match")) Err(ValidationError::new("password_match"))
} else { } else {
@ -340,7 +303,7 @@ fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
} }
} }
fn validate_username(username: &str) -> Result<(), ValidationError> { pub fn validate_username(username: &str) -> Result<(), ValidationError> {
if username.contains(&['<', '>', '&', '@', '\'', '"'][..]) { if username.contains(&['<', '>', '&', '@', '\'', '"'][..]) {
Err(ValidationError::new("username_illegal_char")) Err(ValidationError::new("username_illegal_char"))
} else { } else {
@ -348,16 +311,15 @@ fn validate_username(username: &str) -> Result<(), ValidationError> {
} }
} }
#[post("/users/new", data = "<data>")] #[post("/users/new", data = "<form>")]
fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Template> { pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Result<Redirect, Ructe> {
if !Instance::get_local(&*conn) if !Instance::get_local(&*conn)
.map(|i| i.open_registrations) .map(|i| i.open_registrations)
.unwrap_or(true) .unwrap_or(true)
{ {
return Ok(Redirect::to(uri!(new))); // Actually, it is an error return Ok(Redirect::to(uri!(new))); // Actually, it is an error
} }
let form = data.get();
form.validate() form.validate()
.map(|_| { .map(|_| {
NewUser::new_local( NewUser::new_local(
@ -371,26 +333,24 @@ fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Temp
).update_boxes(&*conn); ).update_boxes(&*conn);
Redirect::to(uri!(super::session::new)) Redirect::to(uri!(super::session::new))
}) })
.map_err(|e| { .map_err(|err| {
Template::render( render!(users::new(
"users/new", &(&*conn, &intl.catalog, None),
json!({ Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true), &*form,
"errors": e.inner(), err
"form": form ))
}),
)
}) })
} }
#[get("/@/<name>/outbox")] #[get("/@/<name>/outbox")]
fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> { pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_local(&*conn, &name)?; let user = User::find_local(&*conn, &name)?;
Some(user.outbox(&*conn)) Some(user.outbox(&*conn))
} }
#[post("/@/<name>/inbox", data = "<data>")] #[post("/@/<name>/inbox", data = "<data>")]
fn inbox( pub fn inbox(
name: String, name: String,
conn: DbConn, conn: DbConn,
data: String, data: String,
@ -433,7 +393,7 @@ fn inbox(
} }
#[get("/@/<name>/followers")] #[get("/@/<name>/followers")]
fn ap_followers( pub fn ap_followers(
name: String, name: String,
conn: DbConn, conn: DbConn,
_ap: ApRequest, _ap: ApRequest,
@ -459,7 +419,7 @@ fn ap_followers(
} }
#[get("/@/<name>/atom.xml")] #[get("/@/<name>/atom.xml")]
fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> { pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
let author = User::find_by_fqn(&*conn, &name)?; let author = User::find_by_fqn(&*conn, &name)?;
let feed = FeedBuilder::default() let feed = FeedBuilder::default()
.title(author.display_name.clone()) .title(author.display_name.clone())

View File

@ -6,7 +6,7 @@ use webfinger::*;
use plume_models::{BASE_URL, ap_url, db_conn::DbConn, blogs::Blog, users::User}; use plume_models::{BASE_URL, ap_url, db_conn::DbConn, blogs::Blog, users::User};
#[get("/.well-known/nodeinfo")] #[get("/.well-known/nodeinfo")]
fn nodeinfo() -> Content<String> { pub fn nodeinfo() -> Content<String> {
Content(ContentType::new("application", "jrd+json"), json!({ Content(ContentType::new("application", "jrd+json"), json!({
"links": [ "links": [
{ {
@ -18,7 +18,7 @@ fn nodeinfo() -> Content<String> {
} }
#[get("/.well-known/host-meta")] #[get("/.well-known/host-meta")]
fn host_meta() -> String { pub fn host_meta() -> String {
format!(r#" format!(r#"
<?xml version="1.0"?> <?xml version="1.0"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
@ -27,11 +27,6 @@ fn host_meta() -> String {
"#, url = ap_url(&format!("{domain}/.well-known/webfinger?resource={{uri}}", domain = BASE_URL.as_str()))) "#, url = ap_url(&format!("{domain}/.well-known/webfinger?resource={{uri}}", domain = BASE_URL.as_str())))
} }
#[derive(FromForm)]
struct WebfingerQuery {
resource: String
}
struct WebfingerResolver; struct WebfingerResolver;
impl Resolver<DbConn> for WebfingerResolver { impl Resolver<DbConn> for WebfingerResolver {
@ -50,9 +45,9 @@ impl Resolver<DbConn> for WebfingerResolver {
} }
} }
#[get("/.well-known/webfinger?<query>")] #[get("/.well-known/webfinger?<resource>")]
fn webfinger(query: WebfingerQuery, conn: DbConn) -> Content<String> { pub fn webfinger(resource: String, conn: DbConn) -> Content<String> {
match WebfingerResolver::endpoint(query.resource, conn).and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound)) { match WebfingerResolver::endpoint(resource, conn).and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound)) {
Ok(wf) => Content(ContentType::new("application", "jrd+json"), wf), Ok(wf) => Content(ContentType::new("application", "jrd+json"), wf),
Err(err) => Content(ContentType::new("text", "plain"), String::from(match err { Err(err) => Content(ContentType::new("text", "plain"), String::from(match err {
ResolverError::InvalidResource => "Invalid resource. Make sure to request an acct: URI", ResolverError::InvalidResource => "Invalid resource. Make sure to request an acct: URI",

160
src/template_utils.rs Normal file
View File

@ -0,0 +1,160 @@
use plume_models::{Connection, users::User};
use rocket::response::Content;
use rocket_i18n::Catalog;
use templates::Html;
pub use askama_escape::escape;
pub type BaseContext<'a> = &'a(&'a Connection, &'a Catalog, Option<User>);
pub type Ructe = Content<Vec<u8>>;
#[macro_export]
macro_rules! render {
($group:tt :: $page:tt ( $( $param:expr ),* ) ) => {
{
use rocket::{http::ContentType, response::Content};
use templates;
let mut res = vec![];
templates::$group::$page(
&mut res,
$(
$param
),*
).unwrap();
Content(ContentType::HTML, res)
}
}
}
pub enum Size {
Small,
Medium,
}
impl Size {
fn as_str(&self) -> &'static str {
match self {
Size::Small => "small",
Size::Medium => "medium",
}
}
}
pub fn avatar(conn: &Connection, user: &User, size: Size, pad: bool, catalog: &Catalog) -> Html<String> {
let name = escape(&user.name(conn)).to_string();
Html(format!(
r#"<div
class="avatar {size} {padded}"
style="background-image: url('{url}');"
title="{title}"
aria-label="{title}"
></div>"#,
size = size.as_str(),
padded = if pad { "padded" } else { "" },
url = user.avatar_url(conn),
title = i18n!(catalog, "{0}'s avatar"; name),
))
}
pub fn tabs(links: &[(&str, &str, bool)]) -> Html<String> {
let mut res = String::from(r#"<div class="tabs">"#);
for (url, title, selected) in links {
res.push_str(r#"<a href=""#);
res.push_str(url);
if *selected {
res.push_str(r#"" class="selected">"#);
} else {
res.push_str("\">");
}
res.push_str(title);
res.push_str("</a>");
}
res.push_str("</div>");
Html(res)
}
pub fn paginate(catalog: &Catalog, page: i32, total: i32) -> Html<String> {
let mut res = String::new();
res.push_str(r#"<div class="pagination">"#);
if page != 1 {
res.push_str(format!(r#"<a href="?page={}">{}</a>"#, page - 1, catalog.gettext("Previous page")).as_str());
}
if page < total {
res.push_str(format!(r#"<a href="?page={}">{}</a>"#, page + 1, catalog.gettext("Next page")).as_str());
}
res.push_str("</div>");
Html(res)
}
#[macro_export]
macro_rules! icon {
($name:expr) => {
Html(concat!(r#"<svg class="feather"><use xlink:href="/static/images/feather-sprite.svg#"#, $name, "\"/></svg>"))
}
}
macro_rules! input {
($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
{
use validator::ValidationErrorsKind;
use std::borrow::Cow;
Html(format!(r#"
<label for="{name}">
{label}
{optional}
{details}
</label>
{error}
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props}/>
"#,
name = stringify!($name),
label = i18n!($catalog, $label),
kind = stringify!($kind),
optional = if $optional { format!("<small>{}</small>", i18n!($catalog, "Optional")) } else { String::new() },
details = if $details.len() > 0 {
format!("<small>{}</small>", i18n!($catalog, $details))
} else {
String::new()
},
error = if let Some(ValidationErrorsKind::Field(errs)) = $err.errors().get(stringify!($name)) {
format!(r#"<p class="error">{}</p>"#, i18n!($catalog, &*errs[0].message.clone().unwrap_or(Cow::from("Unknown error"))))
} else {
String::new()
},
val = escape(&$form.$name),
props = $props
))
}
};
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, true, $details, $form, $err, $props)
};
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, true, "", $form, $err, $props)
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, false, $details, $form, $err, $props)
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, false, "", $form, $err, $props)
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr) => {
input!($catalog, $name ($kind), $label, false, "", $form, $err, "")
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => {
{
Html(format!(r#"
<label for="{name}">{label}</label>
<input type="{kind}" id="{name}" name="{name}" {props}/>
"#,
name = stringify!($name),
label = i18n!($catalog, $label),
kind = stringify!($kind),
props = $props
))
}
};
}

View File

@ -328,7 +328,7 @@ main .article-meta .tags li a {
main .article-meta .reshares .action:hover { color: #7765E3; } main .article-meta .reshares .action:hover { color: #7765E3; }
main .article-meta .likes .action svg.feather, main .article-meta .likes .action svg.feather,
main .article-meta .reshares .action i { main .article-meta .reshares .action svg.feather {
transition: background 0.1s ease-in; transition: background 0.1s ease-in;
display: flex; display: flex;
align-items: center; align-items: center;
@ -352,12 +352,12 @@ main .article-meta .tags li a {
background: rgba(233, 47, 47, 0.15); background: rgba(233, 47, 47, 0.15);
} }
main .article-meta .reshares .action i { main .article-meta .reshares .action svg.feather {
color: #7765E3; color: #7765E3;
border: solid #7765E3 thin; border: solid #7765E3 thin;
font-weight: 600; font-weight: 600;
} }
main .article-meta .reshares .action:hover i { main .article-meta .reshares .action:hover svg.feather {
background: rgba(119, 101, 227, 0.15); background: rgba(119, 101, 227, 0.15);
} }
@ -366,14 +366,14 @@ main .article-meta .tags li a {
background: rgba(233, 47, 47, 0.25); background: rgba(233, 47, 47, 0.25);
color: #E92F2F; color: #E92F2F;
} }
main .article-meta .reshares .action.reshared i { background: #7765E3; } main .article-meta .reshares .action.reshared svg.feather { background: #7765E3; }
main .article-meta .reshares .action.reshared:hover i { main .article-meta .reshares .action.reshared:hover svg.feather {
background: rgba(119, 101, 227, 0.25); background: rgba(119, 101, 227, 0.25);
color: #7765E3; color: #7765E3;
} }
main .article-meta .likes .action.liked svg.feather, main .article-meta .likes .action.liked svg.feather,
main .article-meta .reshares .action.reshared i { main .article-meta .reshares .action.reshared svg.feather {
color: #F4F4F4; color: #F4F4F4;
font-weight: 900; font-weight: 900;
} }
@ -722,9 +722,14 @@ form.inline input[type="submit"]:not(.button) {
align-items: center; align-items: center;
} }
.stats p {
text-align: center;
}
.stats em { .stats em {
text-align: center;
font-weight: bold; font-weight: bold;
display: block;
margin: 1em 0;
} }
/*== Pagination ==*/ /*== Pagination ==*/

View File

@ -1,77 +1,75 @@
{% import "macros" as macros %} @use template_utils::*;
@(ctx: BaseContext, title: &str, head: Content, header: Content, content: Content)
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>{% block title %}{% endblock title %} ⋅ {{ "Plume" | _ }}</title> <title>@i18n!(ctx.1, title) ⋅ @i18n!(ctx.1, "Plume")</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/static/css/main.css" /> <link rel="stylesheet" href="/static/css/main.css" />
<link rel="stylesheet" href="/static/css/feather.css" /> <link rel="stylesheet" href="/static/css/feather.css" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/png" href="/static/icons/trwnh/feather-filled/plumeFeatherFilled64.png"> <link rel="icon" type="image/png" href="/static/icons/trwnh/feather-filled/plumeFeatherFilled64.png">
{% block head %}{% endblock head %} @:head()
</head> </head>
<body> <body>
<header> <header>
<nav id="menu"> <nav id="menu">
<a href="#" aria-label="{{ "Menu" | _ }}" title="{{ "Menu" | _ }}"><i class="icon icon-menu"></i></a> <a href="#" aria-label="@i18n!(ctx.1, "Menu")" title="@i18n!(ctx.1, "Menu")"><i class="icon icon-menu"></i></a>
</nav> </nav>
<div id="content"> <div id="content">
<nav> <nav>
<a href="/" class="title"> <a href="/" class="title">
<img src="/static/icons/trwnh/feather/plumeFeather256.png"> <img src="/static/icons/trwnh/feather/plumeFeather256.png">
<p>{{ "Plume" | _ }}</p> <p>@i18n!(ctx.1, "Plume")</p>
</a> </a>
<hr/> <hr/>
{% block header %} @:header()
{% endblock header %}
</nav> </nav>
<nav> <nav>
{% if account %} @if ctx.2.is_some() {
<a href="/dashboard"> <a href="/dashboard">
<i class="icon icon-home" aria-label="{{ "Dashboard" | _ }}"></i> <i class="icon icon-home" aria-label="@i18n!(ctx.1, "Dashboard")"></i>
<span class="mobile-label">{{ "Dashboard" | _ }}</span> <span class="mobile-label">@i18n!(ctx.1, "Dashboard")</span>
</a> </a>
<a href="/notifications"> <a href="/notifications">
<i class="icon icon-bell" aria-label="{{ "Notifications" | _ }}"></i> <i class="icon icon-bell" aria-label="@i18n!(ctx.1, "Notifications")"></i>
<span class="mobile-label">{{ "Notifications" | _ }}</span> <span class="mobile-label">@i18n!(ctx.1, "Notifications")</span>
</a> </a>
<a href="/logout"> <a href="/logout">
<i class="icon icon-log-out" aria-label="{{ "Log Out" | _ }}"></i> <i class="icon icon-log-out" aria-label="@i18n!(ctx.1, "Log Out")"></i>
<span class="mobile-label">{{ "Log Out" | _ }}</span> <span class="mobile-label">@i18n!(ctx.1, "Log Out")</span>
</a> </a>
<a href="/me" title="{{ "My account" | _ }}"> <a href="/me" title="@i18n!(ctx.1, "My account")">
{{ macros::avatar(user=account) }} @avatar(ctx.0, &ctx.2.clone().unwrap(), Size::Small, false, &ctx.1)
<span class="mobile-label">{{ "My account" | _ }}</span> <span class="mobile-label">@i18n!(ctx.1, "My account")</span>
</a> </a>
{% else %} } else {
<a href="/login"> <a href="/login">
<i class="icon icon-log-in"></i> <i class="icon icon-log-in"></i>
<span class="mobile-label">{{ "Log In" | _ }}</span> <span class="mobile-label">@i18n!(ctx.1, "Log In")</span>
</a> </a>
<a href="/users/new"> <a href="/users/new">
<i class="icon icon-user-plus"></i> <i class="icon icon-user-plus"></i>
<span class="mobile-label">{{ "Register" | _ }}</span> <span class="mobile-label">@i18n!(ctx.1, "Register")</span>
</a> </a>
{% endif %} }
</nav> </nav>
</div> </div>
</header> </header>
<main> <main>
{% block content %} @:content()
{% endblock content %}
</main> </main>
<footer> <footer>
<span>Plume 0.2.0</span> <span>@concat!("Plume ", env!("CARGO_PKG_VERSION"))</span>
<a href="/about">{{ "About this instance" | _ }}</a> <a href="/about">@i18n!(ctx.1, "About this instance")</a>
<a href="https://github.com/Plume-org/Plume">{{ "Source code" | _ }}</a> <a href="https://github.com/Plume-org/Plume">@i18n!(ctx.1, "Source code")</a>
<a href="https://riot.im/app/#/room/#plume:disroot.org">{{ "Matrix room" | _ }}</a> <a href="https://riot.im/app/#/room/#plume:disroot.org">@i18n!(ctx.1, "Matrix room")</a>
{% if account %} @if ctx.2.clone().map(|a| a.is_admin).unwrap_or(false) {
{% if account.is_admin %} <a href="/admin">@i18n!(ctx.1, "Administration")</a>
<a href="/admin">{{ "Administration" | _ }}</a> }
{% endif %}
{% endif %}
</footer> </footer>
<script src="/static/js/menu.js"></script> <script src="/static/js/menu.js"></script>
</body> </body>

View File

@ -1,50 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ blog.title }}
{% endblock title %}
{% block header %}
<a href="/~/{{ blog.fqn }}">{{ blog.title }}</a>
{% endblock header %}
{% block content %}
<h1>{{ blog.title }} <small>~{{ blog.fqn }}</small></h1>
<p>{{ blog.summary }}</p>
<p>
{{ "{{ count }} authors in this blog: " | _n(singular="One author in this blog: ", count = n_authors) }}
{% for author in authors %}
<a class="author" href="/@/{{ author.fqn }}">{{ author.name }}</a>{% if not loop.last %},{% endif %}
{% endfor %}
</p>
<p>
{{ "{{ count }} articles in this blog" | _n(singular="One article in this blog", count = n_articles) }}
</p>
<section>
<h2>
{{ "Latest articles" | _ }}
<small><a href="/~/{{ blog.fqn }}/atom.xml" title="Atom feed">{{ macros::feather(name="rss") }}</a></small>
</h2>
{% if posts | length < 1 %}
<p>{{ "No posts to see here yet." | _ }}</p>
{% endif %}
{% if is_author %}
<a href="/~/{{ blog.fqn }}/new/" class="button inline-block">{{ "New article" | _ }}</a>
{% endif %}
<div class="cards">
{% for article in posts %}
{{ macros::post_card(article=article) }}
{% endfor %}
</div>
{{ macros::paginate(page=page, total=n_pages) }}
</section>
{% if is_author %}
<h2>{{ "Danger zone" | _ }}</h2>
<p>{{ "Be very careful, any action taken here can't be cancelled." | _ }}
<form method="post" action="/~/{{ blog.fqn }}/delete">
<input type="submit" class="inline-block button destructive" value="{{ "Delete this blog" | _ }}">
</form>
{% endif %}
{% endblock content %}

View File

@ -0,0 +1,49 @@
@use plume_models::blogs::Blog;
@use plume_models::posts::Post;
@use plume_models::users::User;
@use templates::{base, partials::post_card};
@use template_utils::*;
@(ctx: BaseContext, blog: Blog, fqn: String, authors: &Vec<User>, total_articles: usize, page: i32, n_pages: i32, is_author: bool, posts: Vec<Post>)
@:base(ctx, blog.title.as_ref(), {}, {
<a href="/~/@fqn">@blog.title</a>
}, {
<h1>@blog.title <small>~@fqn</small></h1>
<p>@blog.summary</p>
<p>
@i18n!(ctx.1, "One author in this blog: ", "{0} authors in this blog: ", authors.len())
@for author in authors {
<a class="author" href="/@@/@author.get_fqn(ctx.0)">@author.name(ctx.0)</a>
}
</p>
<p>
@i18n!(ctx.1, "One article in this blog", "{0} articles in this blog", total_articles)
</p>
<section>
<h2>
@i18n!(ctx.1, "Latest articles")
<small><a href="/~/@fqn/atom.xml" title="Atom feed">@icon!("rss")</a></small>
</h2>
@if posts.len() < 1 {
<p>@i18n!(ctx.1, "No posts to see here yet.")</p>
}
@if is_author {
<a href="/~/@fqn/new/" class="button inline-block">@i18n!(ctx.1, "New article")</a>
}
<div class="cards">
@for article in posts {
@:post_card(ctx, article)
}
</div>
@paginate(ctx.1, page, n_pages)
</section>
@if is_author {
<h2>@i18n!(ctx.1, "Danger zone")</h2>
<p>@i18n!(ctx.1, "Be very careful, any action taken here can't be cancelled.")</p>
<form method="post" action="/~/@fqn/delete">
<input type="submit" class="inline-block button destructive" value="@i18n!(ctx.1, "Delete this blog")">
</form>
}
})

View File

@ -1,15 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "New blog" | _ }}
{% endblock title %}
{% block content %}
<h1>{{ "Create a blog" | _ }}</h1>
<form method="post">
{{ macros::input(name="title", label="Title", errors=errors, form=form, props='required minlength="1"') }}
<input type="submit" value="{{ "Create blog" | _ }}"/>
</form>
{% endblock content %}

View File

@ -0,0 +1,14 @@
@use validator::ValidationErrors;
@use templates::base;
@use template_utils::*;
@use routes::blogs::NewBlogForm;
@(ctx: BaseContext, form: &NewBlogForm, errors: ValidationErrors)
@:base(ctx, "New Blog", {}, {}, {
<h1>@i18n!(ctx.1, "Create a blog")</h1>
<form method="post">
@input!(ctx.1, title (text), "Title", form, errors, "required minlength=\"1\"")
<input type="submit" value="@i18n!(ctx.1, "Create blog")"/>
</form>
})

View File

@ -1,5 +0,0 @@
{% extends "errors/base" %}
{% block error %}
<h1>{{ "You are not authorized." | _ }}</h1>
{% endblock error %}

View File

@ -1,6 +0,0 @@
{% extends "errors/base" %}
{% block error %}
<h1>{{ "We couldn't find this page." | _ }}</h1>
<h2>{{ "The link that led you here may be broken." | _ }}</h2>
{% endblock error %}

View File

@ -1,6 +0,0 @@
{% extends "errors/base" %}
{% block error %}
<h1>{{ "Something broke on our side." | _ }}</h1>
<p>{{ "Sorry about that. If you think this is a bug, please report it." | _ }}</p>
{% endblock error %}

View File

@ -1,15 +0,0 @@
{% extends "base" %}
{% block title %}
{{ error_message }}
{% endblock title %}
{% block content %}
<main class="error">
{% block error %}
{% endblock error %}
<p>
{{ error_message }}
</p>
</main>
{% endblock content %}

View File

@ -0,0 +1,9 @@
@use templates::base as base_template;
@use template_utils::*;
@(ctx: BaseContext, error_message: &str, error: Content)
@:base_template(ctx, error_message, {}, {}, {
@:error()
<p>@error_message</p>
})

View File

@ -1,6 +0,0 @@
{% extends "errors/base" %}
{% block error %}
<h1>{{ "Invalid CSRF token." | _ }}</h1>
<p>{{ "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." | _ }}</p>
{% endblock error %}

View File

@ -0,0 +1,12 @@
@use templates::errors::base;
@use template_utils::*;
@(ctx: BaseContext)
@:base(ctx, "", {
<h1>@i18n!(ctx.1, "Invalid CSRF token.")</h1>
<p>@i18n!(ctx.1, r#"Something is wrong with your CSRF token.
Make sure cookies are enabled in you browser, and try reloading this page.
If you continue to see this error message, please report it."#)
</p>
})

View File

@ -0,0 +1,8 @@
@use templates::errors::base;
@use template_utils::*;
@(ctx: BaseContext, error_message: &str)
@:base(ctx, error_message, {
<h1>@i18n!(ctx.1, "You are not authorized.")</h1>
})

View File

@ -0,0 +1,10 @@
@use templates::errors::base;
@use template_utils::*;
@(ctx: BaseContext)
@:base(ctx, "Page not found", {
<h1>@i18n!(ctx.1, "We couldn't find this page.")</h1>
<p>@i18n!(ctx.1, "The link that led you here may be broken.")</p>
})

View File

@ -0,0 +1,9 @@
@use templates::errors::base;
@use template_utils::*;
@(ctx: BaseContext)
@:base(ctx, "Internal server error", {
<h1>@i18n!(ctx.1, "Something broke on our side.")</h1>
<p>@i18n!(ctx.1, "Sorry about that. If you think this is a bug, please report it.")</p>
})

View File

@ -1,41 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
About {{ instance.name }}
{% endblock title %}
{% block content %}
<h1>{{ "About {{ instance_name }}" | _(instance_name=instance.name) }}</h1>
<section>
{{ instance.short_description_html | safe }}
</section>
<div class="banner">
<section class="stats">
<div>
<p>{{ "Home to" | _ }}</p>
<em>{{ n_users }}</em>
<p>{{ "people" | _ }}</p>
</div>
<div>
<p>{{ "Who wrote" | _ }}</p>
<em>{{ n_articles }}</em>
<p>{{ "articles" | _ }}</p>
</div>
<div>
<p>{{ "And connected to" | _ }}</p>
<em>{{ n_instances }}</em>
<p>{{ "other instances" | _ }}</p>
</div>
<div>
<p>{{ "Administred by" | _ }}</p>
{{ macros::avatar(user=admin) }}
<p><a href="/@/{{ admin.fqn }}">{{ admin.name }}</a><small>(@{{ admin.fqn }})</small></p>
</div>
</section>
<p>{{ "Runs Plume {{ version }}" | _(version=version) }}
</div>
<section>
{{ instance.long_description_html | safe }}
</section>
{% endblock content %}

View File

@ -0,0 +1,34 @@
@use templates::base;
@use template_utils::*;
@use plume_models::{instance::Instance, users::User};
@(ctx: BaseContext, instance: Instance, admin: User, n_users: usize, n_articles: usize, n_instances: i64)
@:base(ctx, i18n!(ctx.1, "About {0}"; instance.name.clone()).as_str(), {}, {}, {
<h1>@i18n!(ctx.1, "About {0}"; instance.name)</h1>
<section>
@Html(instance.short_description_html)
</section>
<div class="banner">
<section class="stats">
<div>
<p>@Html(i18n!(ctx.1, "Home to <em>{0}</em> users"; n_users))</p>
</div>
<div>
<p>@Html(i18n!(ctx.1, "Who wrote <em>{0}</em> articles"; n_articles))</p>
</div>
<div>
<p>@Html(i18n!(ctx.1, "And connected to <em>{0}</em> other instances"; n_instances))</p>
</div>
<div>
<p>@i18n!(ctx.1, "Administred by")</p>
@avatar(ctx.0, &admin, Size::Small, false, ctx.1)
<p><a href="/@@/@admin.get_fqn(ctx.0)">@admin.name(ctx.0)</a><small>@@@admin.get_fqn(ctx.0)</small></p>
</div>
</section>
<p>@i18n!(ctx.1, "Runs Plume {0}"; env!("CARGO_PKG_VERSION"))</p>
</div>
<section>
@Html(instance.long_description_html)
</section>
})

View File

@ -1,35 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "Administration of {{ instance.name }}" | _(instance=instance) }}
{% endblock title %}
{% block content %}
<h1>{{ "Administration" | _ }}</h1>
{{ macros::tabs(links=['/admin', '/admin/instances', '/admin/users'], titles=['Configuration', 'Instances', 'Users'], selected=1) }}
<form method="post">
{{ macros::input(name="name", label="Name", errors=errors, form=form, props='minlenght="1"', default=instance) }}
<label for="open_registrations">
{% if instance.open_registrations %}
<input type="checkbox" name="open_registrations" id="open_registrations" checked>
{% else %}
<input type="checkbox" name="open_registrations" id="open_registrations">
{% endif %}
{{ "Allow anyone to register" | _ }}
</label>
<label for="short_description">{{ "Short description" | _ }}<small>{{ "Markdown is supported" | _ }}</small></label>
<textarea id="short_description" name="short_description">{{ form.short_description | default(value=instance.short_description | safe) }}</textarea>
<label for="long_description">{{ "Long description" | _ }}<small>{{ "Markdown is supported" | _ }}</small></label>
<textarea id="long_description" name="long_description">{{ form.long_description | default(value=instance.long_description | safe) }}</textarea>
{{ macros::input(name="default_license", label="Default license", errors=errors, form=form, props='minlenght="1"', default=instance) }}
<input type="submit" value="{{ "Save settings" | _ }}"/>
</form>
{% endblock content %}

View File

@ -0,0 +1,39 @@
@use templates::base;
@use template_utils::*;
@use plume_models::instance::Instance;
@use routes::instance::InstanceSettingsForm;
@use validator::ValidationErrors;
@(ctx: BaseContext, instance: Instance, form: InstanceSettingsForm, errors: ValidationErrors)
@:base(ctx, i18n!(ctx.1, "Administration of {0}"; instance.name.clone()).as_str(), {}, {}, {
<h1>@i18n!(ctx.1, "Administration")</h1>
@tabs(&[
("/admin", i18n!(ctx.1, "Configuration"), true),
("/admin/instances", i18n!(ctx.1, "Instances"), false),
("/admin/users", i18n!(ctx.1, "Users"), false),
])
<form method="post">
@input!(ctx.1, name (text), "Name", form, errors.clone(), "props")
<label for="open_registrations">
@if instance.open_registrations {
<input type="checkbox" name="open_registrations" id="open_registrations" checked>
} else {
<input type="checkbox" name="open_registrations" id="open_registrations">
}
@i18n!(ctx.1, "Allow anyone to register")
<label for="short_description">@i18n!(ctx.1, "Short description")<small>@i18n!(ctx.1, "Markdown is supported")</small></label>
<textarea id="short_description" name="short_description">@Html(form.short_description)</textarea>
<label for="long_description">@i18n!(ctx.1, "Long description")<small>@i18n!(ctx.1, "Markdown is supported")</small></label>
<textarea id="long_description" name="long_description">@Html(form.long_description)</textarea>
@input!(ctx.1, default_license (text), "Default license", form, errors, "minlenght=\"1\"")
<input type="submit" value="@i18n!(ctx.1, "Save settings")"/>
</form>
})

View File

@ -1,32 +0,0 @@
<section class="spaced">
<div class="cards">
<div class="presentation card">
<h2>{{ "What is Plume?" | _ }}</h2>
<main>
<p>{{ "Plume is a decentralized blogging engine." | _ }}</p>
<p>{{ "Authors can manage various blogs from an unique website." | _ }}</p>
<p>{{ "Articles are also visible on other Plume websites, and you can interact with them directly from other platforms like Mastodon." | _ }}</p>
</main>
<a href="/users/new">{{ "Create your account" | _ }}</a>
</div>
<div class="presentation card">
<h2>{{ "About {{ instance_name }}" | _(instance_name=instance.name) }}</h2>
<main>
{{ instance.short_description_html | safe }}
<section class="stats">
<div>
<p>{{ "Home to" | _ }}</p>
<em>{{ n_users }}</em>
<p>{{ "people" | _ }}</p>
</div>
<div>
<p>{{ "Who wrote" | _ }}</p>
<em>{{ n_articles }}</em>
<p>{{ "articles" | _ }}</p>
</div>
</section>
</main>
<a href="/about">{{ "Read the detailed rules" | _ }}</a>
</div>
</div>
</section>

View File

@ -1,23 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "All the articles of the Fediverse" | _ }}
{% endblock title %}
{% block content %}
<h1>{{ "All the articles of the Fediverse" | _ }}</h1>
{% if account %}
{{ macros::tabs(links=['/', '/feed', '/federated', '/local'], titles=['Latest articles', 'Your feed', 'Federated feed', 'Local feed'], selected=3) }}
{% else %}
{{ macros::tabs(links=['/', '/federated', '/local'], titles=['Latest articles', 'Federated feed', 'Local feed'], selected=2) }}
{% endif %}
<div class="cards">
{% for article in articles %}
{{ macros::post_card(article=article) }}
{% endfor %}
</div>
{{ macros::paginate(page=page, total=n_pages) }}
{% endblock content %}

View File

@ -0,0 +1,31 @@
@use templates::{base, partials::post_card};
@use template_utils::*;
@use plume_models::posts::Post;
@(ctx: BaseContext, articles: Vec<Post>, page: i32, n_pages: i32)
@:base(ctx, "All the articles of the Fediverse", {}, {}, {
<h1>@i18n!(ctx.1, "All the articles of the Fediverse")</h1>
@if let Some(_) = ctx.2 {
@tabs(&[
("/", i18n!(ctx.1, "Latest articles"), false),
("/feed", i18n!(ctx.1, "Your feed"), false),
("/federated", i18n!(ctx.1, "Federated feed"), true),
("/local", i18n!(ctx.1, "Local feed"), false),
])
} else {
@tabs(&[
("/", i18n!(ctx.1, "Latest articles"), false),
("/federated", i18n!(ctx.1, "Federated feed"), true),
("/local", i18n!(ctx.1, "Local feed"), false),
])
}
<div class="cards">
@for article in articles {
@:post_card(ctx, article)
}
</div>
@paginate(ctx.1, page, n_pages)
})

View File

@ -1,25 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "Your feed" | _ }}
{% endblock title %}
{% block content %}
<h1>
{{ "Your feed" | _ }}
</h1>
{{ macros::tabs(links=['/', '/feed', '/federated', '/local'], titles=['Latest articles', 'Your feed', 'Federated feed', 'Local feed'], selected=2) }}
{% if articles | length > 0 %}
<div class="cards">
{% for article in articles %}
{{ macros::post_card(article=article) }}
{% endfor %}
</div>
{{ macros::paginate(page=page, total=n_pages) }}
{% else %}
<p class="center">{{ "Nothing to see here yet. Try to follow more people." | _ }}</p>
{% endif %}
{% endblock content %}

View File

@ -0,0 +1,27 @@
@use templates::{base, partials::post_card};
@use template_utils::*;
@use plume_models::posts::Post;
@(ctx: BaseContext, articles: Vec<Post>, page: i32, n_pages: i32)
@:base(ctx, "Your feed", {}, {}, {
<h1>@i18n!(ctx.1, "Your feed")</h1>
@tabs(&[
("/", i18n!(ctx.1, "Latest articles"), false),
("/feed", i18n!(ctx.1, "Your feed"), true),
("/federated", i18n!(ctx.1, "Federated feed"), false),
("/local", i18n!(ctx.1, "Local feed"), false),
])
@if !articles.is_empty() {
<div class="cards">
@for article in articles {
@:post_card(ctx, article)
}
</div>
} else {
<p class="center">@i18n!(ctx.1, "Nothing to see here yet. Try to follow more people.")</p>
}
@paginate(ctx.1, page, n_pages)
})

View File

@ -1,25 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ instance.name }}
{% endblock title %}
{% block content %}
<h1>{{ "Welcome to {{ instance_name | escape }}" | _(instance_name=instance.name) }}</h1>
{% if account %}
{{ macros::tabs(links=['/', '/feed', '/federated', '/local'], titles=['Latest articles', 'Your feed', 'Federated feed', 'Local feed'], selected=1) }}
{{ macros::home_feed(title='Your feed', link='/feed', articles=user_feed) }}
{{ macros::home_feed(title='Federated feed', link='/federated', articles=federated) }}
{{ macros::home_feed(title='Local feed', link='/local', articles=local) }}
{% include "instance/description" %}
{% else %}
{{ macros::tabs(links=['/', '/federated', '/local'], titles=['Latest articles', 'Federated feed', 'Local feed'], selected=1) }}
{{ macros::home_feed(title='Federated feed', link='/federated', articles=federated) }}
{% include "instance/description" %}
{{ macros::home_feed(title='Local feed', link='/local', articles=local) }}
{% endif %}
{% endblock content %}

View File

@ -0,0 +1,34 @@
@use templates::{base, partials::*};
@use template_utils::*;
@use plume_models::instance::Instance;
@use plume_models::posts::Post;
@(ctx: BaseContext, instance: Instance, n_users: i32, n_articles: i32, local: Vec<Post>, federated: Vec<Post>, user_feed: Option<Vec<Post>>)
@:base(ctx, instance.name.clone().as_ref(), {}, {}, {
<h1>@i18n!(ctx.1, "Welcome on {}"; instance.name.as_str())</h1>
@if ctx.2.is_some() {
@tabs(&[
("/", i18n!(ctx.1, "Latest articles"), true),
("/feed", i18n!(ctx.1, "Your feed"), false),
("/federated", i18n!(ctx.1, "Federated feed"), false),
("/local", i18n!(ctx.1, "Local feed"), false),
])
@:home_feed(ctx, user_feed.unwrap_or_default(), "/feed", "Your feed")
@:home_feed(ctx, federated, "/federated", "Federated feed")
@:home_feed(ctx, local, "/local", "Local feed")
@:instance_description(ctx, instance, n_users, n_articles)
} else {
@tabs(&[
("/", i18n!(ctx.1, "Latest articles"), true),
("/federated", i18n!(ctx.1, "Federated feed"), false),
("/local", i18n!(ctx.1, "Local feed"), false),
])
@:home_feed(ctx, federated, "/federated", "Federated feed")
@:home_feed(ctx, local, "/local", "Local feed")
@:instance_description(ctx, instance, n_users, n_articles)
}
})

View File

@ -1,33 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "Administration of {{ instance.name }}" | _(instance=instance) }}
{% endblock title %}
{% block content %}
<h1>{{ "Instances" | _ }}</h1>
{{ macros::tabs(links=['/admin', '/admin/instances', '/admin/users'], titles=['Configuration', 'Instances', 'Users'], selected=2) }}
<div class="list">
{% for instance in instances %}
<div class="flex">
<p class="grow">
<a href="https://{{ instance.public_domain }}">{{ instance.name }}</a>
<small>{{ instance.public_domain }}</small>
</p>
{% if not instance.local %}
<form class="inline" method="post" action="/admin/instances/{{ instance.id }}/block">
{% if instance.blocked %}
<input type="submit" value="{{ 'Unblock' | _ }}">
{% else %}
<input type="submit" value="{{ 'Block' | _ }}">
{% endif %}
</form>
{% endif %}
</div>
{% endfor %}
</div>
{{ macros::paginate(page=page, total=n_pages) }}
{% endblock content %}

View File

@ -0,0 +1,32 @@
@use templates::base;
@use template_utils::*;
@use plume_models::instance::Instance;
@(ctx: BaseContext, instance: Instance, instances: Vec<Instance>, page: i32, n_pages: i32)
@:base(ctx, i18n!(ctx.1, "Administration of {0}"; instance.name.clone()).as_str(), {}, {}, {
<h1>@i18n!(ctx.1, "Instances")</h1>
@tabs(&[
("/admin", i18n!(ctx.1, "Configuration"), false),
("/admin/instances", i18n!(ctx.1, "Instances"), true),
("/admin/users", i18n!(ctx.1, "Users"), false),
])
<div class="list">
@for instance in instances {
<div class="flex">
<p class="grow">
<a href="https://@instance.public_domain">@instance.name</a>
<small>@instance.public_domain</small>
</p>
@if !instance.local {
<form class="inline" method="post" action="/admin/instances/@instance.id/block">
<input type="submit" value="@i18n!(ctx.1, if instance.blocked { "Unblock" } else { "Block"})">
</form>
}
</div>
}
</div>
@paginate(ctx.1, page, n_pages)
})

View File

@ -1,23 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "Articles from {{ instance.name }}" | _(instance=instance) }}
{% endblock title %}
{% block content %}
<h1>{{ "Articles from {{ instance.name }}" | _(instance=instance) }}</h1>
{% if account %}
{{ macros::tabs(links=['/', '/feed', '/federated', '/local'], titles=['Latest articles', 'Your feed', 'Federated feed', 'Local feed'], selected=4) }}
{% else %}
{{ macros::tabs(links=['/', '/federated', '/local'], titles=['Latest articles', 'Federated feed', 'Local feed'], selected=3) }}
{% endif %}
<div class="cards">
{% for article in articles %}
{{ macros::post_card(article=article) }}
{% endfor %}
</div>
{{ macros::paginate(page=page, total=n_pages) }}
{% endblock content %}

View File

@ -0,0 +1,32 @@
@use templates::{base, partials::post_card};
@use template_utils::*;
@use plume_models::posts::Post;
@use plume_models::instance::Instance;
@(ctx: BaseContext, instance: Instance, articles: Vec<Post>, page: i32, n_pages: i32)
@:base(ctx, i18n!(ctx.1, "Articles from {}"; instance.name.clone()).as_str(), {}, {}, {
<h1>@i18n!(ctx.1, "Articles from {}"; instance.name)</h1>
@if let Some(_) = ctx.2 {
@tabs(&[
("/", i18n!(ctx.1, "Latest articles"), false),
("/feed", i18n!(ctx.1, "Your feed"), false),
("/federated", i18n!(ctx.1, "Federated feed"), false),
("/local", i18n!(ctx.1, "Local feed"), true),
])
} else {
@tabs(&[
("/", i18n!(ctx.1, "Latest articles"), false),
("/federated", i18n!(ctx.1, "Federated feed"), false),
("/local", i18n!(ctx.1, "Local feed"), true),
])
}
<div class="cards">
@for article in articles {
@:post_card(ctx, article)
}
</div>
@paginate(ctx.1, page, n_pages)
})

View File

@ -1,30 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "Users" | _ }}
{% endblock title %}
{% block content %}
<h1>{{ "Users" | _ }}</h1>
{{ macros::tabs(links=['/admin', '/admin/instances', '/admin/users'], titles=['Configuration', 'Instances', 'Users'], selected=3) }}
<div class="list">
{% for user in users %}
<div class="flex">
{{ macros::avatar(user=user) }}
<p class="grow">
<a href="/@/{{ user.fqn }}">{{ user.name }}</a>
<small>@{{ user.username }}</small>
</p>
{% if not user.is_admin %}
<form class="inline" method="post" href="/admin/users/{{ user.id }}/ban">
<input type="submit" value="{{ 'Ban' | _ }}">
</form>
{% endif %}
</div>
{% endfor %}
</div>
{{ macros::paginate(page=page, total=n_pages) }}
{% endblock content %}

View File

@ -0,0 +1,33 @@
@use templates::base;
@use template_utils::*;
@use plume_models::users::User;
@(ctx: BaseContext, users: Vec<User>, page: i32, n_pages: i32)
@:base(ctx, "Users", {}, {}, {
<h1>@i18n!(ctx.1, "Users")</h1>
@tabs(&[
("/admin", i18n!(ctx.1, "Configuration"), false),
("/admin/instances", i18n!(ctx.1, "Instances"), false),
("/admin/users", i18n!(ctx.1, "Users"), true),
])
<div class="list">
@for user in users {
<div class="flex">
@avatar(ctx.0, &user, Size::Small, false, ctx.1)
<p class="grow">
<a href="/@@/@user.get_fqn(ctx.0)">@user.name(ctx.0)</a>
<small>@format!("@{}", user.username)</small>
</p>
@if !user.is_admin {
<form class="inline" method="post" action="/admin/users/@user.id/ban">
<input type="submit" value="@i18n!(ctx.1, "Ban")">
</form>
}
</div>
}
</div>
@paginate(ctx.1, page, n_pages)
})

View File

@ -1,112 +0,0 @@
{% macro post_card(article) %}
<div class="card">
{% if article.cover %}
<div class="cover" style="background-image: url('{{ article.cover.url }}')"></div>
{% endif %}
<h3><a href="{{ article.url }}">{{ article.post.title }}</a></h3>
<main>
<p>
{% if article.post.subtitle | length > 0 %}
{{ article.post.subtitle }}
{% else %}
{{ article.post.content | safe | striptags | truncate(length=200) }}
{% endif %}
</p>
</main>
<p class="author">
{{ "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" | _(
link_1='<a href="/@/',
link_2=article.author.fqn,
link_3='/">',
name=article.author.name,
link_4="</a>")
}}
{% if article.post.published %}⋅ {{ article.date | date(format="%B %e") }}{% endif %}
⋅ <a href="/~/{{ article.blog.fqn }}/">{{ article.blog.title }}</a>
{% if not article.post.published %}⋅ {{ "Draft" | _ }}{% endif %}
</p>
</div>
{% endmacro post_card %}
{% macro input(name, label, errors="", form="", type="text", props="", optional=false, default='', details=' ') %}
<label for="{{ name }}">
{{ label | _ }}
{% if optional %}
<small>{{ "Optional" | _ }}</small>
{% endif %}
<small>{{ details | _ }}</small>
</label>
{% if errors is defined and errors[name] %}
{% for err in errors[name] %}
<p class="error">{{ err.message | default(value="Unknown error") | _ }}</p>
{% endfor %}
{% endif %}
{% set default = default[name] | default(value="") %}
<input type="{{ type }}" id="{{ name }}" name="{{ name }}" value="{{ form[name] | default(value=default) }}" {{ props | safe }}/>
{% endmacro input %}
{% macro paginate(page, total, previous="Previous page", next="Next page", query="") %}
{% if query %}
{% set query = query ~ "&" %}
{% endif %}
<div class="pagination">
{% if page != 1 %}
<a href="?{{ query }}page={{ page - 1 }}">{{ previous | _ }}</a>
{% endif %}
{% if page < total %}
<a href="?{{ query }}page={{ page + 1 }}">{{ next | _ }}</a>
{% endif %}
</div>
{% endmacro %}
{% macro comment(comm) %}
<div class="comment" id="comment-{{ comm.id }}">
<a class="author" href="/@/{{ comm.author.fqn }}/">
{{ macros::avatar(user=comm.author, pad=true) }}
<span class="display-name">{{ comm.author.name }}</span>
<small>@{{ comm.author.fqn }}</small>
</a>
<div class="text">
{% if comm.sensitive %}
<details>
<summary>{{ comm.spoiler_text }}</summary>
{% endif %}
{{ comm.content | safe }}
{% if comm.sensitive %}
</details>
{% endif %}
</div>
<a class="button icon icon-message-circle" href="?responding_to={{ comm.id }}">{{ "Respond" | _ }}</a>
{% for res in comm.responses %}
{{ self::comment(comm=res) }}
{% endfor %}
</div>
{% endmacro %}
{% macro tabs(links, titles, selected) %}
<div class="tabs">
{% for link in links %}
{% set idx = loop.index0 %}
<a href="{{ link }}" {% if loop.index == selected %}class="selected"{% endif %}>{{ titles[idx] | _ }}</a>
{% endfor %}
</div>
{% endmacro %}
{% macro feather(name) %}
<svg class="feather">
<use xlink:href="/static/images/feather-sprite.svg#{{ name }}"/>
</svg>
{% endmacro %}
{% macro home_feed(title, link, articles) %}
{% if articles | length > 0 %}
<h2>{{ title | _ }} &mdash; <a href="{{ link }}">{{ "View all" | _ }}</a></h2>
<div class="cards spaced">
{% for article in articles %}
{{ macros::post_card(article=article) }}
{% endfor %}
</div>
{% endif %}
{% endmacro %}
{% macro avatar(user, size="small", pad=true) %}
<div
class="avatar {{ size }} {% if pad %}padded{% endif %}"
style="background-image: url('{{ user.avatar }}');"
title="{{ "{{ name }}'s avatar" | _(name=user.name) }}"
aria-label="{{ "{{ name }}'s avatar" | _(name=user.name) }}"
></div>
{% endmacro %}

View File

@ -1,35 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "Media details" | _ }}
{% endblock title %}
{% block content %}
<h1>{{ "Media details" }}</h1>
<section>
<a href="/medias">{{ "Go back to the gallery" | _ }}</a>
</section>
<section>
<figure class="media">
{{ media.html | safe }}
<figcaption>{{ media.alt_text }}</figcaption>
</figure>
<div>
<p>
{{ "Markdown code" | _ }}
<small>{{ "Copy it in your articles to insert this media." }}</small>
</p>
<code>{{ media.md }}</code>
</div>
<div>
<form class="inline" method="post" action="/medias/{{ media.id }}/avatar">
<input class="button" type="submit" value="{{ 'Use as avatar' | _ }}">
</form>
<form class="inline" method="post" action="/medias/{{ media.id }}/delete">
<input class="button" type="submit" value="{{ 'Delete' | _ }}">
</form>
</div>
</section>
{% endblock content %}

View File

@ -0,0 +1,36 @@
@use templates::base;
@use template_utils::*;
@use plume_models::medias::{Media, MediaCategory};
@(ctx: BaseContext, media: Media)
@:base(ctx, "Media details", {}, {}, {
<h1>@i18n!(ctx.1, "Media details")</h1>
<section>
<a href="/medias">@i18n!(ctx.1, "Go back to the gallery")</a>
</section>
<section>
<figure class="media">
@Html(media.html(ctx.0))
<figcaption>@media.alt_text</figcaption>
</figure>
<div>
<p>
@i18n!(ctx.1, "Markdown code")
<small>@i18n!(ctx.1, "Copy it in your articles to insert this media.")</small>
</p>
<code>@media.markdown(ctx.0)</code>
</div>
<div>
@if media.category() == MediaCategory::Image {
<form class="inline" method="post" action="/medias/@media.id/avatar">
<input class="button" type="submit" value="@i18n!(ctx.1, "Use as avatar")">
</form>
}
<form class="inline" method="post" action="/medias/@media.id/delete">
<input class="button" type="submit" value="@i18n!(ctx.1, "Delete")">
</form>
</div>
</section>
})

View File

@ -1,31 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "Your media" | _ }}
{% endblock title %}
{% block content %}
<h1>{{ "Your media" | _ }}</h1>
<div>
<a href="/medias/new" class="inline-block button">Upload</a>
</div>
<section>
{% if medias | length < 1 %}
<p>{{ "You don't have any media yet." | _ }}</p>
{% endif %}
<div class="list">
{% for media in medias %}
<div class="card flex">
{{ media.html_preview | safe }}
<main class="grow">
<p><a href="/medias/{{ media.id }}">{{ media.alt_text }}</a></p>
</main>
<a href="/medias/{{ media.id }}/delete">{{ "Delete" | _ }}</a>
</div>
{% endfor %}
</div>
{# TODO: macros::paginate(page=page, total=n_pages) #}
</section>
{% endblock content %}

View File

@ -0,0 +1,29 @@
@use templates::base;
@use template_utils::*;
@use plume_models::medias::Media;
@(ctx: BaseContext, medias: Vec<Media>)
@:base(ctx, "Your media", {}, {}, {
<h1>@i18n!(ctx.1, "Your media")</h1>
<div>
<a href="/medias/new" class="inline-block button">@i18n!(ctx.1, "Upload")</a>
</div>
<section>
@if medias.is_empty() {
<p>@i18n!(ctx.1, "You don't have any media yet.")</p>
}
<div class="list">
@for media in medias {
<div class="card flex">
@Html(media.preview_html(ctx.0))
<main class="grow">
<p><a href="/medias/@media.id">@media.alt_text</a></p>
</main>
<a href="/medias/@media.id/delete">@i18n!(ctx.1, "Delete")</a>
</div>
}
</div>
</section>
})

View File

@ -1,17 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "Media upload" | _ }}
{% endblock title %}
{% block content %}
<h1>{{ "Media upload" | _ }}</h1>
<form method="post" enctype="multipart/form-data">
{{ macros::input(name="alt", label="Description", errors=errors, form=form, props='required minlength="1"', details='Useful for visually impaired people and licensing') }}
{{ macros::input(name="cw", label="Content warning", errors=errors, form=form, details='Let it empty if there is none') }}
{{ macros::input(name="file", type='file', label="File", errors=errors, form=form, props='required') }}
<input type="submit" value="{{ "Send" | _ }}"/>
</form>
{% endblock content %}

View File

@ -0,0 +1,28 @@
@use templates::base;
@use template_utils::*;
@(ctx: BaseContext)
@:base(ctx, "Media upload", {}, {}, {
<h1>@i18n!(ctx.1, "Media upload")</h1>
<form method="post" enctype="multipart/form-data">
<label for="alt">
@i18n!(ctx.1, "Description")
<small>@i18n!(ctx.1, "Useful for visually impaired people and licensing")</small>
</label>
<input type="text" id="alt" name="alt" required minlenght="1"/>
<label for="cw">
@i18n!(ctx.1, "Content warning")
<small>@i18n!(ctx.1, "Let it empty if there is none")</small>
</label>
<input type="txt" id="cw" name="cw"/>
<label for="file">
@i18n!(ctx.1, "File")
</label>
<input type="file" id="file" name="file" required/>
<input type="submit" value="@i18n!(ctx.1, "Send")"/>
</form>
})

View File

@ -1,66 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "Notifications" | _ }}
{% endblock title %}
{% block content %}
<h1>{{ "Notifications" | _ }}</h1>
<div class="list">
{% for notification in notifications %}
<div class="card flex">
{% if notification.kind == "COMMENT" %}
<i class="icon icon-message-circle left-icon"></i>
<main class="grow">
<h3><a href="{{ notification.object.post.url }}#comment-{{ notification.object.id }}">
{{ "{{ user }} commented your article." | _(user=notification.object.user.name | escape) }}
</a></h3>
<p><a href="{{ notification.object.post.url }}">{{ notification.object.post.post.title }}</a></p>
</main>
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
{% elif notification.kind == "FOLLOW" %}
<i class="icon icon-user-plus left-icon"></i>
<main class="grow">
<h3><a href="/@/{{ notification.object.follower.fqn }}/">
{{ "{{ user }} is now following you." | _(user=notification.object.follower.name | escape) }}
</a></h3>
</main>
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
{% elif notification.kind == "LIKE" %}
<i class="icon icon-heart left-icon"></i>
<main class="grow">
<h3>
{{ "{{ user }} liked your article." | _(user=notification.object.user.name | escape) }}
</h3>
<p><a href="{{ notification.object.post.url }}">{{ notification.object.post.post.title }}</a></p>
</main>
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
{% elif notification.kind == "MENTION" %}
<i class="icon icon-at-sign left-icon"></i>
<main class="grow">
<h3><a href="{{ notification.object.url }}">
{{ "{{ user }} mentioned you." | _(user=notification.object.user.name | escape) }}
</a></h3>
</main>
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
{% elif notification.kind == "RESHARE" %}
<i class="icon icon-repeat left-icon"></i>
<main class="grow">
<h3>
{{ "{{ user }} boosted your article." | _(user=notification.object.user.name | escape) }}
</h3>
<p><a href="{{ notification.object.post.url }}">{{ notification.object.post.post.title }}</a></p>
</main>
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
{% endif %}
</div>
{% endfor %}
</div>
{{ macros::paginate(page=page, total=n_pages) }}
{% endblock content %}

View File

@ -0,0 +1,33 @@
@use templates::base;
@use template_utils::*;
@use plume_models::notifications::Notification;
@(ctx: BaseContext, notifications: Vec<Notification>, page: i32, n_pages: i32)
@:base(ctx, "Notifications", {}, {}, {
<h1>@i18n!(ctx.1, "Notifications")</h1>
<div class="list">
@for notification in notifications {
<div class="card flex">
<i class="icon @notification.icon_class() left-icon"></i>
<main class="grow">
<h3>
@if let Some(url) = notification.get_url(ctx.0) {
<a href="@url">
@i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).name(ctx.0))
</a>
} else {
@i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).name(ctx.0))
}
</h3>
@if let Some(post) = notification.get_post(ctx.0) {
<p><a href="@post.url(ctx.0)">@post.title</a></p>
}
</main>
<p><small>@notification.creation_date.format("%B %e, %H:%M")</small></p>
</div>
}
</div>
@paginate(ctx.1, page, n_pages)
})

View File

@ -0,0 +1,27 @@
@use template_utils::*;
@use plume_models::comments::Comment;
@use plume_models::users::User;
@(ctx: BaseContext, comm: &Comment, author: User)
<div class="comment" id="comment-@comm.id">
<a class="author" href="/@@/@author.get_fqn(ctx.0)/">
@avatar(ctx.0, &author, Size::Small, true, ctx.1)
<span class="display-name">@author.name(ctx.0)</span>
<small>@author.get_fqn(ctx.0)</small>
</a>
<div class="text">
@if comm.sensitive {
<details>
<summary>@comm.spoiler_text</summary>
}
@Html(&comm.content)
@if comm.sensitive {
</details>
}
</div>
<a class="button icon icon-message-circle" href="?responding_to=@comm.id">@i18n!(ctx.1, "Respond")</a>
@for res in comm.get_responses(ctx.0) {
@:comment(ctx, &res, res.get_author(ctx.0))
}
</div>

View File

@ -0,0 +1,14 @@
@use templates::partials::post_card;
@use plume_models::posts::Post;
@use template_utils::*;
@(ctx: BaseContext, articles: Vec<Post>, link: &str, title: &str)
@if articles.len() > 0 {
<h2>@i18n!(ctx.1, title) &mdash; <a href="@link">@i18n!(ctx.1, "View all")</a></h2>
<div class="cards spaced">
@for article in articles {
@:post_card(ctx, article)
}
</div>
}

View File

@ -0,0 +1,33 @@
@use template_utils::*;
@use plume_models::instance::Instance;
@(ctx: BaseContext, instance: Instance, n_users: i32, n_articles: i32)
<section class="spaced">
<div class="cards">
<div class="presentation card">
<h2>@i18n!(ctx.1, "What is Plume?")</h2>
<main>
<p>@i18n!(ctx.1, "Plume is a decentralized blogging engine.")</p>
<p>@i18n!(ctx.1, "Authors can manage various blogs from an unique website.")</p>
<p>@i18n!(ctx.1, "Articles are also visible on other Plume websites, and you can interact with them directly from other platforms like Mastodon.")</p>
</main>
<a href="/users/new">@i18n!(ctx.1, "Create your account")</a>
</div>
<div class="presentation card">
<h2>@i18n!(ctx.1, "About {0}"; instance.name)</h2>
<main>
@Html(instance.short_description_html)
<section class="stats">
<div>
<p>@Html(i18n!(ctx.1, "Home to <em>{0}</em> people"; n_users))</p>
</div>
<div>
<p>@Html(i18n!(ctx.1, "Who wrote <em>{0}</em> articles"; n_articles))</p>
</div>
</section>
</main>
<a href="/about">@i18n!(ctx.1, "Read the detailed rules")</a>
</div>
</div>
</section>

View File

@ -0,0 +1,29 @@
@use template_utils::*;
@use plume_models::posts::Post;
@(ctx: BaseContext, article: Post)
<div class="card">
@if article.cover_id.is_some() {
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
}
<h3><a href="@article.url(ctx.0)">@article.title</a></h3>
<main>
<p>@article.subtitle</p>
</main>
<p class="author">
@Html(i18n!(ctx.1, "By {0}"; format!(
"<a href=\"/@/{}/\">{}</a>",
escape(&article.get_authors(ctx.0)[0].get_fqn(ctx.0)),
escape(&article.get_authors(ctx.0)[0].name(ctx.0))
)))
@if article.published {
⋅ @article.creation_date.format("%B %e, %Y")
}
<a href="/~/@article.get_blog(ctx.0).get_fqn(ctx.0)/">@article.get_blog(ctx.0).title</a>
@if !article.published {
⋅ @i18n!(ctx.1, "Draft")
}
</p>
</div>

View File

@ -1,152 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block head %}
<meta property="og:title" content="{{ article.post.title }}"/>
<meta property="og:type" content="article"/>
{% if article.cover %}
<meta property="og:image" content="{{ article.cover.url | safe }}"/>
{% endif %}
<meta property="og:url" content="{{ article.url | safe }}"/>
{% if article.post.subtitle %}
<meta property="og:description" content="{{ article.post.subtitle }}"/>
{% endif %}
{% endblock head %}
{% block title %}
{{ article.post.title }}
{% endblock title %}
{% block header %}
<a href="/~/{{ blog.fqn }}">{{ blog.title }}</a>
{% endblock header %}
{% block content %}
<h1 class="article">{{ article.post.title }}</h1>
<h2 class="article">{{ article.post.subtitle }}</h2>
<div class="article-info">
<span class="author">{{ "Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}" | _(
link_1='<a href="/@/',
url=author.fqn,
link_2='/">',
name=author.name,
link_3="</a>"
)
}}</span>
&mdash;
<span class="date">{{ date | date(format="%B %e, %Y") }}</span>
{% if is_author %}
&mdash;
<a href="{{ article.url}}edit">{{ "Edit" | _ }}</a>
&mdash;
<form class="inline" method="post" action="{{ article.url}}delete">
<input onclick="return confirm('Are you sure you?')" type="submit" value="{{ 'Delete this article' | _ }}">
</form>
{% endif %}
{% if not article.post.published %}
<span class="badge">{{ "Draft" }}</span>
{% endif %}
</div>
{% if article.cover %}
<div class="cover" style="background-image: url('{{ article.cover.url }}')"></div>
{% endif %}
<article>
{{ article.post.content | safe }}
</article>
<div class="article-meta">
<p>{{ "This article is under the {{ license }} license." | _(license=article.post.license) }}</p>
<ul class="tags">
{% for tag in article.tags %}
{% if not tag.is_hashtag %}
<li><a href="/tag/{{ tag.tag }}">{{ tag.tag }}</a></li>
{% endif %}
{% endfor %}
</ul>
<div class="flex">
{{ macros::avatar(user=author, pad=true, size="medium") }}
<div class="grow">
<h2><a href="/@/{{ author.fqn }}">{{ author.name }}</a></h2>
<p>{{ author.summary | safe }}</h2>
</div>
<a href="/@/{{ author.fqn }}/follow" class="button">
{% if is_following %}
{{ "Unfollow" | _ }}
{% else %}
{{ "Follow" | _ }}
{% endif %}
</a>
</div>
{% if account %}
<div class="actions">
<form class="likes" action="{{ article.url }}like" method="POST">
<p aria-label="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}" title="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}">{{ n_likes }}</p>
{% if has_liked %}
<button type="submit" class="action liked">{{ macros::feather(name="heart") }}{{ "I don't like this anymore" | _ }}</button>
{% else %}
<button type="submit" class="action">{{ macros::feather(name="heart") }}{{ "Add yours" | _ }}</button>
{% endif %}
</form>
<form class="reshares" action="{{ article.url }}reshare" method="POST">
<p aria-label="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}" title="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}">{{ n_reshares }}</p>
{% if has_reshared %}
<button type="submit" class="action reshared"><i class="icon icon-repeat"></i>{{ "I don't want to boost this anymore" | _ }}</button>
{% else %}
<button type="submit" class="action"><i class="icon icon-repeat"></i>{{ "Boost" | _ }}</button>
{% endif %}
</form>
</div>
{% else %}
<p class="center">{{ "Login or use your Fediverse account to interact with this article" | _ }}</p>
<div class="actions">
<div class="likes">
<p aria-label="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}" title="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}">{{ n_likes }}</p>
<a href="/login?m=Login%20to%20like" class="action">{{ macros::feather(name="heart") }}{{ "Add yours" | _ }}</a>
</div>
<div class="reshares">
<p aria-label="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}" title="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}">{{ n_reshares }}</p>
<a href="/login?m=Login%20to%20boost" class="action"><i class="icon icon-repeat"></i>{{ "Boost" | _ }}</a>
</div>
</div>
{% endif %}
<div class="comments">
<h2>{{ "Comments" | _ }}</h2>
{% if account %}
<form method="post" action="{{ article.url }}comment">
{{ macros::input(
name="warning",
label="Content warning",
optional=true,
form=comment_form,
errors=comment_errors,
default=default)
}}
<label for="plume-editor">{{ "Your comment" | _ }}</label>
{% if previous %}
<input type="hidden" name="responding_to" value="{{ previous.id }}"/>
{% endif %}
{# Ugly, but we don't have the choice if we don't want weird paddings #}
<textarea id="plume-editor" name="content">{% filter trim %}{% if previous %}{% if previous.author.fqn != user_fqn %}@{{ previous.author.fqn }} {% endif %}{% for mention in previous.mentions %}{% if mention != user_fqn %}@{{ mention }} {% endif %}{% endfor %}{% endif %}{% endfilter %}</textarea>
<input type="submit" value="{{ "Submit comment" | _ }}" />
</form>
{% endif %}
{% if comments | length > 0 %}
<div class="list">
{% for comment in comments %}
{{ macros::comment(comm=comment) }}
{% endfor %}
</div>
{% else %}
<p class="center">{{ "No comments yet. Be the first to react!" | _ }}</p>
{% endif %}
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,146 @@
@use templates::{base, partials::comment};
@use template_utils::*;
@use plume_models::blogs::Blog;
@use plume_models::comments::Comment;
@use plume_models::posts::Post;
@use plume_models::tags::Tag;
@use plume_models::users::User;
@use validator::ValidationErrors;
@use routes::comments::NewCommentForm;
@(ctx: BaseContext, article: Post, blog: Blog, comment_form: &NewCommentForm, comment_errors: ValidationErrors, tags: Vec<Tag>, comments: Vec<Comment>, previous_comment: Option<Comment>, n_likes: usize, n_reshares: usize, has_liked: bool, has_reshared: bool, is_following: bool, author: User)
@:base(ctx, &article.title.clone(), {
<meta property="og:title" content="article.title"/>
<meta property="og:type" content="article"/>
@if article.cover_id.is_some() {
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
}
<meta property="og:url" content="@Html(article.url(ctx.0))"/>
<meta property="og:description" content="@article.subtitle"/>
}, {
<a href="/~/@blog.get_fqn(ctx.0)">@blog.title</a>
}, {
<h1 class="article">@&article.title</h1>
<h2 class="article">@&article.subtitle</h2>
<div class="article-info">
<span class="author">
@Html(i18n!(ctx.1, "Written by {0}"; format!("<a href=\"/@/{}/\">{}</a>", escape(&author.get_fqn(ctx.0)), escape(&author.name(ctx.0)))))
</span>
&mdash;
<span class="date">@article.creation_date.format("%B %e, %Y")</span>
@if ctx.2.clone().map(|u| u.id == author.id).unwrap_or(false) {
&mdash;
<a href="@article.url(ctx.0)/edit">@i18n!(ctx.1, "Edit")</a>
&mdash;
<form class="inline" method="post" action="@article.url(ctx.0)/delete">
<input onclick="return confirm('Are you sure you?')" type="submit" value="@i18n!(ctx.1, "Delete this article")">
</form>
}
@if !article.published {
<span class="badge">@i18n!(ctx.1, "Draft")</span>
}
</div>
@if article.cover_id.is_some() {
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
}
<article>
@Html(&article.content)
</article>
<div class="article-meta">
<p>@i18n!(ctx.1, "This article is under the {0} license."; &article.license)</p>
<ul class="tags">
@for tag in tags {
@if !tag.is_hashtag {
<li><a href="/tag/@tag.tag">@tag.tag</a></li>
}
}
</ul>
<div class="flex">
@avatar(ctx.0, &author, Size::Medium, true, ctx.1)
<div class="grow">
<h2><a href="/@@/@author.get_fqn(ctx.0)">@author.name(ctx.0)</a></h2>
<p>@Html(&author.summary)</h2>
</div>
<a href="/@@/@author.get_fqn(ctx.0)/follow" class="button">
@if is_following {
@i18n!(ctx.1, "Unfollow")
} else {
@i18n!(ctx.1, "Follow")
}
</a>
</div>
@if ctx.2.is_some() {
<div class="actions">
<form class="likes" action="@article.url(ctx.0)/like" method="POST">
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes", &n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes", n_likes)">
@n_likes
</p>
@if has_liked {
<button type="submit" class="action liked">@icon!("heart") @i18n!(ctx.1, "I don't like this anymore")</button>
} else {
<button type="submit" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</button>
}
</form>
<form class="reshares" action="@article.url(ctx.0)/reshare" method="POST">
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boost", &n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts", n_reshares)">
@n_reshares
</p>
@if has_reshared {
<button type="submit" class="action reshared">@icon!("repeat") @i18n!(ctx.1, "I don't want to boost this anymore")</button>
} else {
<button type="submit" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</button>
}
</form>
</div>
} else {
<p class="center">@i18n!(ctx.1, "Login or use your Fediverse account to interact with this article")</p>
<div class="actions">
<div class="likes">
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes", &n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes", n_likes)">
@n_likes
</p>
<a href="/login?m=Login%20to%20like" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</a>
</div>
<div class="reshares">
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boost", &n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts", n_reshares)">
@n_reshares
</p>
<a href="/login?m=Login%20to%20boost" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</a>
</div>
</div>
}
<div class="comments">
<h2>@i18n!(ctx.1, "Comments")</h2>
@if ctx.2.is_some() {
<form method="post" action="@article.url(ctx.0)/comment">
@input!(ctx.1, warning (optional text), "Content warning", comment_form, comment_errors, "")
<label for="plume-editor">@i18n!(ctx.1, "Your comment")</label>
@if let Some(ref prev) = previous_comment {
<input type="hidden" name="responding_to" value="@prev.id"/>
}
<textarea id="plume-editor" name="content">@comment_form.content</textarea>
<input type="submit" value="@i18n!(ctx.1, "Submit comment")" />
</form>
}
@if !comments.is_empty() {
<div class="list">
@for comm in comments {
@:comment(ctx, &comm, comm.get_author(ctx.0))
}
</div>
} else {
<p class="center">@i18n!(ctx.1, "No comments yet. Be the first to react!")</p>
}
</div>
</div>
})

View File

@ -1,68 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{% if editing %}
{{ "Edit {{ post }}" | _(post=form.title) }}
{% else %}
{{ "New post" | _ }}
{% endif %}
{% endblock title %}
{% block content %}
<h1>
{% if editing %}
{{ "Edit {{ post }}" | _(post=form.title) }}
{% else %}
{{ "Create a post" | _ }}
{% endif %}
</h1>
<form class="new-post" method="post">
{{ macros::input(name="title", label="Title", errors=errors, form=form, props="required") }}
{{ macros::input(name="subtitle", label="Subtitle", errors=errors, form=form, optional=true) }}
{% if errors is defined and errors.content %}
{% for err in errors.content %}
<p class="error">{{ err.message | default(value="Unknown error") | _ }}</p>
{% endfor %}
{% endif %}
<label for="plume-editor">{{ "Content" | _ }}<small>{{ "Markdown is supported" | _ }}</small></label>
<textarea id="plume-editor" name="content" rows="20">{{ form.content | default(value="") }}</textarea>
{{ macros::input(name="tags", label="Tags, separated by commas", errors=errors, form=form, optional=true) }}
{% set license_infos = "Default license will be {{ instance.default_license }}" | _(instance=instance) %}
{{ macros::input(name="license", label="License", errors=errors, form=form, optional=true, details=license_infos) }}
<label for="cover">{{ "Illustration" | _ }}<small>{{ "Optional" | _ }}</small></label>
<select id="cover" name="cover">
<option value="none" {% if form is undefined or form.cover is undefined %}selected{% endif %}>{{ "None" | _ }}</option>
{% for media in medias %}
{% if media.category == "image" %}
<option value="{{ media.id }}" {% if form is defined and form.cover is defined and form.cover == media.id %}selected{% endif %}>
{{ media.alt_text | default(value=media.content_warning) }}
</option>
{% endif %}
{% endfor %}
</select>
{% if is_draft %}
<label for="draft">
<input type="checkbox" name="draft" id="draft" checked>
{{ "This is a draft, don't publish it yet." | _ }}
</label>
{% endif %}
{% if editing %}
<input type="submit" value="{{ "Update" | _ }}" />
{% else %}
{% if is_draft %}
<input type="submit" value="{{ "Update or publish" | _ }}" />
{% else %}
<input type="submit" value="{{ "Publish" | _ }}" />
{% endif %}
{% endif %}
</form>
<script src="/static/js/autoExpand.js"></script>
{% endblock content %}

Some files were not shown because too many files have changed in this diff Show More