feat: show attachments, initial work on threads

This commit is contained in:
əlemi 2024-04-22 01:01:20 +02:00
parent e9a19b3cb4
commit 1171d4cd06
Signed by: alemi
GPG key ID: A4895B84D311642C
5 changed files with 82 additions and 56 deletions

View file

@ -152,6 +152,11 @@
img.inline-avatar { img.inline-avatar {
max-height: 2em; max-height: 2em;
} }
img.attachment {
max-height: 15em;
border: 5px solid #bf616a;
padding: 5px;
}
div.tl-header { div.tl-header {
background-color: #bf616a55; background-color: #bf616a55;
color: #bf616a; color: #bf616a;

View file

@ -2,13 +2,13 @@ mod activity;
pub use activity::ActivityLine; pub use activity::ActivityLine;
mod object; mod object;
pub use object::{Object, ObjectInline}; pub use object::Object;
mod user; mod user;
pub use user::ActorBanner; pub use user::ActorBanner;
mod timeline; mod timeline;
pub use timeline::{TimelineFeed, Timeline}; pub use timeline::{TimelineFeed, TimelineReplies, Timeline};
use leptos::*; use leptos::*;

View file

@ -6,64 +6,28 @@ use apb::{target::Addressed, Base, Object};
#[component] #[component]
pub fn Object(object: serde_json::Value) -> impl IntoView { pub fn Object(object: serde_json::Value) -> impl IntoView {
let oid = object.id().unwrap_or_default().to_string(); let uid = object.id().unwrap_or_default().to_string();
let in_reply_to = object.in_reply_to().id().unwrap_or_default();
let summary = object.summary().unwrap_or_default().to_string(); let summary = object.summary().unwrap_or_default().to_string();
let content = dissolve::strip_html_tags(object.content().unwrap_or_default()); let content = dissolve::strip_html_tags(object.content().unwrap_or_default());
let author_id = object.attributed_to().id().unwrap_or_default(); let author_id = object.attributed_to().id().unwrap_or_default();
let author = CACHE.get_or(&author_id, serde_json::Value::String(author_id.clone())); let author = CACHE.get_or(&author_id, serde_json::Value::String(author_id.clone()));
view! { let attachments = object.attachment()
<div> .map(|x| view! {
<table class="w-100 post-table pa-1 mb-s" > <p><img class="attachment" src={x.url().id().unwrap_or_default()} /></p>
{move || if !in_reply_to.is_empty() {
Some(view! {
<tr class="post-table" >
<td class="post-table pa-1" colspan="2" >
"in reply to "<small><a class="clean hover" href={Uri::web(FetchKind::Object, &in_reply_to)}>{Uri::pretty(&in_reply_to)}</a></small>
</td>
</tr>
}) })
} else { None }} .collect_view();
{move || if !summary.is_empty() {
Some(view! {
<tr class="post-table" >
<td class="post-table pa-1" colspan="2" >{summary.clone()}</td>
</tr>
})
} else { None }}
<tr class="post-table" >
<td class="post-table pa-1" colspan="2" >{
content.into_iter().map(|x| view! { <p>{x}</p> }).collect_view()
}</td>
</tr>
<tr class="post-table" >
<td class="post-table pa-1" ><ActorBanner object=author /></td>
<td class="post-table pa-1 center" >
<a class="clean hover" href={oid} target="_blank">
<DateTime t=object.published() />
<PrivacyMarker addressed=object.addressed() />
</a>
</td>
</tr>
</table>
</div>
}
}
#[component]
pub fn ObjectInline(object: serde_json::Value) -> impl IntoView {
let summary = object.summary().unwrap_or_default().to_string();
let content = dissolve::strip_html_tags(object.content().unwrap_or_default());
let author_id = object.attributed_to().id().unwrap_or_default();
let author = CACHE.get_or(&author_id, serde_json::Value::String(author_id.clone()));
view! { view! {
<table class="align w-100"> <table class="align w-100">
<tr> <tr>
<td><ActorBanner object=author /></td> <td><ActorBanner object=author /></td>
<td class="rev" > <td class="rev" >
{object.in_reply_to().id().map(|reply| view! {
<small><i><a class="clean mr-1" href={Uri::web(FetchKind::Object, &reply)} title={reply}>reply</a></i></small>
})}
<a class="clean hover" href={Uri::web(FetchKind::Object, object.id().unwrap_or_default())}> <a class="clean hover" href={Uri::web(FetchKind::Object, object.id().unwrap_or_default())}>
<DateTime t=object.published() /> <DateTime t=object.published() />
</a> </a>
<sup><small><a class="clean" href={uid} target="_blank">""</a></small></sup>
<PrivacyMarker addressed=object.addressed() /> <PrivacyMarker addressed=object.addressed() />
</td> </td>
</tr> </tr>
@ -72,6 +36,7 @@ pub fn ObjectInline(object: serde_json::Value) -> impl IntoView {
{if summary.is_empty() { None } else { Some(view! { <code class="color ml-1">{summary}</code> })}} {if summary.is_empty() { None } else { Some(view! { <code class="color ml-1">{summary}</code> })}}
{content.into_iter().map(|x| view! { <p>{x}</p> }).collect_view()} {content.into_iter().map(|x| view! { <p>{x}</p> }).collect_view()}
</blockquote> </blockquote>
{attachments}
} }
} }

View file

@ -44,6 +44,52 @@ impl Timeline {
} }
} }
#[component]
pub fn TimelineRepliesRecursive(tl: Timeline, root: String) -> impl IntoView {
let root_values = move || tl.feed
.get()
.into_iter()
.filter_map(|x| CACHE.get(&x))
.filter(|x| x.object().get().map(|o| o.in_reply_to().id().map(|r| r == root).unwrap_or(false)).unwrap_or(false))
.collect::<Vec<serde_json::Value>>();
view! {
<For
each=move || root_values()
key=|k| k.id().unwrap_or_default().to_string()
children=move |object: serde_json::Value| {
let oid = object.id().unwrap_or_default().to_string();
view! {
<Object object=object />
<TimelineRepliesRecursive tl=tl root=oid />
}
}
/ >
}
}
#[component]
pub fn TimelineReplies(tl: Timeline, root: String) -> impl IntoView {
let auth = use_context::<Auth>().expect("missing auth context");
view! {
<div>
<TimelineRepliesRecursive tl=tl root=root />
</div>
<div class="center mt-1 mb-1" >
<button type="button"
on:click=move |_| {
spawn_local(async move {
if let Err(e) = tl.more(auth).await {
tracing::error!("error fetching more items for timeline: {e}");
}
})
}
>more</button>
</div>
}
}
#[component] #[component]
pub fn TimelineFeed(tl: Timeline) -> impl IntoView { pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
let auth = use_context::<Auth>().expect("missing auth context"); let auth = use_context::<Auth>().expect("missing auth context");
@ -57,7 +103,7 @@ pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
Some(apb::BaseType::Object(apb::ObjectType::Activity(_))) => { Some(apb::BaseType::Object(apb::ObjectType::Activity(_))) => {
let object_id = item.object().id().unwrap_or_default(); let object_id = item.object().id().unwrap_or_default();
let object = CACHE.get(&object_id).map(|obj| { let object = CACHE.get(&object_id).map(|obj| {
view! { <ObjectInline object=obj /> } view! { <Object object=obj /> }
}); });
view! { view! {
<ActivityLine activity=item /> <ActivityLine activity=item />

View file

@ -162,7 +162,7 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView {
view!{ view!{
<Object object=object /> <Object object=object />
<div class="ml-1 mr-1 mt-2"> <div class="ml-1 mr-1 mt-2">
<TimelineFeed tl=tl /> <TimelineReplies tl=tl root=o.id().unwrap_or_default().to_string() />
</div> </div>
}.into_view() }.into_view()
}, },
@ -203,6 +203,7 @@ pub fn DebugPage() -> impl IntoView {
let (object, set_object) = create_signal(serde_json::Value::String( let (object, set_object) = create_signal(serde_json::Value::String(
"use this view to fetch remote AP objects and inspect their content".into()) "use this view to fetch remote AP objects and inspect their content".into())
); );
let cached_ref: NodeRef<html::Input> = create_node_ref();
let auth = use_context::<Auth>().expect("missing auth context"); let auth = use_context::<Auth>().expect("missing auth context");
view! { view! {
<div> <div>
@ -210,7 +211,14 @@ pub fn DebugPage() -> impl IntoView {
<div class="mt-1" > <div class="mt-1" >
<form on:submit=move|ev| { <form on:submit=move|ev| {
ev.prevent_default(); ev.prevent_default();
let cached = cached_ref.get().map(|x| x.checked()).unwrap_or_default();
let fetch_url = url_ref.get().map(|x| x.value()).unwrap_or("".into()); let fetch_url = url_ref.get().map(|x| x.value()).unwrap_or("".into());
if cached {
match CACHE.get(&fetch_url) {
Some(x) => set_object.set(x),
None => set_object.set(serde_json::Value::String("not in cache!".into())),
}
} else {
let url = format!("{URL_BASE}/dbg?id={fetch_url}"); let url = format!("{URL_BASE}/dbg?id={fetch_url}");
spawn_local(async move { spawn_local(async move {
match Http::fetch::<serde_json::Value>(&url, auth).await { match Http::fetch::<serde_json::Value>(&url, auth).await {
@ -218,11 +226,13 @@ pub fn DebugPage() -> impl IntoView {
Err(e) => set_object.set(serde_json::Value::String(e.to_string())), Err(e) => set_object.set(serde_json::Value::String(e.to_string())),
} }
}); });
}
} > } >
<table class="align w-100" > <table class="align w-100" >
<tr> <tr>
<td><input class="w-100" type="text" node_ref=url_ref placeholder="AP id" /></td> <td><input class="w-100" type="text" node_ref=url_ref placeholder="AP id" /></td>
<td><input type="submit" class="w-100" value="fetch" /></td> <td><input type="submit" class="w-100" value="fetch" /></td>
<td><input type="checkbox" title="cached" value="cached" node_ref=cached_ref /></td>
</tr> </tr>
</table> </table>
</form> </form>