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
fix-mobile-margin
Baptiste Gelez 5 years ago committed by GitHub
parent 5f059c3e98
commit 70af57c6e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

3
.gitignore vendored

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

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

584
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -2,25 +2,28 @@
authors = ["Bat' <baptiste@gelez.xyz>"]
name = "plume"
version = "0.2.0"
[dependencies]
activitypub = "0.1.3"
askama_escape = "0.1"
atom_syndication = "0.6"
canapi = "0.1"
colored = "1.6"
dotenv = "0.13"
failure = "0.1"
gettext-rs = "0.4"
guid-create = "0.1"
heck = "0.3.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"
scheduled-thread-pool = "0.2.0"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
serde_qs = "0.4"
tera = "0.11"
validator = "0.7"
validator = "0.8"
validator_derive = "0.7"
webfinger = "0.3.1"
@ -54,26 +57,13 @@ path = "plume-common"
[dependencies.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]
git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "0dfb822d5cbf65a5eee698099368b7c0f4c61fa4"
rev = "717fad53cfd2ee5cbee5b4571f6190644f9dddd7"
[dependencies.rocket_i18n]
git = "https://github.com/BaptisteGelez/rocket_i18n"
rev = "75a3bfd7b847324c078a355a7f101f8241a9f59b"
[build-dependencies]
ructe = "0.5.2"
rocket_i18n = { version = "0.3.1", features = ["build"] }
[features]
default = ["postgres"]

@ -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"]);
}

@ -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.
## 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
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
<p>{{ "Hello, world!" | _ }}</p>
```html
<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:
```po
@ -69,7 +72,7 @@ For plural forms, the syntax is a bit different:
```po
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[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 HTML/Tera templates, we use HTML5 syntax.
For HTML/Ructe templates, we use HTML5 syntax.

@ -23,16 +23,21 @@ Sometimes, strings may change depending on a number (for instance, a post counte
```
msgid "One post"
msgid_plural "{{ count }} posts"
msgid_plural "{0} posts"
msgstr[0] ""
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
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

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

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

@ -1,4 +1,3 @@
use gettextrs::gettext;
use heck::CamelCase;
use openssl::rand::rand_bytes;
use pulldown_cmark::{Event, Parser, Options, Tag, html};
@ -23,8 +22,13 @@ pub fn make_actor_id(name: &str) -> String {
.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> {
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)]

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

@ -68,26 +68,10 @@ impl Comment {
.len() // TODO count in database?
}
pub fn to_json(&self, conn: &Connection, others: &[Comment]) -> serde_json::Value {
let mut json = serde_json::to_value(self).expect("Comment::to_json: serialization error");
json["author"] = self.get_author(conn).to_json(conn);
let mentions = Mention::list_for_comment(conn, self.id)
.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 get_responses(&self, conn: &Connection) -> Vec<Comment> {
comments::table.filter(comments::in_response_to_id.eq(self.id))
.load::<Comment>(conn)
.expect("Comment::get_responses: loading error")
}
pub fn update_ap_url(&self, conn: &Connection) -> Comment {

@ -20,9 +20,9 @@ pub struct Instance {
pub open_registrations: bool,
pub short_description: SafeString,
pub long_description: SafeString,
pub default_license: String,
pub long_description_html: String,
pub short_description_html: String,
pub default_license : String,
pub long_description_html: SafeString,
pub short_description_html: SafeString,
}
#[derive(Clone, Insertable)]
@ -244,14 +244,15 @@ pub(crate) mod tests {
default_license,
local,
long_description,
long_description_html,
short_description,
short_description_html,
name,
open_registrations,
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);
Ok(())
});
@ -282,14 +283,14 @@ pub(crate) mod tests {
default_license,
local,
long_description,
long_description_html,
short_description,
short_description_html,
name,
open_registrations,
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));
@ -391,12 +392,12 @@ pub(crate) mod tests {
);
assert_eq!(
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_html,
"<p><a href=\"#link\">short</a></p>\n"
SafeString::new("<p><a href=\"#link\">short</a></p>\n")
);
Ok(())

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

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

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

