feat(web): better timelines, add use obj, add notifs
This commit is contained in:
parent
cc45de7e6d
commit
d275ce7f04
8 changed files with 88 additions and 43 deletions
|
@ -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 />
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
},
|
},
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::*,
|
||||||
|
|
Loading…
Reference in a new issue