2024-05-01 16:06:46 +02:00
use std ::sync ::Arc ;
2024-05-02 02:58:06 +02:00
use apb ::{ ActivityMut , Actor , Base , Object , ObjectMut } ;
2024-04-17 22:07:47 +02:00
use leptos ::* ;
use leptos_router ::* ;
use crate ::prelude ::* ;
#[ component ]
pub fn AboutPage ( ) -> impl IntoView {
view! {
< div >
< Breadcrumb > about < / Breadcrumb >
< div class = " mt-s mb-s " >
< p > < code > μ pub < / code > " is a micro social network powered by " < a href = " " > ActivityPub < / a > < / p >
2024-04-17 23:07:56 +02:00
< p > < i > " the " < a href = " https://en.wikipedia.org/wiki/Fediverse " > fediverse < / a > " is an ensemble of social networks, which, while independently hosted, can communicate with each other " < / i > < / p >
< p > content is aggregated in timelines , logged out users can only access global server timeline < / p >
< / div >
< / div >
}
}
#[ component ]
pub fn ConfigPage ( ) -> impl IntoView {
view! {
< div >
< Breadcrumb > config < / Breadcrumb >
< div class = " mt-s mb-s " >
< p > < code > " not implemented :( " < / code > < / p >
2024-04-17 22:07:47 +02:00
< / div >
< / div >
}
}
2024-05-01 18:24:32 +02:00
fn send_follow_request ( target : String ) {
let auth = use_context ::< Auth > ( ) . expect ( " missing auth context " ) ;
spawn_local ( async move {
let payload = serde_json ::Value ::Object ( serde_json ::Map ::default ( ) )
. set_activity_type ( Some ( apb ::ActivityType ::Follow ) )
. set_object ( apb ::Node ::link ( target . clone ( ) ) )
. set_to ( apb ::Node ::links ( vec! [ target ] ) ) ;
if let Err ( e ) = Http ::post ( & auth . outbox ( ) , & payload , auth ) . await {
tracing ::error! ( " failed sending follow request: {e} " ) ;
}
} )
}
2024-04-17 22:07:47 +02:00
#[ component ]
2024-04-19 06:58:54 +02:00
pub fn UserPage ( tl : Timeline ) -> impl IntoView {
2024-04-17 22:07:47 +02:00
let params = use_params_map ( ) ;
let auth = use_context ::< Auth > ( ) . expect ( " missing auth context " ) ;
2024-04-22 02:48:33 +02:00
let id = params . get ( )
. get ( " id " )
. cloned ( )
. unwrap_or_default ( ) ;
let mut uid = id
. replace ( " /web/objects/ " , " " )
. replacen ( '+' , " https:// " , 1 )
. replace ( '@' , " / " ) ;
if ! uid . starts_with ( " http " ) {
uid = format! ( " {URL_BASE} /web/objects/ {uid} " ) ;
}
let actor = create_local_resource ( move | | params . get ( ) . get ( " id " ) . cloned ( ) . unwrap_or_default ( ) , move | id | {
2024-04-17 22:07:47 +02:00
async move {
2024-04-21 18:56:25 +02:00
match CACHE . get ( & Uri ::full ( FetchKind ::User , & id ) ) {
2024-04-17 22:07:47 +02:00
Some ( x ) = > Some ( x . clone ( ) ) ,
None = > {
2024-04-21 18:56:25 +02:00
let user : serde_json ::Value = Http ::fetch ( & Uri ::api ( FetchKind ::User , & id , true ) , auth ) . await . ok ( ) ? ;
2024-05-01 16:06:46 +02:00
let user = Arc ::new ( user ) ;
2024-04-21 18:56:25 +02:00
CACHE . put ( Uri ::full ( FetchKind ::User , & id ) , user . clone ( ) ) ;
2024-04-17 22:07:47 +02:00
Some ( user )
} ,
}
}
} ) ;
view! {
< div >
< Breadcrumb back = true > users ::view < / Breadcrumb >
< div >
2024-04-22 02:48:33 +02:00
{ move | | {
let uid = uid . clone ( ) ;
match actor . get ( ) {
None = > view! { < p class = " center " > loading .. . < / p > } . into_view ( ) ,
Some ( None ) = > {
view! { < p class = " center " > < code > loading failed < / code > < sup > < small > < a class = " clean " href = { uid } target = " _blank " > " ↗ " < / a > < / small > < / sup > < / p > } . into_view ( )
} ,
Some ( Some ( object ) ) = > {
let uid = object . id ( ) . unwrap_or_default ( ) . to_string ( ) ;
let avatar_url = object . icon ( ) . get ( ) . map ( | x | x . url ( ) . id ( ) . unwrap_or_default ( ) ) . unwrap_or_default ( ) ;
let background_url = object . image ( ) . get ( ) . map ( | x | x . url ( ) . id ( ) . unwrap_or_default ( ) ) . unwrap_or_default ( ) ;
let display_name = object . name ( ) . unwrap_or_default ( ) . to_string ( ) ;
let username = object . preferred_username ( ) . unwrap_or_default ( ) . to_string ( ) ;
let summary = object . summary ( ) . unwrap_or_default ( ) . to_string ( ) ;
let domain = object . id ( ) . unwrap_or_default ( ) . replace ( " https:// " , " " ) . split ( '/' ) . next ( ) . unwrap_or_default ( ) . to_string ( ) ;
let actor_type = object . actor_type ( ) . unwrap_or ( apb ::ActorType ::Person ) ;
let actor_type_tag = if actor_type = = apb ::ActorType ::Person { None } else {
Some ( view! { < sup class = " ml-s " > < small > " [ " { actor_type . as_ref ( ) . to_lowercase ( ) } " ] " < / small > < / sup > } )
} ;
let created = object . published ( ) ;
2024-05-02 00:59:47 +02:00
let following = object . following_count ( ) . unwrap_or ( 0 ) ;
let followers = object . followers_count ( ) . unwrap_or ( 0 ) ;
let statuses = object . statuses_count ( ) . unwrap_or ( 0 ) ;
2024-04-22 02:48:33 +02:00
let tl_url = format! ( " {} /outbox/page " , Uri ::api ( FetchKind ::User , & id . clone ( ) , false ) ) ;
if ! tl . next . get ( ) . starts_with ( & tl_url ) {
tl . reset ( tl_url ) ;
}
2024-05-02 00:59:47 +02:00
let following_me = object . following_me ( ) . unwrap_or ( false ) ;
let followed_by_me = object . followed_by_me ( ) . unwrap_or ( false ) ;
let _uid = uid . clone ( ) ;
2024-05-01 18:24:32 +02:00
2024-04-22 02:48:33 +02:00
view! {
< div class = " ml-3 mr-3 " >
< div
class = " banner "
style = { format! ( " background: center / cover url( {background_url} ); " ) }
>
2024-05-01 18:24:32 +02:00
< div style = " height: 10em " > < / div > // TODO bad way to have it fixed height ewwww
2024-04-22 02:48:33 +02:00
< / div >
< div class = " overlap " >
< table class = " pl-2 pr-2 align w-100 " style = " table-layout: fixed " >
< tr >
< td rowspan = 4 style = " width: 8em " >
< img class = " avatar-circle avatar-border mr-s " src = { avatar_url } style = " height: 7em; width: 7em " / >
< / td >
< td rowspan = 2 class = " bottom " >
< b class = " big " > { display_name } < / b > { actor_type_tag }
< / td >
< td rowspan = 2 class = " bottom rev " title = " statuses " > { statuses } " " < span class = " emoji " > " \u{1f582} " < / span > < / td >
< / tr >
< tr > < / tr >
< tr >
< td class = " top " >
2024-05-01 18:24:32 +02:00
< small > < a class = " clean hover " href = { uid . clone ( ) } target = " _blank " > { username . clone ( ) } @ { domain } < / a > < / small >
2024-04-22 02:48:33 +02:00
< / td >
< td class = " rev " title = " following " > { following } " " < span class = " emoji " > " 👥 " < / span > < / td >
< / tr >
< tr >
< td >
< DateTime t = created / >
< / td >
< td class = " rev " title = " followers " > { followers } " " < span class = " emoji " > " 📢 " < / span > < / td >
< / tr >
< / table >
2024-05-02 02:29:29 +02:00
< div class = " rev mr-1 " class :hidden = move | | ! auth . present ( ) | | auth . user_id ( ) = = uid >
2024-05-02 00:59:47 +02:00
{ if followed_by_me {
view! { < code class = " color " > following < / code > } . into_view ( )
} else {
view! { < input type = " submit " value = " follow " on :click = move | _ | send_follow_request ( _uid . clone ( ) ) / > } . into_view ( )
} }
{ if following_me {
Some ( view! { < code class = " ml-1 color " > follows you < / code > } )
} else {
None
} }
2024-05-01 18:24:32 +02:00
< / div >
2024-05-03 00:52:56 +02:00
< p class = " ml-2 mt-1 " inner_html = { mdhtml ::safe_html ( & summary ) } > < / p >
2024-04-22 02:48:33 +02:00
< / div >
2024-04-18 03:34:41 +02:00
< / div >
2024-04-22 02:48:33 +02:00
< TimelineFeed tl = tl / >
} . into_view ( )
} ,
}
2024-04-17 22:07:47 +02:00
} }
< / div >
< / div >
}
}
#[ component ]
2024-04-19 06:59:34 +02:00
pub fn ObjectPage ( tl : Timeline ) -> impl IntoView {
2024-04-17 22:07:47 +02:00
let params = use_params_map ( ) ;
let auth = use_context ::< Auth > ( ) . expect ( " missing auth context " ) ;
2024-04-22 02:48:33 +02:00
let mut uid = params . get ( ) . get ( " id " )
. cloned ( )
. unwrap_or_default ( )
. replace ( " /web/objects/ " , " " )
. replacen ( '+' , " https:// " , 1 )
. replace ( '@' , " / " ) ;
if ! uid . starts_with ( " http " ) {
uid = format! ( " {URL_BASE} /web/objects/ {uid} " ) ;
}
2024-04-17 22:07:47 +02:00
let object = create_local_resource ( move | | params . get ( ) . get ( " id " ) . cloned ( ) . unwrap_or_default ( ) , move | oid | {
async move {
2024-04-21 18:56:25 +02:00
match CACHE . get ( & Uri ::full ( FetchKind ::Object , & oid ) ) {
2024-04-17 22:07:47 +02:00
Some ( x ) = > Some ( x . clone ( ) ) ,
None = > {
2024-04-21 18:56:25 +02:00
let obj = Http ::fetch ::< serde_json ::Value > ( & Uri ::api ( FetchKind ::Object , & oid , true ) , auth ) . await . ok ( ) ? ;
2024-05-01 16:06:46 +02:00
let obj = Arc ::new ( obj ) ;
2024-04-21 18:56:25 +02:00
CACHE . put ( Uri ::full ( FetchKind ::Object , & oid ) , obj . clone ( ) ) ;
2024-04-17 22:07:47 +02:00
Some ( obj )
}
}
}
} ) ;
view! {
< div >
< Breadcrumb back = true > objects ::view < / Breadcrumb >
< div class = " ma-2 " >
{ move | | match object . get ( ) {
2024-04-22 02:48:33 +02:00
None = > view! { < p class = " center " > loading .. . < / p > } . into_view ( ) ,
Some ( None ) = > {
let uid = uid . clone ( ) ;
view! { < p class = " center " > < code > loading failed < / code > < sup > < small > < a class = " clean " href = { uid } target = " _blank " > " ↗ " < / a > < / small > < / sup > < / p > } . into_view ( )
} ,
2024-04-19 06:59:34 +02:00
Some ( Some ( o ) ) = > {
2024-04-21 18:56:25 +02:00
let object = o . clone ( ) ;
let tl_url = format! ( " {} /page " , Uri ::api ( FetchKind ::Context , & o . context ( ) . id ( ) . unwrap_or_default ( ) , false ) ) ;
2024-04-19 06:59:34 +02:00
if ! tl . next . get ( ) . starts_with ( & tl_url ) {
tl . reset ( tl_url ) ;
}
view! {
2024-04-21 18:56:25 +02:00
< Object object = object / >
2024-04-19 06:59:34 +02:00
< div class = " ml-1 mr-1 mt-2 " >
2024-04-22 01:01:20 +02:00
< TimelineReplies tl = tl root = o . id ( ) . unwrap_or_default ( ) . to_string ( ) / >
2024-04-19 06:59:34 +02:00
< / div >
} . into_view ( )
} ,
2024-04-17 22:07:47 +02:00
} }
< / div >
< / div >
}
}
#[ component ]
pub fn TimelinePage ( name : & 'static str , tl : Timeline ) -> impl IntoView {
let auth = use_context ::< Auth > ( ) . expect ( " missing auth context " ) ;
view! {
< div >
< Breadcrumb back = false >
{ name }
< a class = " clean ml-1 " href = " # " on :click = move | _ | {
tl . reset ( tl . next . get ( ) . split ( '?' ) . next ( ) . unwrap_or_default ( ) . to_string ( ) ) ;
spawn_local ( async move {
if let Err ( e ) = tl . more ( auth ) . await {
tracing ::error! ( " error fetching more items for timeline: {e} " ) ;
}
} )
} > < span class = " emoji " >
" \u{1f5d8} "
< / span > < / a >
< / Breadcrumb >
< div class = " mt-s mb-s " >
< TimelineFeed tl = tl / >
< / div >
< / div >
}
}
2024-04-21 19:37:20 +02:00
#[ component ]
pub fn DebugPage ( ) -> impl IntoView {
2024-05-01 16:06:46 +02:00
let ( object , set_object ) = create_signal ( Arc ::new ( serde_json ::Value ::String (
2024-04-21 19:37:20 +02:00
" use this view to fetch remote AP objects and inspect their content " . into ( ) )
2024-05-01 16:06:46 +02:00
) ) ;
2024-04-22 01:01:20 +02:00
let cached_ref : NodeRef < html ::Input > = create_node_ref ( ) ;
2024-04-21 19:37:20 +02:00
let auth = use_context ::< Auth > ( ) . expect ( " missing auth context " ) ;
2024-04-22 02:32:09 +02:00
let ( query , set_query ) = create_signal ( " " . to_string ( ) ) ;
2024-04-21 19:37:20 +02:00
view! {
< div >
< Breadcrumb back = true > debug < / Breadcrumb >
< div class = " mt-1 " >
2024-04-21 23:41:30 +02:00
< form on :submit = move | ev | {
ev . prevent_default ( ) ;
2024-04-22 01:01:20 +02:00
let cached = cached_ref . get ( ) . map ( | x | x . checked ( ) ) . unwrap_or_default ( ) ;
2024-04-22 02:32:09 +02:00
let fetch_url = query . get ( ) ;
2024-04-22 01:01:20 +02:00
if cached {
match CACHE . get ( & fetch_url ) {
Some ( x ) = > set_object . set ( x ) ,
2024-05-01 16:06:46 +02:00
None = > set_object . set ( Arc ::new ( serde_json ::Value ::String ( " not in cache! " . into ( ) ) ) ) ,
2024-04-21 23:41:30 +02:00
}
2024-04-22 01:01:20 +02:00
} else {
let url = format! ( " {URL_BASE} /dbg?id= {fetch_url} " ) ;
spawn_local ( async move {
match Http ::fetch ::< serde_json ::Value > ( & url , auth ) . await {
2024-05-01 16:06:46 +02:00
Ok ( x ) = > set_object . set ( Arc ::new ( x ) ) ,
Err ( e ) = > set_object . set ( Arc ::new ( serde_json ::Value ::String ( e . to_string ( ) ) ) ) ,
2024-04-22 01:01:20 +02:00
}
} ) ;
}
2024-04-21 23:41:30 +02:00
} >
2024-04-21 19:37:20 +02:00
< table class = " align w-100 " >
< tr >
2024-04-22 02:32:09 +02:00
< td >
< small > < a
href = { move | | Uri ::web ( FetchKind ::Object , & query . get ( ) ) }
> obj < / a >
" "
< a
href = { move | | Uri ::web ( FetchKind ::User , & query . get ( ) ) }
> usr < / a > < / small >
< / td >
2024-04-22 03:45:30 +02:00
< td class = " w-100 " > < input class = " w-100 " type = " text " on :input = move | ev | set_query . set ( event_target_value ( & ev ) ) placeholder = " AP id " / > < / td >
2024-04-21 23:41:30 +02:00
< td > < input type = " submit " class = " w-100 " value = " fetch " / > < / td >
2024-04-22 01:01:20 +02:00
< td > < input type = " checkbox " title = " cached " value = " cached " node_ref = cached_ref / > < / td >
2024-04-21 19:37:20 +02:00
< / tr >
< / table >
2024-04-21 23:41:30 +02:00
< / form >
2024-04-21 19:37:20 +02:00
< / div >
< pre class = " ma-1 " >
2024-05-01 16:06:46 +02:00
{ move | | serde_json ::to_string_pretty ( object . get ( ) . as_ref ( ) ) . unwrap_or ( " unserializable " . to_string ( ) ) }
2024-04-21 19:37:20 +02:00
< / pre >
< / div >
}
}