diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index c81ef23..1f62828 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -1798,8 +1798,8 @@ (span ("class" "notification chip") (text "{{ total }} votes")) (text "{% if not poll[2] -%}") (span - ("class" "notification chip") - (text "Expires in ") + ("class" "notification chip flex items-center gap-1") + (text "Expires in") (span ("class" "poll_date") ("data-created" "{{ poll[0].created }}") diff --git a/crates/app/src/public/html/misc/requests.lisp b/crates/app/src/public/html/misc/requests.lisp index b9700f0..8f4bdb6 100644 --- a/crates/app/src/public/html/misc/requests.lisp +++ b/crates/app/src/public/html/misc/requests.lisp @@ -62,12 +62,15 @@ ("class" "card-nest") (div ("class" "card small flex items-center gap-2") - (text "{{ icon \"user-plus\" }}") + (a + ("href" "/api/v1/auth/user/find/{{ request.id }}") + (text "{{ components::avatar(username=request.id, selector_type=\"id\") }}")) (span (text "{{ text \"requests:label.user_follow_request\" }}"))) (div ("class" "card flex flex-col gap-2") (span + ("class" "flex items-center gap-2") (text "{{ text \"requests:label.user_follow_request_message\" }}")) (div ("class" "card flex flex-wrap w-full secondary gap-2") diff --git a/crates/app/src/public/html/profile/private.lisp b/crates/app/src/public/html/profile/private.lisp index 11740c9..8bd94e9 100644 --- a/crates/app/src/public/html/profile/private.lisp +++ b/crates/app/src/public/html/profile/private.lisp @@ -35,6 +35,7 @@ (text "{{ icon \"user-plus\" }}") (span (text "{{ text \"auth:action.request_to_follow\" }}"))) + (text "{% if follow_requested -%}") (button ("onclick" "cancel_follow_user(event)") ("class" "lowered red{% if not follow_requested -%} hidden{%- endif %}") @@ -42,7 +43,7 @@ (text "{{ icon \"user-minus\" }}") (span (text "{{ text \"auth:action.cancel_follow_request\" }}"))) - (text "{% else %}") + (text "{%- endif %} {% else %}") (button ("onclick" "toggle_follow_user(event)") ("class" "lowered red") diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp index 6be8134..046f425 100644 --- a/crates/app/src/public/html/profile/settings.lisp +++ b/crates/app/src/public/html/profile/settings.lisp @@ -137,6 +137,12 @@ (text "{{ icon \"rss\" }}") (span (text "{{ text \"auth:label.following\" }}"))) + (a + ("data-tab-button" "account/followers") + ("href" "#/account/followers") + (text "{{ icon \"rss\" }}") + (span + (text "{{ text \"auth:label.followers\" }}"))) (a ("data-tab-button" "account/blocks") ("href" "#/account/blocks") @@ -457,7 +463,7 @@ (text "{{ icon \"external-link\" }}") (span (text "{{ text \"requests:action.view_profile\" }}"))))) - (text "{% endfor %}")))) + (text "{% endfor %} {{ components::pagination(page=page, items=following|length, key=\"#/account/following\") }}")))) (script (text "globalThis.toggle_follow_user = async (uid) => { await trigger(\"atto::debounce\", [\"users::follow\"]); @@ -473,6 +479,62 @@ ]); }); };"))) + (div + ("class" "w-full flex flex-col gap-2 hidden") + ("data-tab" "account/followers") + (div + ("class" "card lowered flex flex-col gap-2") + (a + ("href" "#/account") + ("class" "button secondary") + (text "{{ icon \"arrow-left\" }}") + (span + (text "{{ text \"general:action.back\" }}"))) + (div + ("class" "card-nest") + (div + ("class" "card flex items-center gap-2 small") + (text "{{ icon \"rss\" }}") + (span + (text "{{ text \"auth:label.followers\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (text "{% for userfollow in followers %} {% set user = userfollow[1] %}") + (div + ("class" "card secondary flex flex-wrap gap-2 items-center justify-between") + (div + ("class" "flex gap-2") + (text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}")) + (div + ("class" "flex gap-2") + (button + ("class" "lowered red small") + ("onclick" "force_unfollow_me('{{ user.id }}')") + (text "{{ icon \"user-minus\" }}") + (span + (str (text "stacks:label.remove")))) + (a + ("href" "/@{{ user.username }}") + ("class" "button lowered small") + (text "{{ icon \"external-link\" }}") + (span + (text "{{ text \"requests:action.view_profile\" }}"))))) + (text "{% endfor %} {{ components::pagination(page=page, items=following|length, key=\"#/account/followers\") }}")))) + (script + (text "globalThis.force_unfollow_me = async (uid) => { + await trigger(\"atto::debounce\", [\"users::follow\"]); + + fetch(`/api/v1/auth/user/${uid}/force_unfollow_me`, { + method: \"POST\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + };"))) (div ("class" "w-full flex flex-col gap-2 hidden") ("data-tab" "account/blocks") diff --git a/crates/app/src/routes/api/v1/auth/social.rs b/crates/app/src/routes/api/v1/auth/social.rs index 88a78b5..17ca6cf 100644 --- a/crates/app/src/routes/api/v1/auth/social.rs +++ b/crates/app/src/routes/api/v1/auth/social.rs @@ -154,6 +154,31 @@ pub async fn accept_follow_request( } } +pub async fn force_unfollow_me_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageFollowing) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if let Ok(userfollow) = data.get_userfollow_by_receiver_initiator(user.id, id).await { + match data.delete_userfollow(userfollow.id, &user, false).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "User is no longer following you".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } + } else { + return Json(Error::GeneralNotFound("user follow".to_string()).into()); + } +} + /// Toggle blocking on the given user. pub async fn block_request( jar: CookieJar, diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 517016b..0fbfc40 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -293,6 +293,10 @@ pub fn routes() -> Router { "/auth/user/{id}/follow/accept", post(auth::social::accept_follow_request), ) + .route( + "/auth/user/{id}/force_unfollow_me", + post(auth::social::force_unfollow_me_request), + ) .route("/auth/user/{id}/block", post(auth::social::block_request)) .route( "/auth/user/{id}/block_ip", diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs index 4d12556..11966a6 100644 --- a/crates/app/src/routes/pages/profile.rs +++ b/crates/app/src/routes/pages/profile.rs @@ -63,11 +63,27 @@ pub async fn settings_request( } }; + let followers = match data + .0 + .fill_userfollows_with_initiator( + data.0 + .get_userfollows_by_receiver(profile.id, 12, req.page) + .await + .unwrap_or(Vec::new()), + &None, + false, + ) + .await + { + Ok(r) => r, + Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)), + }; + let following = match data .0 .fill_userfollows_with_receiver( data.0 - .get_userfollows_by_initiator_all(profile.id) + .get_userfollows_by_initiator(profile.id, 12, req.page) .await .unwrap_or(Vec::new()), &None, @@ -138,6 +154,7 @@ pub async fn settings_request( context.insert("page", &req.page); context.insert("uploads", &uploads); context.insert("stacks", &stacks); + context.insert("followers", &followers); context.insert("following", &following); context.insert("blocks", &blocks); context.insert("stackblocks", &stackblocks); diff --git a/justfile b/justfile index a83d0c4..56aa26b 100644 --- a/justfile +++ b/justfile @@ -10,5 +10,6 @@ doc: cargo doc --document-private-items --no-deps test: + sudo pkill -e tetratto cd example && LITTLEWEB=true PORT=4119 cargo run & cd example && cargo run