Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CHANGES-MADE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Project Log: PurpleForest - Un-follow Feature Implementation

## 1. Feature Overview
Developed the "Un-follow" functionality to allow users to stop seeing blooms from specific accounts. This required building a "Reverse Path" that mirrors the "Follow" logic but executes a deletion in the database.

---

## 2. Structural Changes

### A. Database Layer (The Memory)
* **Logic:** Implemented the removal of data using the SQL `DELETE` command.
* **Precision:** Used a `WHERE` clause targeting both the `follower` and the `followee` IDs.
* **The "Why":** This ensures the operation only removes the specific relationship between two users without accidentally deleting all follows for a single user.

### B. Backend Layer (The Brain)
* **Data Logic (`data/follows.py`):**
* Created the `unfollow` function.
* Used **parameterized queries** with a dictionary to safely pass user IDs to the database driver.
* **API Endpoint (`endpoints.py`):**
* Developed the `do_unfollow` controller.
* **Contract Matching:** Designed the function to accept `target_username` directly from the URL path to stay consistent with the existing Frontend API service.
* **Security:** Applied the `@jwt_required` middleware to ensure only the authenticated account owner can modify their following list.
* **Routing (`main.py`):** Registered the new `/unfollow/<target_username>` route and imported the necessary logic from the endpoints module.

### C. Frontend Layer (The Face)
* **Visibility Logic (`profile.mjs`):**
* Modified the `createProfile` component to prevent the button from being hidden when a user is already followed.
* **The Toggle:** Implemented an `if/else` block to dynamically change the button text between "Follow" and "Un-follow" based on the `is_following` state.
* **Event Handling (`profile.mjs`):**
* Upgraded the `handleFollow` function into a **"Smart Switch."**
* **The Logic:** The handler now inspects the current text of the button. If it says "Un-follow," it calls the `unfollowUser` API; otherwise, it calls the standard `followUser` API.

---

## 4. Verification & Testing
* **Action Verification:** Confirmed that clicking "Follow" changes the UI to "Un-follow" and vice-versa.
* **Data Integrity:** Used **DBeaver** to verify that the corresponding row in the `follows` table is physically removed upon clicking "Un-follow."
* **Social Feed Test:** Verified that un-following a user immediately stops their posts from appearing in the Home timeline upon the next refresh.

10 changes: 10 additions & 0 deletions backend/data/follows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -41,3 +50,4 @@ def get_inverse_followed_usernames(followee: User) -> List[str]:
)
rows = cur.fetchall()
return [row[0] for row in rows]

20 changes: 19 additions & 1 deletion backend/endpoints.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -149,6 +149,24 @@ def do_follow():
}
)

@jwt_required()
def do_unfollow(target_username): # Flask passes the name from the URL here
# 1. Identify the user who is logged in
current_user = get_current_user()

# 2. Identify the user to be unfollowed
target_user = get_user(target_username)

# 3. Handle if the target doesn't exist
if target_user is None:
return make_response(
(f"Cannot unfollow {target_username} - user does not exist", 404)
)

# 4. PERFORM THE ACTION (Calling your function from data/follows.py)
unfollow(current_user, target_user)

return jsonify({"success": True})

@jwt_required()
def send_bloom():
Expand Down
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from data.users import lookup_user
from endpoints import (
do_follow,
do_unfollow,
get_bloom,
hashtag,
home_timeline,
Expand Down Expand Up @@ -54,6 +55,7 @@ def main():
app.add_url_rule("/profile", view_func=self_profile)
app.add_url_rule("/profile/<profile_username>", view_func=other_profile)
app.add_url_rule("/follow", methods=["POST"], view_func=do_follow)
app.add_url_rule("/unfollow/<target_username>", methods=["POST"], view_func=do_unfollow)
app.add_url_rule("/suggested-follows/<limit_str>", view_func=suggested_follows)

app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom)
Expand Down
12 changes: 10 additions & 2 deletions front-end/components/profile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) {
followerCountEl.textContent = profileData.followers?.length || 0;
followingCountEl.textContent = profileData.follows?.length || 0;
followButtonEl.setAttribute("data-username", profileData.username || "");
followButtonEl.hidden = profileData.is_self || profileData.is_following;
followButtonEl.hidden = profileData.is_self;
if (profileData.is_following) {
followButtonEl.textContent = "Un-follow"
} else {followButtonEl.textContent = "Follow";
}
followButtonEl.addEventListener("click", handleFollow);
if (!isLoggedIn) {
followButtonEl.style.display = "none";
Expand Down Expand Up @@ -62,7 +66,11 @@ async function handleFollow(event) {
const username = button.getAttribute("data-username");
if (!username) return;

await apiService.followUser(username);
if (button.textContent === "Un-follow") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your "Smart Switch" works, and I like that you found a single handler for both directions. One thing to chew on — no change needed here, just a thought for future you: the handler decides which API to call by reading the button's visible text, button.textContent === "Un-follow". That quietly ties your logic to whatever the label happens to say. What do you think would happen to this if if someone later renamed the button to "Unfollow" (no hyphen), translated the page, or dropped an icon inside the button?

You already hold the real answer to "am I following this person?" in your data — profileData.is_following. Is there a way you could branch on that state instead of the on-screen wording? Something to explore when you're curious, not to change for this PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback. In the future, I'll try to use the data state (like a data-following attribute) to drive the logic so the frontend logic doesn't have to rely on what the UI is showing. Thanks for the suggestion.

await apiService.unfollowUser(username);
} else {
await apiService.followUser(username);
}
await apiService.getWhoToFollow();
}

Expand Down