From 2dc9e1dee27e0f557e1c724247f1856a1d635dbb Mon Sep 17 00:00:00 2001 From: donarbl Date: Sat, 28 Feb 2026 10:45:48 +0000 Subject: [PATCH] feat: implement unfollow feature --- backend/data/follows.py | 11 ++++++++++- backend/endpoints.py | 13 ++++++++++++- backend/main.py | 2 ++ front-end/components/profile.mjs | 19 +++++++++++++++++-- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/backend/data/follows.py b/backend/data/follows.py index a4b6314..1d44f22 100644 --- a/backend/data/follows.py +++ b/backend/data/follows.py @@ -19,7 +19,16 @@ def follow(follower: User, followee: User): except UniqueViolation: # Already following - treat as idempotent request. pass - + +def unfollow(follower: User, followee: User): + with db_cursor() as cur: + cur.execute( + "DELETE FROM follows WHERE follower = %(follower_id)s AND followee = %(followee_id)s", + dict( + follower_id=follower.id, + followee_id=followee.id, + ), + ) def get_followed_usernames(follower: User) -> List[str]: """get_followed_usernames returns a list of usernames followee follows.""" diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..1f58ad5 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -1,6 +1,6 @@ from typing import Dict, Union from data import blooms -from data.follows import follow, get_followed_usernames, get_inverse_followed_usernames +from data.follows import follow, unfollow, get_followed_usernames, get_inverse_followed_usernames from data.users import ( UserRegistrationError, get_suggested_follows, @@ -148,7 +148,18 @@ def do_follow(): "success": True, } ) +@jwt_required() +def do_unfollow(unfollow_username): + current_user = get_current_user() + + unfollow_user = get_user(unfollow_username) + if unfollow_user is None: + return make_response( + (f"Cannot unfollow {unfollow_username} - user does not exist", 404) + ) + unfollow(current_user, unfollow_user) + return jsonify({"success": True}) @jwt_required() def send_bloom(): diff --git a/backend/main.py b/backend/main.py index 7ba155f..a69c9e9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,6 +4,7 @@ from data.users import lookup_user from endpoints import ( do_follow, + do_unfollow, get_bloom, hashtag, home_timeline, @@ -54,6 +55,7 @@ def main(): app.add_url_rule("/profile", view_func=self_profile) app.add_url_rule("/profile/", view_func=other_profile) app.add_url_rule("/follow", methods=["POST"], view_func=do_follow) + app.add_url_rule("/unfollow/", methods=["POST"], view_func=do_unfollow) app.add_url_rule("/suggested-follows/", view_func=suggested_follows) app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom) diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index ec4f200..66f5265 100644 --- a/front-end/components/profile.mjs +++ b/front-end/components/profile.mjs @@ -29,6 +29,15 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { followButtonEl.setAttribute("data-username", profileData.username || ""); followButtonEl.hidden = profileData.is_self || profileData.is_following; followButtonEl.addEventListener("click", handleFollow); + const unfollowButtonEl = profileElement.querySelector("[data-action='unfollow']"); + if (unfollowButtonEl) { + unfollowButtonEl.setAttribute("data-username", profileData.username || ""); + unfollowButtonEl.hidden = profileData.is_self || !profileData.is_following; + unfollowButtonEl.addEventListener("click", handleUnfollow); + if (!isLoggedIn) { + unfollowButtonEl.style.display = "none"; + } +} if (!isLoggedIn) { followButtonEl.style.display = "none"; } @@ -65,5 +74,11 @@ async function handleFollow(event) { await apiService.followUser(username); await apiService.getWhoToFollow(); } - -export {createProfile, handleFollow}; +async function handleUnfollow(event) { + const button = event.target; + const username = button.getAttribute("data-username"); + if (!username) return; + await apiService.unfollowUser(username); + await apiService.getProfile(username); +} +export {createProfile, handleFollow, handleUnfollow};