feat(web): better layout for lemmy posts

image on the side that expands on click, text that "reflows" under the
image, attachments don't overflow etc. also mentions. also refactored a
bit. since i refactored its hard to split these 3 changes so have one
big commit aha
This commit is contained in:
əlemi 2024-07-04 02:14:50 +02:00
parent b53bd5527f
commit 5e7b2354e2
Signed by: alemi
GPG key ID: A4895B84D311642C
9 changed files with 178 additions and 131 deletions

View file

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

View file

@ -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! { <Object object=item.clone() />{sep.clone()} }.into_view()),
Some(view! { <Object object=item.clone() reply=replies />{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! { <Object object=obj /> }
view! { <Object object=obj reply=replies /> }
}.into_view()),
apb::ActivityType::Follow =>
cache::OBJECTS.get(&object_id).map(|obj| {

View file

@ -0,0 +1 @@
pub mod item;

View file

@ -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::*;

View file

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

View file

@ -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::<Signal<crate::Config>>().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! {
<p class="center">
<img
class="w-100 attachment"
class:expand=expand
src={move || if sensitive && !expand.get() {
URL_SENSITIVE.to_string()
} else {
href.clone()
}}
title={object.name().unwrap_or_default().to_string()}
on:click=move |_| set_expand.set(!expand.get())
/>
</p>
}.into_view(),
"video" => {
let _href = href.clone();
view! {
<div class="center cursor box ml-1"
on:click=move |_| set_expand.set(!expand.get())
title={object.name().unwrap_or_default().to_string()}
>
<video controls class="attachment" class:expand=expand prop:loop=move || config.get().loop_videos >
{move || if sensitive && !expand.get() { None } else { Some(view! { <source src={_href.clone()} type={media_type.clone()} /> }) }}
<a href={href.clone()} target="_blank">video clip</a>
</video>
</div>
}.into_view()
},
"audio" =>
view! {
<p class="center">
<audio controls class="w-100" prop:loop=move || config.get().loop_videos >
<source src={href.clone()} type={media_type} />
<a href={href} target="_blank">audio clip</a>
</audio>
</p>
}.into_view(),
"link" | "text" =>
view! {
<p class="mt-s mb-s">
<a title={href.clone()} href={href.clone()} rel="noreferrer nofollow" target="_blank">
{Uri::pretty(&href, 50)}
</a>
</p>
}.into_view(),
_ =>
view! {
<p class="center box">
<code class="cw color center">
<a href={href} target="_blank">{media_type}</a>
</code>
{object.name().map(|name| {
view! { <p class="tiny-text"><small>{name.to_string()}</small></p> }
})}
</p>
}.into_view(),
}
}

View file

@ -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};
lazy_static::lazy_static! {
static ref REGEX: Regex = regex::Regex::new("<a href=\"(.+)\" class=\"u-url mention\">@(\\w+)(@\\w+|)</a>").expect("failed compiling @ regex");
}
#[component]
pub fn Attachment(
object: serde_json::Value,
#[prop(optional)]
sensitive: bool
pub fn Object(
object: crate::Object,
#[prop(optional)] reply: bool,
) -> impl IntoView {
let config = use_context::<Signal<crate::Config>>().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! {
<p class="center">
<img
class="w-100 attachment"
class:expand=expand
src={move || if sensitive && !expand.get() {
URL_SENSITIVE.to_string()
} else {
href.clone()
}}
title={object.name().unwrap_or_default().to_string()}
on:click=move |_| set_expand.set(!expand.get())
/>
</p>
}.into_view(),
"video" => {
let _href = href.clone();
view! {
<div class="center cursor box ml-1"
on:click=move |_| set_expand.set(!expand.get())
title={object.name().unwrap_or_default().to_string()}
>
<video controls class="attachment" class:expand=expand prop:loop=move || config.get().loop_videos >
{move || if sensitive && !expand.get() { None } else { Some(view! { <source src={_href.clone()} type={media_type.clone()} /> }) }}
<a href={href.clone()} target="_blank">video clip</a>
</video>
</div>
}.into_view()
},
"audio" =>
view! {
<p class="center">
<audio controls class="w-100" prop:loop=move || config.get().loop_videos >
<source src={href.clone()} type={media_type} />
<a href={href} target="_blank">audio clip</a>
</audio>
</p>
}.into_view(),
"link" | "text" =>
view! {
<p class="center mt-s mb-s">
<a href={href.clone()} title={href.clone()} rel="noreferrer nofollow" target="_blank">
<input style="max-width: 100%" type="submit" class="w-100" value={Uri::pretty(&href, 20)} title={object.name().unwrap_or_else(|_| href.as_str()).to_string()} />
</a>
</p>
}.into_view(),
_ =>
view! {
<p class="center box">
<code class="cw color center">
<a href={href} target="_blank">{media_type}</a>
</code>
{object.name().map(|name| {
view! { <p class="tiny-text"><small>{name.to_string()}</small></p> }
})}
</p>
}.into_view(),
}
}
#[component]
pub fn Object(object: crate::Object) -> 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! { <div class="pb-1"></div> })
};
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!(
"<a class=\"clean dim\" href=\"{}\" title=\"{}\"><span class=\"border-button mr-s\"><code class=\"color mr-s\">@</code>{}</span></a>",
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! {
<a class="clean dim" href={Uri::web(U::Actor, &x)} rel="nofollow noreferrer">
@ -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!"
>
<code class="color">&</code>
<code class="color mr-s">&</code>
<small>
{Uri::pretty(&x, 30)}
</small>
@ -142,13 +75,11 @@ pub fn Object(object: crate::Object) -> impl IntoView {
</a>
});
let post_image = object.image().get().and_then(|x| x.url().id().str()).map(|x| view! {
<div class="flex-pic-container">
<a href={x.clone()} target="_blank">
<div class="flex-pic" style={format!("background-image: url('{x}')")}>
</div>
</a>
</div>
let post_image = object.image().get().and_then(|x| x.url().id().str()).map(|x| {
let (expand, set_expand) = create_signal(false);
view! {
<img src={x} class="flex-pic box cursor" class:flex-pic-expand=expand on:click=move|_| set_expand.set(!expand.get()) />
}
});
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! {
<article class="ml-1 mr-1" style="display: flex">
<article class="float-container ml-1 mr-1" >
{post_image}
<div style="flex: 3">
<div>
<h4 class="mt-s mb-1" title={t.as_ref().to_string()}>
<b>{object.name().unwrap_or_default().to_string()}</b>
</h4>
@ -209,7 +140,7 @@ pub fn Object(object: crate::Object) -> impl IntoView {
</table>
{post}
<div class="mt-s ml-1 rev">
{audience_badge}
{if !reply { audience_badge } else { None }}
<ReplyButton n=comments target=oid.clone() />
<LikeButton n=likes liked=already_liked target=oid.clone() author=author_id private=!public />
<RepostButton n=shares target=oid />

View file

@ -1 +1,4 @@
pub mod view;
pub mod item;
pub mod attachment;

View file

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