use apb::{ActivityMut, DocumentMut, Object, ObjectMut};
use leptos::prelude::*;
use crate::{prelude::*, DEFAULT_COLOR};
#[component]
pub fn ConfigPage(setter: WriteSignal<crate::Config>) -> impl IntoView {
let config = use_context::<Signal<crate::Config>>().expect("missing config context");
let auth = use_context::<Auth>().expect("missing auth context");
let (color, set_color) = leptos_use::use_css_var("--accent");
let (_color_rgb, set_color_rgb) = leptos_use::use_css_var("--accent-rgb");
let previous_color = config.get_untracked().accent_color;
set_color_rgb.set(parse_hex(&previous_color));
set_color.set(previous_color);
let display_name_ref: NodeRef<leptos::html::Input> = NodeRef::new();
let summary_ref: NodeRef<leptos::html::Textarea> = NodeRef::new();
let avatar_url_ref: NodeRef<leptos::html::Input> = NodeRef::new();
let banner_url_ref: NodeRef<leptos::html::Input> = NodeRef::new();
let myself = cache::OBJECTS.get(&auth.userid.get_untracked().unwrap_or_default());
let (curr_display_name, curr_summary, curr_icon, curr_banner) = match myself.as_ref() {
None => (String::default(), String::default(), String::default(), String::default()),
Some(myself) => (
myself.name().unwrap_or_default().to_string(),
myself.summary().unwrap_or_default().to_string(),
myself.icon().inner().and_then(|x| Ok(x.url().id()?.to_string())).unwrap_or_default(),
myself.image().inner().and_then(|x| Ok(x.url().id()?.to_string())).unwrap_or_default(),
),
};
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_untracked();
mock.$field = event_target_checked(&ev);
setter.set(mock);
}
};
(filter $field:ident) => {
move |ev| {
let mut mock = config.get_untracked();
mock.filters.$field = event_target_checked(&ev);
setter.set(mock);
}
};
}
let (privacy, set_privacy) = signal(config.get().default_privacy);
Effect::watch(
move || privacy.get(),
move |prv, old_prv, _state| {
if let Some(old_prv_value) = old_prv {
if prv != old_prv_value {
let mut mock = config.get_untracked();
mock.default_privacy = *prv;
setter.set(mock);
}
}
},
false,
);
view! {
<div>
<p class="center mt-0"><small>config is saved in your browser local storage</small></p>
<p>
<span title="embedded video attachments will loop like gifs if this option is enabled">
<input type="checkbox" class="mr-1"
prop:checked=get_cfg!(loop_videos)
on:input=set_cfg!(loop_videos)
/> loop videos
</span>
</p>
<p>
<span title="any post with a summary is considered to have a content warning, and collapsed by default if this option is enabled">
<input type="checkbox" class="mr-1"
prop:checked=get_cfg!(collapse_content_warnings)
on:input=set_cfg!(collapse_content_warnings)
/> collapse content warnings
</span>
</p>
<p>
<span title="new posts will be fetched automatically when scrolling down enough">
<input type="checkbox" class="mr-1"
prop:checked=get_cfg!(infinite_scroll)
on:input=set_cfg!(infinite_scroll)
/> infinite scroll
</span>
</p>
<p>
<span title="default privacy setting applied when loading frontend">
default privacy <PrivacySelector getter=privacy setter=set_privacy full_width=false />
</span>
</p>
<p>
<span title="customize frontend accent color with">
accent color
<input type="text" class="ma-1"
style="width: 8ch;"
placeholder=DEFAULT_COLOR
value=color
on:input=move|ev| {
let mut val = event_target_value(&ev);
if val.is_empty() { val = DEFAULT_COLOR.to_string() };
let mut mock = config.get();
set_color_rgb.set(parse_hex(&val));
set_color.set(val.clone());
mock.accent_color = val;
setter.set(mock);
} />
</span>
</p>
<hr />
<p><code title="unchecked elements won't show in timelines">filters</code></p>
<ul>
<li><span title="replies to other posts"><input type="checkbox" prop:checked=get_cfg!(filter replies) on:input=move |ev| {
let mut mock = config.get();
mock.filters.replies = event_target_checked(&ev);
setter.set(mock);
cache::TIMELINES.clear();
}/>" replies"</span></li>
<li><span title="like activities"><input type="checkbox" prop:checked=get_cfg!(filter likes) on:input=set_cfg!(filter likes) />" likes"</span></li>
<li><span title="create activities with object"><input type="checkbox" prop:checked=get_cfg!(filter creates) on:input=set_cfg!(filter creates)/>" creates"</span></li>
<li><span title="update activities, to objects or actors"><input type="checkbox" prop:checked=get_cfg!(filter updates) on:input=set_cfg!(filter updates)/>" updates"</span></li>
<li><span title="delete activities"><input type="checkbox" prop:checked=get_cfg!(filter deletes) on:input=set_cfg!(filter deletes)/>" deletes"</span></li>
<li><span title="announce activities with object"><input type="checkbox" prop:checked=get_cfg!(filter announces) on:input=set_cfg!(filter announces) />" announces"</span></li>
<li><span title="follow, accept and reject activities"><input type="checkbox" prop:checked=get_cfg!(filter follows) on:input=set_cfg!(filter follows) />" follows"</span></li>
<li><span title="objects without a related activity to display"><input type="checkbox" prop:checked=get_cfg!(filter orphans) on:input=set_cfg!(filter orphans) />" fetched"</span></li>
</ul>
<hr />
<div class="border ma-2 pa-1" class:hidden=auth.anonymous()>
<code class="center cw color mb-1">update profile</code>
<div class="col-side mb-0">display name</div>
<div class="col-main">
<input class="w-100" type="text" node_ref=display_name_ref placeholder="bmdieGo=" value={curr_display_name}/>
</div>
<div class="col-side mb-0">avatar url</div>
<div class="col-main">
<input class="w-100" type="text" node_ref=avatar_url_ref placeholder="https://cdn.alemi.dev/social/circle-square.png" value={curr_icon} />
</div>
<div class="col-side mb-0">banner url</div>
<div class="col-main">
<input class="w-100" type="text" node_ref=banner_url_ref placeholder="https://cdn.alemi.dev/social/gradient.png" value={curr_banner} />
</div>
<div class="col-side mb-0">summary</div>
<div class="col-main">
<textarea rows="5" class="w-100" node_ref=summary_ref placeholder="when you lose control of yourself, who's controlling you?">{curr_summary}</textarea>
</div>
<input class="w-100" type="submit" value="submit"
on:click=move|e| {
e.prevent_default();
let display_name = display_name_ref.get()
.map(|x| x.value())
.filter(|x| !x.is_empty());
let summary = summary_ref.get()
.map(|x| x.value())
.filter(|x| !x.is_empty());
let avatar = avatar_url_ref.get()
.map(|x| x.value())
.filter(|x| !x.is_empty())
.map(|x|
apb::new()
.set_document_type(Some(apb::DocumentType::Image))
.set_url(apb::Node::link(x))
);
let banner = banner_url_ref.get()
.map(|x| x.value())
.filter(|x| !x.is_empty())
.map(|x|
apb::new()
.set_document_type(Some(apb::DocumentType::Image))
.set_url(apb::Node::link(x))
);
let id = auth.userid.get_untracked().unwrap_or_default();
let Some(me) = cache::OBJECTS.get(&id) else {
tracing::error!("self user not in cache! can't update");
return;
};
let payload = apb::new()
.set_activity_type(Some(apb::ActivityType::Update))
.set_to(apb::Node::links(vec![apb::target::PUBLIC.to_string(), format!("{id}/followers")]))
.set_object(apb::Node::object(
(*me).clone()
.set_name(display_name)
.set_summary(summary)
.set_icon(apb::Node::maybe_object(avatar))
.set_image(apb::Node::maybe_object(banner))
.set_published(Some(chrono::Utc::now()))
));
leptos::task::spawn_local(async move {
if let Err(e) = Http::post(&auth.outbox(), &payload, auth).await {
tracing::error!("could not send update activity: {e}");
}
});
}
/>
</div>
</div>
}
}
fn parse_hex(hex: &str) -> String {
if hex.len() < 7 { return "0, 0, 0".into(); }
let r = i64::from_str_radix(&hex[1..3], 16).unwrap_or_default();
let g = i64::from_str_radix(&hex[3..5], 16).unwrap_or_default();
let b = i64::from_str_radix(&hex[5..7], 16).unwrap_or_default();
format!("{r}, {g}, {b}")
}