@ -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);
json!({
"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))),
})
format!("/~/{}/{}", blog.get_fqn(conn), self.slug)
}
pub fn compute_id(&self, conn: &Connection) -> String {
@ -784,6 +776,10 @@ impl Post {
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 {

@ -26,6 +26,7 @@ use rocket::{
request::{self, FromRequest, Request},
};
use serde_json;
use std::cmp::PartialEq;
use url::Url;
use webfinger::*;
@ -797,20 +798,8 @@ impl User {
CustomPerson::new(actor, ap_signature)
}
pub fn to_json(&self, conn: &Connection) -> serde_json::Value {
let mut json = serde_json::to_value(self).expect("User::to_json: serializing error");
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 avatar_url(&self, conn: &Connection) -> String {
self.avatar_id.and_then(|id| Media::get(conn, id).map(|m| m.url(conn))).unwrap_or("/static/default-avatar.png".to_string())
}
pub fn webfinger(&self, conn: &Connection) -> Webfinger {
@ -874,6 +863,14 @@ impl User {
pub fn needs_update(&self) -> bool {
(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 {
@ -946,6 +943,12 @@ impl Signer for User {
}
}
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl NewUser {
/// Creates a new local user
pub fn new_local(

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,5 +1,5 @@
use canapi::Provider;
use rocket_contrib::Json;
use rocket_contrib::json::Json;
use serde_json;
use plume_api::apps::AppEndpoint;
@ -10,7 +10,7 @@ use plume_models::{
};
#[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();
Json(json!(post))
}

@ -1,4 +1,5 @@
use rocket_contrib::Json;
use rocket::request::Form;
use rocket_contrib::json::Json;
use serde_json;
use plume_common::utils::random_hex;
@ -10,7 +11,7 @@ use plume_models::{
};
#[derive(FromForm)]
struct OAuthRequest {
pub struct OAuthRequest {
client_id: String,
client_secret: String,
password: String,
@ -18,8 +19,8 @@ struct OAuthRequest {
scopes: String,
}
#[get("/oauth2?<query>")]
fn oauth(query: OAuthRequest, conn: DbConn) -> Json<serde_json::Value> {
#[get("/oauth2?<query..>")]
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");
if app.client_secret == query.client_secret {
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,
user_id: user.id,
value: random_hex(),
scopes: query.scopes,
scopes: query.scopes.clone(),
});
Json(json!({
"token": token.value

@ -1,6 +1,6 @@
use canapi::Provider;
use rocket::http::uri::Origin;
use rocket_contrib::Json;
use rocket_contrib::json::Json;
use serde_json;
use serde_qs;
@ -15,13 +15,13 @@ use api::authorization::*;
use Searcher;
#[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();
Json(json!(post))
}
#[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 post = <Post as Provider<(&Connection, &UnmanagedSearcher, Option<i32>)>>::list(&(&*conn, &search, auth.map(|a| a.0.user_id)), query);
Json(json!(post))

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

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

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

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

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

@ -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::utils;
@ -12,7 +13,7 @@ use plume_models::{
use Worker;
#[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 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)]
fn create_auth(blog: String, slug: String) -> Flash<Redirect>{
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect>{
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)
)
}

@ -1,31 +1,29 @@
use guid_create::GUID;
use multipart::server::{Multipart, save::{SavedData, SaveResult}};
use rocket::{Data, http::ContentType, response::{Redirect, status}};
use rocket_contrib::Template;
use serde_json;
use rocket_i18n::I18n;
use std::fs;
use plume_models::{db_conn::DbConn, medias::*, users::User};
use template_utils::Ructe;
#[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);
Template::render("medias/index", json!({
"account": user.to_json(&*conn),
"medias": medias.into_iter().map(|m| m.to_json(&*conn)).collect::<Vec<serde_json::Value>>()
}))
render!(medias::index(
&(&*conn, &intl.catalog, Some(user)),
medias
))
}
#[get("/medias/new")]
fn new(user: User, conn: DbConn) -> Template {
Template::render("medias/new", json!({
"account": user.to_json(&*conn),
"form": {},
"errors": {}
}))
pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
render!(medias::new(
&(&*conn, &intl.catalog, Some(user))
))
}
#[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() {
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>")]
fn details(id: i32, user: User, conn: DbConn) -> Template {
let media = Media::get(&*conn, id);
Template::render("medias/details", json!({
"account": user.to_json(&*conn),
"media": media.map(|m| m.to_json(&*conn))
}))
pub fn details(id: i32, user: User, conn: DbConn, intl: I18n) -> Ructe {
let media = Media::get(&*conn, id).expect("Media::details: media not found");
render!(medias::details(
&(&*conn, &intl.catalog, Some(user)),
media
))
}
#[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)?;
media.delete(&*conn);
Some(Redirect::to(uri!(list)))
}
#[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)?;
user.set_avatar(&*conn, media.id);
Some(Redirect::to(uri!(details: id = id)))

@ -1,70 +1,23 @@
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
use rocket::{
http::{RawStr,
uri::{FromUriParam, UriDisplay}},
http::RawStr,
request::FromFormValue,
response::NamedFile
};
use std::{
fmt,
path::{Path, PathBuf}
response::NamedFile,
};
use std::path::{Path, PathBuf};
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;
#[derive(FromForm)]
pub struct Page {
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 }
}
}
#[derive(Copy, Clone)]
pub struct Page(i32);
impl<'v> FromFormValue<'v> for Page {
type Error = &'v RawStr;
fn from_form_value(form_value: &'v RawStr) -> Result<Page, &'v RawStr> {
match form_value.parse::<i32>() {
Ok(page) => Ok(Page{page}),
Ok(page) => Ok(Page(page)),
_ => Err(form_value),
}
}
@ -72,9 +25,7 @@ impl<'v> FromFormValue<'v> for Page {
impl Page {
pub fn first() -> Page {
Page {
page: 1
}
Page(1)
}
/// Computes the total number of pages needed to display n_items
@ -87,7 +38,7 @@ impl Page {
}
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;
#[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()
}

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

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

@ -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::utils;
@ -12,7 +13,7 @@ use plume_models::{
use Worker;
#[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 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)]
fn create_auth(blog: String, slug: String) -> Flash<Redirect> {
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> {
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)
)
}

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

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

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

@ -5,9 +5,9 @@ use rocket::{
request::LenientForm,
response::{status, Content, Flash, Redirect},
};
use rocket_contrib::Template;
use rocket_i18n::I18n;
use serde_json;
use validator::{Validate, ValidationError};
use validator::{Validate, ValidationError, ValidationErrors};
use inbox::Inbox;
use plume_common::activity_pub::{
@ -22,11 +22,12 @@ use plume_models::{
reshares::Reshare, users::*,
};
use routes::Page;
use template_utils::Ructe;
use Worker;
use Searcher;
#[get("/me")]
fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
match user {
Some(user) => Ok(Redirect::to(uri!(details: name = user.username))),
None => Err(utils::requires_login("", uri!(me))),
@ -34,7 +35,7 @@ fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
}
#[get("/@/<name>", rank = 2)]
fn details(
pub fn details(
name: String,
conn: DbConn,
account: Option<User>,
@ -42,111 +43,96 @@ fn details(
fetch_articles_conn: DbConn,
fetch_followers_conn: DbConn,
update_conn: DbConn,
intl: I18n,
searcher: Searcher,
) -> Template {
may_fail!(
account.map(|a| a.to_json(&*conn)),
User::find_by_fqn(&*conn, &name),
"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 {
// Fetch new articles
let user_clone = user.clone();
let searcher = searcher.clone();
worker.execute(move || {
for create_act in user_clone.fetch_outbox::<Create>() {
match create_act.create_props.object_object::<Article>() {
Ok(article) => {
Post::from_activity(
&(&fetch_articles_conn, &searcher),
article,
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),
},
) -> Result<Ructe, Ructe> {
let user = User::find_by_fqn(&*conn, &name).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, account.clone()))))?;
let recents = Post::get_recents_for_author(&*conn, &user, 6);
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
if !user.get_instance(&*conn).local {
// Fetch new articles
let user_clone = user.clone();
let searcher = searcher.clone();
worker.execute(move || {
for create_act in user_clone.fetch_outbox::<Create>() {
match create_act.create_props.object_object::<Article>() {
Ok(article) => {
Post::from_activity(
&(&*fetch_articles_conn, &searcher),
article,
user_clone.clone().into_id(),
);
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(
"users/details",
json!({
"user": user.to_json(&*conn),
"instance_url": user.get_instance(&*conn).public_domain,
"is_remote": user.instance_id != Instance::local_id(&*conn),
"follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
"account": account.clone().map(|a| a.to_json(&*conn)),
"recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
"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),
"n_followers": n_followers
}),
)
// 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),
},
);
}
});
// 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")]
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);
Template::render(
"users/dashboard",
json!({
"account": user.to_json(&*conn),
"blogs": blogs,
"drafts": Post::drafts_by_author(&*conn, &user).into_iter().map(|a| a.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
}),
)
render!(users::dashboard(
&(&*conn, &intl.catalog, Some(user.clone())),
blogs,
Post::drafts_by_author(&*conn, &user)
))
}
#[get("/dashboard", rank = 2)]
fn dashboard_auth() -> Flash<Redirect> {
pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
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),
)
}
#[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)?;
if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) {
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)]
fn follow_auth(name: String) -> Flash<Redirect> {
pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
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),
)
}
#[get("/@/<name>/followers?<page>")]
fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page: Page) -> Template {
may_fail!(
account.map(|a| a.to_json(&*conn)),
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(
"users/followers",
json!({
"user": user.to_json(&*conn),
"instance_url": user.get_instance(&*conn).public_domain,
"is_remote": user.instance_id != Instance::local_id(&*conn),
"follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
"followers": user.get_followers_page(&*conn, page.limits()).into_iter().map(|f| f.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
"account": account.clone().map(|a| a.to_json(&*conn)),
"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)
}),
)
}
)
pub fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page: Page, intl: I18n) -> Result<Ructe, Ructe> {
let user = User::find_by_fqn(&*conn, &name).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, account.clone()))))?;
let followers_count = user.get_followers(&*conn).len(); // TODO: count in DB
Ok(render!(users::followers(
&(&*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,
user.get_followers_page(&*conn, page.limits()),
page.0,
Page::total(followers_count as i32)
)))
}
#[get("/@/<name>/followers", rank = 2)]
fn followers(name: String, conn: DbConn, account: Option<User>) -> Template {
followers_paginated(name, conn, account, Page::first())
pub fn followers(name: String, conn: DbConn, account: Option<User>, intl: I18n) -> Result<Ructe, Ructe> {
followers_paginated(name, conn, account, Page::first(), intl)
}
#[get("/@/<name>", rank = 1)]
fn activity_details(
pub fn activity_details(
name: String,
conn: DbConn,
_ap: ApRequest,
@ -223,71 +197,60 @@ fn activity_details(
}
#[get("/users/new")]
fn new(user: Option<User>, conn: DbConn) -> Template {
Template::render(
"users/new",
json!({
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
"account": user.map(|u| u.to_json(&*conn)),
"errors": null,
"form": null
}),
)
pub fn new(user: Option<User>, conn: DbConn, intl: I18n) -> Ructe {
render!(users::new(
&(&*conn, &intl.catalog, user),
Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
&NewUserForm::default(),
ValidationErrors::default()
))
}
#[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('@') {
Some(Template::render(
"users/edit",
json!({
"account": user.to_json(&*conn)
}),
))
Some(render!(users::edit(
&(&*conn, &intl.catalog, Some(user.clone())),
UpdateUserForm {
display_name: user.display_name.clone(),
email: user.email.clone().unwrap_or_default(),
summary: user.summary.to_string(),
},
ValidationErrors::default()
)))
} else {
None
}
}
#[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(
"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),
)
}
#[derive(FromForm)]
struct UpdateUserForm {
display_name: Option<String>,
email: Option<String>,
summary: Option<String>,
pub struct UpdateUserForm {
pub display_name: String,
pub email: String,
pub summary: String,
}
#[put("/@/<_name>/edit", data = "<data>")]
fn update(_name: String, conn: DbConn, user: User, data: LenientForm<UpdateUserForm>) -> Redirect {
#[put("/@/<_name>/edit", data = "<form>")]
pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm<UpdateUserForm>) -> Redirect {
user.update(
&*conn,
data.get()
.display_name
.clone()
.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()),
if !form.display_name.is_empty() { form.display_name.clone() } else { user.display_name.clone() },
if !form.email.is_empty() { form.email.clone() } else { user.email.clone().unwrap_or_default() },
if !form.summary.is_empty() { form.summary.clone() } else { user.summary.to_string() },
);
Redirect::to(uri!(me))
}
#[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)?;
if user.id == account.id {
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(
schema(
function = "passwords_match",
@ -310,29 +273,29 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher
message = "Passwords are not matching"
)
)]
struct NewUserForm {
pub struct NewUserForm {
#[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 \""))]
username: String,
pub username: String,
#[validate(email(message = "Invalid email"))]
email: String,
pub email: String,
#[validate(
length(
min = "8",
message = "Password should be at least 8 characters long"
)
)]
password: String,
pub password: String,
#[validate(
length(
min = "8",
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 {
Err(ValidationError::new("password_match"))
} 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(&['<', '>', '&', '@', '\'', '"'][..]) {
Err(ValidationError::new("username_illegal_char"))
} else {
@ -348,16 +311,15 @@ fn validate_username(username: &str) -> Result<(), ValidationError> {
}
}
#[post("/users/new", data = "<data>")]
fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Template> {
if !Instance::get_local(&*conn)
#[post("/users/new", data = "<form>")]
pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Result<Redirect, Ructe> {
if !Instance::get_local(&*conn)
.map(|i| i.open_registrations)
.unwrap_or(true)
{
return Ok(Redirect::to(uri!(new))); // Actually, it is an error
}
let form = data.get();
form.validate()
.map(|_| {
NewUser::new_local(
@ -371,26 +333,24 @@ fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Temp
).update_boxes(&*conn);
Redirect::to(uri!(super::session::new))
})
.map_err(|e| {
Template::render(
"users/new",
json!({
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
"errors": e.inner(),
"form": form
}),
)
.map_err(|err| {
render!(users::new(
&(&*conn, &intl.catalog, None),
Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
&*form,
err
))
})
}
#[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)?;
Some(user.outbox(&*conn))
}
#[post("/@/<name>/inbox", data = "<data>")]
fn inbox(
pub fn inbox(
name: String,
conn: DbConn,
data: String,
@ -433,7 +393,7 @@ fn inbox(
}
#[get("/@/<name>/followers")]
fn ap_followers(
pub fn ap_followers(
name: String,
conn: DbConn,
_ap: ApRequest,
@ -459,7 +419,7 @@ fn ap_followers(
}
#[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 feed = FeedBuilder::default()
.title(author.display_name.clone())

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

@ -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
))
}
};
}

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

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

@ -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 %}

@ -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>
}
})

@ -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 %}

@ -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>
})

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

@ -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 %}

@ -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 %}

@ -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 %}

@ -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>
})

@ -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 %}

@ -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>
})

@ -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>
})

@ -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>
})

@ -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>
})

@ -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 %}

@ -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>
})

@ -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 %}

@ -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>
})

@ -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>

@ -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 %}

@ -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)
})

@ -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 %}

@ -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)
})

@ -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 %}

@ -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)
}
})

@ -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 %}

@ -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)
})

@ -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 %}

@ -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)
})

@ -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 %}

@ -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)
})

@ -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 %}

@ -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 %}

@ -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>
})

@ -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 %}

@ -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>
})

@ -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 %}

@ -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>
})

@ -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 %}

@ -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)
})

@ -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>

@ -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>
}

@ -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>

@ -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>

@ -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 %}

@ -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>
})

@ -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

Loading…
Cancel
Save