2024-05-01 16:06:46 +02:00
|
|
|
use std::{collections::BTreeSet, pin::Pin, sync::Arc};
|
2024-04-17 22:07:47 +02:00
|
|
|
|
2024-05-02 02:08:34 +02:00
|
|
|
use apb::{Activity, ActivityMut, Base, Object, ObjectMut};
|
2024-04-17 22:07:47 +02:00
|
|
|
use leptos::*;
|
|
|
|
use crate::prelude::*;
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
pub struct Timeline {
|
|
|
|
pub feed: RwSignal<Vec<String>>,
|
|
|
|
pub next: RwSignal<String>,
|
2024-04-23 23:37:39 +02:00
|
|
|
pub over: RwSignal<bool>,
|
2024-04-29 02:53:33 +02:00
|
|
|
pub loading: RwSignal<bool>,
|
2024-04-17 22:07:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Timeline {
|
|
|
|
pub fn new(url: String) -> Self {
|
|
|
|
let feed = create_rw_signal(vec![]);
|
|
|
|
let next = create_rw_signal(url);
|
2024-04-23 23:37:39 +02:00
|
|
|
let over = create_rw_signal(false);
|
2024-04-29 02:53:33 +02:00
|
|
|
let loading = create_rw_signal(false);
|
|
|
|
Timeline { feed, next, over, loading }
|
2024-04-17 22:07:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn reset(&self, url: String) {
|
|
|
|
self.feed.set(vec![]);
|
|
|
|
self.next.set(url);
|
2024-04-23 23:37:39 +02:00
|
|
|
self.over.set(false);
|
2024-04-17 22:07:47 +02:00
|
|
|
}
|
|
|
|
|
2024-05-01 18:22:25 +02:00
|
|
|
pub async fn more(&self, auth: Auth) -> reqwest::Result<()> {
|
2024-04-29 02:53:33 +02:00
|
|
|
self.loading.set(true);
|
|
|
|
let res = self.more_inner(auth).await;
|
|
|
|
self.loading.set(false);
|
|
|
|
res
|
|
|
|
}
|
|
|
|
|
2024-05-01 18:22:25 +02:00
|
|
|
async fn more_inner(&self, auth: Auth) -> reqwest::Result<()> {
|
2024-04-17 22:07:47 +02:00
|
|
|
use apb::{Collection, CollectionPage};
|
|
|
|
|
|
|
|
let feed_url = self.next.get();
|
|
|
|
let collection : serde_json::Value = Http::fetch(&feed_url, auth).await?;
|
|
|
|
let activities : Vec<serde_json::Value> = collection
|
|
|
|
.ordered_items()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let mut feed = self.feed.get();
|
2024-04-24 05:47:18 +02:00
|
|
|
let mut older = process_activities(activities, auth)
|
|
|
|
.await
|
|
|
|
.into_iter()
|
|
|
|
.filter(|x| !feed.contains(x))
|
|
|
|
.collect();
|
2024-04-17 22:07:47 +02:00
|
|
|
feed.append(&mut older);
|
|
|
|
self.feed.set(feed);
|
|
|
|
|
|
|
|
if let Some(next) = collection.next().id() {
|
|
|
|
self.next.set(next);
|
2024-04-23 23:37:39 +02:00
|
|
|
} else {
|
|
|
|
self.over.set(true);
|
2024-04-17 22:07:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-22 01:01:20 +02:00
|
|
|
#[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))
|
2024-04-22 01:44:00 +02:00
|
|
|
.filter(|x| match x.object_type() {
|
2024-04-22 04:43:23 +02:00
|
|
|
Some(apb::ObjectType::Activity(apb::ActivityType::Create)) => {
|
|
|
|
let Some(oid) = x.object().id() else { return false; };
|
|
|
|
let Some(object) = CACHE.get(&oid) else { return false; };
|
|
|
|
let Some(reply) = object.in_reply_to().id() else { return false; };
|
|
|
|
reply == root
|
|
|
|
},
|
2024-04-22 02:10:27 +02:00
|
|
|
Some(apb::ObjectType::Activity(_)) => x.object().id().map(|o| o == root).unwrap_or(false),
|
2024-04-22 01:44:00 +02:00
|
|
|
_ => x.in_reply_to().id().map(|r| r == root).unwrap_or(false),
|
|
|
|
})
|
2024-05-01 16:06:46 +02:00
|
|
|
.collect::<Vec<crate::Object>>();
|
2024-04-22 01:01:20 +02:00
|
|
|
|
|
|
|
view! {
|
|
|
|
<For
|
2024-04-22 01:44:00 +02:00
|
|
|
each=root_values
|
2024-04-22 01:01:20 +02:00
|
|
|
key=|k| k.id().unwrap_or_default().to_string()
|
2024-05-01 16:06:46 +02:00
|
|
|
children=move |object: crate::Object| {
|
2024-04-22 02:10:27 +02:00
|
|
|
match object.object_type() {
|
2024-04-22 04:47:26 +02:00
|
|
|
Some(apb::ObjectType::Activity(apb::ActivityType::Create)) => {
|
2024-04-22 04:49:24 +02:00
|
|
|
let oid = object.object().id().unwrap_or_default().to_string();
|
2024-04-22 04:47:26 +02:00
|
|
|
if let Some(note) = CACHE.get(&oid) {
|
|
|
|
view! {
|
|
|
|
<ActivityLine activity=object />
|
|
|
|
<Object object=note />
|
|
|
|
<div class="depth-r">
|
|
|
|
<TimelineRepliesRecursive tl=tl root=oid />
|
|
|
|
</div>
|
|
|
|
}.into_view()
|
|
|
|
} else {
|
|
|
|
view! {
|
|
|
|
<ActivityLine activity=object />
|
|
|
|
}.into_view()
|
|
|
|
}
|
|
|
|
},
|
2024-04-22 02:10:27 +02:00
|
|
|
Some(apb::ObjectType::Activity(_)) => view! {
|
|
|
|
<ActivityLine activity=object />
|
|
|
|
}.into_view(),
|
|
|
|
_ => {
|
|
|
|
let oid = object.id().unwrap_or_default().to_string();
|
|
|
|
view! {
|
|
|
|
<Object object=object />
|
2024-04-22 03:45:30 +02:00
|
|
|
<div class="depth-r">
|
2024-04-22 02:10:27 +02:00
|
|
|
<TimelineRepliesRecursive tl=tl root=oid />
|
|
|
|
</div>
|
|
|
|
}.into_view()
|
|
|
|
},
|
2024-04-22 01:01:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
/ >
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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>
|
2024-04-25 03:20:54 +02:00
|
|
|
<div class="center mt-1 mb-1" class:hidden=tl.over >
|
2024-04-22 01:01:20 +02:00
|
|
|
<button type="button"
|
2024-04-29 02:53:33 +02:00
|
|
|
prop:disabled=tl.loading
|
2024-04-22 01:01:20 +02:00
|
|
|
on:click=move |_| {
|
|
|
|
spawn_local(async move {
|
|
|
|
if let Err(e) = tl.more(auth).await {
|
|
|
|
tracing::error!("error fetching more items for timeline: {e}");
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2024-04-29 02:53:33 +02:00
|
|
|
>{move || if tl.loading.get() { "loading" } else { "more" }}</button>
|
2024-04-22 01:01:20 +02:00
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
#[component]
|
|
|
|
pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
|
|
|
|
let auth = use_context::<Auth>().expect("missing auth context");
|
|
|
|
view! {
|
|
|
|
<For
|
|
|
|
each=move || tl.feed.get()
|
|
|
|
key=|k| k.to_string()
|
|
|
|
children=move |id: String| {
|
|
|
|
match CACHE.get(&id) {
|
2024-04-24 04:07:45 +02:00
|
|
|
Some(item) => match item.object_type() {
|
|
|
|
// special case for placeholder activities
|
|
|
|
Some(apb::ObjectType::Note) => view! {
|
|
|
|
<Object object=item.clone() />
|
|
|
|
<hr/ >
|
|
|
|
}.into_view(),
|
|
|
|
// everything else
|
2024-04-24 04:50:17 +02:00
|
|
|
Some(apb::ObjectType::Activity(t)) => {
|
2024-04-21 17:43:36 +02:00
|
|
|
let object_id = item.object().id().unwrap_or_default();
|
2024-04-24 04:50:17 +02:00
|
|
|
let object = match t {
|
|
|
|
apb::ActivityType::Create | apb::ActivityType::Announce =>
|
|
|
|
CACHE.get(&object_id).map(|obj| {
|
|
|
|
view! { <Object object=obj /> }
|
|
|
|
}.into_view()),
|
|
|
|
apb::ActivityType::Follow =>
|
|
|
|
CACHE.get(&object_id).map(|obj| {
|
2024-05-02 02:08:34 +02:00
|
|
|
view! {
|
|
|
|
<div class="ml-1">
|
|
|
|
<ActorBanner object=obj />
|
|
|
|
<FollowRequestButtons activity_id=id actor_id=object_id />
|
|
|
|
</div>
|
|
|
|
}
|
2024-04-24 04:50:17 +02:00
|
|
|
}.into_view()),
|
|
|
|
_ => None,
|
|
|
|
};
|
2024-04-21 17:43:36 +02:00
|
|
|
view! {
|
|
|
|
<ActivityLine activity=item />
|
|
|
|
{object}
|
2024-04-19 06:59:34 +02:00
|
|
|
<hr/ >
|
2024-04-21 17:43:36 +02:00
|
|
|
}.into_view()
|
|
|
|
},
|
2024-04-24 04:07:45 +02:00
|
|
|
// should never happen
|
2024-04-19 06:59:34 +02:00
|
|
|
_ => view! { <p><code>type not implemented</code></p><hr /> }.into_view(),
|
2024-04-17 22:07:47 +02:00
|
|
|
},
|
|
|
|
None => view! {
|
|
|
|
<p><code>{id}</code>" "[<a href={uri}>go</a>]</p>
|
|
|
|
}.into_view(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/ >
|
2024-04-29 02:53:33 +02:00
|
|
|
<div class="center mt-1 mb-1" class:hidden=tl.over >
|
2024-04-17 22:07:47 +02:00
|
|
|
<button type="button"
|
2024-04-29 02:53:33 +02:00
|
|
|
prop:disabled=tl.loading
|
2024-04-17 22:07:47 +02:00
|
|
|
on:click=move |_| {
|
|
|
|
spawn_local(async move {
|
|
|
|
if let Err(e) = tl.more(auth).await {
|
|
|
|
tracing::error!("error fetching more items for timeline: {e}");
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2024-04-29 02:53:33 +02:00
|
|
|
>{move || if tl.loading.get() { "loading" } else { "more" }}</button>
|
2024-04-17 22:07:47 +02:00
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-01 18:22:25 +02:00
|
|
|
async fn process_activities(activities: Vec<serde_json::Value>, auth: Auth) -> Vec<String> {
|
2024-04-21 18:56:25 +02:00
|
|
|
let mut sub_tasks : Vec<Pin<Box<dyn futures::Future<Output = ()>>>> = Vec::new();
|
2024-04-17 22:07:47 +02:00
|
|
|
let mut gonna_fetch = BTreeSet::new();
|
2024-04-21 18:56:25 +02:00
|
|
|
let mut actors_seen = BTreeSet::new();
|
2024-04-17 22:07:47 +02:00
|
|
|
let mut out = Vec::new();
|
|
|
|
|
|
|
|
for activity in activities {
|
2024-05-02 02:07:54 +02:00
|
|
|
let activity_type = activity.activity_type().unwrap_or(apb::ActivityType::Activity);
|
2024-04-17 22:07:47 +02:00
|
|
|
// save embedded object if present
|
|
|
|
if let Some(object) = activity.object().get() {
|
2024-04-21 18:56:25 +02:00
|
|
|
// also fetch actor attributed to
|
|
|
|
if let Some(attributed_to) = object.attributed_to().id() {
|
|
|
|
actors_seen.insert(attributed_to);
|
|
|
|
}
|
2024-04-17 22:07:47 +02:00
|
|
|
if let Some(object_uri) = object.id() {
|
2024-05-01 16:06:46 +02:00
|
|
|
CACHE.put(object_uri.to_string(), Arc::new(object.clone()));
|
2024-04-21 18:56:25 +02:00
|
|
|
} else {
|
|
|
|
tracing::warn!("embedded object without id: {object:?}");
|
2024-04-17 22:07:47 +02:00
|
|
|
}
|
|
|
|
} else { // try fetching it
|
|
|
|
if let Some(object_id) = activity.object().id() {
|
|
|
|
if !gonna_fetch.contains(&object_id) {
|
2024-05-02 02:07:54 +02:00
|
|
|
let fetch_kind = match activity_type {
|
|
|
|
apb::ActivityType::Follow => FetchKind::User,
|
|
|
|
_ => FetchKind::Object,
|
|
|
|
};
|
2024-04-17 22:07:47 +02:00
|
|
|
gonna_fetch.insert(object_id.clone());
|
2024-05-02 02:07:54 +02:00
|
|
|
sub_tasks.push(Box::pin(fetch_and_update_with_user(fetch_kind, object_id, auth)));
|
2024-04-17 22:07:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// save activity, removing embedded object
|
|
|
|
let object_id = activity.object().id();
|
|
|
|
if let Some(activity_id) = activity.id() {
|
|
|
|
out.push(activity_id.to_string());
|
|
|
|
CACHE.put(
|
|
|
|
activity_id.to_string(),
|
2024-05-01 16:06:46 +02:00
|
|
|
Arc::new(activity.clone().set_object(apb::Node::maybe_link(object_id)))
|
2024-04-17 22:07:47 +02:00
|
|
|
);
|
2024-04-24 04:07:45 +02:00
|
|
|
} else if let Some(object_id) = activity.object().id() {
|
|
|
|
out.push(object_id);
|
2024-04-17 22:07:47 +02:00
|
|
|
}
|
2024-04-23 17:35:03 +02:00
|
|
|
|
|
|
|
if let Some(uid) = activity.attributed_to().id() {
|
|
|
|
if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) {
|
|
|
|
gonna_fetch.insert(uid.clone());
|
|
|
|
sub_tasks.push(Box::pin(fetch_and_update(FetchKind::User, uid, auth)));
|
|
|
|
}
|
|
|
|
}
|
2024-04-17 22:07:47 +02:00
|
|
|
|
|
|
|
if let Some(uid) = activity.actor().id() {
|
|
|
|
if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) {
|
|
|
|
gonna_fetch.insert(uid.clone());
|
2024-04-21 18:56:25 +02:00
|
|
|
sub_tasks.push(Box::pin(fetch_and_update(FetchKind::User, uid, auth)));
|
2024-04-17 22:07:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-21 18:56:25 +02:00
|
|
|
for user in actors_seen {
|
|
|
|
sub_tasks.push(Box::pin(fetch_and_update(FetchKind::User, user, auth)));
|
|
|
|
}
|
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
futures::future::join_all(sub_tasks).await;
|
|
|
|
|
|
|
|
out
|
|
|
|
}
|
|
|
|
|
2024-05-01 18:22:25 +02:00
|
|
|
async fn fetch_and_update(kind: FetchKind, id: String, auth: Auth) {
|
2024-04-18 05:00:44 +02:00
|
|
|
match Http::fetch(&Uri::api(kind, &id, false), auth).await {
|
2024-05-01 16:06:46 +02:00
|
|
|
Ok(data) => CACHE.put(id, Arc::new(data)),
|
2024-04-17 22:07:47 +02:00
|
|
|
Err(e) => console_warn(&format!("could not fetch '{id}': {e}")),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-01 18:22:25 +02:00
|
|
|
async fn fetch_and_update_with_user(kind: FetchKind, id: String, auth: Auth) {
|
2024-04-21 18:56:25 +02:00
|
|
|
fetch_and_update(kind.clone(), id.clone(), auth).await;
|
|
|
|
if let Some(obj) = CACHE.get(&id) {
|
|
|
|
if let Some(actor_id) = match kind {
|
|
|
|
FetchKind::Object => obj.attributed_to().id(),
|
|
|
|
FetchKind::Activity => obj.actor().id(),
|
|
|
|
FetchKind::User | FetchKind::Context => None,
|
|
|
|
} {
|
|
|
|
fetch_and_update(FetchKind::User, actor_id, auth).await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|