Merge branch 'master' of https://github.com/Plume-org/Plume into icons

This commit is contained in:
Madeorsk 2018-09-04 21:55:44 +02:00
commit a30b99f93e
25 changed files with 299 additions and 108 deletions

22
docs/ENV-VARS.md Normal file
View File

@ -0,0 +1,22 @@
# Useful Environment Variables
Plume relies on some environment variables for some configuration options. You can either set them before
starting the app with `cargo run` or write them in a `.env` file to have automatically loaded.
Here are the variables that Plume uses:
- `BASE_URL`: the domain name, or IP and port on which Plume is listening. It is used in all federation-related code.
- `DB_URL`: the URL of the PostgreSQL database, used by Plume (`postgres://plume:plume@localhost/plume` by default).
- `POSTGRES_USER`: if you just want to use a different PostgreSQL user name, and keep the rest of the default URL.
- `POSTGRES_PASSWORD`: same as `POSTGRES_USER`, but for the password.
- `USE_HTTPS`: if it is `0`, federation and medias will be using HTTP by default (`1` by default).
- `ROCKET_ADDRESS`: the adress on which Plume should listen (`0.0.0.0` by default).
- `ROCKET_PORT`: the port on which Plume should listen ([`7878` by default](https://twitter.com/ag_dubs/status/852559264510070784))
- `ROCKET_SECRET_KEY`: key used to sign private cookies and for CSRF protection. If it is not set, it will be regenerated everytime you restart Plume,
meaning that all your users will get disconnected. You can generate one with `openssl rand -base64 32`.
## Diesel
Diesel, the tool we use to run migrations may be configured with the `DATABASE_URL` which should contain the URL of the
PostgreSQL database. Otherwise, you can specify `--database-url YOUR-URL` everytime you run a `diesel` command.

View File

@ -2,5 +2,6 @@
- [Installing Plume (for development or production)](INSTALL.md)
- [Updating your instance](UPDATE.md)
- [Useful Environment Variables](ENV-VARS.md)
- [Development Guide](DEVELOPMENT.md)
- [Making Plume available in your language](INTERNATIONALIZATION.md)

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE follows DROP COLUMN ap_url;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE follows ADD COLUMN ap_url TEXT NOT NULL DEFAULT '';

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE posts DROP COLUMN subtitle;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE posts ADD COLUMN subtitle TEXT NOT NULL DEFAULT '';

View File

@ -1,7 +1,7 @@
use activitypub::{Actor, activity::{Accept, Follow as FollowAct}, actor::Person};
use activitypub::{Actor, activity::{Accept, Follow as FollowAct, Undo}, actor::Person};
use diesel::{self, PgConnection, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox}, sign::Signer};
use plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox, Deletable}, sign::Signer};
use blogs::Blog;
use notifications::*;
use users::User;
@ -12,19 +12,42 @@ use schema::follows;
pub struct Follow {
pub id: i32,
pub follower_id: i32,
pub following_id: i32
pub following_id: i32,
pub ap_url: String,
}
#[derive(Insertable)]
#[table_name = "follows"]
pub struct NewFollow {
pub follower_id: i32,
pub following_id: i32
pub following_id: i32,
pub ap_url: String,
}
impl Follow {
insert!(follows, NewFollow);
get!(follows);
find_by!(follows, find_by_ap_url, ap_url as String);
pub fn find(conn: &PgConnection, from: i32, to: i32) -> Option<Follow> {
follows::table.filter(follows::follower_id.eq(from))
.filter(follows::following_id.eq(to))
.get_result(conn)
.ok()
}
pub fn into_activity(&self, conn: &PgConnection) -> FollowAct {
let user = User::get(conn, self.follower_id).unwrap();
let target = User::get(conn, self.following_id).unwrap();
let mut act = FollowAct::default();
act.follow_props.set_actor_link::<Id>(user.clone().into_id()).expect("Follow::into_activity: actor error");
act.follow_props.set_object_object(user.into_activity(&*conn)).unwrap();
act.object_props.set_id_string(self.ap_url.clone()).unwrap();
act.object_props.set_to_link(target.clone().into_id()).expect("New Follow error while setting 'to'");
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("New Follow error while setting 'cc'");
act
}
/// from -> The one sending the follow request
/// target -> The target of the request, responding with Accept
@ -36,9 +59,12 @@ impl Follow {
from_id: i32,
target_id: i32
) -> Follow {
let from_url: String = from.clone().into_id().into();
let target_url: String = target.clone().into_id().into();
let res = Follow::insert(conn, NewFollow {
follower_id: from_id,
following_id: target_id
following_id: target_id,
ap_url: format!("{}/follow/{}", from_url, target_url),
});
let mut accept = Accept::default();
@ -77,3 +103,21 @@ impl Notify<PgConnection> for Follow {
});
}
}
impl Deletable<PgConnection, Undo> for Follow {
fn delete(&self, conn: &PgConnection) -> Undo {
diesel::delete(self).execute(conn).expect("Coudn't delete follow");
let mut undo = Undo::default();
undo.undo_props.set_actor_link(User::get(conn, self.follower_id).unwrap().into_id()).expect("Follow::delete: actor error");
undo.object_props.set_id_string(format!("{}/undo", self.ap_url)).expect("Follow::delete: id error");
undo.undo_props.set_object_object(self.into_activity(conn)).expect("Follow::delete: object error");
undo
}
fn delete_id(id: String, conn: &PgConnection) {
if let Some(follow) = Follow::find_by_ap_url(conn, id) {
follow.delete(conn);
}
}
}

