diff --git a/backend/data/follows.py b/backend/data/follows.py index a4b6314e..a73fe379 100644 --- a/backend/data/follows.py +++ b/backend/data/follows.py @@ -20,6 +20,19 @@ def follow(follower: User, followee: User): # Already following - treat as idempotent request. pass +def unfollow(follower: User, followee: User): + with db_cursor() as cur: + try: + cur.execute( + "DELETE FROM follows WHERE follower = %(follower_id)s AND followee = %(followee_id)s", + dict( + follower_id=follower.id, + followee_id=followee.id, + ), + ) + except UniqueViolation: + pass + 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 0e177a07..f197321c 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,21 @@ def do_follow(): } ) +@jwt_required() +def do_unfollow(profile_username): + current_user = get_current_user() + unfollow_user = get_user(profile_username) + if unfollow_user is None: + return make_response( + (f"Cannot follow {profile_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 7ba155fa..a8e07215 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 ec4f2009..3fdad5b9 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 || ""; @@ -29,8 +30,12 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { followButtonEl.setAttribute("data-username", profileData.username || ""); followButtonEl.hidden = profileData.is_self || profileData.is_following; followButtonEl.addEventListener("click", handleFollow); + unfollowButtonEl.setAttribute("data-username", profileData.username || ""); + unfollowButtonEl.hidden = profileData.is_self || !profileData.is_following; + unfollowButtonEl.addEventListener("click", handleUnfollow); if (!isLoggedIn) { followButtonEl.style.display = "none"; + unfollowButtonEl.style.display = "none"; } if (whoToFollow.length > 0) { @@ -66,4 +71,12 @@ async function handleFollow(event) { 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); +} + +export { createProfile, handleFollow }; diff --git a/front-end/index.html b/front-end/index.html index 89d6b130..1b83f4c1 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/package-lock.json b/front-end/package-lock.json index 2e25ea1f..a3c7aa0a 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -14,13 +14,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.0.tgz", - "integrity": "sha512-dJ0dMbZeHhI+wb77+ljx/FeC8VBP6j/rj9OAojO08JI80wTZy6vRk9KvHKiDCUh4iMpEiseMgqRBIeW+eKX6RA==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.51.0" + "playwright": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -55,13 +55,13 @@ } }, "node_modules/playwright": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.0.tgz", - "integrity": "sha512-442pTfGM0xxfCYxuBa/Pu6B2OqxqqaYq39JS8QDMGThUvIOCd6s0ANDog3uwA0cHavVlnTQzGCN7Id2YekDSXA==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.51.0" + "playwright-core": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -74,9 +74,9 @@ } }, "node_modules/playwright-core": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.0.tgz", - "integrity": "sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", "dev": true, "license": "Apache-2.0", "bin": {