feat(web): better timelines, add use obj, add notifs

This commit is contained in:
əlemi 2024-06-08 03:39:38 +02:00
parent cc45de7e6d
commit d275ce7f04
Signed by: alemi
GPG key ID: A4895B84D311642C
8 changed files with 88 additions and 43 deletions

View file

@ -4,6 +4,44 @@ use crate::prelude::*;
use leptos_use::{storage::use_local_storage, use_cookie, utils::{FromToStringCodec, JsonCodec}}; use leptos_use::{storage::use_local_storage, use_cookie, utils::{FromToStringCodec, JsonCodec}};
#[derive(Clone, Copy)]
pub struct Feeds {
// object feeds
pub home: Timeline,
pub global: Timeline,
// notification feeds
pub private: Timeline,
pub public: Timeline,
// exploration feeds
pub user: Timeline,
pub server: Timeline,
pub context: Timeline,
}
impl Feeds {
pub fn new(username: &str) -> Self {
Feeds {
home: Timeline::new(format!("{URL_BASE}/actors/{username}/feed/page")),
global: Timeline::new(format!("{URL_BASE}/feed/page")),
private: Timeline::new(format!("{URL_BASE}/actors/{username}/inbox/page")),
public: Timeline::new(format!("{URL_BASE}/inbox/page")),
user: Timeline::new(format!("{URL_BASE}/actors/{username}/outbox/page")),
server: Timeline::new(format!("{URL_BASE}/outbox/page")),
context: Timeline::new(format!("{URL_BASE}/outbox/page")), // TODO ehhh
}
}
pub fn reset(&self) {
self.home.reset(None);
self.global.reset(None);
self.private.reset(None);
self.public.reset(None);
self.user.reset(None);
self.server.reset(None);
self.context.reset(None);
}
}
#[component] #[component]
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
@ -12,18 +50,16 @@ pub fn App() -> impl IntoView {
let (config, set_config, _) = use_local_storage::<crate::Config, JsonCodec>("config"); let (config, set_config, _) = use_local_storage::<crate::Config, JsonCodec>("config");
let auth = Auth { token, userid }; let auth = Auth { token, userid };
provide_context(auth);
provide_context(config);
let username = auth.userid.get_untracked() let username = auth.userid.get_untracked()
.map(|x| x.split('/').last().unwrap_or_default().to_string()) .map(|x| x.split('/').last().unwrap_or_default().to_string())
.unwrap_or_default(); .unwrap_or_default();
let home_tl = Timeline::new(format!("{URL_BASE}/actors/{username}/inbox/page"));
let user_tl = Timeline::new(format!("{URL_BASE}/actors/{username}/outbox/page"));
let server_tl = Timeline::new(format!("{URL_BASE}/inbox/page"));
let local_tl = Timeline::new(format!("{URL_BASE}/outbox/page"));
let context_tl = Timeline::new(format!("{URL_BASE}/outbox/page")); // TODO ehhh let feeds = Feeds::new(&username);
provide_context(auth);
provide_context(config);
provide_context(feeds);
let reply_controls = ReplyControls::default(); let reply_controls = ReplyControls::default();
provide_context(reply_controls); provide_context(reply_controls);
@ -35,13 +71,14 @@ pub fn App() -> impl IntoView {
let title_target = move || if auth.present() { "/web/home" } else { "/web/server" }; let title_target = move || if auth.present() { "/web/home" } else { "/web/server" };
local_tl.more(auth); // public outbox never contains private posts
spawn_local(async move { spawn_local(async move {
// refresh token first, or verify that we're still authed // refresh token first, or verify that we're still authed
if Auth::refresh(auth.token, set_token, set_userid).await { if Auth::refresh(auth.token, set_token, set_userid).await {
home_tl.more(auth); // home inbox requires auth to be read feeds.home.more(auth); // home inbox requires auth to be read
feeds.private.more(auth);
} }
server_tl.more(auth); // server inbox may contain private posts feeds.global.more(auth);
feeds.public.more(auth); // server inbox may contain private posts
}); });
@ -67,8 +104,6 @@ pub fn App() -> impl IntoView {
<LoginBox <LoginBox
token_tx=set_token token_tx=set_token
userid_tx=set_userid userid_tx=set_userid
home_tl=home_tl
server_tl=server_tl
/> />
<hr class="mt-1 mb-1" /> <hr class="mt-1 mb-1" />
<div class:hidden=move || !auth.present() > <div class:hidden=move || !auth.present() >
@ -105,16 +140,17 @@ pub fn App() -> impl IntoView {
} }
/> />
<Route path="/web/home" view=move || view! { <TimelinePage name="home" tl=home_tl /> } /> <Route path="/web/home" view=move || view! { <TimelinePage name="home" tl=feeds.home /> } />
<Route path="/web/server" view=move || view! { <TimelinePage name="server" tl=server_tl /> } /> <Route path="/web/server" view=move || view! { <TimelinePage name="server" tl=feeds.global /> } />
<Route path="/web/local" view=move || view! { <TimelinePage name="local" tl=local_tl /> } /> <Route path="/web/local" view=move || view! { <TimelinePage name="local" tl=feeds.server /> } />
<Route path="/web/inbox" view=move || view! { <TimelinePage name="inbox" tl=feeds.private /> } />
<Route path="/web/about" view=AboutPage /> <Route path="/web/about" view=AboutPage />
<Route path="/web/config" view=move || view! { <ConfigPage setter=set_config /> } /> <Route path="/web/config" view=move || view! { <ConfigPage setter=set_config /> } />
<Route path="/web/config/dev" view=DebugPage /> <Route path="/web/config/dev" view=DebugPage />
<Route path="/web/actors/:id" view=move || view! { <UserPage tl=user_tl /> } /> <Route path="/web/actors/:id" view=UserPage />
<Route path="/web/objects/:id" view=move || view! { <ObjectPage tl=context_tl /> } /> <Route path="/web/objects/:id" view=ObjectPage />
// <Route path="/web/activities/:id" view=move || view! { <ActivityPage tl=context_tl /> } /> // <Route path="/web/activities/:id" view=move || view! { <ActivityPage tl=context_tl /> } />
<Route path="/web/search" view=SearchPage /> <Route path="/web/search" view=SearchPage />

View file

@ -5,21 +5,20 @@ use crate::prelude::*;
pub fn LoginBox( pub fn LoginBox(
token_tx: WriteSignal<Option<String>>, token_tx: WriteSignal<Option<String>>,
userid_tx: WriteSignal<Option<String>>, userid_tx: WriteSignal<Option<String>>,
home_tl: Timeline,
server_tl: Timeline,
) -> impl IntoView { ) -> impl IntoView {
let auth = use_context::<Auth>().expect("missing auth context"); let auth = use_context::<Auth>().expect("missing auth context");
let username_ref: NodeRef<html::Input> = create_node_ref(); let username_ref: NodeRef<html::Input> = create_node_ref();
let password_ref: NodeRef<html::Input> = create_node_ref(); let password_ref: NodeRef<html::Input> = create_node_ref();
let feeds = use_context::<Feeds>().expect("missing feeds context");
view! { view! {
<div> <div>
<div class="w-100" class:hidden=move || !auth.present() > <div class="w-100" class:hidden=move || !auth.present() >
"hi "<a href={move || Uri::web(U::Actor, &auth.username() )} >{move || auth.username() }</a> "hi "<a href={move || Uri::web(U::Actor, &auth.username() )} >{move || auth.username() }</a>
<input style="float:right" type="submit" value="logout" on:click=move |_| { <input style="float:right" type="submit" value="logout" on:click=move |_| {
token_tx.set(None); token_tx.set(None);
home_tl.reset(format!("{URL_BASE}/outbox/page")); feeds.reset();
server_tl.reset(format!("{URL_BASE}/inbox/page")); feeds.global.more(auth);
server_tl.more(auth); feeds.public.more(auth);
} /> } />
</div> </div>
<div class:hidden=move || auth.present() > <div class:hidden=move || auth.present() >
@ -45,11 +44,15 @@ pub fn LoginBox(
userid_tx.set(Some(auth_response.user)); userid_tx.set(Some(auth_response.user));
token_tx.set(Some(auth_response.token)); token_tx.set(Some(auth_response.token));
// reset home feed and point it to our user's inbox // reset home feed and point it to our user's inbox
home_tl.reset(format!("{URL_BASE}/actors/{}/inbox/page", username)); feeds.home.reset(Some(format!("{URL_BASE}/actors/{username}/feed/page")));
home_tl.more(auth); feeds.home.more(auth);
feeds.private.reset(Some(format!("{URL_BASE}/actors/{username}/inbox/page")));
feeds.private.more(auth);
// reset server feed: there may be more content now that we're authed // reset server feed: there may be more content now that we're authed
server_tl.reset(format!("{URL_BASE}/inbox/page")); feeds.global.reset(Some(format!("{URL_BASE}/feed/page")));
server_tl.more(auth); feeds.global.more(auth);
feeds.server.reset(Some(format!("{URL_BASE}/inbox/page")));
feeds.server.more(auth);
}); });
} > } >
<table class="w-100 align"> <table class="w-100 align">

View file

@ -40,6 +40,7 @@ pub fn Navigator() -> impl IntoView {
<tr><td colspan="2"><a href="/web/home"><input class="w-100" type="submit" class:hidden=move || !auth.present() value="home timeline" /></a></td></tr> <tr><td colspan="2"><a href="/web/home"><input class="w-100" type="submit" class:hidden=move || !auth.present() value="home timeline" /></a></td></tr>
<tr><td colspan="2"><a href="/web/server"><input class="w-100" type="submit" value="server timeline" /></a></td></tr> <tr><td colspan="2"><a href="/web/server"><input class="w-100" type="submit" value="server timeline" /></a></td></tr>
<tr><td colspan="2"><a href="/web/local"><input class="w-100" type="submit" value="local timeline" /></a></td></tr> <tr><td colspan="2"><a href="/web/local"><input class="w-100" type="submit" value="local timeline" /></a></td></tr>
<tr><td colspan="2"><a href="/web/inbox"><input class="w-100" type="submit" class:hidden=move || !auth.present() value="notifications" /></a></td></tr>
<tr> <tr>
<td class="w-50"><a href="/web/about"><input class="w-100" type="submit" value="about" /></a></td> <td class="w-50"><a href="/web/about"><input class="w-100" type="submit" value="about" /></a></td>
<td class="w-50"><a href="/web/config"><input class="w-100" type="submit" value="config" /></a></td> <td class="w-50"><a href="/web/config"><input class="w-100" type="submit" value="config" /></a></td>

View file

@ -30,10 +30,12 @@ impl Timeline {
self.feed.get().is_empty() self.feed.get().is_empty()
} }
pub fn reset(&self, url: String) { pub fn reset(&self, url: Option<String>) {
self.feed.set(vec![]); self.feed.set(vec![]);
self.next.set(url);
self.over.set(false); self.over.set(false);
if let Some(url) = url {
self.next.set(url);
}
} }
pub fn more(&self, auth: Auth) { pub fn more(&self, auth: Auth) {

View file

@ -7,9 +7,10 @@ use crate::prelude::*;
use apb::{Base, Object}; use apb::{Base, Object};
#[component] #[component]
pub fn ObjectPage(tl: Timeline) -> impl IntoView { pub fn ObjectPage() -> impl IntoView {
let params = use_params_map(); let params = use_params_map();
let auth = use_context::<Auth>().expect("missing auth context"); let auth = use_context::<Auth>().expect("missing auth context");
let feeds = use_context::<Feeds>().expect("missing feeds context");
let object = create_local_resource( let object = create_local_resource(
move || params.get().get("id").cloned().unwrap_or_default(), move || params.get().get("id").cloned().unwrap_or_default(),
move |oid| async move { move |oid| async move {
@ -36,8 +37,8 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView {
}; };
if let Ok(ctx) = obj.context().id() { if let Ok(ctx) = obj.context().id() {
let tl_url = format!("{}/context/page", Uri::api(U::Object, ctx, false)); let tl_url = format!("{}/context/page", Uri::api(U::Object, ctx, false));
if !tl.next.get_untracked().starts_with(&tl_url) { if !feeds.context.next.get_untracked().starts_with(&tl_url) {
tl.reset(tl_url); feeds.context.reset(Some(tl_url));
} }
} }
@ -50,10 +51,10 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView {
objects::view objects::view
<a <a
class="clean ml-1" href="#" class="clean ml-1" href="#"
class:hidden=move || tl.is_empty() class:hidden=move || feeds.context.is_empty()
on:click=move |_| { on:click=move |_| {
tl.reset(tl.next.get().split('?').next().unwrap_or_default().to_string()); feeds.context.reset(Some(feeds.context.next.get().split('?').next().unwrap_or_default().to_string()));
tl.more(auth); feeds.context.more(auth);
}><span class="emoji"> }><span class="emoji">
"\u{1f5d8}" "\u{1f5d8}"
</span></a> </span></a>
@ -71,7 +72,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">
<TimelineReplies tl=tl root=o.id().unwrap_or_default().to_string() /> <TimelineReplies tl=feeds.context root=o.id().unwrap_or_default().to_string() />
</div> </div>
}.into_view() }.into_view()
}, },

View file

@ -9,7 +9,7 @@ pub fn TimelinePage(name: &'static str, tl: Timeline) -> impl IntoView {
<Breadcrumb back=false> <Breadcrumb back=false>
{name} {name}
<a class="clean ml-1" href="#" on:click=move |_| { <a class="clean ml-1" href="#" on:click=move |_| {
tl.reset(tl.next.get().split('?').next().unwrap_or_default().to_string()); tl.reset(Some(tl.next.get().split('?').next().unwrap_or_default().to_string()));
tl.more(auth); tl.more(auth);
}><span class="emoji"> }><span class="emoji">
"\u{1f5d8}" "\u{1f5d8}"

View file

@ -20,8 +20,9 @@ fn send_follow_request(target: String) {
} }
#[component] #[component]
pub fn UserPage(tl: Timeline) -> impl IntoView { pub fn UserPage() -> impl IntoView {
let params = use_params_map(); let params = use_params_map();
let feeds = use_context::<Feeds>().expect("missing feeds context");
let auth = use_context::<Auth>().expect("missing auth context"); let auth = use_context::<Auth>().expect("missing auth context");
let id = params.get() let id = params.get()
.get("id") .get("id")
@ -33,8 +34,8 @@ pub fn UserPage(tl: Timeline) -> impl IntoView {
move |id| { move |id| {
async move { async move {
let tl_url = format!("{}/outbox/page", Uri::api(U::Actor, &id, false)); let tl_url = format!("{}/outbox/page", Uri::api(U::Actor, &id, false));
if !tl.next.get_untracked().starts_with(&tl_url) { if !feeds.user.next.get_untracked().starts_with(&tl_url) {
tl.reset(tl_url); feeds.user.reset(Some(tl_url));
} }
match CACHE.get(&Uri::full(U::Actor, &id)) { match CACHE.get(&Uri::full(U::Actor, &id)) {
Some(x) => Some(x.clone()), Some(x) => Some(x.clone()),
@ -54,10 +55,10 @@ pub fn UserPage(tl: Timeline) -> impl IntoView {
users::view users::view
<a <a
class="clean ml-1" href="#" class="clean ml-1" href="#"
class:hidden=move || tl.is_empty() class:hidden=move || feeds.user.is_empty()
on:click=move |_| { on:click=move |_| {
tl.reset(tl.next.get().split('?').next().unwrap_or_default().to_string()); feeds.user.reset(Some(feeds.user.next.get().split('?').next().unwrap_or_default().to_string()));
tl.more(auth); feeds.user.more(auth);
}><span class="emoji"> }><span class="emoji">
"\u{1f5d8}" "\u{1f5d8}"
</span></a> </span></a>
@ -138,7 +139,7 @@ pub fn UserPage(tl: Timeline) -> impl IntoView {
<p class="ml-2 mt-1 center" inner_html={mdhtml::safe_html(&summary)}></p> <p class="ml-2 mt-1 center" inner_html={mdhtml::safe_html(&summary)}></p>
</div> </div>
</div> </div>
<TimelineFeed tl=tl /> <TimelineFeed tl=feeds.user />
}.into_view() }.into_view()
}, },
} }

View file

@ -1,6 +1,7 @@
pub use crate::{ pub use crate::{
Http, Uri, Http, Uri,
CACHE, URL_BASE, CACHE, URL_BASE,
app::Feeds,
auth::Auth, auth::Auth,
page::*, page::*,
components::*, components::*,