View File

@ -33,7 +33,8 @@ pub struct Post {
pub published: bool,
pub license: String,
pub creation_date: NaiveDateTime,
pub ap_url: String
pub ap_url: String,
pub subtitle: String,
}
#[derive(Insertable)]
@ -46,7 +47,8 @@ pub struct NewPost {
pub published: bool,
pub license: String,
pub creation_date: Option<NaiveDateTime>,
pub ap_url: String
pub ap_url: String,
pub subtitle: String,
}
impl Post {
@ -185,6 +187,7 @@ impl Post {
article.object_props.set_attributed_to_link_vec::<Id>(authors).expect("Article::into_activity: attributedTo error");
article.object_props.set_content_string(self.content.get().clone()).expect("Article::into_activity: content error");
article.object_props.set_published_utctime(Utc.from_utc_datetime(&self.creation_date)).expect("Article::into_activity: published error");
article.object_props.set_summary_string(self.subtitle.clone()).expect("Article::into_activity: summary error");
article.object_props.set_tag_link_vec(mentions).expect("Article::into_activity: tag error");
article.object_props.set_url_string(self.ap_url.clone()).expect("Article::into_activity: url error");
article.object_props.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect()).expect("Article::into_activity: to error");
@ -250,7 +253,8 @@ impl FromActivity<Article, PgConnection> for Post {
license: String::from("CC-0"), // TODO
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url: article.object_props.url_string().unwrap_or(article.object_props.id_string().expect("Post::from_activity: url + id error")),
creation_date: Some(article.object_props.published_utctime().expect("Post::from_activity: published error").naive_utc())
creation_date: Some(article.object_props.published_utctime().expect("Post::from_activity: published error").naive_utc()),
subtitle: article.object_props.summary_string().expect("Post::from_activity: summary error")
});
for author in authors.into_iter() {

View File

@ -42,6 +42,7 @@ table! {
id -> Int4,
follower_id -> Int4,
following_id -> Int4,
ap_url -> Text,
}
}
@ -124,6 +125,7 @@ table! {
license -> Varchar,
creation_date -> Timestamp,
ap_url -> Varchar,
subtitle -> Text,
}
}

View File

@ -567,7 +567,7 @@ impl WithInbox for User {
}
fn is_local(&self) -> bool {
self.instance_id == 0
self.instance_id == 1
}
}

View File

@ -508,5 +508,15 @@ msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one"
msgstr ""
#, fuzzy
msgid "Subtitle"
msgstr "Titel"
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""
#~ msgid "Your password should be at least 8 characters long"
#~ msgstr "Das Passwort sollte mindestens 8 Zeichen lang sein"

View File

@ -497,3 +497,12 @@ msgstr ""
msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one"
msgstr ""
msgid "Subtitle"
msgstr ""
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""

View File

@ -503,4 +503,16 @@ msgstr "Envoyer"
msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one"
msgstr "Désolé, mais les inscriptions sont fermées sur cette instance. Essayez d'en trouver une autre."
msgstr ""
"Désolé, mais les inscriptions sont fermées sur cette instance. Essayez d'en "
"trouver une autre."
#, fuzzy
msgid "Subtitle"
msgstr "Titre"
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""

