diff --git a/web/index.html b/web/index.html index 596e4386..3304329e 100644 --- a/web/index.html +++ b/web/index.html @@ -94,6 +94,9 @@ article p { margin: 0 0 0 .5em; } + article.float-container { + overflow-y: hidden; + } b.displayname { overflow-wrap: break-word; } @@ -132,15 +135,16 @@ position: sticky; background-color: var(--background); } - div.border { - border: 1px dashed var(--accent); - } span.border-button { border: 1px solid var(--background-dim); } span.border-button:hover { background-color: var(--background-dim); } + div.border, + span.border { + border: 1px dashed var(--accent); + } div.inline { display: inline; } @@ -203,18 +207,25 @@ min-width: 2em; max-width: 2em; } - div.flex-pic-container { - flex: 1; - border: solid 3px #bf616a; - margin-right: .5em; + img.flex-pic { + float: left; + width: 10em; + height: 10em; + object-fit: cover; + margin-right: 1em; + margin-top: .5em; + margin-bottom: .5em; + margin-left: .5em; } - div.flex-pic { - background-size: cover; - margin: 5px; - height: calc(100% - 10px); /* TODO can we avoid this calc() without having this overflow??? */ + img.flex-pic-expand { + width: unset; + height: unset; + max-width: calc(100% - 1.5em); + max-height: 55vh; } .box { border: 3px solid var(--accent); + box-sizing: border-box; } .cursor { cursor: pointer; diff --git a/web/src/components/activity.rs b/web/src/activities/item.rs similarity index 95% rename from web/src/components/activity.rs rename to web/src/activities/item.rs index 09721f80..d2e09e4f 100644 --- a/web/src/components/activity.rs +++ b/web/src/activities/item.rs @@ -55,14 +55,14 @@ pub fn Item( match item.object_type().unwrap_or(apb::ObjectType::Object) { // special case for placeholder activities apb::ObjectType::Note | apb::ObjectType::Document(_) => - Some(view! { {sep.clone()} }.into_view()), + Some(view! { {sep.clone()} }.into_view()), // everything else apb::ObjectType::Activity(t) => { let object_id = item.object().id().str().unwrap_or_default(); let object = match t { apb::ActivityType::Create | apb::ActivityType::Announce => cache::OBJECTS.get(&object_id).map(|obj| { - view! { } + view! { } }.into_view()), apb::ActivityType::Follow => cache::OBJECTS.get(&object_id).map(|obj| { diff --git a/web/src/activities/mod.rs b/web/src/activities/mod.rs new file mode 100644 index 00000000..a35e98d6 --- /dev/null +++ b/web/src/activities/mod.rs @@ -0,0 +1 @@ +pub mod item; diff --git a/web/src/components/mod.rs b/web/src/components/mod.rs index c4301994..b331057d 100644 --- a/web/src/components/mod.rs +++ b/web/src/components/mod.rs @@ -1,15 +1,9 @@ mod login; pub use login::*; -mod activity; -pub use activity::*; - mod navigation; pub use navigation::*; -mod object; -pub use object::*; - mod user; pub use user::*; diff --git a/web/src/lib.rs b/web/src/lib.rs index af53682c..cb059e0d 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -3,12 +3,14 @@ mod app; mod components; mod page; mod config; -mod objects; mod actors; -mod getters; +mod activities; +mod objects; mod timeline; +mod getters; + pub use app::App; pub use config::Config; pub use auth::Auth; diff --git a/web/src/objects/attachment.rs b/web/src/objects/attachment.rs new file mode 100644 index 00000000..d19a6e2a --- /dev/null +++ b/web/src/objects/attachment.rs @@ -0,0 +1,100 @@ +use leptos::*; +use crate::{prelude::*, URL_SENSITIVE}; + +use apb::{field::OptionalString, target::Addressed, ActivityMut, Base, Collection, CollectionMut, Document, Object, ObjectMut}; + +#[component] +pub fn Attachment( + object: serde_json::Value, + #[prop(optional)] + sensitive: bool +) -> impl IntoView { + let config = use_context::>().expect("missing config context"); + let (expand, set_expand) = create_signal(false); + let href = object.url().id().str().unwrap_or_default(); + let media_type = object.media_type() + .unwrap_or("link") // TODO make it an Option rather than defaulting to link everywhere + .to_string(); + let mut kind = media_type + .split('/') + .next() + .unwrap_or("link") + .to_string(); + + // TODO in theory we should match on document_type, but mastodon and misskey send all attachments + // as "Documents" regardless of type, so we're forced to ignore the actual AP type and just match + // using media_type, uffff + // + // those who correctly send Image type objects without a media type get shown as links here, this + // is a dirty fix to properly display as images + if kind == "link" && matches!(object.document_type(), Ok(apb::DocumentType::Image)) { + kind = "image".to_string(); + } + + match kind.as_str() { + "image" => + view! { +

+ +

+ }.into_view(), + + "video" => { + let _href = href.clone(); + view! { +
+ +
+ }.into_view() + }, + + "audio" => + view! { +

+ +

+ }.into_view(), + + "link" | "text" => + view! { +

+ + {Uri::pretty(&href, 50)} + +

+ }.into_view(), + + _ => + view! { +

+ + {media_type} + + {object.name().map(|name| { + view! {

{name.to_string()}

} + })} +

+ }.into_view(), + } +} + + diff --git a/web/src/components/object.rs b/web/src/objects/item.rs similarity index 69% rename from web/src/components/object.rs rename to web/src/objects/item.rs index 9afaab72..ed27fa6b 100644 --- a/web/src/components/object.rs +++ b/web/src/objects/item.rs @@ -1,109 +1,22 @@ use std::sync::Arc; +use cache::WEBFINGER; use leptos::*; -use crate::{prelude::*, URL_SENSITIVE}; +use regex::Regex; +use crate::prelude::*; use apb::{field::OptionalString, target::Addressed, ActivityMut, Base, Collection, CollectionMut, Document, Object, ObjectMut}; -#[component] -pub fn Attachment( - object: serde_json::Value, - #[prop(optional)] - sensitive: bool -) -> impl IntoView { - let config = use_context::>().expect("missing config context"); - let (expand, set_expand) = create_signal(false); - let href = object.url().id().str().unwrap_or_default(); - let media_type = object.media_type() - .unwrap_or("link") // TODO make it an Option rather than defaulting to link everywhere - .to_string(); - let mut kind = media_type - .split('/') - .next() - .unwrap_or("link") - .to_string(); - - // TODO in theory we should match on document_type, but mastodon and misskey send all attachments - // as "Documents" regardless of type, so we're forced to ignore the actual AP type and just match - // using media_type, uffff - // - // those who correctly send Image type objects without a media type get shown as links here, this - // is a dirty fix to properly display as images - if kind == "link" && matches!(object.document_type(), Ok(apb::DocumentType::Image)) { - kind = "image".to_string(); - } - - match kind.as_str() { - "image" => - view! { -

