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,16 +56,7 @@ fn to_inline(tag: Tag) -> Tag { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Returns (HTML, mentions, hashtags)
 | ||||
| pub fn md_to_html( | ||||
|     md: &str, | ||||
|     base_url: &str, | ||||
|     inline: bool, | ||||
| ) -> (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| { | ||||
| 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) => { | ||||
| @ -81,11 +72,13 @@ pub fn md_to_html( | ||||
|     }; | ||||
|     *state = s; | ||||
|     Some(res) | ||||
|         }) | ||||
|         .flat_map(IntoIterator::into_iter) | ||||
|         // Ignore headings, images, and tables if inline = true
 | ||||
|         .scan(vec![], |state: &mut Vec<Tag>, evt| { | ||||
|             if inline { | ||||
| } | ||||
| 
 | ||||
| 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); | ||||
| @ -102,7 +95,78 @@ pub fn md_to_html( | ||||
|     } 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<'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 | ||||
|         // 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![], 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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user