2024-04-15 05:27:53 +02:00
|
|
|
pub mod context;
|
2024-04-14 06:45:51 +02:00
|
|
|
|
2024-04-16 06:34:50 +02:00
|
|
|
use apb::{target::Addressed, Activity, Actor, Base, Collection, Object, ObjectMut};
|
|
|
|
use leptos::{leptos_dom::logging::{console_error, console_log}, *};
|
2024-04-15 05:00:23 +02:00
|
|
|
use leptos_router::*;
|
2024-04-14 06:45:51 +02:00
|
|
|
|
2024-04-16 06:34:50 +02:00
|
|
|
use crate::context::{Http, Timeline, Uri, CACHE};
|
2024-04-15 05:27:53 +02:00
|
|
|
|
2024-04-15 05:00:23 +02:00
|
|
|
pub const URL_BASE: &str = "https://feditest.alemi.dev";
|
|
|
|
pub const URL_PREFIX: &str = "/web";
|
2024-04-14 06:45:51 +02:00
|
|
|
|
|
|
|
#[derive(Debug, serde::Serialize)]
|
|
|
|
struct LoginForm {
|
|
|
|
email: String,
|
|
|
|
password: String,
|
|
|
|
}
|
|
|
|
|
2024-04-16 06:34:50 +02:00
|
|
|
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
|
|
pub struct Auth {
|
|
|
|
pub token: String,
|
|
|
|
pub user: String,
|
|
|
|
pub expires: chrono::DateTime<chrono::Utc>,
|
2024-04-15 22:03:32 +02:00
|
|
|
}
|
|
|
|
|
2024-04-16 06:34:50 +02:00
|
|
|
pub trait MaybeToken {
|
|
|
|
fn present(&self) -> bool;
|
|
|
|
fn token(&self) -> String;
|
2024-04-15 22:20:33 +02:00
|
|
|
}
|
|
|
|
|
2024-04-17 03:12:17 +02:00
|
|
|
impl MaybeToken for Option<String> {
|
2024-04-16 06:34:50 +02:00
|
|
|
fn token(&self) -> String {
|
|
|
|
match self {
|
|
|
|
None => String::new(),
|
2024-04-17 03:12:17 +02:00
|
|
|
Some(x) => x.clone(),
|
2024-04-16 06:34:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
fn present(&self) -> bool {
|
|
|
|
match self {
|
|
|
|
None => false,
|
2024-04-17 03:12:17 +02:00
|
|
|
Some(x) => !x.is_empty(),
|
2024-04-16 06:34:50 +02:00
|
|
|
}
|
|
|
|
}
|
2024-04-15 22:32:05 +02:00
|
|
|
}
|
|
|
|
|
2024-04-14 06:45:51 +02:00
|
|
|
#[component]
|
|
|
|
pub fn LoginBox(
|
2024-04-17 03:12:17 +02:00
|
|
|
token_tx: WriteSignal<Option<String>>,
|
|
|
|
token: Signal<Option<String>>,
|
|
|
|
username: Signal<Option<String>>,
|
|
|
|
username_tx: WriteSignal<Option<String>>,
|
|
|
|
home_tl: Timeline,
|
2024-04-14 06:45:51 +02:00
|
|
|
) -> impl IntoView {
|
|
|
|
let username_ref: NodeRef<html::Input> = create_node_ref();
|
|
|
|
let password_ref: NodeRef<html::Input> = create_node_ref();
|
|
|
|
view! {
|
|
|
|
<div>
|
2024-04-17 03:12:17 +02:00
|
|
|
<div class="w-100" class:hidden=move || !token.get().present() >
|
|
|
|
"Hello "<a href={move || Uri::web("users", &username.get().unwrap_or_default() )} >{move || username.get().unwrap_or_default() }</a>
|
2024-04-15 03:38:16 +02:00
|
|
|
<input style="float:right" type="submit" value="logout" on:click=move |_| {
|
2024-04-17 03:12:17 +02:00
|
|
|
token_tx.set(None);
|
2024-04-14 06:45:51 +02:00
|
|
|
} />
|
|
|
|
</div>
|
2024-04-17 03:12:17 +02:00
|
|
|
<div class:hidden=move || token.get().present() >
|
2024-04-15 17:54:28 +02:00
|
|
|
<input class="w-100" type="text" node_ref=username_ref placeholder="username" />
|
|
|
|
<input class="w-100" type="text" node_ref=password_ref placeholder="password" />
|
|
|
|
<input class="w-100" type="submit" value="login" on:click=move |_| {
|
|
|
|
console_log("logging in...");
|
|
|
|
let email = username_ref.get().map(|x| x.value()).unwrap_or("".into());
|
|
|
|
let password = password_ref.get().map(|x| x.value()).unwrap_or("".into());
|
|
|
|
spawn_local(async move {
|
|
|
|
let auth = reqwest::Client::new()
|
|
|
|
.post(format!("{URL_BASE}/auth"))
|
|
|
|
.json(&LoginForm { email, password })
|
|
|
|
.send()
|
|
|
|
.await.unwrap()
|
2024-04-16 06:34:50 +02:00
|
|
|
.json::<Auth>()
|
2024-04-15 17:54:28 +02:00
|
|
|
.await.unwrap();
|
2024-04-15 22:32:05 +02:00
|
|
|
console_log(&format!("logged in until {}", auth.expires));
|
2024-04-17 03:12:17 +02:00
|
|
|
let username = auth.user.split('/').last().unwrap_or_default().to_string();
|
|
|
|
// reset home feed and point it to our user's inbox
|
|
|
|
home_tl.set_feed(vec![]);
|
|
|
|
home_tl.set_next(format!("{URL_BASE}/users/{}/inbox/page", username));
|
|
|
|
// update our username and token cookies
|
|
|
|
username_tx.set(Some(username));
|
|
|
|
token_tx.set(Some(auth.token));
|
2024-04-15 17:54:28 +02:00
|
|
|
});
|
|
|
|
} />
|
2024-04-14 06:45:51 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-16 06:42:02 +02:00
|
|
|
#[component]
|
|
|
|
pub fn TimelineNavigation() -> impl IntoView {
|
2024-04-17 03:12:17 +02:00
|
|
|
let auth = use_context::<Signal<Option<String>>>().expect("missing auth context");
|
2024-04-16 06:42:02 +02:00
|
|
|
view! {
|
|
|
|
<a href="/web/home" >
|
|
|
|
<input class="w-100"
|
|
|
|
type="submit"
|
|
|
|
class:hidden=move || !auth.get().present()
|
2024-04-17 06:51:21 +02:00
|
|
|
// class:active=move || use_location().pathname.get().ends_with("/home")
|
2024-04-16 06:42:02 +02:00
|
|
|
value="home timeline"
|
|
|
|
/>
|
|
|
|
</a>
|
|
|
|
<a href="/web/server" >
|
|
|
|
<input
|
|
|
|
class="w-100"
|
2024-04-17 06:51:21 +02:00
|
|
|
// class:active=move || use_location().pathname.get().ends_with("/server")
|
2024-04-16 06:42:02 +02:00
|
|
|
type="submit"
|
|
|
|
value="server timeline"
|
|
|
|
/>
|
|
|
|
</a>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-14 06:45:51 +02:00
|
|
|
#[component]
|
2024-04-17 06:46:59 +02:00
|
|
|
pub fn PostBox(username: Signal<Option<String>>) -> impl IntoView {
|
2024-04-17 03:12:17 +02:00
|
|
|
let auth = use_context::<Signal<Option<String>>>().expect("missing auth context");
|
2024-04-14 06:45:51 +02:00
|
|
|
let summary_ref: NodeRef<html::Input> = create_node_ref();
|
|
|
|
let content_ref: NodeRef<html::Textarea> = create_node_ref();
|
2024-04-17 06:46:59 +02:00
|
|
|
let public_ref: NodeRef<html::Input> = create_node_ref();
|
|
|
|
let followers_ref: NodeRef<html::Input> = create_node_ref();
|
2024-04-14 06:45:51 +02:00
|
|
|
view! {
|
2024-04-16 06:34:50 +02:00
|
|
|
<div class:hidden=move || !auth.get().present() >
|
2024-04-17 06:46:59 +02:00
|
|
|
<table class="align w-100">
|
|
|
|
<tr>
|
|
|
|
<td><input type="checkbox" title="public" value="public" node_ref=public_ref /></td>
|
|
|
|
<td class="w-100"><input class="w-100" type="text" node_ref=summary_ref title="summary" /></td>
|
|
|
|
<td><input type="checkbox" title="followers" value="followers" node_ref=followers_ref checked /></td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td colspan="3">
|
|
|
|
<textarea rows="5" class="w-100" node_ref=content_ref placeholder="leptos is kinda fun!" ></textarea>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td colspan="3">
|
|
|
|
<button class="w-100" type="button" on:click=move |_| {
|
|
|
|
spawn_local(async move {
|
|
|
|
let summary = summary_ref.get().map(|x| x.value());
|
|
|
|
let content = content_ref.get().map(|x| x.value()).unwrap_or_default();
|
|
|
|
let public = public_ref.get().map(|x| x.checked()).unwrap_or_default();
|
|
|
|
let followers = followers_ref.get().map(|x| x.checked()).unwrap_or_default();
|
|
|
|
match Http::post(
|
|
|
|
&format!("{URL_BASE}/users/test/outbox"),
|
|
|
|
&serde_json::Value::Object(serde_json::Map::default())
|
|
|
|
.set_object_type(Some(apb::ObjectType::Note))
|
|
|
|
.set_summary(summary.as_deref())
|
|
|
|
.set_content(Some(&content))
|
|
|
|
.set_to(
|
|
|
|
if public {
|
|
|
|
apb::Node::links(vec![apb::target::PUBLIC.to_string()])
|
|
|
|
} else { apb::Node::Empty }
|
|
|
|
)
|
|
|
|
.set_cc(
|
|
|
|
if followers {
|
|
|
|
apb::Node::links(vec![format!("{URL_BASE}/users/{}/followers", username.get().unwrap_or_default())])
|
|
|
|
} else { apb::Node::Empty }
|
|
|
|
),
|
|
|
|
auth
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
Err(e) => console_error(&format!("error posting note: {e}")),
|
|
|
|
Ok(()) => {
|
|
|
|
if let Some(x) = summary_ref.get() { x.set_value("") }
|
|
|
|
if let Some(x) = content_ref.get() { x.set_value("") }
|
|
|
|
},
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} >post</button>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</table>
|
2024-04-14 06:45:51 +02:00
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[component]
|
|
|
|
pub fn TimelinePicker(
|
|
|
|
tx: WriteSignal<String>,
|
|
|
|
rx: ReadSignal<String>,
|
|
|
|
) -> impl IntoView {
|
|
|
|
let targets = (
|
|
|
|
"https://feditest.alemi.dev/users/test/inbox/page".to_string(),
|
|
|
|
"https://feditest.alemi.dev/users/test/outbox/page".to_string(),
|
|
|
|
"https://feditest.alemi.dev/inbox/page".to_string(),
|
|
|
|
"https://feditest.alemi.dev/outbox/page".to_string(),
|
|
|
|
);
|
|
|
|
let (my_in, my_out, our_in, our_out) = targets.clone();
|
|
|
|
let (my_in_, my_out_, our_in_, our_out_) = targets;
|
|
|
|
view! {
|
2024-04-15 03:38:16 +02:00
|
|
|
<input type="submit" class:active=move || rx.get() == my_in_ on:click=move |_| tx.set(my_in.clone()) value="my inbox" />
|
|
|
|
<input type="submit" class:active=move || rx.get() == my_out_ on:click=move |_| tx.set(my_out.clone()) value="my outbox" />
|
|
|
|
<input type="submit" class:active=move || rx.get() == our_in_ on:click=move |_| tx.set(our_in.clone()) value="global inbox" />
|
|
|
|
<input type="submit" class:active=move || rx.get() == our_out_ on:click=move |_| tx.set(our_out.clone()) value="global outbox" />
|
2024-04-14 06:45:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[component]
|
2024-04-15 03:38:16 +02:00
|
|
|
pub fn ActorBanner(object: serde_json::Value) -> impl IntoView {
|
2024-04-14 06:45:51 +02:00
|
|
|
match object {
|
|
|
|
serde_json::Value::String(id) => view! {
|
|
|
|
<div><b>{id}</b></div>
|
|
|
|
},
|
|
|
|
serde_json::Value::Object(_) => {
|
2024-04-15 22:03:32 +02:00
|
|
|
let uid = object.id().unwrap_or_default().to_string();
|
2024-04-16 06:34:50 +02:00
|
|
|
let uri = Uri::web("users", &uid);
|
2024-04-14 06:45:51 +02:00
|
|
|
let avatar_url = object.icon().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default();
|
|
|
|
let display_name = object.name().unwrap_or_default().to_string();
|
|
|
|
let username = object.preferred_username().unwrap_or_default().to_string();
|
|
|
|
let domain = object.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string();
|
|
|
|
view! {
|
|
|
|
<div>
|
|
|
|
<table class="align" >
|
|
|
|
<tr>
|
2024-04-17 05:59:10 +02:00
|
|
|
<td rowspan="2" ><a href={uri.clone()} ><img class="avatar-circle" src={avatar_url} /></a></td>
|
2024-04-14 06:45:51 +02:00
|
|
|
<td><b>{display_name}</b></td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
2024-04-17 05:59:10 +02:00
|
|
|
<td class="top" ><a class="hover" href={uri} ><small>{username}@{domain}</small></a></td>
|
2024-04-14 06:45:51 +02:00
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
},
|
|
|
|
_ => view! {
|
|
|
|
<div><b>invalid actor</b></div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-15 03:38:16 +02:00
|
|
|
#[component]
|
2024-04-15 23:11:36 +02:00
|
|
|
pub fn UserPage() -> impl IntoView {
|
2024-04-15 03:38:16 +02:00
|
|
|
let params = use_params_map();
|
2024-04-17 03:12:17 +02:00
|
|
|
let auth = use_context::<Signal<Option<String>>>().expect("missing auth context");
|
2024-04-16 06:34:50 +02:00
|
|
|
let actor = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |id| {
|
2024-04-15 03:38:16 +02:00
|
|
|
async move {
|
2024-04-16 06:34:50 +02:00
|
|
|
match CACHE.get(&Uri::full("users", &id)) {
|
2024-04-15 23:11:36 +02:00
|
|
|
Some(x) => Some(x.clone()),
|
|
|
|
None => {
|
2024-04-17 03:12:17 +02:00
|
|
|
let user : serde_json::Value = Http::fetch(&Uri::api("users", &id), auth).await.ok()?;
|
2024-04-16 06:34:50 +02:00
|
|
|
CACHE.put(Uri::full("users", &id), user.clone());
|
2024-04-15 23:11:36 +02:00
|
|
|
Some(user)
|
|
|
|
},
|
2024-04-15 05:27:53 +02:00
|
|
|
}
|
2024-04-15 03:38:16 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
view! {
|
2024-04-16 08:02:03 +02:00
|
|
|
<div class="ml-1">
|
|
|
|
<div class="tl-header w-100 center mb-s" >view::user</div>
|
|
|
|
<div class="boxscroll" >
|
|
|
|
{move || match actor.get() {
|
|
|
|
None => view! { <p>loading...</p> }.into_view(),
|
|
|
|
Some(None) => view! { <p><code>error loading</code></p> }.into_view(),
|
|
|
|
Some(Some(x)) => view! {
|
|
|
|
<div class="ml-3 mr-3 mt-3">
|
|
|
|
<ActorBanner object=x.clone() />
|
|
|
|
<p
|
|
|
|
class="pb-2 pt-2 pr-2 pl-2"
|
|
|
|
style={format!(
|
|
|
|
"background-image: url({}); background-size: cover;",
|
|
|
|
x.image().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default()
|
|
|
|
)}
|
|
|
|
>
|
2024-04-16 08:22:52 +02:00
|
|
|
{
|
|
|
|
dissolve::strip_html_tags(x.summary().unwrap_or(""))
|
|
|
|
.into_iter()
|
|
|
|
.map(|x| view! { <p>{x}</p> })
|
|
|
|
.collect_view()
|
|
|
|
}
|
2024-04-16 08:02:03 +02:00
|
|
|
</p>
|
|
|
|
<ul>
|
|
|
|
<li><code>type</code>" "<b>{x.actor_type().unwrap_or(apb::ActorType::Person).as_ref().to_string()}</b></li>
|
|
|
|
<li><code>following</code>" "<b>{x.following().get().map(|x| x.total_items().unwrap_or(0))}</b></li>
|
|
|
|
<li><code>followers</code>" "<b>{x.followers().get().map(|x| x.total_items().unwrap_or(0))}</b></li>
|
|
|
|
<li><code>created</code>" "{x.published().map(|x| x.to_rfc3339())}</li>
|
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<TimelineFeed tl=Timeline::new(format!("{}/outbox/page", Uri::api("users", x.id().unwrap_or_default()))) />
|
|
|
|
}.into_view(),
|
|
|
|
}}
|
|
|
|
</div>
|
|
|
|
</div>
|
2024-04-15 03:38:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-14 06:45:51 +02:00
|
|
|
#[component]
|
2024-04-15 22:03:32 +02:00
|
|
|
pub fn ObjectPage() -> impl IntoView {
|
|
|
|
let params = use_params_map();
|
2024-04-17 03:12:17 +02:00
|
|
|
let auth = use_context::<Signal<Option<String>>>().expect("missing auth context");
|
2024-04-16 06:34:50 +02:00
|
|
|
let object = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |oid| {
|
2024-04-15 22:03:32 +02:00
|
|
|
async move {
|
2024-04-16 06:34:50 +02:00
|
|
|
match CACHE.get(&Uri::full("objects", &oid)) {
|
2024-04-15 22:20:33 +02:00
|
|
|
Some(x) => Some(x.clone()),
|
2024-04-16 06:34:50 +02:00
|
|
|
None => {
|
2024-04-17 03:12:17 +02:00
|
|
|
let obj = Http::fetch::<serde_json::Value>(&Uri::api("objects", &oid), auth).await.ok()?;
|
2024-04-16 06:34:50 +02:00
|
|
|
CACHE.put(Uri::full("objects", &oid), obj.clone());
|
|
|
|
Some(obj)
|
|
|
|
}
|
2024-04-15 22:03:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
view! {
|
2024-04-16 08:02:03 +02:00
|
|
|
<div class="ml-1">
|
|
|
|
<div class="tl-header w-100 center mb-s" >view::object</div>
|
|
|
|
<div class="boxscroll ma-2" >
|
|
|
|
{move || match object.get() {
|
|
|
|
Some(Some(o)) => view!{ <Object object=o /> }.into_view(),
|
|
|
|
Some(None) => view! { <p><code>loading failed</code></p> }.into_view(),
|
|
|
|
None => view! { <p> loading ... </p> }.into_view(),
|
|
|
|
}}
|
|
|
|
</div>
|
2024-04-16 07:34:16 +02:00
|
|
|
</div>
|
2024-04-15 22:03:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[component]
|
|
|
|
pub fn Object(object: serde_json::Value) -> impl IntoView {
|
|
|
|
let summary = object.summary().unwrap_or_default().to_string();
|
2024-04-16 07:34:16 +02:00
|
|
|
let content = dissolve::strip_html_tags(object.content().unwrap_or_default());
|
2024-04-17 07:19:50 +02:00
|
|
|
let date = object.published().map(|x| x.format("%Y/%m/%d %H:%M:%S").to_string()).unwrap_or_default();
|
|
|
|
let date_rfc = object.published().map(|x| x.to_rfc3339()).unwrap_or_default();
|
2024-04-15 22:03:32 +02:00
|
|
|
let author_id = object.attributed_to().id().unwrap_or_default();
|
2024-04-16 07:34:16 +02:00
|
|
|
let author = CACHE.get(&author_id).unwrap_or(serde_json::Value::String(author_id.clone()));
|
2024-04-15 22:03:32 +02:00
|
|
|
view! {
|
2024-04-16 07:34:16 +02:00
|
|
|
<div>
|
2024-04-17 05:51:10 +02:00
|
|
|
<table class="w-100 post-table pa-1 mb-s" >
|
2024-04-16 07:51:22 +02:00
|
|
|
{move || if !summary.is_empty() {
|
|
|
|
view! {
|
|
|
|
<tr class="post-table" >
|
|
|
|
<td class="post-table pa-1" colspan="2" >{summary.clone()}</td>
|
|
|
|
</tr>
|
|
|
|
}.into_view()
|
|
|
|
} else {
|
|
|
|
view! { }.into_view()
|
|
|
|
}}
|
2024-04-16 07:34:16 +02:00
|
|
|
<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>
|
2024-04-17 07:19:50 +02:00
|
|
|
<td class="post-table pa-1 center" ><small title={date_rfc} >{date}</small></td>
|
2024-04-16 07:34:16 +02:00
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
</div>
|
2024-04-15 22:03:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[component]
|
|
|
|
pub fn InlineActivity(activity: serde_json::Value) -> impl IntoView {
|
2024-04-16 07:34:16 +02:00
|
|
|
let object_id = activity.object().id().unwrap_or_default();
|
|
|
|
let object = CACHE.get(&object_id).unwrap_or(serde_json::Value::String(object_id.clone()));
|
2024-04-14 06:45:51 +02:00
|
|
|
let addressed = activity.addressed();
|
|
|
|
let audience = format!("[ {} ]", addressed.join(", "));
|
2024-04-16 07:34:16 +02:00
|
|
|
let actor_id = activity.actor().id().unwrap_or_default();
|
|
|
|
let actor = match CACHE.get(&actor_id) {
|
|
|
|
Some(a) => a,
|
|
|
|
None => serde_json::Value::String(actor_id.clone()),
|
|
|
|
};
|
2024-04-14 06:45:51 +02:00
|
|
|
let privacy = if addressed.iter().any(|x| x == apb::target::PUBLIC) {
|
|
|
|
"[public]"
|
|
|
|
} else if addressed.iter().any(|x| x.ends_with("/followers")) {
|
|
|
|
"[followers]"
|
|
|
|
} else {
|
|
|
|
"[private]"
|
|
|
|
};
|
2024-04-17 07:19:50 +02:00
|
|
|
let date = object.published().map(|x| x.format("%Y/%m/%d %H:%M:%S").to_string()).unwrap_or_else(||
|
|
|
|
activity.published().map(|x| x.format("%Y/%m/%d %H:%M:%S").to_string()).unwrap_or_default()
|
2024-04-15 22:03:32 +02:00
|
|
|
);
|
2024-04-14 06:45:51 +02:00
|
|
|
let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity);
|
|
|
|
view! {
|
2024-04-16 07:34:16 +02:00
|
|
|
<div>
|
|
|
|
<table class="align w-100" >
|
|
|
|
<tr>
|
|
|
|
<td rowspan="2" >
|
|
|
|
<ActorBanner object=actor />
|
|
|
|
</td>
|
|
|
|
<td class="rev" >
|
2024-04-17 07:19:50 +02:00
|
|
|
<small><u class="moreinfo" title={audience} >{privacy}</u></small>
|
|
|
|
<code class="color ml-1 moreinfo" title={object_id.clone()} >{kind.as_ref().to_string()}</code>
|
2024-04-16 07:34:16 +02:00
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td class="rev">
|
2024-04-17 05:59:10 +02:00
|
|
|
<a class="hover" href={Uri::web("objects", &object_id)} >
|
2024-04-17 07:19:50 +02:00
|
|
|
<small>{date}</small>
|
2024-04-16 07:34:16 +02:00
|
|
|
</a>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
</div>
|
2024-04-14 06:45:51 +02:00
|
|
|
{match kind {
|
|
|
|
// post
|
2024-04-16 07:34:16 +02:00
|
|
|
apb::ActivityType::Create => view! { <Object object=object /> }.into_view(),
|
|
|
|
_ => view! {}.into_view(),
|
2024-04-14 06:45:51 +02:00
|
|
|
}}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-16 06:34:50 +02:00
|
|
|
#[component]
|
|
|
|
pub fn About() -> impl IntoView {
|
|
|
|
view! {
|
2024-04-16 08:19:09 +02:00
|
|
|
<div class="ml-1">
|
|
|
|
<div class="tl-header w-100 center mb-s" >landing</div>
|
|
|
|
<div class="boxscroll mt-s mb-s" >
|
|
|
|
nothing to see here! pick a timeline to start browsing
|
|
|
|
</div>
|
|
|
|
</div>
|
2024-04-16 06:34:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-15 03:03:01 +02:00
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
#[error("{0}")]
|
|
|
|
struct OmgReqwestErrorIsNotClonable(String);
|
|
|
|
|
2024-04-14 06:45:51 +02:00
|
|
|
#[component]
|
2024-04-16 08:02:03 +02:00
|
|
|
pub fn TimelinePage(name: &'static str, tl: Timeline) -> impl IntoView {
|
2024-04-15 03:03:01 +02:00
|
|
|
view! {
|
|
|
|
<div class="ml-1">
|
2024-04-16 06:34:50 +02:00
|
|
|
<div class="tl-header w-100 center mb-s" >{name}</div>
|
|
|
|
<div class="boxscroll mt-s mb-s" >
|
2024-04-16 08:02:03 +02:00
|
|
|
<TimelineFeed tl=tl />
|
2024-04-15 05:00:23 +02:00
|
|
|
</div>
|
2024-04-15 03:03:01 +02:00
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
2024-04-16 08:02:03 +02:00
|
|
|
|
|
|
|
#[component]
|
|
|
|
pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
|
2024-04-17 03:12:17 +02:00
|
|
|
let auth = use_context::<Signal<Option<String>>>().expect("missing auth context");
|
2024-04-16 08:02:03 +02:00
|
|
|
view! {
|
|
|
|
<For
|
|
|
|
each=move || tl.feed.get()
|
|
|
|
key=|k| k.to_string()
|
|
|
|
children=move |id: String| {
|
|
|
|
match CACHE.get(&id) {
|
|
|
|
Some(object) => {
|
|
|
|
view! {
|
|
|
|
<div class="ml-1 mr-1 mt-1">
|
|
|
|
<InlineActivity activity=object />
|
|
|
|
</div>
|
|
|
|
<hr/ >
|
|
|
|
}.into_view()
|
|
|
|
},
|
|
|
|
None => view! {
|
|
|
|
<p><code>{id}</code>" "[<a href={uri}>go</a>]</p>
|
|
|
|
}.into_view(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/ >
|
|
|
|
<div class="center" >
|
|
|
|
<button type="button"
|
|
|
|
on:click=move |_| {
|
|
|
|
spawn_local(async move {
|
|
|
|
if let Err(e) = tl.more(auth).await {
|
|
|
|
console_error(&format!("error fetching more items for timeline: {e}"));
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
>more</button>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|