- -

- }.into_view(), - - "video" => { - let _href = href.clone(); - view! { -
- -
- }.into_view() - }, - - "audio" => - view! { -

- -

- }.into_view(), - - "link" | "text" => - view! { -

- - - -

- }.into_view(), - - _ => - view! { -

- - {media_type} - - {object.name().map(|name| { - view! {

{name.to_string()}

} - })} -

- }.into_view(), - } +lazy_static::lazy_static! { + static ref REGEX: Regex = regex::Regex::new("@(\\w+)(@\\w+|)").expect("failed compiling @ regex"); } - #[component] -pub fn Object(object: crate::Object) -> impl IntoView { +pub fn Object( + object: crate::Object, + #[prop(optional)] reply: bool, +) -> impl IntoView { let oid = object.id().unwrap_or_default().to_string(); - let content = mdhtml::safe_html(object.content().unwrap_or_default()); let author_id = object.attributed_to().id().str().unwrap_or_default(); let author = cache::OBJECTS.get_or(&author_id, serde_json::Value::String(author_id.clone()).into()); let sensitive = object.sensitive().unwrap_or_default(); @@ -127,6 +40,26 @@ pub fn Object(object: crate::Object) -> impl IntoView { Some(view! {
}) }; + let mut content = mdhtml::safe_html(object.content().unwrap_or_default()); + + let mut results = vec![]; + for (matched, [id, username, _domain]) in REGEX.captures_iter(&content).map(|c| c.extract()) { + // TODO what the fuck mastodon........... why are you putting the fancy url in the A HREF???????? + let id = id.replace('@', "users/"); + // TODO ughh ugly on-the-fly html editing, can this be avoided? + let to_replace = format!( + "@{}", + Uri::web(U::Actor, &id), id, username + ); + results.push((matched.to_string(), to_replace)); + } + + for (from, to) in results { + content = content.replace(&from, &to); + } + + + let audience_badge = object.audience().id().str() .map(|x| view! { @@ -134,7 +67,7 @@ pub fn Object(object: crate::Object) -> impl IntoView { class="border-button" title="this is a group: all interactions will be broadcasted to group members!" > - & + & {Uri::pretty(&x, 30)} @@ -142,13 +75,11 @@ pub fn Object(object: crate::Object) -> impl IntoView { }); - let post_image = object.image().get().and_then(|x| x.url().id().str()).map(|x| view! { - + let post_image = object.image().get().and_then(|x| x.url().id().str()).map(|x| { + let (expand, set_expand) = create_signal(false); + view! { + + } }); let post_inner = view! { @@ -165,9 +96,9 @@ pub fn Object(object: crate::Object) -> impl IntoView { }.into_view(), // lemmy with Page, peertube with Video Ok(apb::ObjectType::Document(t)) => view! { -
+
{post_image} -
+

{object.name().unwrap_or_default().to_string()}

@@ -209,7 +140,7 @@ pub fn Object(object: crate::Object) -> impl IntoView { {post}
- {audience_badge} + {if !reply { audience_badge } else { None }} diff --git a/web/src/objects/mod.rs b/web/src/objects/mod.rs index 1bedf719..b6f22119 100644 --- a/web/src/objects/mod.rs +++ b/web/src/objects/mod.rs @@ -1 +1,4 @@ pub mod view; +pub mod item; + +pub mod attachment; diff --git a/web/src/prelude.rs b/web/src/prelude.rs index 0c0186a4..9f6e85f7 100644 --- a/web/src/prelude.rs +++ b/web/src/prelude.rs @@ -11,14 +11,19 @@ pub use crate::{ follow::FollowList, posts::ActorPosts, }, + activities::{ + item::Item, + }, + objects::{ + view::ObjectView, + attachment::Attachment, + item::{Object, Summary, LikeButton, RepostButton, ReplyButton}, + }, timeline::{ Timeline, feed::Feed, thread::Thread, }, - objects::{ - view::ObjectView, - } }; pub use uriproxy::UriClass as U;