Hide cw pictures behind a summary/details (#483)
* Hide cw pictures behind a summary/details * refactor md_to_html a bit and add cw support * use random id for cw checkbox
This commit is contained in:
parent
eabe73ddc0
commit
12c2078c89
@ -56,53 +56,117 @@ fn to_inline(tag: Tag) -> Tag {
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> {
|
||||
let (s, res) = match evt {
|
||||
Event::Text(txt) => match state.take() {
|
||||
Some(mut prev_txt) => {
|
||||
prev_txt.push_str(&txt);
|
||||
(Some(prev_txt), vec![])
|
||||
}
|
||||
None => (Some(txt.into_owned()), vec![]),
|
||||
},
|
||||
e => match state.take() {
|
||||
Some(prev) => (None, vec![Event::Text(Cow::Owned(prev)), e]),
|
||||
None => (None, vec![e]),
|
||||
},
|
||||
};
|
||||
*state = s;
|
||||
Some(res)
|
||||
}
|
||||
|
||||
fn inline_tags<'a>(
|
||||
(state, inline): &mut (Vec<Tag<'a>>, bool),
|
||||
evt: Event<'a>,
|
||||
) -> Option<Event<'a>> {
|
||||
if *inline {
|
||||
let new_evt = match evt {
|
||||
Event::Start(t) => {
|
||||
let tag = to_inline(t);
|
||||
state.push(tag.clone());
|
||||
Event::Start(tag)
|
||||
}
|
||||
Event::End(t) => match state.pop() {
|
||||
Some(other) => Event::End(other),
|
||||
None => Event::End(t),
|
||||
},
|
||||
e => e,
|
||||
};
|
||||
Some(new_evt)
|
||||
} else {
|
||||
Some(evt)
|
||||
}
|
||||
}
|
||||
|
||||
pub type MediaProcessor<'a> = Box<'a + Fn(i32) -> Option<(String, Option<String>)>>;
|
||||
|
||||
fn process_image<'a, 'b>(
|
||||
evt: Event<'a>,
|
||||
inline: bool,
|
||||
processor: &Option<MediaProcessor<'b>>,
|
||||
) -> Event<'a> {
|
||||
if let Some(ref processor) = *processor {
|
||||
match evt {
|
||||
Event::Start(Tag::Image(id, title)) => {
|
||||
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
|
||||
if inline || cw.is_none() {
|
||||
Event::Start(Tag::Image(Cow::Owned(url), title))
|
||||
} else {
|
||||
// there is a cw, and where are not inline
|
||||
Event::Html(Cow::Owned(format!(
|
||||
r#"<label for="postcontent-cw-{id}">
|
||||
<input type="checkbox" id="postcontent-cw-{id}" checked="checked" class="cw-checkbox">
|
||||
<span class="cw-container">
|
||||
<span class="cw-text">
|
||||
{cw}
|
||||
</span>
|
||||
<img src="{url}" alt=""#,
|
||||
id = random_hex(),
|
||||
cw = cw.unwrap(),
|
||||
url = url
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
Event::Start(Tag::Image(id, title))
|
||||
}
|
||||
}
|
||||
Event::End(Tag::Image(id, title)) => {
|
||||
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
|
||||
if inline || cw.is_none() {
|
||||
Event::End(Tag::Image(Cow::Owned(url), title))
|
||||
} else {
|
||||
Event::Html(Cow::Borrowed(
|
||||
r#""/>
|
||||
</span>
|
||||
</label>"#,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Event::End(Tag::Image(id, title))
|
||||
}
|
||||
}
|
||||
e => e,
|
||||
}
|
||||
} else {
|
||||
evt
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns (HTML, mentions, hashtags)
|
||||
pub fn md_to_html(
|
||||
pub fn md_to_html<'a>(
|
||||
md: &str,
|
||||
base_url: &str,
|
||||
inline: bool,
|
||||
media_processor: Option<MediaProcessor<'a>>,
|
||||
) -> (String, HashSet<String>, HashSet<String>) {
|
||||
let parser = Parser::new_ext(md, Options::all());
|
||||
|
||||
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
|
||||
.scan(None, |state: &mut Option<String>, evt| {
|
||||
let (s, res) = match evt {
|
||||
Event::Text(txt) => match state.take() {
|
||||
Some(mut prev_txt) => {
|
||||
prev_txt.push_str(&txt);
|
||||
(Some(prev_txt), vec![])
|
||||
}
|
||||
None => (Some(txt.into_owned()), vec![]),
|
||||
},
|
||||
e => match state.take() {
|
||||
Some(prev) => (None, vec![Event::Text(Cow::Owned(prev)), e]),
|
||||
None => (None, vec![e]),
|
||||
},
|
||||
};
|
||||
*state = s;
|
||||
Some(res)
|
||||
})
|
||||
// Flatten text because pulldown_cmark break #hashtag in two individual text elements
|
||||
.scan(None, flatten_text)
|
||||
.flat_map(IntoIterator::into_iter)
|
||||
.map(|evt| process_image(evt, inline, &media_processor))
|
||||
// Ignore headings, images, and tables if inline = true
|
||||
.scan(vec![], |state: &mut Vec<Tag>, evt| {
|
||||
if inline {
|
||||
let new_evt = match evt {
|
||||
Event::Start(t) => {
|
||||
let tag = to_inline(t);
|
||||
state.push(tag.clone());
|
||||
Event::Start(tag)
|
||||
}
|
||||
Event::End(t) => match state.pop() {
|
||||
Some(other) => Event::End(other),
|
||||
None => Event::End(t),
|
||||
},
|
||||
e => e,
|
||||
};
|
||||
Some(new_evt)
|
||||
} else {
|
||||
Some(evt)
|
||||
}
|
||||
})
|
||||
.scan((vec![], inline), inline_tags)
|
||||
.map(|evt| match evt {
|
||||
Event::Text(txt) => {
|
||||
let (evts, _, _, _, new_mentions, new_hashtags) = txt.chars().fold(
|
||||
@ -273,7 +337,7 @@ mod tests {
|
||||
|
||||
for (md, mentions) in tests {
|
||||
assert_eq!(
|
||||
md_to_html(md, "", false).1,
|
||||
md_to_html(md, "", false, None).1,
|
||||
mentions
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
@ -298,7 +362,7 @@ mod tests {
|
||||
|
||||
for (md, mentions) in tests {
|
||||
assert_eq!(
|
||||
md_to_html(md, "", false).2,
|
||||
md_to_html(md, "", false, None).2,
|
||||
mentions
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
@ -310,11 +374,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_inline() {
|
||||
assert_eq!(
|
||||
md_to_html("# Hello", "", false).0,
|
||||
md_to_html("# Hello", "", false, None).0,
|
||||
String::from("<h1>Hello</h1>\n")
|
||||
);
|
||||
assert_eq!(
|
||||
md_to_html("# Hello", "", true).0,
|
||||
md_to_html("# Hello", "", true, None).0,
|
||||
String::from("<p>Hello</p>\n")
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ use std::collections::HashSet;
|
||||
|
||||
use comment_seers::{CommentSeers, NewCommentSeers};
|
||||
use instance::Instance;
|
||||
use medias::Media;
|
||||
use mentions::Mention;
|
||||
use notifications::*;
|
||||
use plume_common::activity_pub::{
|
||||
@ -102,14 +103,16 @@ impl Comment {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
|
||||
pub fn to_activity<'b>(&self, conn: &'b Connection) -> Result<Note> {
|
||||
let author = User::get(conn, self.author_id)?;
|
||||
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||
self.content.get().as_ref(),
|
||||
&Instance::get_local(conn)?.public_domain,
|
||||
true,
|
||||
Some(Media::get_media_processor(conn, vec![&author])),
|
||||
);
|
||||
|
||||
let author = User::get(conn, self.author_id)?;
|
||||
let mut note = Note::default();
|
||||
let to = vec![Id::new(PUBLIC_VISIBILTY.to_string())];
|
||||
|
||||
|
@ -3,6 +3,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use std::iter::Iterator;
|
||||
|
||||
use ap_url;
|
||||
use medias::Media;
|
||||
use plume_common::utils::md_to_html;
|
||||
use safe_string::SafeString;
|
||||
use schema::{instances, users};
|
||||
@ -128,8 +129,18 @@ impl Instance {
|
||||
short_description: SafeString,
|
||||
long_description: SafeString,
|
||||
) -> Result<()> {
|
||||
let (sd, _, _) = md_to_html(short_description.as_ref(), &self.public_domain, true);
|
||||
let (ld, _, _) = md_to_html(long_description.as_ref(), &self.public_domain, false);
|
||||
let (sd, _, _) = md_to_html(
|
||||
short_description.as_ref(),
|
||||
&self.public_domain,
|
||||
true,
|
||||
Some(Media::get_media_processor(conn, vec![])),
|
||||
);
|
||||
let (ld, _, _) = md_to_html(
|
||||
long_description.as_ref(),
|
||||
&self.public_domain,
|
||||
false,
|
||||
Some(Media::get_media_processor(conn, vec![])),
|
||||
);
|
||||
diesel::update(self)
|
||||
.set((
|
||||
instances::name.eq(name),
|
||||
|
@ -5,7 +5,7 @@ use guid_create::GUID;
|
||||
use reqwest;
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use plume_common::activity_pub::Id;
|
||||
use plume_common::{activity_pub::Id, utils::MediaProcessor};
|
||||
|
||||
use instance::Instance;
|
||||
use safe_string::SafeString;
|
||||
@ -124,10 +124,9 @@ impl Media {
|
||||
}
|
||||
|
||||
pub fn markdown(&self, conn: &Connection) -> Result<SafeString> {
|
||||
let url = self.url(conn)?;
|
||||
Ok(match self.category() {
|
||||
MediaCategory::Image => {
|
||||
SafeString::new(&format!("![{}]({})", escape(&self.alt_text), url))
|
||||
SafeString::new(&format!("![{}]({})", escape(&self.alt_text), self.id))
|
||||
}
|
||||
MediaCategory::Audio | MediaCategory::Video => self.html(conn)?,
|
||||
MediaCategory::Unknown => SafeString::new(""),
|
||||
@ -225,6 +224,19 @@ impl Media {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> {
|
||||
let uid = user.iter().map(|u| u.id).collect::<Vec<_>>();
|
||||
Box::new(move |id| {
|
||||
let media = Media::get(conn, id).ok()?;
|
||||
// if owner is user or check is disabled
|
||||
if uid.contains(&media.owner_id) || uid.is_empty() {
|
||||
Some((media.url(conn).ok()?, media.content_warning))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -207,17 +207,19 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
let domain = &Instance::get_local(&conn)
|
||||
.map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))?
|
||||
.public_domain;
|
||||
let (content, mentions, hashtags) = md_to_html(
|
||||
query.source.clone().unwrap_or_default().clone().as_ref(),
|
||||
domain,
|
||||
false,
|
||||
);
|
||||
|
||||
let author = User::get(
|
||||
conn,
|
||||
user_id.expect("<Post as Provider>::create: no user_id error"),
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Author not found".into()))?;
|
||||
|
||||
let (content, mentions, hashtags) = md_to_html(
|
||||
query.source.clone().unwrap_or_default().clone().as_ref(),
|
||||
domain,
|
||||
false,
|
||||
Some(Media::get_media_processor(conn, vec![&author])),
|
||||
);
|
||||
|
||||
let blog = match query.blog_id {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
@ -757,7 +759,7 @@ impl Post {
|
||||
post.license = license;
|
||||
}
|
||||
|
||||
let mut txt_hashtags = md_to_html(&post.source, "", false)
|
||||
let mut txt_hashtags = md_to_html(&post.source, "", false, None)
|
||||
.2
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
@ -995,7 +997,7 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
||||
}
|
||||
|
||||
// save mentions and tags
|
||||
let mut hashtags = md_to_html(&post.source, "", false)
|
||||
let mut hashtags = md_to_html(&post.source, "", false, None)
|
||||
.2
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
|
@ -19,7 +19,7 @@ lazy_static! {
|
||||
static ref CLEAN: Builder<'static> = {
|
||||
let mut b = Builder::new();
|
||||
b.add_generic_attributes(iter::once("id"))
|
||||
.add_tags(&["iframe", "video", "audio"])
|
||||
.add_tags(&["iframe", "video", "audio", "label", "input"])
|
||||
.id_prefix(Some("postcontent-"))
|
||||
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
|
||||
.add_tag_attributes(
|
||||
@ -27,7 +27,23 @@ lazy_static! {
|
||||
["width", "height", "src", "frameborder"].iter().cloned(),
|
||||
)
|
||||
.add_tag_attributes("video", ["src", "title", "controls"].iter())
|
||||
.add_tag_attributes("audio", ["src", "title", "controls"].iter());
|
||||
.add_tag_attributes("audio", ["src", "title", "controls"].iter())
|
||||
.add_tag_attributes("label", ["for"].iter())
|
||||
.add_tag_attributes("input", ["type", "checked"].iter())
|
||||
.add_allowed_classes("input", ["cw-checkbox"].iter())
|
||||
.add_allowed_classes("span", ["cw-container", "cw-text"].iter())
|
||||
.attribute_filter(|elem, att, val| match (elem, att) {
|
||||
("input", "type") => Some("checkbox".into()),
|
||||
("input", "checked") => Some("checked".into()),
|
||||
("label", "for") => {
|
||||
if val.starts_with("postcontent-cw-") {
|
||||
Some(val.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => Some(val.into()),
|
||||
});
|
||||
b
|
||||
};
|
||||
}
|
||||
|
@ -209,7 +209,13 @@ impl User {
|
||||
.set((
|
||||
users::display_name.eq(name),
|
||||
users::email.eq(email),
|
||||
users::summary_html.eq(utils::md_to_html(&summary, "", false).0),
|
||||
users::summary_html.eq(utils::md_to_html(
|
||||
&summary,
|
||||
"",
|
||||
false,
|
||||
Some(Media::get_media_processor(conn, vec![self])),
|
||||
)
|
||||
.0),
|
||||
users::summary.eq(summary),
|
||||
))
|
||||
.execute(conn)?;
|
||||
@ -868,7 +874,7 @@ impl NewUser {
|
||||
display_name,
|
||||
is_admin,
|
||||
summary: summary.to_owned(),
|
||||
summary_html: SafeString::new(&utils::md_to_html(&summary, "", false).0),
|
||||
summary_html: SafeString::new(&utils::md_to_html(&summary, "", false, None).0),
|
||||
email: Some(email),
|
||||
hashed_password: Some(password),
|
||||
instance_id: Instance::get_local(conn)?.id,
|
||||
|
@ -280,7 +280,21 @@ pub fn update(
|
||||
|
||||
blog.title = form.title.clone();
|
||||
blog.summary = form.summary.clone();
|
||||
blog.summary_html = SafeString::new(&utils::md_to_html(&form.summary, "", true).0);
|
||||
blog.summary_html = SafeString::new(
|
||||
&utils::md_to_html(
|
||||
&form.summary,
|
||||
"",
|
||||
true,
|
||||
Some(Media::get_media_processor(
|
||||
&conn,
|
||||
blog.list_authors(&conn)
|
||||
.expect("Couldn't get list of authors")
|
||||
.iter()
|
||||
.collect(),
|
||||
)),
|
||||
)
|
||||
.0,
|
||||
);
|
||||
blog.icon_id = form.icon;
|
||||
blog.banner_id = form.banner;
|
||||
blog.save_changes::<Blog>(&*conn)
|
||||
|
@ -15,8 +15,8 @@ use plume_common::{
|
||||
utils,
|
||||
};
|
||||
use plume_models::{
|
||||
blogs::Blog, comments::*, db_conn::DbConn, instance::Instance, mentions::Mention, posts::Post,
|
||||
safe_string::SafeString, tags::Tag, users::User,
|
||||
blogs::Blog, comments::*, db_conn::DbConn, instance::Instance, medias::Media,
|
||||
mentions::Mention, posts::Post, safe_string::SafeString, tags::Tag, users::User,
|
||||
};
|
||||
use routes::errors::ErrorPage;
|
||||
use Worker;
|
||||
@ -49,6 +49,7 @@ pub fn create(
|
||||
.expect("comments::create: local instance error")
|
||||
.public_domain,
|
||||
true,
|
||||
Some(Media::get_media_processor(&conn, vec![&user])),
|
||||
);
|
||||
let comm = Comment::insert(
|
||||
&*conn,
|
||||
|
@ -264,6 +264,13 @@ pub fn update(
|
||||
.expect("posts::update: Error getting local instance")
|
||||
.public_domain,
|
||||
false,
|
||||
Some(Media::get_media_processor(
|
||||
&conn,
|
||||
b.list_authors(&conn)
|
||||
.expect("Could not get author list")
|
||||
.iter()
|
||||
.collect(),
|
||||
)),
|
||||
);
|
||||
|
||||
// update publication date if when this article is no longer a draft
|
||||
@ -424,6 +431,13 @@ pub fn create(
|
||||
.expect("post::create: local instance error")
|
||||
.public_domain,
|
||||
false,
|
||||
Some(Media::get_media_processor(
|
||||
&conn,
|
||||
blog.list_authors(&conn)
|
||||
.expect("Could not get author list")
|
||||
.iter()
|
||||
.collect(),
|
||||
)),
|
||||
);
|
||||
|
||||
let searcher = rockets.searcher;
|
||||
|
@ -322,3 +322,36 @@ main .article-meta {
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
|
||||
// content warning
|
||||
.cw-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.cw-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="checkbox"].cw-checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input:checked ~ .cw-container:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
input:checked ~ .cw-container > .cw-text {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
color: white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user