From 4d6d460dcd2c95021144706a8d00e0d19a8d81e2 Mon Sep 17 00:00:00 2001 From: donarbl Date: Sat, 31 Jan 2026 19:39:20 +0000 Subject: [PATCH] added unfollow --- backend/data/follows.py | 9 +++++++++ backend/endpoints.py | 25 ++++++++++++++++++++++++- backend/main.py | 3 ++- front-end/components/profile.mjs | 17 ++++++++++++++++- front-end/index.html | 1 + front-end/lib/api.mjs | 3 ++- 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/backend/data/follows.py b/backend/data/follows.py index a4b6314..2e1c73b 100644 --- a/backend/data/follows.py +++ b/backend/data/follows.py @@ -20,6 +20,15 @@ def follow(follower: User, followee: User): # 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..2e4e174 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, @@ -149,6 +149,29 @@ def do_follow(): } ) +MAX_BLOOM_LENGTH = 280 + +@jwt_required() +def do_unfollow(): + type_check_error = verify_request_fields({"follow_username": str}) + if type_check_error is not None: + return type_check_error + + current_user = get_current_user() + + follow_username = request.json["follow_username"] + follow_user = get_user(follow_username) + if follow_user is None: + return make_response( + (f"Cannot unfollow {follow_username} - user does not exist", 404) + ) + + unfollow(current_user, follow_user) + return jsonify( + { + "success": True, + } + ) @jwt_required() def send_bloom(): diff --git a/backend/main.py b/backend/main.py index 7ba155f..3202ee2 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,8 +55,8 @@ 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) app.add_url_rule("/bloom/", methods=["GET"], view_func=get_bloom) app.add_url_rule("/blooms/", view_func=user_blooms) diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index ec4f200..088a50b 100644 --- a/front-end/components/profile.mjs +++ b/front-end/components/profile.mjs @@ -19,6 +19,7 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { ); const followerCountEl = profileElement.querySelector("[data-follower-count]"); const followButtonEl = profileElement.querySelector("[data-action='follow']"); + const unfollowButtonEl = profileElement.querySelector("[data-action='unfollow']"); const whoToFollowContainer = profileElement.querySelector(".profile__who-to-follow"); // Populate with data usernameEl.querySelector("h2").textContent = profileData.username || ""; @@ -27,10 +28,14 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { followerCountEl.textContent = profileData.followers?.length || 0; followingCountEl.textContent = profileData.follows?.length || 0; followButtonEl.setAttribute("data-username", profileData.username || ""); + unfollowButtonEl.setAttribute("data-username", profileData.username || ""); followButtonEl.hidden = profileData.is_self || profileData.is_following; + unfollowButtonEl.hidden = profileData.is_self || !profileData.is_following; followButtonEl.addEventListener("click", handleFollow); + unfollowButtonEl.addEventListener("click", handleUnfollow); if (!isLoggedIn) { followButtonEl.style.display = "none"; + unfollowButtonEl.style.display = "none"; } if (whoToFollow.length > 0) { @@ -44,6 +49,7 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { const followButton = wtfElement.querySelector("button"); followButton.setAttribute("data-username", userToFollow.username); followButton.addEventListener("click", handleFollow); + if (!isLoggedIn) { followButton.style.display = "none"; } @@ -65,5 +71,14 @@ async function handleFollow(event) { await apiService.followUser(username); await apiService.getWhoToFollow(); } +async function handleUnfollow(event) { + const button = event.target; + const username = button.getAttribute("data-username"); + if (!username) return; + + await apiService.unfollowUser(username); + await apiService.getWhoToFollow(); +} + -export {createProfile, handleFollow}; +export {createProfile, handleFollow, handleUnfollow}; diff --git a/front-end/index.html b/front-end/index.html index 89d6b13..a6c2d7a 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -186,6 +186,7 @@

Create your account

+

Who to follow

diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index f4b5339..ea89dfe 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -261,8 +261,9 @@ async function followUser(username) { async function unfollowUser(username) { try { - const data = await _apiRequest(`/unfollow/${username}`, { + const data = await _apiRequest("/unfollow", { method: "POST", + body: JSON.stringify({follow_username: username}), }); if (data.success) {