feat: follow-by-handle form on following and users pages

This commit is contained in:
2026-05-13 01:35:54 +02:00
parent 20e70325c6
commit bc6c767c29
4 changed files with 26 additions and 2 deletions

View File

@@ -4,6 +4,12 @@
{% if let Some(err) = error %} {% if let Some(err) = error %}
<p class="error">{{ err }}</p> <p class="error">{{ err }}</p>
{% endif %} {% endif %}
<form method="POST" action="/users/{{ user_id }}/follow" class="follow-form">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<input type="hidden" name="redirect_after" value="/social/following">
<input type="text" name="handle" placeholder="@user@instance.tld" required>
<button type="submit">Follow</button>
</form>
{% if actors.is_empty() %} {% if actors.is_empty() %}
<p>Not following anyone yet. Follow remote users from your <a href="/users/{{ user_id }}">profile page</a>.</p> <p>Not following anyone yet. Follow remote users from your <a href="/users/{{ user_id }}">profile page</a>.</p>
{% else %} {% else %}

View File

@@ -2,6 +2,14 @@
{% block content %} {% block content %}
<div class="users-list"> <div class="users-list">
<h2 class="page-title">Members</h2> <h2 class="page-title">Members</h2>
{% if let Some(viewer_id) = ctx.user_id %}
<form method="POST" action="/users/{{ viewer_id }}/follow" class="follow-form">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<input type="hidden" name="redirect_after" value="/users">
<input type="text" name="handle" placeholder="@user@instance.tld" required>
<button type="submit">Follow</button>
</form>
{% endif %}
{% for user in users %} {% for user in users %}
<div class="user-row"> <div class="user-row">
<div class="user-avatar">{{ user.initial }}</div> <div class="user-avatar">{{ user.initial }}</div>

View File

@@ -85,6 +85,8 @@ pub struct FollowForm {
pub handle: String, pub handle: String,
#[serde(rename = "_csrf", default)] #[serde(rename = "_csrf", default)]
pub csrf_token: String, pub csrf_token: String,
#[serde(default)]
pub redirect_after: Option<String>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]

View File

@@ -718,12 +718,20 @@ pub async fn follow_remote_user(
if crate::csrf::mismatch(&csrf, &form.csrf_token) { if crate::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response(); return StatusCode::FORBIDDEN.into_response();
} }
let redirect_base = form
.redirect_after
.as_deref()
.filter(|u| u.starts_with('/') && !u.starts_with("//"))
.unwrap_or(&format!("/users/{}", profile_user_uuid))
.to_string();
match state.ap_service.follow(user_id.value(), &form.handle).await { match state.ap_service.follow(user_id.value(), &form.handle).await {
Ok(()) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(), Ok(()) => Redirect::to(&redirect_base).into_response(),
Err(e) => { Err(e) => {
tracing::error!("follow error: {:?}", e); tracing::error!("follow error: {:?}", e);
let msg = encode_error(&e.to_string()); let msg = encode_error(&e.to_string());
Redirect::to(&format!("/users/{}?error={}", profile_user_uuid, msg)).into_response() let sep = if redirect_base.contains('?') { '&' } else { '?' };
Redirect::to(&format!("{}{}error={}", redirect_base, sep, msg)).into_response()
} }
} }
} }