View File

@ -496,4 +496,15 @@ msgstr "Enviar"
msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one"
msgstr ""
"Lamentámolo, pero o rexistro en esta instancia está pechado. Inténteo en outra instancia"
"Lamentámolo, pero o rexistro en esta instancia está pechado. Inténteo en "
"outra instancia"
#, fuzzy
msgid "Subtitle"
msgstr "Título"
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""

View File

@ -512,6 +512,16 @@ msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one"
msgstr ""
#, fuzzy
msgid "Subtitle"
msgstr "Tittel"
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""
#~ msgid "One reshare"
#~ msgid_plural "{{ count }} reshares"
#~ msgstr[0] "Én deling"

108
po/pl.po
View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2018-07-28 14:56+0200\n"
"PO-Revision-Date: 2018-09-04 17:35+0200\n"
"Last-Translator: Marcin Mikołajczak <me@m4sk.in>\n"
"Language-Team: none\n"
"Language: pl\n"
@ -12,7 +12,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
"X-Generator: Poedit 2.0.9\n"
"X-Generator: Poedit 2.1.1\n"
msgid "Latest articles"
msgstr "Najnowsze artykuły"
@ -92,16 +92,15 @@ msgstr "Dodaj swoje"
msgid "One Boost"
msgid_plural "{{ count }} Boosts"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
msgstr[0] "Jedno podbicie"
msgstr[1] "{{ count }} podbicia"
msgstr[2] "{{ count }} podbić"
#, fuzzy
msgid "I don&#x27;t want to boost this anymore"
msgstr "Cofnij udostępnienie"
msgstr "Cofnij podbicie"
msgid "Boost"
msgstr ""
msgstr "Podbij"
msgid "Comments"
msgstr "Komentarze"
@ -165,9 +164,8 @@ msgstr "Obserwuj"
msgid "Unfollow"
msgstr "Przestań obserwować"
#, fuzzy
msgid "Recently boosted"
msgstr "Ostatnio udostępniono"
msgstr "Ostatnio podbite"
msgid "One follower"
msgid_plural "{{ count }} followers"
@ -247,9 +245,8 @@ msgstr "Musisz się zalogować, aby zobaczyć swoje powiadomienia"
msgid "You need to be logged in order to write a new post"
msgstr "Musisz się zalogować, aby utworzyć wpis"
#, fuzzy
msgid "You need to be logged in order to boost a post"
msgstr "Musisz się zalogować, aby polubić wpis"
msgstr "Musisz się zalogować, aby podbić wpis"
msgid "Invalid username or password"
msgstr "Nieprawidłowa nazwa użytkownika lub hasło"
@ -268,18 +265,17 @@ msgstr ""
"Napisano przez {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}"
"{{ link_3 }}"
#, fuzzy
msgid "{{ data }} boosted your article"
msgstr "{{ data }} skomentował Twój artykuł"
msgstr "{{ data }} podbił(a) Twój artykuł"
msgid "{{ data }} started following you"
msgstr "{{ data }} zaczął Cię obserwować"
msgstr "{{ data }} zaczął(-ęła) Cię obserwować"
msgid "{{ data }} liked your article"
msgstr "{{ data }} polubił Twój artykuł"
msgstr "{{ data }} polubił(a) Twój artykuł"
msgid "{{ data }} commented your article"
msgstr "{{ data }} skomentował Twój artykuł"
msgstr "{{ data }} skomentował(a) Twój artykuł"
msgid "We couldn&#x27;t find this page."
msgstr "Nie udało się odnaleźć tej strony."
@ -294,7 +290,7 @@ msgid "You are not author in this blog."
msgstr "Nie jesteś autorem tego bloga."
msgid "{{ data }} mentioned you."
msgstr "{{ data }} wspomniał o Tobie."
msgstr "{{ data }} wspomniał(a) o Tobie."
msgid "Your comment"
msgstr "Twój komentarz"
@ -318,9 +314,8 @@ msgid "We need an email or a username to identify you"
msgstr ""
"Potrzebujemy nazwy użytkownika lub adresu e-mail, aby Cię zidentyfikować"
#, fuzzy
msgid "Your password can't be empty"
msgstr "Twój komentarz nie może być pusty"
msgstr "Twoje hasło nie może być puste"
msgid "Passwords are not matching"
msgstr "Hasła nie pasują do siebie"
@ -361,20 +356,19 @@ msgid "Next page"
msgstr "Następna strona"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} wspomniał o Tobie."
msgstr "{{ user }} wspomniał(a) o Tobie."
msgid "{{ user }} commented your article."
msgstr "{{ user }} skomentował Twój artykuł."
msgstr "{{ user }} skomentował(a) Twój artykuł."
msgid "{{ user }} is now following you."
msgstr "{{ user }} zaczął Cię obserwować."
msgstr "{{ user }} zaczął(-ęła) Cię obserwować."
msgid "{{ user }} liked your article."
msgstr "{{ user }} polubił Twój artykuł."
msgstr "{{ user }} polubił(a) Twój artykuł."
#, fuzzy
msgid "{{ user }} boosted your article."
msgstr "{{ user }} skomentował Twój artykuł."
msgstr "{{ user }} podbił(a) Twój artykuł."
msgid "Source code"
msgstr "Kod źródłowy"
@ -441,7 +435,7 @@ msgid "people"
msgstr "osób"
msgid "Who wrote"
msgstr "Którzy napisali"
msgstr "Które napisały"
msgid "articles"
msgstr "artykuły"
@ -449,79 +443,83 @@ msgstr "artykuły"
msgid "Read the detailed rules"
msgstr "Przeczytaj szczegółowe zasady"
#, fuzzy
msgid "Delete this article"
msgstr "Najnowsze artykuły"
msgstr "Usuń ten artykuł"
msgid "And connected to"
msgstr ""
msgstr "Połączony z"
#, fuzzy
msgid "other instances"
msgstr "O tej instancji"
msgstr "innych instancji"
#, fuzzy
msgid "Administred by"
msgstr "Administracja"
msgstr "Administrowany przez"
msgid "Runs Plume {{ version }}"
msgstr ""
msgstr "Działa na Plume {{ version }}"
#, fuzzy
msgid "Your media"
msgstr "Twój komentarz"
msgstr "Twoja zawartość multimedialna"
msgid "Go to your gallery"
msgstr ""
msgstr "Przejdź do swojej galerii"
msgid "{{ name}}'s avatar'"
msgstr ""
msgstr "Awatar {{name}}"
msgid "Media details"
msgstr ""
msgstr "Szczegóły zawartości multimedialnej"
msgid "Go back to the gallery"
msgstr ""
msgstr "Powróć do galerii"
#, fuzzy
msgid "Markdown code"
msgstr "Markdown jest obsługiwany"
msgstr "Kod Markdown"
msgid "Copy it in your articles to insert this media."
msgstr ""
msgstr "Skopiuj do swoich artykułów, aby wstawić tę zawartość multimedialną."
msgid "Use as avatar"
msgstr ""
msgstr "Użyj jako awataru"
msgid "Delete"
msgstr ""
msgstr "Usuń"
msgid "Upload"
msgstr ""
msgstr "Wyślij"
msgid "You don't have any media yet."
msgstr ""
msgstr "Nie masz żadnej zawartości multimedialnej."
msgid "Media upload"
msgstr ""
msgstr "Wysyłanie zawartości multimedialnej"
#, fuzzy
msgid "Description"
msgstr "Szczegółowy opis"
msgstr "Opis"
#, fuzzy
msgid "Content warning"
msgstr "Zawartość"
msgstr "Ostrzeżenie o zawartości"
msgid "File"
msgstr ""
msgstr "Plik"
msgid "Send"
msgstr ""
msgstr "Wyślij"
msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one"
msgstr ""
"Przepraszamy, rejestracja jest zamknięta na tej instancji. Spróbuj znaleźć "
"inną"
msgid "Subtitle"
msgstr "Podtytuł"
msgid "Login to like"
msgstr "Zaloguj się aby polubić"
msgid "Login to boost"
msgstr "Zaloguj się aby podbić"
#~ msgid "One reshare"
#~ msgid_plural "{{ count }} reshares"

