2024-05-01 16:46:19 +02:00
|
|
|
use apb::{ActivityMut, Base, BaseMut, Object, ObjectMut};
|
2024-04-17 22:07:47 +02:00
|
|
|
|
|
|
|
use leptos::*;
|
2024-05-23 16:41:45 +02:00
|
|
|
use crate::{prelude::*, WEBFINGER};
|
2024-04-17 22:07:47 +02:00
|
|
|
|
2024-05-01 16:46:19 +02:00
|
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
|
|
pub struct ReplyControls {
|
|
|
|
pub context: RwSignal<Option<String>>,
|
|
|
|
pub reply_to: RwSignal<Option<String>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ReplyControls {
|
|
|
|
pub fn reply(&self, oid: &str) {
|
|
|
|
if let Some(obj) = CACHE.get(oid) {
|
|
|
|
self.context.set(obj.context().id());
|
|
|
|
self.reply_to.set(obj.id().map(|x| x.to_string()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn clear(&self) {
|
|
|
|
self.context.set(None);
|
|
|
|
self.reply_to.set(None);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn post_author(post_id: &str) -> Option<crate::Object> {
|
|
|
|
let usr = CACHE.get(post_id)?.attributed_to().id()?;
|
|
|
|
CACHE.get(&usr)
|
|
|
|
}
|
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
#[component]
|
2024-05-01 18:22:25 +02:00
|
|
|
pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
|
2024-04-17 22:07:47 +02:00
|
|
|
let auth = use_context::<Auth>().expect("missing auth context");
|
2024-05-01 16:46:19 +02:00
|
|
|
let reply = use_context::<ReplyControls>().expect("missing reply controls");
|
2024-04-23 02:48:30 +02:00
|
|
|
let (posting, set_posting) = create_signal(false);
|
|
|
|
let (error, set_error) = create_signal(None);
|
2024-05-23 16:41:45 +02:00
|
|
|
let (content, set_content) = create_signal("".to_string());
|
2024-04-17 22:07:47 +02:00
|
|
|
let summary_ref: NodeRef<html::Input> = create_node_ref();
|
|
|
|
let public_ref: NodeRef<html::Input> = create_node_ref();
|
|
|
|
let followers_ref: NodeRef<html::Input> = create_node_ref();
|
2024-04-23 02:48:30 +02:00
|
|
|
let private_ref: NodeRef<html::Input> = create_node_ref();
|
2024-05-23 16:41:45 +02:00
|
|
|
|
|
|
|
// TODO is this too abusive with resources? im even checking if TLD exists...
|
|
|
|
let mentions = create_local_resource(
|
|
|
|
move || content.get(),
|
|
|
|
move |c| async move {
|
|
|
|
let mut out = Vec::new();
|
|
|
|
for word in c.split(' ') {
|
|
|
|
if !word.starts_with('@') { break };
|
|
|
|
let stripped = word.replacen('@', "", 1);
|
|
|
|
if let Some((user, domain)) = stripped.split_once('@') {
|
|
|
|
if let Some(tld) = domain.split('.').last() {
|
|
|
|
if tld::exist(tld) {
|
|
|
|
if let Some(uid) = WEBFINGER.blocking_resolve(user, domain).await {
|
|
|
|
out.push(uid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
out
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
view! {
|
2024-05-13 01:22:12 +02:00
|
|
|
<div>
|
2024-05-01 16:46:19 +02:00
|
|
|
{move ||
|
|
|
|
reply.reply_to.get().map(|r| {
|
|
|
|
let actor_strip = post_author(&r).map(|x| view! { <ActorStrip object=x /> });
|
|
|
|
view! {
|
|
|
|
<span class="nowrap">
|
|
|
|
<span
|
|
|
|
class="cursor emoji emoji-btn mr-s ml-s"
|
|
|
|
on:click=move|_| reply.clear()
|
|
|
|
title={format!("> {r} | ctx: {}", reply.context.get().unwrap_or_default())}
|
|
|
|
>
|
2024-05-23 16:41:45 +02:00
|
|
|
"✒️"
|
2024-05-01 16:46:19 +02:00
|
|
|
</span>
|
2024-05-23 16:41:45 +02:00
|
|
|
{actor_strip}
|
|
|
|
<small class="tiny ml-1">"["<a class="clean" title="remove reply" href="#" on:click=move |_| reply.clear() >reply</a>"]"</small>
|
2024-05-01 16:46:19 +02:00
|
|
|
</span>
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2024-05-23 16:41:45 +02:00
|
|
|
{move ||
|
|
|
|
mentions.get()
|
|
|
|
.map(|x| x.into_iter().map(|u| match CACHE.get(&u) {
|
|
|
|
Some(u) => view! { <span class="nowrap"><span class="emoji mr-s ml-s">"📨"</span><ActorStrip object=u /></span> }.into_view(),
|
|
|
|
None => view! { <span class="nowrap"><span class="emoji mr-s ml-s">"📨"</span><a href={Uri::web(U::User, &u)}>{u}</a></span> }.into_view(),
|
|
|
|
})
|
|
|
|
.collect_view())
|
|
|
|
}
|
2024-04-17 22:07:47 +02:00
|
|
|
<table class="align w-100">
|
|
|
|
<tr>
|
2024-05-13 04:03:29 +02:00
|
|
|
<td><input type="checkbox" on:input=move |ev| advanced.set(event_target_checked(&ev)) title="toggle advanced controls" /></td>
|
2024-04-23 03:05:22 +02:00
|
|
|
<td class="w-100"><input class="w-100" type="text" node_ref=summary_ref title="summary" /></td>
|
2024-04-17 22:07:47 +02:00
|
|
|
</tr>
|
2024-04-23 02:48:30 +02:00
|
|
|
</table>
|
|
|
|
|
2024-05-23 16:41:45 +02:00
|
|
|
<textarea rows="6" class="w-100" title="content" placeholder="\n look at nothing\n what do you see?"
|
|
|
|
prop:value=content
|
|
|
|
on:input=move |ev| set_content.set(event_target_value(&ev))
|
|
|
|
></textarea>
|
2024-04-23 02:48:30 +02:00
|
|
|
|
|
|
|
<table class="align rev w-100">
|
2024-04-17 22:07:47 +02:00
|
|
|
<tr>
|
2024-04-23 02:48:30 +02:00
|
|
|
<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);
|
2024-04-17 22:07:47 +02:00
|
|
|
spawn_local(async move {
|
2024-05-02 16:20:41 +02:00
|
|
|
let summary = get_if_some(summary_ref);
|
2024-05-23 16:41:45 +02:00
|
|
|
let content = content.get();
|
2024-05-23 02:30:42 +02:00
|
|
|
let mut cc_vec = Vec::new();
|
|
|
|
let mut to_vec = Vec::new();
|
|
|
|
if get_checked(followers_ref) {
|
|
|
|
cc_vec.push(format!("{URL_BASE}/users/{}/followers", auth.username()));
|
|
|
|
}
|
|
|
|
if get_checked(public_ref) {
|
|
|
|
cc_vec.push(apb::target::PUBLIC.to_string());
|
|
|
|
}
|
|
|
|
if let Some(r) = reply.reply_to.get() {
|
|
|
|
if let Some(au) = post_author(&r) {
|
|
|
|
if let Some(uid) = au.id() {
|
|
|
|
to_vec.push(uid.to_string());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-05-23 16:41:45 +02:00
|
|
|
for mention in mentions.get().as_deref().unwrap_or(&[]) {
|
|
|
|
to_vec.push(mention.to_string());
|
|
|
|
}
|
2024-04-23 02:48:30 +02:00
|
|
|
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))
|
2024-05-01 16:46:19 +02:00
|
|
|
.set_context(apb::Node::maybe_link(reply.context.get()))
|
|
|
|
.set_in_reply_to(apb::Node::maybe_link(reply.reply_to.get()))
|
2024-05-23 02:30:42 +02:00
|
|
|
.set_to(apb::Node::links(to_vec))
|
|
|
|
.set_cc(apb::Node::links(cc_vec));
|
2024-05-01 18:22:25 +02:00
|
|
|
match Http::post(&auth.outbox(), &payload, auth).await {
|
2024-04-23 02:48:30 +02:00
|
|
|
Err(e) => set_error.set(Some(e.to_string())),
|
2024-04-17 22:07:47 +02:00
|
|
|
Ok(()) => {
|
2024-04-23 02:48:30 +02:00
|
|
|
set_error.set(None);
|
2024-04-17 22:07:47 +02:00
|
|
|
if let Some(x) = summary_ref.get() { x.set_value("") }
|
2024-05-23 16:41:45 +02:00
|
|
|
set_content.set("".to_string());
|
2024-04-17 22:07:47 +02:00
|
|
|
},
|
|
|
|
}
|
2024-04-23 02:48:30 +02:00
|
|
|
set_posting.set(false);
|
2024-04-17 22:07:47 +02:00
|
|
|
})
|
|
|
|
} >post</button>
|
|
|
|
</td>
|
|
|
|
</tr>
|
2024-04-23 02:48:30 +02:00
|
|
|
<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>
|
2024-04-17 22:07:47 +02:00
|
|
|
</table>
|
2024-04-23 02:48:30 +02:00
|
|
|
{move|| error.get().map(|x| view! { <blockquote class="mt-s">{x}</blockquote> })}
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[component]
|
2024-05-01 18:22:25 +02:00
|
|
|
pub fn AdvancedPostBox(advanced: WriteSignal<bool>) -> impl IntoView {
|
2024-04-23 02:48:30 +02:00
|
|
|
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);
|
2024-04-24 05:38:09 +02:00
|
|
|
let sensitive_ref: NodeRef<html::Input> = create_node_ref();
|
2024-04-23 02:48:30 +02:00
|
|
|
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! {
|
2024-05-13 01:22:12 +02:00
|
|
|
<div>
|
2024-04-23 02:48:30 +02:00
|
|
|
|
|
|
|
<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>
|
2024-04-23 03:46:14 +02:00
|
|
|
|
2024-04-23 03:59:12 +02:00
|
|
|
<input class="w-100" type="text" node_ref=object_id_ref title="objectId" placeholder="objectId" />
|
|
|
|
|
|
|
|
<div class:hidden=move|| !embedded.get()>
|
2024-04-23 03:46:14 +02:00
|
|
|
<input class="w-100" type="text" node_ref=name_ref title="name" placeholder="name" />
|
|
|
|
<input class="w-100" type="text" node_ref=context_ref title="context" placeholder="context" />
|
|
|
|
<input class="w-100" type="text" node_ref=reply_ref title="inReplyTo" placeholder="inReplyTo" />
|
2024-04-23 02:48:30 +02:00
|
|
|
|
2024-04-24 05:38:09 +02:00
|
|
|
<table class="align w-100">
|
|
|
|
<tr>
|
|
|
|
<td><input type="checkbox" title="sensitive" checked node_ref=sensitive_ref/>
|
|
|
|
</td>
|
|
|
|
<td class="w-100">
|
|
|
|
<input class="w-100" type="text" node_ref=summary_ref title="summary" placeholder="summary" />
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
|
2024-04-23 02:48:30 +02:00
|
|
|
<textarea rows="5" class="w-100" node_ref=content_ref title="content" placeholder="content" ></textarea>
|
|
|
|
</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>
|
2024-05-01 18:22:25 +02:00
|
|
|
<td class="w-33"><input class="w-100" type="text" node_ref=cc_ref title="cc" placeholder="cc" value=format!("{URL_BASE}/users/{}/followers", auth.username()) /></td>
|
2024-04-23 02:48:30 +02:00
|
|
|
<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())
|
2024-04-23 03:46:14 +02:00
|
|
|
.set_id(object_id.as_deref())
|
2024-04-23 02:48:30 +02:00
|
|
|
.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)
|
|
|
|
}
|
|
|
|
);
|
2024-05-01 18:22:25 +02:00
|
|
|
let target_url = format!("{URL_BASE}/users/{}/outbox", auth.username());
|
2024-04-23 02:48:30 +02:00
|
|
|
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> })}
|
2024-04-17 22:07:47 +02:00
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-23 02:48:30 +02:00
|
|
|
fn get_if_some(node: NodeRef<html::Input>) -> Option<String> {
|
|
|
|
node.get()
|
|
|
|
.map(|x| x.value())
|
2024-05-02 16:20:41 +02:00
|
|
|
.filter(|x| !x.is_empty())
|
2024-04-23 02:48:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fn get_vec_if_some(node: NodeRef<html::Input>) -> Vec<String> {
|
|
|
|
node.get()
|
|
|
|
.map(|x| x.value())
|
2024-05-02 16:20:41 +02:00
|
|
|
.filter(|x| !x.is_empty())
|
2024-04-23 02:48:30 +02:00
|
|
|
.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]
|
|
|
|
fn SelectOption(is: &'static str, value: ReadSignal<String>) -> impl IntoView {
|
|
|
|
view! {
|
|
|
|
<option value=is selected=move || value.get() == is >
|
|
|
|
{is}
|
|
|
|
</option>
|
|
|
|
}
|
|
|
|
}
|