Skip to content
Open
7 changes: 7 additions & 0 deletions backend/data/follows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
63 changes: 60 additions & 3 deletions 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, get_followed_usernames, get_inverse_followed_usernames, unfollow
from data.users import (
UserRegistrationError,
get_suggested_follows,
Expand Down Expand Up @@ -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():
Expand All @@ -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(
{
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -245,3 +301,4 @@ def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, No
)
)
return None

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/<unfollow_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
3 changes: 1 addition & 2 deletions backend/populate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const createBloom = (template, bloom) => {
function _formatHashtags(text) {
if (!text) return text;
return text.replace(
/\B#[^#]+/g,
/(^|\s)(#[\w]+)/g,
(match) => `<a href="/hashtag/${match.slice(1)}">${match}</a>`
);
}
Expand Down
38 changes: 33 additions & 5 deletions front-end/components/profile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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";
}
Expand All @@ -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};
7 changes: 7 additions & 0 deletions front-end/views/hashtag.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions front-end/views/profile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down