View File

@ -486,3 +486,12 @@ msgstr ""
msgid "Sorry, but registrations are closed on this instance. Try to find another one"
msgstr ""
msgid "Subtitle"
msgstr ""
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""

View File

@ -55,6 +55,10 @@ pub trait Inbox {
"Announce" => {
Reshare::delete_id(act.undo_props.object_object::<Announce>()?.object_props.id_string()?, conn);
Ok(())
},
"Follow" => {
Follow::delete_id(act.undo_props.object_object::<Like>()?.object_props.id_string()?, conn);
Ok(())
}
_ => Err(InboxError::CantUndo)?
}

View File

@ -57,7 +57,8 @@ fn details_response(blog: String, slug: String, conn: DbConn, user: Option<User>
"date": &post.creation_date.timestamp(),
"previous": query.and_then(|q| q.responding_to.map(|r| Comment::get(&*conn, r).expect("Error retrieving previous comment").to_json(&*conn, &vec![]))),
"user_fqn": user.clone().map(|u| u.get_fqn(&*conn)).unwrap_or(String::new()),
"is_author": user.map(|u| post.get_authors(&*conn).into_iter().any(|a| u.id == a.id)).unwrap_or(false)
"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)
}))
})
})
@ -98,6 +99,7 @@ fn new(blog: String, user: User, conn: DbConn) -> Template {
struct NewPostForm {
#[validate(custom(function = "valid_slug", message = "Invalid title"))]
pub title: String,
pub subtitle: String,
pub content: String,
pub license: String
}
@ -150,7 +152,8 @@ fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: D
Instance::get_local(&*conn).map(|i| i.default_license).unwrap_or(String::from("CC-0"))
},
ap_url: "".to_string(),
creation_date: None
creation_date: None,
subtitle: form.subtitle.clone()
});
let post = post.update_ap_url(&*conn);
PostAuthor::insert(&*conn, NewPostAuthor {

View File

@ -1,5 +1,5 @@
use activitypub::{
activity::{Create, Follow},
activity::Create,
collection::OrderedCollection,
object::Article
};
@ -16,7 +16,7 @@ use workerpool::thunk::*;
use plume_common::activity_pub::{
ActivityStream, broadcast, Id, IntoId, ApRequest,
inbox::{FromActivity, Notify}
inbox::{FromActivity, Notify, Deletable}
};
use plume_common::utils;
use plume_models::{
@ -71,7 +71,8 @@ fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fe
.unwrap_or_else(|| User::fetch_from_url(&*fecth_followers_conn, user_id).expect("Couldn't fetch follower"));
follows::Follow::insert(&*fecth_followers_conn, follows::NewFollow {
follower_id: follower.id,
following_id: user_clone.id
following_id: user_clone.id,
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
});
}
}));
@ -116,20 +117,20 @@ fn dashboard_auth() -> Flash<Redirect> {
#[get("/@/<name>/follow")]
fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Redirect {
let target = User::find_by_fqn(&*conn, name.clone()).unwrap();
if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) {
let delete_act = follow.delete(&*conn);
worker.execute(Thunk::of(move || broadcast(&user, delete_act, vec![target])));
} else {
let f = follows::Follow::insert(&*conn, follows::NewFollow {
follower_id: user.id,
following_id: target.id
following_id: target.id,
ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url),
});
f.notify(&*conn);
let mut act = Follow::default();
act.follow_props.set_actor_link::<Id>(user.clone().into_id()).unwrap();
act.follow_props.set_object_object(user.into_activity(&*conn)).unwrap();
act.object_props.set_id_string(format!("{}/follow/{}", user.ap_url, target.ap_url)).unwrap();
act.object_props.set_to_link(target.clone().into_id()).expect("New Follow error while setting 'to'");
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("New Follow error while setting 'cc'");
let act = f.into_activity(&*conn);
worker.execute(Thunk::of(move || broadcast(&user, act, vec![target])));
}
Redirect::to(uri!(details: name = name))
}

