diff --git a/backend/data/follows.py b/backend/data/follows.py index a4b6314..93494f7 100644 --- a/backend/data/follows.py +++ b/backend/data/follows.py @@ -20,6 +20,13 @@ def follow(follower: User, followee: User): # Already following - treat as idempotent request. pass +def unfollow(follower: User, followee: User): + """Remove a follow relationship between two users.""" + with db_cursor() as cur: + cur.execute( + "DELETE FROM follows WHERE follower = %s AND followee = %s", + (follower.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..2844a62 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, get_followed_usernames, get_inverse_followed_usernames, unfollow from data.users import ( UserRegistrationError, get_suggested_follows, @@ -149,6 +149,22 @@ def do_follow(): } ) +@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(): @@ -157,8 +173,20 @@ def send_bloom(): return type_check_error user = get_current_user() + #Extract content safely + content = request.json.get("content", "") + # Limit content length + MAX_BLOOM_LENGTH = 280 + if len(content) > MAX_BLOOM_LENGTH: + return make_response( + { + "success": False, + "message": f"Bloom cannot exceed {MAX_BLOOM_LENGTH} characters", + }, + 400, + ) - blooms.add_bloom(sender=user, content=request.json["content"]) + blooms.add_bloom(sender=user, content=content) return jsonify( { @@ -205,12 +233,40 @@ def home_timeline(): return jsonify(sorted_blooms) - def user_blooms(profile_username): user_blooms = blooms.get_blooms_for_user(profile_username) user_blooms.reverse() return jsonify(user_blooms) +def update_rebloom_counter(bloom_id): + try: + id_int = int(bloom_id) + except ValueError: + return make_response((f"Invalid bloom id", 400)) + blooms.update_rebloom_counter(id_int) + return jsonify( + { + "success": True, + } + ) + +@jwt_required() +def send_rebloom(): + user = get_current_user() + bloom_id = request.json["id"] + try: + id_int = int(bloom_id) + except ValueError: + return make_response((f"Invalid bloom id", 400)) + blooms.add_rebloom(sender=user, id=id_int) + + return jsonify( + { + "success": True, + } + ) + + @jwt_required() def suggested_follows(limit_str): @@ -245,3 +301,4 @@ def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, No ) ) return None + 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/backend/populate.py b/backend/populate.py index 414218b..8ca3293 100644 --- a/backend/populate.py +++ b/backend/populate.py @@ -64,8 +64,7 @@ def main(): writer_access_token = create_user("AS", "neverSt0pTalking") send_bloom( writer_access_token, - "In this essay I will convince you that my views are correct in ways you have never imagined. If it doesn't change your life, read it again. Marshmallows are magnificent. They have great squish, tasty good, and you can even toast them over a fire. Toast them just right until they have a tiny bit of crunch when you bite into them, and have just started melting in the middle.", - ) + "In this essay I will convince you that my views are correct in ways you have never imagined. Marshmallows are magnificent!" ) justsomeguy_access_token = create_user("JustSomeGuy", "mysterious") send_bloom(justsomeguy_access_token, "Hello.") diff --git a/backend/requirements.txt b/backend/requirements.txt index e03836c..5e11b05 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,7 +11,7 @@ idna==3.10 itsdangerous==2.2.0 Jinja2==3.1.5 MarkupSafe==3.0.2 -psycopg2==2.9.10 +psycopg2-binary==2.9.10 pycparser==2.22 PyJWT==2.10.1 python-dotenv==1.0.1 diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c..8a75cc3 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -37,7 +37,7 @@ const createBloom = (template, bloom) => { function _formatHashtags(text) { if (!text) return text; return text.replace( - /\B#[^#]+/g, + /(^|\s)(#[\w]+)/g, (match) => `${match}` ); } diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index ec4f200..da2d6ea 100644 --- a/front-end/components/profile.mjs +++ b/front-end/components/profile.mjs @@ -27,12 +27,20 @@ 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.addEventListener("click", handleFollow); + + // Set button text and action based on follow status + if (profileData.is_following) { + followButtonEl.textContent = "Unfollow"; + followButtonEl.addEventListener("click", handleUnfollow); + } else { + followButtonEl.textContent = "Follow"; + followButtonEl.addEventListener("click", handleFollow); + } + if (!isLoggedIn) { followButtonEl.style.display = "none"; } - +//follow button based on condition if (whoToFollow.length > 0) { const whoToFollowList = whoToFollowContainer.querySelector("[data-who-to-follow]"); const whoToFollowTemplate = document.querySelector("#who-to-follow-chip"); @@ -43,7 +51,17 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { usernameLink.setAttribute("href", `/profile/${userToFollow.username}`); const followButton = wtfElement.querySelector("button"); followButton.setAttribute("data-username", userToFollow.username); - followButton.addEventListener("click", handleFollow); + + if (userToFollow.is_following) { + followButton.textContent = "Unfollow"; + followButton.addEventListener("click", handleUnfollow); + } + else { + followButton.textContent = "Follow"; + followButton.addEventListener("click", handleFollow); + } + + if (!isLoggedIn) { followButton.style.display = "none"; } @@ -65,5 +83,15 @@ async function handleFollow(event) { await apiService.followUser(username); await apiService.getWhoToFollow(); } +//unfollow handler +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/views/hashtag.mjs b/front-end/views/hashtag.mjs index 7b7e996..3e51bad 100644 --- a/front-end/views/hashtag.mjs +++ b/front-end/views/hashtag.mjs @@ -15,6 +15,13 @@ import {createHeading} from "../components/heading.mjs"; // Hashtag view: show all tweets containing this tag function hashtagView(hashtag) { + //only fetch data from the server and avoid the flashing on slow network + if (state.currentHashtag !== hashtag) { + state.currentHashtag = hashtag; + state.hashtagBlooms = []; + apiService.getBloomsByHashtag(hashtag); + } + //this one if statement fixes the flashing behavior by checking if the hashtag selected is already running or not destroy(); apiService.getBloomsByHashtag(hashtag); diff --git a/front-end/views/profile.mjs b/front-end/views/profile.mjs index dd2b92a..31139a9 100644 --- a/front-end/views/profile.mjs +++ b/front-end/views/profile.mjs @@ -39,8 +39,8 @@ function profileView(username) { createLogin ); document - .querySelector("[data-action='login']") - ?.addEventListener("click", handleLogin); + .querySelector("[data-form='login']") + ?.addEventListener("submit", handleLogin); const profileData = state.profiles.find((p) => p.username === username); if (profileData) {