Compare commits
2 commits
18f712194c
...
227e9c625b
Author | SHA1 | Date | |
---|---|---|---|
227e9c625b | |||
c068822b3c |
10 changed files with 176 additions and 38 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -4510,6 +4510,8 @@ dependencies = [
|
||||||
"mdhtml",
|
"mdhtml",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde-inline-default",
|
||||||
|
"serde_default",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
|
@ -19,6 +19,8 @@ console_error_panic_hook = "0.1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
serde_default = "0.1"
|
||||||
|
serde-inline-default = "0.2"
|
||||||
dashmap = "5.5"
|
dashmap = "5.5"
|
||||||
leptos = { version = "0.6", features = ["csr", "tracing"] }
|
leptos = { version = "0.6", features = ["csr", "tracing"] }
|
||||||
leptos_router = { version = "0.6", features = ["csr"] }
|
leptos_router = { version = "0.6", features = ["csr"] }
|
||||||
|
|
|
@ -2,7 +2,7 @@ use leptos::*;
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
use leptos_use::{use_cookie, use_cookie_with_options, utils::FromToStringCodec, UseCookieOptions};
|
use leptos_use::{storage::use_local_storage, use_cookie, use_cookie_with_options, utils::{FromToStringCodec, JsonCodec}, UseCookieOptions};
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
@ -12,10 +12,12 @@ pub fn App() -> impl IntoView {
|
||||||
UseCookieOptions::default()
|
UseCookieOptions::default()
|
||||||
.max_age(1000 * 60 * 60 * 6)
|
.max_age(1000 * 60 * 60 * 6)
|
||||||
);
|
);
|
||||||
|
let (config, set_config, _) = use_local_storage::<crate::Config, JsonCodec>("config");
|
||||||
let (userid, set_userid) = use_cookie::<String, FromToStringCodec>("user_id");
|
let (userid, set_userid) = use_cookie::<String, FromToStringCodec>("user_id");
|
||||||
|
|
||||||
let auth = Auth { token, userid };
|
let auth = Auth { token, userid };
|
||||||
provide_context(auth);
|
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())
|
||||||
|
@ -105,7 +107,7 @@ 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=home_tl /> } />
|
||||||
<Route path="/web/server" view=move || view! { <TimelinePage name="server" tl=server_tl /> } />
|
<Route path="/web/server" view=move || view! { <TimelinePage name="server" tl=server_tl /> } />
|
||||||
|
|
||||||
<Route path="/web/config" view=ConfigPage />
|
<Route path="/web/config" view=move || view! { <ConfigPage setter=set_config /> } />
|
||||||
<Route path="/web/about" view=AboutPage />
|
<Route path="/web/about" view=AboutPage />
|
||||||
|
|
||||||
<Route path="/web/users/:id" view=move || view! { <UserPage tl=user_tl /> } />
|
<Route path="/web/users/:id" view=move || view! { <UserPage tl=user_tl /> } />
|
||||||
|
|
|
@ -33,36 +33,51 @@ pub fn ActivityLine(activity: crate::Object) -> impl IntoView {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Item(item: crate::Object) -> impl IntoView {
|
pub fn Item(
|
||||||
|
item: crate::Object,
|
||||||
|
#[prop(optional)] sep: bool,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let config = use_context::<Signal<crate::Config>>().expect("missing config context");
|
||||||
let id = item.id().unwrap_or_default().to_string();
|
let id = item.id().unwrap_or_default().to_string();
|
||||||
|
let sep = if sep { Some(view! { <hr /> }) } else { None };
|
||||||
match item.object_type() {
|
match item.object_type() {
|
||||||
// special case for placeholder activities
|
// special case for placeholder activities
|
||||||
Some(apb::ObjectType::Note) | Some(apb::ObjectType::Document(_)) =>
|
Some(apb::ObjectType::Note) | Some(apb::ObjectType::Document(_)) => (move || {
|
||||||
view! { <Object object=item /> }.into_view(),
|
if config.get().filters.orphans {
|
||||||
|
Some(view! { <Object object=item.clone() />{sep.clone()} })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}).into_view(),
|
||||||
// everything else
|
// everything else
|
||||||
Some(apb::ObjectType::Activity(t)) => {
|
Some(apb::ObjectType::Activity(t)) => (move || {
|
||||||
let object_id = item.object().id().unwrap_or_default();
|
if config.get().filters.visible(apb::ObjectType::Activity(t)) {
|
||||||
let object = match t {
|
let object_id = item.object().id().unwrap_or_default();
|
||||||
apb::ActivityType::Create | apb::ActivityType::Announce =>
|
let object = match t {
|
||||||
CACHE.get(&object_id).map(|obj| {
|
apb::ActivityType::Create | apb::ActivityType::Announce =>
|
||||||
view! { <Object object=obj /> }
|
CACHE.get(&object_id).map(|obj| {
|
||||||
}.into_view()),
|
view! { <Object object=obj /> }
|
||||||
apb::ActivityType::Follow =>
|
}.into_view()),
|
||||||
CACHE.get(&object_id).map(|obj| {
|
apb::ActivityType::Follow =>
|
||||||
view! {
|
CACHE.get(&object_id).map(|obj| {
|
||||||
<div class="ml-1">
|
view! {
|
||||||
<ActorBanner object=obj />
|
<div class="ml-1">
|
||||||
<FollowRequestButtons activity_id=id actor_id=object_id />
|
<ActorBanner object=obj />
|
||||||
</div>
|
<FollowRequestButtons activity_id=id.clone() actor_id=object_id />
|
||||||
}
|
</div>
|
||||||
}.into_view()),
|
}
|
||||||
_ => None,
|
}.into_view()),
|
||||||
};
|
_ => None,
|
||||||
view! {
|
};
|
||||||
<ActivityLine activity=item />
|
Some(view! {
|
||||||
{object}
|
<ActivityLine activity=item.clone() />
|
||||||
}.into_view()
|
{object}
|
||||||
},
|
{sep.clone()}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}).into_view(),
|
||||||
// should never happen
|
// should never happen
|
||||||
_ => view! { <p><code>type not implemented</code></p> }.into_view(),
|
_ => view! { <p><code>type not implemented</code></p> }.into_view(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ pub fn Attachment(
|
||||||
#[prop(optional)]
|
#[prop(optional)]
|
||||||
sensitive: bool
|
sensitive: bool
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
let config = use_context::<Signal<crate::Config>>().expect("missing config context");
|
||||||
let (expand, set_expand) = create_signal(false);
|
let (expand, set_expand) = create_signal(false);
|
||||||
let href = object.url().id().unwrap_or_default();
|
let href = object.url().id().unwrap_or_default();
|
||||||
let media_type = object.media_type()
|
let media_type = object.media_type()
|
||||||
|
@ -47,7 +48,7 @@ pub fn Attachment(
|
||||||
on:click=move |_| set_expand.set(!expand.get())
|
on:click=move |_| set_expand.set(!expand.get())
|
||||||
title={object.name().unwrap_or_default().to_string()}
|
title={object.name().unwrap_or_default().to_string()}
|
||||||
>
|
>
|
||||||
<video controls loop class="attachment" class:expand=expand >
|
<video controls class="attachment" class:expand=expand prop:loop=move || config.get().loop_videos >
|
||||||
{move || if sensitive && !expand.get() { None } else { Some(view! { <source src={_href.clone()} type={media_type.clone()} /> }) }}
|
{move || if sensitive && !expand.get() { None } else { Some(view! { <source src={_href.clone()} type={media_type.clone()} /> }) }}
|
||||||
<a href={href.clone()} target="_blank">video clip</a>
|
<a href={href.clone()} target="_blank">video clip</a>
|
||||||
</video>
|
</video>
|
||||||
|
@ -58,7 +59,7 @@ pub fn Attachment(
|
||||||
"audio" =>
|
"audio" =>
|
||||||
view! {
|
view! {
|
||||||
<p class="center">
|
<p class="center">
|
||||||
<audio controls class="w-100">
|
<audio controls class="w-100" prop:loop=move || config.get().loop_videos >
|
||||||
<source src={href.clone()} type={media_type} />
|
<source src={href.clone()} type={media_type} />
|
||||||
<a href={href} target="_blank">audio clip</a>
|
<a href={href} target="_blank">audio clip</a>
|
||||||
</audio>
|
</audio>
|
||||||
|
@ -106,7 +107,7 @@ pub fn Object(object: crate::Object) -> impl IntoView {
|
||||||
Some(view! { <div class="pb-1"></div> })
|
Some(view! { <div class="pb-1"></div> })
|
||||||
};
|
};
|
||||||
let post_inner = view! {
|
let post_inner = view! {
|
||||||
<Summary summary=object.summary().map(|x| x.to_string()) open=false >
|
<Summary summary=object.summary().map(|x| x.to_string()) >
|
||||||
<p inner_html={content}></p>
|
<p inner_html={content}></p>
|
||||||
{attachments_padding}
|
{attachments_padding}
|
||||||
{attachments}
|
{attachments}
|
||||||
|
@ -152,11 +153,12 @@ pub fn Object(object: crate::Object) -> impl IntoView {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Summary(summary: Option<String>, open: bool, children: Children) -> impl IntoView {
|
pub fn Summary(summary: Option<String>, children: Children) -> impl IntoView {
|
||||||
|
let config = use_context::<Signal<crate::Config>>().expect("missing config context");
|
||||||
match summary.filter(|x| !x.is_empty()) {
|
match summary.filter(|x| !x.is_empty()) {
|
||||||
None => children().into_view(),
|
None => children().into_view(),
|
||||||
Some(summary) => view! {
|
Some(summary) => view! {
|
||||||
<details class="pa-s" prop:open=open>
|
<details class="pa-s" prop:open=move || !config.get().collapse_content_warnings>
|
||||||
<summary>
|
<summary>
|
||||||
<code class="cw center color ml-s w-100">{summary}</code>
|
<code class="cw center color ml-s w-100">{summary}</code>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
|
@ -140,8 +140,7 @@ pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
|
||||||
children=move |id: String| {
|
children=move |id: String| {
|
||||||
match CACHE.get(&id) {
|
match CACHE.get(&id) {
|
||||||
Some(i) => view! {
|
Some(i) => view! {
|
||||||
<Item item=i />
|
<Item item=i sep=true />
|
||||||
<hr />
|
|
||||||
}.into_view(),
|
}.into_view(),
|
||||||
None => view! {
|
None => view! {
|
||||||
<p><code>{id}</code>" "[<a href={uri}>go</a>]</p>
|
<p><code>{id}</code>" "[<a href={uri}>go</a>]</p>
|
||||||
|
|
47
web/src/config.rs
Normal file
47
web/src/config.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
|
||||||
|
#[serde_inline_default::serde_inline_default]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, serde_default::DefaultFromSerde)]
|
||||||
|
pub struct Config {
|
||||||
|
#[serde(default)]
|
||||||
|
pub filters: FiltersConfig,
|
||||||
|
|
||||||
|
#[serde_inline_default(true)]
|
||||||
|
pub collapse_content_warnings: bool,
|
||||||
|
|
||||||
|
#[serde_inline_default(true)]
|
||||||
|
pub loop_videos: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_inline_default::serde_inline_default]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, serde_default::DefaultFromSerde)]
|
||||||
|
pub struct FiltersConfig {
|
||||||
|
#[serde_inline_default(false)]
|
||||||
|
pub likes: bool,
|
||||||
|
|
||||||
|
#[serde_inline_default(true)]
|
||||||
|
pub creates: bool,
|
||||||
|
|
||||||
|
#[serde_inline_default(true)]
|
||||||
|
pub announces: bool,
|
||||||
|
|
||||||
|
#[serde_inline_default(true)]
|
||||||
|
pub follows: bool,
|
||||||
|
|
||||||
|
#[serde_inline_default(true)]
|
||||||
|
pub orphans: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FiltersConfig {
|
||||||
|
pub fn visible(&self, object_type: apb::ObjectType) -> bool {
|
||||||
|
match object_type {
|
||||||
|
apb::ObjectType::Note | apb::ObjectType::Document(_) => self.orphans,
|
||||||
|
apb::ObjectType::Activity(apb::ActivityType::Like) => self.likes,
|
||||||
|
apb::ObjectType::Activity(apb::ActivityType::Create) => self.creates,
|
||||||
|
apb::ObjectType::Activity(apb::ActivityType::Announce) => self.announces,
|
||||||
|
apb::ObjectType::Activity(
|
||||||
|
apb::ActivityType::Follow | apb::ActivityType::Accept(_) | apb::ActivityType::Reject(_)
|
||||||
|
) => self.follows,
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,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/about"><input class="w-100" type="submit" value="about" /></a></td></tr>
|
<tr><td colspan="2"><a href="/web/about"><input class="w-100" type="submit" value="about" /></a></td></tr>
|
||||||
|
<tr><td colspan="2"><a href="/web/config"><input class="w-100" type="submit" value="config" /></a></td></tr>
|
||||||
<tr><td colspan="2"><a href="/web/debug"><input class="w-100" type="submit" value="debug" class:hidden=move|| !auth.present() /></a></td></tr>
|
<tr><td colspan="2"><a href="/web/debug"><input class="w-100" type="submit" value="debug" class:hidden=move|| !auth.present() /></a></td></tr>
|
||||||
</table>
|
</table>
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,10 @@ mod auth;
|
||||||
mod components;
|
mod components;
|
||||||
mod page;
|
mod page;
|
||||||
mod control;
|
mod control;
|
||||||
|
mod config;
|
||||||
|
|
||||||
pub use app::App;
|
pub use app::App;
|
||||||
|
pub use config::Config;
|
||||||
|
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ use apb::{ActivityMut, Actor, Base, Object, ObjectMut};
|
||||||
|
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
use crate::{prelude::*, DEFAULT_AVATAR_URL};
|
use crate::{prelude::*, Config, DEFAULT_AVATAR_URL};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn AboutPage() -> impl IntoView {
|
pub fn AboutPage() -> impl IntoView {
|
||||||
|
@ -21,17 +21,83 @@ pub fn AboutPage() -> impl IntoView {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ConfigPage() -> impl IntoView {
|
pub fn ConfigPage(setter: WriteSignal<Config>) -> impl IntoView {
|
||||||
|
let config = use_context::<Signal<Config>>().expect("missing config context");
|
||||||
|
|
||||||
|
macro_rules! get_cfg {
|
||||||
|
(filter $field:ident) => {
|
||||||
|
move || config.get().filters.$field
|
||||||
|
};
|
||||||
|
($field:ident) => {
|
||||||
|
move || config.get().$field
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! set_cfg {
|
||||||
|
($field:ident) => {
|
||||||
|
move |ev| {
|
||||||
|
let mut mock = config.get();
|
||||||
|
mock.$field = event_target_checked(&ev);
|
||||||
|
setter.set(mock);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(filter $field:ident) => {
|
||||||
|
move |ev| {
|
||||||
|
let mut mock = config.get();
|
||||||
|
mock.filters.$field = event_target_checked(&ev);
|
||||||
|
setter.set(mock);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumb>config</Breadcrumb>
|
<Breadcrumb>config</Breadcrumb>
|
||||||
|
<p>
|
||||||
|
<input type="checkbox" title="likes" class="mr-1"
|
||||||
|
prop:checked=get_cfg!(loop_videos)
|
||||||
|
on:input=set_cfg!(loop_videos)
|
||||||
|
/> loop videos
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input type="checkbox" title="likes" class="mr-1"
|
||||||
|
prop:checked=get_cfg!(collapse_content_warnings)
|
||||||
|
on:input=set_cfg!(collapse_content_warnings)
|
||||||
|
/> collapse content warnings
|
||||||
|
</p>
|
||||||
<div class="mt-s mb-s" >
|
<div class="mt-s mb-s" >
|
||||||
<p><code>"not implemented :("</code></p>
|
<table class="ma-3 center">
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>filters</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input type="checkbox" prop:checked=get_cfg!(filter likes) on:input=set_cfg!(filter likes) /></td>
|
||||||
|
<td>likes</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input type="checkbox" prop:checked=get_cfg!(filter creates) on:input=set_cfg!(filter creates)/></td>
|
||||||
|
<td>creates</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input type="checkbox" prop:checked=get_cfg!(filter announces) on:input=set_cfg!(filter announces) /></td>
|
||||||
|
<td>announces</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input type="checkbox" prop:checked=get_cfg!(filter follows) on:input=set_cfg!(filter follows) /></td>
|
||||||
|
<td>follows</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input type="checkbox" prop:checked=get_cfg!(filter orphans) on:input=set_cfg!(filter orphans) /></td>
|
||||||
|
<td>orphans</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn send_follow_request(target: String) {
|
fn send_follow_request(target: String) {
|
||||||
let auth = use_context::<Auth>().expect("missing auth context");
|
let auth = use_context::<Auth>().expect("missing auth context");
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
|
|
Loading…
Reference in a new issue