View File

@ -192,9 +192,18 @@
}
article img {
display: block;
margin: 3em auto;
max-width: 100%;
}
article pre {
padding: 1em;
background: #DADADA;
overflow: auto;
border-radius: 5px;
}
/* Article.Meta */
main .article-meta, main .article-meta button {
@ -235,8 +244,8 @@
font-size: 1.5em;
}
main .article-meta .likes button,
main .article-meta .reshares button {
main .article-meta .likes .action,
main .article-meta .reshares .action {
display: flex;
flex-direction: column;
align-items: center;
@ -246,15 +255,16 @@
background: none;
color: #242424;
border: none;
font-size: 1.1em;
}
main .article-meta .likes > p,
main .article-meta .likes button:hover { color: #E92F2F; }
main .article-meta .likes .action:hover { color: #E92F2F; }
main .article-meta .reshares > p,
main .article-meta .reshares button:hover { color: #7765E3; }
main .article-meta .reshares .action:hover { color: #7765E3; }
main .article-meta .likes button svg.feather,
main .article-meta .reshares button i {
main .article-meta .likes .action svg.feather,
main .article-meta .reshares .action i {
transition: background 0.1s ease-in;
display: flex;
align-items: center;
@ -267,39 +277,39 @@
border-radius: 50%;
}
main .article-meta .likes button svg.feather {
main .article-meta .likes .action svg.feather {
padding: 0.7em;
box-sizing: border-box;
color: #E92F2F;
fill: none;
border: solid #E92F2F thin;
}
main .article-meta .likes button:hover svg.feather {
main .article-meta .likes .action:hover svg.feather {
background: rgba(233, 47, 47, 0.15);
}
main .article-meta .reshares button i {
main .article-meta .reshares .action i {
color: #7765E3;
border: solid #7765E3 thin;
font-weight: 600;
}
main .article-meta .reshares button:hover i {
main .article-meta .reshares .action:hover i {
background: rgba(119, 101, 227, 0.15);
}
main .article-meta .likes button.liked svg.feather { background: #E92F2F; fill: currentColor; }
main .article-meta .likes button.liked:hover svg.feather {
main .article-meta .likes .action.liked svg.feather { background: #E92F2F; fill: currentColor; }
main .article-meta .likes .action.liked:hover svg.feather {
background: rgba(233, 47, 47, 0.25);
color: #E92F2F;
}
main .article-meta .reshares button.reshared i { background: #7765E3; }
main .article-meta .reshares button.reshared:hover i {
main .article-meta .reshares .action.reshared i { background: #7765E3; }
main .article-meta .reshares .action.reshared:hover i {
background: rgba(119, 101, 227, 0.25);
color: #7765E3;
}
main .article-meta .likes button.liked svg.feather,
main .article-meta .reshares button.reshared i {
main .article-meta .likes .action.liked svg.feather,
main .article-meta .reshares .action.reshared i {
color: #F4F4F4;
font-weight: 900;
}
@ -796,5 +806,5 @@
}
.avatar.padded {
margin-right: 1em;
margin-right: 2rem;
}

View File

@ -30,7 +30,7 @@
<span class="mobile-label">{{ "Notifications" | _ }}</span>
</a>
<a href="/logout">
<i class="icon icon-log-out aria-label="{{ "Log Out" | _ }}"></i>
<i class="icon icon-log-out" aria-label="{{ "Log Out" | _ }}"></i>
<span class="mobile-label">{{ "Log Out" | _ }}</span>
</a>
<a href="/me">

View File

@ -2,7 +2,13 @@
<div class="card">
<h3><a href="{{ article.url }}">{{ article.post.title }}</a></h3>
<main
<p>{{ article.post.content | safe | striptags | truncate(length=200) }}</p>
<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 }}" | _(

View File

@ -11,6 +11,7 @@
{% block content %}
<h1 class="article">{{ article.post.title }}</h1>
<h2 class="article">{{ article.post.subtitle }}</h2>
<p class="article-info">
<span class="author">{{ "Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}" | _(
link_1='<a href="/@/',
@ -33,6 +34,20 @@
<div class="article-meta">
<p>{{ "This article is under the {{ license }} license." | _(license=article.post.license) }}</p>
<div class="flex">
<img src="{{ author.avatar }}" alt="{{ author.name }}" class="avatar medium padded">
<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">
@ -40,23 +55,34 @@
<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="liked">{{ macros::feather(name="heart") }}{{ "I don't like this anymore" | _ }}</button>
<button type="submit" class="action liked">{{ macros::feather(name="heart") }}{{ "I don't like this anymore" | _ }}</button>
{% else %}
<button type="submit">{{ macros::feather(name="heart") }}{{ "Add yours" | _ }}</button>
<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="reshared"><i class="icon icon-repeat"></i>{{ "I don't want to boost this anymore" | _ }}</button>
<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"><i class="icon icon-repeat"></i>{{ "Boost" | _ }}</button>
<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">

View File

@ -9,6 +9,7 @@
<h1>{{ "Create a post" | _ }}</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 %}