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
106 changes: 101 additions & 5 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class Bloom:
sender: User
content: str
sent_timestamp: datetime.datetime

rebloomer: Optional[str] = None # new optional field
rebloom_count: int = 0
last_rebloomed_at: Optional[datetime.datetime] = None

def add_bloom(*, sender: User, content: str) -> Bloom:
hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")]
Expand Down Expand Up @@ -54,11 +56,16 @@ def get_blooms_for_user(

cur.execute(
f"""SELECT
blooms.id, users.username, content, send_timestamp
FROM
blooms INNER JOIN users ON users.id = blooms.sender_id
blooms.id, users.username, content, send_timestamp,
COUNT(r.id) AS rebloom_count,
MAX(r.rebloomed_at) AS last_rebloomed_at
FROM blooms
INNER JOIN users ON users.id = blooms.sender_id
LEFT JOIN reblooms r ON r.original_bloom_id = blooms.id
WHERE
username = %(sender_username)s
GROUP BY
blooms.id, users.username, content, send_timestamp
{before_clause}
ORDER BY send_timestamp DESC
{limit_clause}
Expand All @@ -68,17 +75,94 @@ def get_blooms_for_user(
rows = cur.fetchall()
blooms = []
for row in rows:
bloom_id, sender_username, content, timestamp = row
bloom_id, sender_username, content, timestamp , rebloom_count, last_rebloomed_at= row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloom_count=rebloom_count,
last_rebloomed_at=last_rebloomed_at,
)
)
return blooms

# Fetch all reblooms made by a user
# returning them as Bloom objects with original content
# but marked as rebloomed by the user
def get_reblooms_for_user(
username: str,
*,
before: Optional[datetime.datetime] = None,
limit: Optional[int] = None,
) -> List[Bloom]:
with db_cursor() as cur:
kwargs = {
"username": username,
}

if before is not None:
before_clause = "AND r.rebloomed_at < %(before_limit)s"
kwargs["before_limit"] = before
else:
before_clause = ""

limit_clause = make_limit_clause(limit, kwargs)

cur.execute(
f"""
SELECT
b.id,
us.username AS original_sender,
b.content,
r.rebloomed_at,
ur.username AS rebloomer_username,
rc.rebloom_count,
rc.last_rebloomed_at
FROM
reblooms r
INNER JOIN blooms b ON r.original_bloom_id = b.id
INNER JOIN users ur ON r.rebloomed_by = ur.id
INNER JOIN users us ON b.sender_id = us.id
INNER JOIN (
SELECT
original_bloom_id,
COUNT(*) AS rebloom_count,
MAX(rebloomed_at) AS last_rebloomed_at
FROM reblooms
GROUP BY original_bloom_id
) rc ON rc.original_bloom_id = b.id
WHERE
ur.username = %(username)s
{before_clause}
ORDER BY
r.rebloomed_at DESC
{limit_clause}

""",
kwargs,
)

rows = cur.fetchall()
reblooms = []

for row in rows:
bloom_id, original_sender, content, timestamp, rebloomer_username , rebloom_count, last_rebloomed_at = row
reblooms.append(
Bloom(
id=bloom_id,
sender=original_sender, # the user who performed the rebloom
content=content, # content of the original bloom
sent_timestamp=timestamp, # time when the rebloom happened
rebloomer=rebloomer_username , # user who rebloomed
rebloom_count=rebloom_count,
last_rebloomed_at=last_rebloomed_at
)
)

return reblooms


def get_bloom(bloom_id: int) -> Optional[Bloom]:
with db_cursor() as cur:
Expand Down Expand Up @@ -140,3 +224,15 @@ def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str:
else:
limit_clause = ""
return limit_clause

#Return True if the user has already rebloomed this bloom.
def has_user_rebloomed(original_bloom_id: int, user_id: int) -> bool :
with db_cursor() as cur :
cur.execute("select 1 from reblooms where original_bloom_id=%s and rebloomed_by=%s",(original_bloom_id,user_id))
return cur.fetchone() is not None

#Insert a rebloom record and return its id.
def add_rebloom(original_bloom_id: int, user_id: int) -> int :
with db_cursor() as cur :
cur.execute("insert into reblooms (original_bloom_id, rebloomed_by) values(%s, %s) returning id",(original_bloom_id, user_id))
return cur.fetchone()[0]
56 changes: 50 additions & 6 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)

from datetime import timedelta
from data.blooms import get_reblooms_for_user

MINIMUM_PASSWORD_LENGTH = 5

Expand Down Expand Up @@ -166,6 +167,41 @@ def send_bloom():
}
)

@jwt_required()
def send_rebloom(original_bloom_id):
user = get_current_user()

bloom = blooms.get_bloom(original_bloom_id)
if bloom is None:
return make_response((f"Bloom not found", 404))

# Check if already rebloomed
if blooms.has_user_rebloomed(original_bloom_id, user.id):
return make_response(({"success": False, "message": "Already rebloomed"}, 400))

# Add rebloom
rebloom_id = blooms.add_rebloom(original_bloom_id, user.id)

return jsonify({"success": True, "rebloom_id": rebloom_id})

from data.blooms import get_blooms_for_user
@jwt_required()
def get_reblooms_for_user_endpoint(username):
reblooms = get_reblooms_for_user(username)
return jsonify([
{
"id": r.id,
"sender": r.sender,
"content": r.content,
"sent_timestamp": str(r.sent_timestamp),
"rebloomer": r.rebloomer, # <-- include the new field
"rebloom_count": r.rebloom_count,
"last_rebloomed_at": str(r.last_rebloomed_at) if r.last_rebloomed_at else None
}
for r in reblooms
])



def get_bloom(id_str):
try:
Expand Down Expand Up @@ -195,14 +231,22 @@ def home_timeline():
# Get the current user's own blooms
own_blooms = blooms.get_blooms_for_user(current_user.username, limit=50)

# Combine own blooms with followed blooms
all_blooms = followed_blooms + own_blooms
all_blooms_dict = {bloom.id: bloom for bloom in followed_blooms + own_blooms}

# Sort by timestamp (newest first)
sorted_blooms = list(
sorted(all_blooms, key=lambda bloom: bloom.sent_timestamp, reverse=True)
)
# Fetch reblooms for the followed users
all_reblooms = []
for user in followed_users + [current_user.username]:
all_reblooms.extend(get_reblooms_for_user(user))

for r in all_reblooms:
if r.id in all_blooms_dict:
all_blooms_dict[r.id].rebloomer = r.rebloomer
all_blooms_dict[r.id].rebloom_count = r.rebloom_count
all_blooms_dict[r.id].last_rebloomed_at = r.last_rebloomed_at


# Sort by timestamp (newest first)
sorted_blooms =sorted(all_blooms_dict.values(), key=lambda b: b.sent_timestamp, reverse=True)
return jsonify(sorted_blooms)


Expand Down
9 changes: 9 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
register,
self_profile,
send_bloom,
send_rebloom,
suggested_follows,
user_blooms,
)
Expand Down Expand Up @@ -42,6 +43,7 @@ def main():
},
)

from endpoints import get_reblooms_for_user_endpoint
app.config["JWT_SECRET_KEY"] = os.environ["JWT_SECRET_KEY"]
jwt = JWTManager(app)
jwt.user_lookup_loader(lookup_user)
Expand All @@ -60,6 +62,13 @@ def main():
app.add_url_rule("/bloom/<id_str>", methods=["GET"], view_func=get_bloom)
app.add_url_rule("/blooms/<profile_username>", view_func=user_blooms)
app.add_url_rule("/hashtag/<hashtag>", view_func=hashtag)

# Add rebloom route
app.add_url_rule("/rebloom/<int:original_bloom_id>", methods=["POST"], view_func=send_rebloom )

# Add route for fetching user reblooms
app.add_url_rule("/reblooms/user/<username>", view_func=get_reblooms_for_user_endpoint)


app.run(host="0.0.0.0", port="3000", debug=True)

Expand Down
9 changes: 9 additions & 0 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,12 @@ CREATE TABLE hashtags (
bloom_id BIGINT NOT NULL REFERENCES blooms(id),
UNIQUE(hashtag, bloom_id)
);

CREATE TABLE IF NOT EXISTS reblooms (
id BIGSERIAL PRIMARY KEY,
original_bloom_id BIGINT NOT NULL REFERENCES blooms(id),
rebloomed_by INT NOT NULL REFERENCES users(id),
rebloomed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_rebloom_per_user UNIQUE (original_bloom_id, rebloomed_by)
);

71 changes: 70 additions & 1 deletion front-end/components/timeline.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ function createTimeline(template, blooms) {
const bloomsFragment = document.createDocumentFragment();
// Accumulate blooms
blooms.forEach((bloom) => {
bloomsFragment.appendChild(createBloom("bloom-template", bloom));
console.log("TIMELINE BLOOM:", bloom);
bloomsFragment.appendChild(createBloomWithRebloom("bloom-template", bloom));
});

// Add all blooms to the content container at once
Expand All @@ -34,4 +35,72 @@ function createTimeline(template, blooms) {
return timelineElement;
}

// Create a bloom element and add rebloom info if available
function createBloomWithRebloom(template, bloom) {

const bloomElement = createBloom(template, bloom);

const rebloomInfo = bloomElement.querySelector("[data-rebloom-info]");
const rebloomerLabel = bloomElement.querySelector("[data-rebloomer]");
const rebloomCountEl = bloomElement.querySelector("[data-rebloom-count]");
const rebloomTimeEl = bloomElement.querySelector("[data-rebloom-time]");

// If bloom is rebloomed :
if (bloom.rebloomer) {
rebloomInfo.hidden = false;

// Show rebloomer
rebloomerLabel.textContent = `Rebloomed by ${bloom.rebloomer}`;

// Show rebloom_count
if (typeof bloom.rebloom_count === "number" && bloom.rebloom_count > 0) {
rebloomCountEl.hidden = false;

const countLabel = bloom.rebloom_count === 1 ? "Time rebloomed" : "Times rebloomed";
rebloomCountEl.textContent = `(${bloom.rebloom_count} ${countLabel})`;
} else {
rebloomCountEl.hidden = true;
}

// Show last_rebloomed_at time
if (bloom.last_rebloomed_at) {
rebloomTimeEl.hidden = false;

const date = new Date(bloom.last_rebloomed_at);
rebloomTimeEl.textContent = date.toLocaleString();
} else {
rebloomTimeEl.hidden = true;
}
} else {
// // If bloom is not rebloomed :
rebloomInfo.hidden = true;
rebloomerLabel.textContent = "";
rebloomCountEl.hidden = true;
rebloomTimeEl.hidden = true;
}

// Add rebloom button if doesn't exist
if (!bloomElement.querySelector("[data-action='rebloom']")) {
const rebloomButton = document.createElement("button");
rebloomButton.type = "button";
rebloomButton.classList.add("bloom__rebloom-button");
rebloomButton.setAttribute("data-action", "rebloom");
rebloomButton.setAttribute("aria-label", "Rebloom");
rebloomButton.textContent = "🔄";
bloomElement.appendChild(rebloomButton);
}

// Log for check the data
console.log("REBLOOM DATA:", {
id: bloom.id,
rebloom_count: bloom.rebloom_count,
last_rebloomed_at: bloom.last_rebloomed_at,
rebloomer: bloom.rebloomer,
});

return bloomElement;
}


export {createTimeline};
export {createBloomWithRebloom};
Loading