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}};
#[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]
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 auth = Auth { token, userid };
provide_context(auth);
provide_context(config);
let username = auth.userid.get_untracked()
.map(|x| x.split('/').last().unwrap_or_default().to_string())
.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();
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" };
local_tl.more(auth); // public outbox never contains private posts
spawn_local(async move {
// refresh token first, or verify that we're still authed
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
token_tx=set_token
userid_tx=set_userid
home_tl=home_tl
server_tl=server_tl
/>
<hr class="mt-1 mb-1" />
<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/server" view=move || view! { <TimelinePage name="server" tl=server_tl /> } />
<Route path="/web/local" view=move || view! { <TimelinePage name="local" tl=local_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=feeds.global /> } />
<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/config" view=move || view! { <ConfigPage setter=set_config /> } />
<Route path="/web/config/dev" view=DebugPage />
<Route path="/web/actors/:id" view=move || view! { <UserPage tl=user_tl /> } />
<Route path="/web/objects/:id" view=move || view! { <ObjectPage tl=context_tl /> } />
<Route path="/web/actors/:id" view=UserPage />
<Route path="/web/objects/:id" view=ObjectPage />
// <Route path="/web/activities/:id" view=move || view! { <ActivityPage tl=context_tl /> } />
<Route path="/web/search" view=SearchPage />

View file

@ -5,21 +5,20 @@ use crate::prelude::*;
pub fn LoginBox(
token_tx: WriteSignal<Option<String>>,
userid_tx: WriteSignal<Option<String>>,
home_tl: Timeline,
server_tl: Timeline,
) -> impl IntoView {
let auth = use_context::<Auth>().expect("missing auth context");
let username_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! {
<div>
<div class="w-100" class:hidden=move || !auth.present() >
"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 |_| {
token_tx.set(None);
home_tl.reset(format!("{URL_BASE}/outbox/page"));
server_tl.reset(format!("{URL_BASE}/inbox/page"));
server_tl.more(auth);
feeds.reset();
feeds.global.more(auth);
feeds.public.more(auth);
} />
</div>
<div class:hidden=move || auth.present() >
@ -45,11 +44,15 @@ pub fn LoginBox(
userid_tx.set(Some(auth_response.user));
token_tx.set(Some(auth_response.token));
// reset home feed and point it to our user's inbox
home_tl.reset(format!("{URL_BASE}/actors/{}/inbox/page", username));
home_tl.more(auth);
feeds.home.reset(Some(format!("{URL_BASE}/actors/{username}/feed/page")));
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
server_tl.reset(format!("{URL_BASE}/inbox/page"));
server_tl.more(auth);
feeds.global.reset(Some(format!("{URL_BASE}/feed/page")));
feeds.global.more(auth);
feeds.server.reset(Some(format!("{URL_BASE}/inbox/page")));
feeds.server.more(auth);
});
} >
<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/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/inbox"><input class="w-100" type="submit" class:hidden=move || !auth.present() value="notifications" /></a></td></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/config"><input class="w-100" type="submit" value="config" /></a></td>

View file

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

View file

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

View file

@ -9,7 +9,7 @@ pub fn TimelinePage(name: &'static str, tl: Timeline) -> impl IntoView {
<Breadcrumb back=false>
{name}
<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);
}><span class="emoji">
"\u{1f5d8}"

View file

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

View file

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