feat: show attachments, initial work on threads
This commit is contained in:
parent
e9a19b3cb4
commit
1171d4cd06
5 changed files with 82 additions and 56 deletions
|
@ -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;
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
||||||
|
|
|
@ -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 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! {
|
|
||||||
<div>
|
|
||||||
<table class="w-100 post-table pa-1 mb-s" >
|
|
||||||
{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 }}
|
|
||||||
{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 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()));
|
||||||
|
let attachments = object.attachment()
|
||||||
|
.map(|x| view! {
|
||||||
|
<p><img class="attachment" src={x.url().id().unwrap_or_default()} /></p>
|
||||||
|
})
|
||||||
|
.collect_view();
|
||||||
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}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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,19 +211,28 @@ 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());
|
||||||
let url = format!("{URL_BASE}/dbg?id={fetch_url}");
|
if cached {
|
||||||
spawn_local(async move {
|
match CACHE.get(&fetch_url) {
|
||||||
match Http::fetch::<serde_json::Value>(&url, auth).await {
|
Some(x) => set_object.set(x),
|
||||||
Ok(x) => set_object.set(x),
|
None => set_object.set(serde_json::Value::String("not in cache!".into())),
|
||||||
Err(e) => set_object.set(serde_json::Value::String(e.to_string())),
|
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
let url = format!("{URL_BASE}/dbg?id={fetch_url}");
|
||||||
|
spawn_local(async move {
|
||||||
|
match Http::fetch::<serde_json::Value>(&url, auth).await {
|
||||||
|
Ok(x) => set_object.set(x),
|
||||||
|
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>
|
||||||
|
|
Loading…
Reference in a new issue