feat(web): advanced post composer
This commit is contained in:
parent
46de9aebd6
commit
53dcfcb993
3 changed files with 213 additions and 39 deletions
|
@ -23,6 +23,7 @@ pub fn App() -> impl IntoView {
|
|||
let screen_width = window().screen().map(|x| x.avail_width().unwrap_or_default()).unwrap_or_default();
|
||||
|
||||
let (menu, set_menu) = create_signal(screen_width <= 786);
|
||||
let (advanced, set_advanced) = create_signal(false);
|
||||
|
||||
spawn_local(async move {
|
||||
if let Err(e) = server_tl.more(auth).await {
|
||||
|
@ -62,7 +63,11 @@ pub fn App() -> impl IntoView {
|
|||
<hr class="mt-1 mb-1" />
|
||||
<Navigator />
|
||||
<hr class="mt-1 mb-1" />
|
||||
<PostBox username=username />
|
||||
{move || if advanced.get() { view! {
|
||||
<AdvancedPostBox username=username advanced=set_advanced/>
|
||||
}} else { view! {
|
||||
<PostBox username=username advanced=set_advanced/>
|
||||
}}}
|
||||
</div>
|
||||
<div class="col-main" class:w-100=move || menu.get() >
|
||||
<Router // TODO maybe set base="/web" ?
|
||||
|
|
|
@ -26,14 +26,18 @@ pub fn DateTime(t: Option<chrono::DateTime<chrono::Utc>>) -> impl IntoView {
|
|||
}
|
||||
}
|
||||
|
||||
pub const PRIVACY_PUBLIC : &str = "💿";
|
||||
pub const PRIVACY_FOLLOWERS : &str = "🔒";
|
||||
pub const PRIVACY_PRIVATE : &str = "📨";
|
||||
|
||||
#[component]
|
||||
pub fn PrivacyMarker(addressed: Vec<String>) -> impl IntoView {
|
||||
let privacy = if addressed.iter().any(|x| x == apb::target::PUBLIC) {
|
||||
"🌐"
|
||||
PRIVACY_PUBLIC
|
||||
} else if addressed.iter().any(|x| x.ends_with("/followers")) {
|
||||
"🔒"
|
||||
PRIVACY_FOLLOWERS
|
||||
} else {
|
||||
"🔗"
|
||||
PRIVACY_PRIVATE
|
||||
};
|
||||
let audience = format!("[ {} ]", addressed.join(", "));
|
||||
view! {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use apb::ObjectMut;
|
||||
use apb::{ActivityMut, ObjectMut};
|
||||
|
||||
use leptos::*;
|
||||
use crate::prelude::*;
|
||||
|
@ -15,68 +15,224 @@ pub fn Navigator() -> impl IntoView {
|
|||
}
|
||||
|
||||
#[component]
|
||||
pub fn PostBox(username: Signal<Option<String>>) -> impl IntoView {
|
||||
pub fn PostBox(username: Signal<Option<String>>, advanced: WriteSignal<bool>) -> impl IntoView {
|
||||
let auth = use_context::<Auth>().expect("missing auth context");
|
||||
let (posting, set_posting) = create_signal(false);
|
||||
let (error, set_error) = create_signal(None);
|
||||
let summary_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let content_ref: NodeRef<html::Textarea> = create_node_ref();
|
||||
let public_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let followers_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let private_ref: NodeRef<html::Input> = create_node_ref();
|
||||
view! {
|
||||
<div class:hidden=move || !auth.present() >
|
||||
<table class="align w-100">
|
||||
<tr>
|
||||
<td><input type="checkbox" title="public" value="public" node_ref=public_ref /></td>
|
||||
<td class="w-100"><input class="w-100" type="text" node_ref=summary_ref title="summary" /></td>
|
||||
<td><input type="checkbox" title="followers" value="followers" node_ref=followers_ref checked /></td>
|
||||
<td><input type="checkbox" on:input=move |ev| advanced.set(event_target_checked(&ev)) title="advanced" /></td>
|
||||
<td><input class="w-100" type="text" node_ref=summary_ref title="summary" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
</table>
|
||||
|
||||
<textarea rows="5" class="w-100" node_ref=content_ref title="content" ></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<table class="align rev w-100">
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<button class="w-100" type="button" on:click=move |_| {
|
||||
<td><input id="priv-public" type="radio" name="privacy" value="public" title="public" node_ref=public_ref /></td>
|
||||
<td><span class="emoji" title="public" >{PRIVACY_PUBLIC}</span></td>
|
||||
<td class="w-100" rowspan="3">
|
||||
<button class="w-100" prop:disabled=posting type="button" style="height: 3em" on:click=move |_| {
|
||||
set_posting.set(true);
|
||||
spawn_local(async move {
|
||||
let summary = summary_ref.get().map(|x| x.value());
|
||||
let content = content_ref.get().map(|x| x.value()).unwrap_or_default();
|
||||
let public = public_ref.get().map(|x| x.checked()).unwrap_or_default();
|
||||
let followers = followers_ref.get().map(|x| x.checked()).unwrap_or_default();
|
||||
match Http::post(
|
||||
&format!("{URL_BASE}/users/test/outbox"),
|
||||
&serde_json::Value::Object(serde_json::Map::default())
|
||||
let (to, cc) = if get_checked(public_ref) {
|
||||
(apb::Node::links(vec![apb::target::PUBLIC.to_string()]), apb::Node::links(vec![format!("{URL_BASE}/users/{}/followers", username.get().unwrap_or_default())]))
|
||||
} else if get_checked(followers_ref) {
|
||||
(apb::Node::links(vec![format!("{URL_BASE}/users/{}/followers", username.get().unwrap_or_default())]), apb::Node::Empty)
|
||||
} else if get_checked(private_ref) {
|
||||
(apb::Node::links(vec![]), apb::Node::Empty)
|
||||
} else {
|
||||
(apb::Node::Empty, apb::Node::Empty)
|
||||
};
|
||||
let payload = serde_json::Value::Object(serde_json::Map::default())
|
||||
.set_object_type(Some(apb::ObjectType::Note))
|
||||
.set_summary(summary.as_deref())
|
||||
.set_content(Some(&content))
|
||||
.set_to(
|
||||
if public {
|
||||
apb::Node::links(vec![apb::target::PUBLIC.to_string()])
|
||||
} else { apb::Node::Empty }
|
||||
)
|
||||
.set_cc(
|
||||
if followers {
|
||||
apb::Node::links(vec![format!("{URL_BASE}/users/{}/followers", username.get().unwrap_or_default())])
|
||||
} else { apb::Node::Empty }
|
||||
),
|
||||
auth
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => tracing::error!("error posting note: {e}"),
|
||||
.set_to(to)
|
||||
.set_cc(cc);
|
||||
let target_url = format!("{URL_BASE}/users/test/outbox");
|
||||
match Http::post(&target_url, &payload, auth).await {
|
||||
Err(e) => set_error.set(Some(e.to_string())),
|
||||
Ok(()) => {
|
||||
set_error.set(None);
|
||||
if let Some(x) = summary_ref.get() { x.set_value("") }
|
||||
if let Some(x) = content_ref.get() { x.set_value("") }
|
||||
},
|
||||
}
|
||||
set_posting.set(false);
|
||||
})
|
||||
} >post</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input id="priv-followers" type="radio" name="privacy" value="followers" title="followers" node_ref=followers_ref checked /></td>
|
||||
<td><span class="emoji" title="followers" >{PRIVACY_FOLLOWERS}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input id="priv-private" type="radio" name="privacy" value="private" title="private" node_ref=private_ref /></td>
|
||||
<td><span class="emoji" title="private" >{PRIVACY_PRIVATE}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
{move|| error.get().map(|x| view! { <blockquote class="mt-s">{x}</blockquote> })}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn AdvancedPostBox(username: Signal<Option<String>>, advanced: WriteSignal<bool>) -> impl IntoView {
|
||||
let auth = use_context::<Auth>().expect("missing auth context");
|
||||
let (posting, set_posting) = create_signal(false);
|
||||
let (error, set_error) = create_signal(None);
|
||||
let (value, set_value) = create_signal("Like".to_string());
|
||||
let (embedded, set_embedded) = create_signal(false);
|
||||
let summary_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let content_ref: NodeRef<html::Textarea> = create_node_ref();
|
||||
let context_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let name_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let reply_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let to_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let object_id_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let bto_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let cc_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let bcc_ref: NodeRef<html::Input> = create_node_ref();
|
||||
view! {
|
||||
<div class:hidden=move || !auth.present() >
|
||||
|
||||
<table class="align w-100">
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" title="advanced" checked on:input=move |ev| {
|
||||
advanced.set(event_target_checked(&ev))
|
||||
}/>
|
||||
</td>
|
||||
<td class="w-100">
|
||||
<select class="w-100" on:change=move |ev| set_value.set(event_target_value(&ev))>
|
||||
<SelectOption value is="Create" />
|
||||
<SelectOption value is="Like" />
|
||||
<SelectOption value is="Follow" />
|
||||
<SelectOption value is="Announce" />
|
||||
<SelectOption value is="Accept" />
|
||||
<SelectOption value is="Reject" />
|
||||
<SelectOption value is="Undo" />
|
||||
<SelectOption value is="Delete" />
|
||||
<SelectOption value is="Update" />
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" title="embedded object" on:input=move |ev| {
|
||||
set_embedded.set(event_target_checked(&ev))
|
||||
}/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class:hidden=move|| !embedded.get()>
|
||||
<input class="w-100" type="text" node_ref=summary_ref title="summary" placeholder="summary" />
|
||||
|
||||
<input class="w-100 ml-s mr-2" type="text" node_ref=name_ref title="name" placeholder="name" />
|
||||
<input class="w-100 ml-s mr-2" type="text" node_ref=context_ref title="context" placeholder="context" />
|
||||
<input class="w-100 ml-s mr-2" type="text" node_ref=reply_ref title="inReplyTo" placeholder="inReplyTo" />
|
||||
|
||||
<textarea rows="5" class="w-100" node_ref=content_ref title="content" placeholder="content" ></textarea>
|
||||
</div>
|
||||
<div class:hidden=embedded>
|
||||
<input class="w-100" type="text" node_ref=object_id_ref title="objectId" placeholder="objectId" />
|
||||
</div>
|
||||
|
||||
<table class="w-100 align">
|
||||
<tr>
|
||||
<td class="w-66"><input class="w-100" type="text" node_ref=to_ref title="to" placeholder="to" value=apb::target::PUBLIC /></td>
|
||||
<td class="w-66"><input class="w-100" type="text" node_ref=bto_ref title="bto" placeholder="bto" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="w-33"><input class="w-100" type="text" node_ref=cc_ref title="cc" placeholder="cc" value=format!("{URL_BASE}/users/{}/followers", username.get().unwrap_or_default()) /></td>
|
||||
<td class="w-33"><input class="w-100" type="text" node_ref=bcc_ref title="bcc" placeholder="bcc" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<button class="w-100" type="button" prop:disabled=posting on:click=move |_| {
|
||||
set_posting.set(true);
|
||||
spawn_local(async move {
|
||||
let content = content_ref.get().filter(|x| !x.value().is_empty()).map(|x| x.value());
|
||||
let summary = get_if_some(summary_ref);
|
||||
let name = get_if_some(name_ref);
|
||||
let context = get_if_some(context_ref);
|
||||
let reply = get_if_some(reply_ref);
|
||||
let object_id = get_if_some(object_id_ref);
|
||||
let to = get_vec_if_some(to_ref);
|
||||
let bto = get_vec_if_some(bto_ref);
|
||||
let cc = get_vec_if_some(cc_ref);
|
||||
let bcc = get_vec_if_some(bcc_ref);
|
||||
let payload = serde_json::Value::Object(serde_json::Map::default())
|
||||
.set_activity_type(Some(value.get().as_str().try_into().unwrap_or(apb::ActivityType::Create)))
|
||||
.set_to(apb::Node::links(to.clone()))
|
||||
.set_bto(apb::Node::links(bto.clone()))
|
||||
.set_cc(apb::Node::links(cc.clone()))
|
||||
.set_bcc(apb::Node::links(bcc.clone()))
|
||||
.set_object(
|
||||
if embedded.get() {
|
||||
apb::Node::object(
|
||||
serde_json::Value::Object(serde_json::Map::default())
|
||||
.set_object_type(Some(apb::ObjectType::Note))
|
||||
.set_name(name.as_deref())
|
||||
.set_summary(summary.as_deref())
|
||||
.set_content(content.as_deref())
|
||||
.set_in_reply_to(apb::Node::maybe_link(reply))
|
||||
.set_context(apb::Node::maybe_link(context))
|
||||
.set_to(apb::Node::links(to))
|
||||
.set_bto(apb::Node::links(bto))
|
||||
.set_cc(apb::Node::links(cc))
|
||||
.set_bcc(apb::Node::links(bcc))
|
||||
)
|
||||
} else {
|
||||
apb::Node::maybe_link(object_id)
|
||||
}
|
||||
);
|
||||
let target_url = format!("{URL_BASE}/users/{}/outbox", username.get().unwrap_or_default());
|
||||
match Http::post(&target_url, &payload, auth).await {
|
||||
Err(e) => set_error.set(Some(e.to_string())),
|
||||
Ok(()) => set_error.set(None),
|
||||
}
|
||||
set_posting.set(false);
|
||||
})
|
||||
} >post</button>
|
||||
{move|| error.get().map(|x| view! { <blockquote class="mt-s">{x}</blockquote> })}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn get_if_some(node: NodeRef<html::Input>) -> Option<String> {
|
||||
node.get()
|
||||
.filter(|x| !x.value().is_empty())
|
||||
.map(|x| x.value())
|
||||
}
|
||||
|
||||
fn get_vec_if_some(node: NodeRef<html::Input>) -> Vec<String> {
|
||||
node.get()
|
||||
.filter(|x| !x.value().is_empty())
|
||||
.map(|x| x.value())
|
||||
.map(|x|
|
||||
x.split(',')
|
||||
.map(|x| x.to_string())
|
||||
.collect()
|
||||
).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_checked(node: NodeRef<html::Input>) -> bool {
|
||||
node.get()
|
||||
.map(|x| x.checked())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Breadcrumb(
|
||||
#[prop(optional)]
|
||||
|
@ -92,3 +248,12 @@ pub fn Breadcrumb(
|
|||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn SelectOption(is: &'static str, value: ReadSignal<String>) -> impl IntoView {
|
||||
view! {
|
||||
<option value=is selected=move || value.get() == is >
|
||||
{is}
|
||||
</option>
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue