From d5222d20a310f59ca598068932725ba59cb7cd5a Mon Sep 17 00:00:00 2001 From: Craig D'Silva Date: Thu, 11 Jun 2026 20:53:00 +0100 Subject: [PATCH 1/9] Rebloom DB query --- backend/data/blooms.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 7e280cf3..33b19157 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -14,6 +14,14 @@ class Bloom: content: str sent_timestamp: datetime.datetime +@dataclass +class Rebloom: + id: int + resender: User + sender: User + content: str + sent_timestamp: datetime.datetime + def add_bloom(*, sender: User, content: str) -> Bloom: hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] @@ -36,6 +44,19 @@ def add_bloom(*, sender: User, content: str) -> Bloom: dict(hashtag=hashtag, bloom_id=bloom_id), ) +def rebloom(*, rebloom_id: int, resender: User, sender: User, content: str) -> Rebloom: + + with db_cursor() as cur: + cur.execute( + "INSERT INTO reblooms (id, resender_id, original_sender_id, content, send_timestamp, times_rebloomed) VALUES (%(rebloom_id)s, %(resender_id)s, %(sender_id)s, %(content)s, %(timestamp)s, 1) ON CONFLICT (resender_id, original_sender_id, content) DO UPDATE SET times_rebloomed = times_rebloomed + 1", + dict( + rebloom_id=rebloom_id, + resender_id=resender.id, + sender_id=sender.id, + content=content, + timestamp=datetime.datetime.now(datetime.UTC), + ), + ) def get_blooms_for_user( username: str, *, before: Optional[int] = None, limit: Optional[int] = None From 933840dda4950590a47c642399a48a89bfe467c0 Mon Sep 17 00:00:00 2001 From: Craig D'Silva Date: Thu, 11 Jun 2026 20:54:06 +0100 Subject: [PATCH 2/9] Add rebloom endpoint on the backend --- backend/endpoints.py | 22 ++++++++++++++++++++++ backend/main.py | 2 ++ 2 files changed, 24 insertions(+) diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a07..6375aeed 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -167,6 +167,28 @@ def send_bloom(): ) +@jwt_required() +def send_rebloom(): + type_check_error = verify_request_fields({"content": str}) + if type_check_error is not None: + return type_check_error + + user = get_current_user() + + if user.username == request.json["sender"]: + return make_response((f"Cannot rebloom own bloom", 422)) + + print(request.json["sender"]) + + # blooms.rebloom(rebloom_id=request.json["id"], resender=user sender=request.json["sender"], content=request.json["content"]) + + return jsonify( + { + "success": True, + } + ) + + def get_bloom(id_str): try: id_int = int(id_str) diff --git a/backend/main.py b/backend/main.py index 7ba155fa..f50a6567 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,6 +12,7 @@ register, self_profile, send_bloom, + send_rebloom, suggested_follows, user_blooms, ) @@ -59,6 +60,7 @@ def main(): app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom) app.add_url_rule("/bloom/", methods=["GET"], view_func=get_bloom) app.add_url_rule("/blooms/", view_func=user_blooms) + app.add_url_rule("/rebloom", methods=["POST"], view_func=send_rebloom) app.add_url_rule("/hashtag/", view_func=hashtag) app.run(host="0.0.0.0", port="3000", debug=True) From 417d10b15e62ffc69d3bd88d478eccd93e24d414 Mon Sep 17 00:00:00 2001 From: Craig D'Silva Date: Thu, 11 Jun 2026 20:57:36 +0100 Subject: [PATCH 3/9] Add HTTP error with status code --- front-end/components/error.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/front-end/components/error.mjs b/front-end/components/error.mjs index 3fc9eeee..80ef1946 100644 --- a/front-end/components/error.mjs +++ b/front-end/components/error.mjs @@ -7,6 +7,7 @@ const _STATUS_MESSAGES = { 404: "Not Found - The requested resource does not exist.", 405: "Not Allowed - The server knows the request method, but the target resource doesn't support this method.", 418: "I'm a teapot - Server refuses to brew coffee with a teapot.", + 422: "Invalid data - The request was well-formed but was unable to be followed due to semantic errors.", 500: "Internal Server Error - Something went wrong on the server.", }; From 8b238b4d7eb089dc7ef2cd65869620cac6c701cf Mon Sep 17 00:00:00 2001 From: Craig D'Silva Date: Thu, 11 Jun 2026 20:58:36 +0100 Subject: [PATCH 4/9] Add rebloom API endpoint on the front-end --- front-end/lib/api.mjs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index f4b5339b..c0d3a899 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -212,6 +212,25 @@ async function postBloom(content) { } } +async function postRebloom(id, sender, content) { + try { + const data = await _apiRequest("/rebloom", { + method: "POST", + body: JSON.stringify({id, sender, content}), + }); + + if (data.success) { + await getBlooms(); + await getProfile(state.currentUser); + } + + return data; + } catch (error) { + // Error already handled by _apiRequest + return {success: false}; + } +} + // ======= USER methods async function getProfile(username) { const endpoint = username ? `/profile/${username}` : "/profile"; @@ -291,6 +310,7 @@ const apiService = { getBloom, getBlooms, postBloom, + postRebloom, getBloomsByHashtag, // User methods From 6644b46832534f2e977a67a1b8bb43f8e61b2d09 Mon Sep 17 00:00:00 2001 From: Craig D'Silva Date: Fri, 12 Jun 2026 17:56:13 +0100 Subject: [PATCH 5/9] Add Rebloom button --- front-end/components/bloom.mjs | 12 ++++++++++++ front-end/index.html | 1 + 2 files changed, 13 insertions(+) diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c3..ec0e2567 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -1,3 +1,6 @@ +import { apiService } from "../lib/api.mjs" +import { state } from "../lib/state.mjs" + /** * Create a bloom component * @param {string} template - The ID of the template to clone @@ -20,6 +23,7 @@ const createBloom = (template, bloom) => { const bloomTime = bloomFrag.querySelector("[data-time]"); const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])"); const bloomContent = bloomFrag.querySelector("[data-content]"); + const rebloomButton = bloomFrag.querySelector("[data-rebloom]") bloomArticle.setAttribute("data-bloom-id", bloom.id); bloomUsername.setAttribute("href", `/profile/${bloom.sender}`); @@ -30,6 +34,14 @@ const createBloom = (template, bloom) => { ...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html") .body.childNodes ); + rebloomButton.hidden = state.currentUser === bloom.sender + rebloomButton.addEventListener("click", async () => { + try { + await apiService.postRebloom(bloom.id, bloom.sender, bloom.content); + } catch (error) { + throw error; + } + }); return bloomFrag; }; diff --git a/front-end/index.html b/front-end/index.html index 89d6b130..b4ea5cf8 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -239,6 +239,7 @@

Share a Bloom

+ From 47b2c29457de4b98a816cfcedcf9bece8ffd6176 Mon Sep 17 00:00:00 2001 From: Craig D'Silva Date: Fri, 12 Jun 2026 18:38:29 +0100 Subject: [PATCH 6/9] Rebloom INSERT query --- backend/data/blooms.py | 5 +++-- backend/endpoints.py | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 33b19157..c09b2b9a 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -48,11 +48,11 @@ def rebloom(*, rebloom_id: int, resender: User, sender: User, content: str) -> R with db_cursor() as cur: cur.execute( - "INSERT INTO reblooms (id, resender_id, original_sender_id, content, send_timestamp, times_rebloomed) VALUES (%(rebloom_id)s, %(resender_id)s, %(sender_id)s, %(content)s, %(timestamp)s, 1) ON CONFLICT (resender_id, original_sender_id, content) DO UPDATE SET times_rebloomed = times_rebloomed + 1", + "INSERT INTO reblooms (id, resender_id, original_sender_name, content, send_timestamp, times_rebloomed) VALUES (%(rebloom_id)s, %(resender_id)s, %(sender_name)s, %(content)s, %(timestamp)s, 1) ON CONFLICT (resender_id, original_sender_name, content) DO UPDATE SET times_rebloomed = reblooms.times_rebloomed + 1", dict( rebloom_id=rebloom_id, resender_id=resender.id, - sender_id=sender.id, + sender_name=sender, content=content, timestamp=datetime.datetime.now(datetime.UTC), ), @@ -161,3 +161,4 @@ def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str: else: limit_clause = "" return limit_clause + diff --git a/backend/endpoints.py b/backend/endpoints.py index 6375aeed..6b6f4a39 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -178,9 +178,7 @@ def send_rebloom(): if user.username == request.json["sender"]: return make_response((f"Cannot rebloom own bloom", 422)) - print(request.json["sender"]) - - # blooms.rebloom(rebloom_id=request.json["id"], resender=user sender=request.json["sender"], content=request.json["content"]) + blooms.rebloom(rebloom_id=request.json["id"], resender=user, sender=request.json["sender"], content=request.json["content"]) return jsonify( { From 14aca30b7292543758af7d422c9a5ab2630eccb4 Mon Sep 17 00:00:00 2001 From: Craig D'Silva Date: Mon, 15 Jun 2026 10:24:45 +0100 Subject: [PATCH 7/9] Get reblooms --- backend/data/blooms.py | 51 ++++++++++++++++++++++++++++++++++-------- backend/endpoints.py | 16 ++++++++++--- backend/main.py | 2 ++ 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index c09b2b9a..28509462 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -21,6 +21,7 @@ class Rebloom: sender: User content: str sent_timestamp: datetime.datetime + rebloomed: int def add_bloom(*, sender: User, content: str) -> Bloom: @@ -44,19 +45,26 @@ def add_bloom(*, sender: User, content: str) -> Bloom: dict(hashtag=hashtag, bloom_id=bloom_id), ) -def rebloom(*, rebloom_id: int, resender: User, sender: User, content: str) -> Rebloom: - +def rebloom(*, rebloom_id: int, resender: User, sender: User, content: str, sent_timestamp: datetime.datetime) -> Rebloom: with db_cursor() as cur: cur.execute( - "INSERT INTO reblooms (id, resender_id, original_sender_name, content, send_timestamp, times_rebloomed) VALUES (%(rebloom_id)s, %(resender_id)s, %(sender_name)s, %(content)s, %(timestamp)s, 1) ON CONFLICT (resender_id, original_sender_name, content) DO UPDATE SET times_rebloomed = reblooms.times_rebloomed + 1", + "INSERT INTO reblooms (id, resender_name, original_sender_name, content, send_timestamp, times_rebloomed) VALUES (%(rebloom_id)s, %(resender_name)s, %(sender_name)s, %(content)s, %(timestamp)s, 1) ON CONFLICT (id) DO UPDATE SET times_rebloomed = reblooms.times_rebloomed + 1", dict( rebloom_id=rebloom_id, - resender_id=resender.id, + resender_name=resender.username, sender_name=sender, content=content, - timestamp=datetime.datetime.now(datetime.UTC), + timestamp=sent_timestamp ), ) + return Rebloom( + id=rebloom_id, + resender=resender, + sender=sender, + content=content, + sent_timestamp=sent_timestamp, + rebloomed=1 + ) def get_blooms_for_user( username: str, *, before: Optional[int] = None, limit: Optional[int] = None @@ -71,8 +79,6 @@ def get_blooms_for_user( else: before_clause = "" - limit_clause = make_limit_clause(limit, kwargs) - cur.execute( f"""SELECT blooms.id, users.username, content, send_timestamp @@ -81,8 +87,6 @@ def get_blooms_for_user( WHERE username = %(sender_username)s {before_clause} - ORDER BY send_timestamp DESC - {limit_clause} """, kwargs, ) @@ -101,6 +105,35 @@ def get_blooms_for_user( return blooms +def get_reblooms_for_user(username: str) -> List[Rebloom]: + with db_cursor() as cur: + cur.execute( + """SELECT + reblooms.id, users.username, original_sender_name, content, send_timestamp, times_rebloomed + FROM + reblooms INNER JOIN users ON users.username = reblooms.resender_name + WHERE + users.username = %(username)s + """, + {"username": username}, + ) + rows = cur.fetchall() + reblooms = [] + for row in rows: + rebloom_id, resender_name, sender_name, content, timestamp, times_rebloomed = row + reblooms.append( + Rebloom( + id=rebloom_id, + resender=resender_name, + sender=sender_name, + content=content, + sent_timestamp=timestamp, + rebloomed=times_rebloomed + ) + ) + return reblooms + + def get_bloom(bloom_id: int) -> Optional[Bloom]: with db_cursor() as cur: cur.execute( diff --git a/backend/endpoints.py b/backend/endpoints.py index 6b6f4a39..f1dea99e 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -111,11 +111,15 @@ def other_profile(profile_username): followers = get_inverse_followed_usernames(profile_user) all_blooms = blooms.get_blooms_for_user(profile_username) - all_blooms.reverse() + all_reblooms = blooms.get_reblooms_for_user(profile_username) + + bloom_data = all_blooms + all_reblooms + bloom_data.reverse() + return jsonify( { "username": profile_username, - "recent_blooms": all_blooms[:10], + "recent_blooms": bloom_data[:10], "follows": get_followed_usernames(profile_user), "followers": list(followers), "is_following": current_user is not None @@ -178,7 +182,7 @@ def send_rebloom(): if user.username == request.json["sender"]: return make_response((f"Cannot rebloom own bloom", 422)) - blooms.rebloom(rebloom_id=request.json["id"], resender=user, sender=request.json["sender"], content=request.json["content"]) + blooms.rebloom(rebloom_id=request.json["id"], resender=user, sender=request.json["sender"], content=request.json["content"], sent_timestamp=request.json["sent_timestamp"]) return jsonify( { @@ -232,6 +236,12 @@ def user_blooms(profile_username): return jsonify(user_blooms) +def user_reblooms(profile_username): + user_reblooms = blooms.get_reblooms_for_user(profile_username) + user_reblooms.reverse() + return jsonify(user_reblooms) + + @jwt_required() def suggested_follows(limit_str): try: diff --git a/backend/main.py b/backend/main.py index f50a6567..62194586 100644 --- a/backend/main.py +++ b/backend/main.py @@ -15,6 +15,7 @@ send_rebloom, suggested_follows, user_blooms, + user_reblooms ) from dotenv import load_dotenv @@ -61,6 +62,7 @@ def main(): app.add_url_rule("/bloom/", methods=["GET"], view_func=get_bloom) app.add_url_rule("/blooms/", view_func=user_blooms) app.add_url_rule("/rebloom", methods=["POST"], view_func=send_rebloom) + app.add_url_rule("/reblooms/", view_func=user_reblooms) app.add_url_rule("/hashtag/", view_func=hashtag) app.run(host="0.0.0.0", port="3000", debug=True) From a68fa2c84ebfdf56a21e2870c997826ccb7fcc61 Mon Sep 17 00:00:00 2001 From: Craig D'Silva Date: Mon, 15 Jun 2026 10:25:17 +0100 Subject: [PATCH 8/9] Render reblooms --- front-end/components/bloom.mjs | 5 ++++- front-end/index.html | 1 + front-end/lib/api.mjs | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index ec0e2567..8508a70d 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -24,6 +24,7 @@ const createBloom = (template, bloom) => { const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])"); const bloomContent = bloomFrag.querySelector("[data-content]"); const rebloomButton = bloomFrag.querySelector("[data-rebloom]") + const timesRebloomedCounter = bloomFrag.querySelector("[data-times-rebloomed]") bloomArticle.setAttribute("data-bloom-id", bloom.id); bloomUsername.setAttribute("href", `/profile/${bloom.sender}`); @@ -37,11 +38,13 @@ const createBloom = (template, bloom) => { rebloomButton.hidden = state.currentUser === bloom.sender rebloomButton.addEventListener("click", async () => { try { - await apiService.postRebloom(bloom.id, bloom.sender, bloom.content); + await apiService.postRebloom(bloom.id, bloom.sender, bloom.content, bloom.sent_timestamp); } catch (error) { throw error; } }); + timesRebloomedCounter.hidden = !bloom.rebloomed; + timesRebloomedCounter.textContent = `Times rebloomed: ${bloom.rebloomed}` return bloomFrag; }; diff --git a/front-end/index.html b/front-end/index.html index b4ea5cf8..dca15e76 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -239,6 +239,7 @@

Share a Bloom

+

diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index c0d3a899..ca26426a 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -212,11 +212,11 @@ async function postBloom(content) { } } -async function postRebloom(id, sender, content) { +async function postRebloom(id, sender, content, sent_timestamp) { try { const data = await _apiRequest("/rebloom", { method: "POST", - body: JSON.stringify({id, sender, content}), + body: JSON.stringify({id, sender, content, sent_timestamp}), }); if (data.success) { From c4806eb1c1891374c48f9b2fa407f894fcb6ea7c Mon Sep 17 00:00:00 2001 From: Craig D'Silva Date: Mon, 15 Jun 2026 10:25:38 +0100 Subject: [PATCH 9/9] Add rebloom schema --- db/schema.sql | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/db/schema.sql b/db/schema.sql index 61e7580c..5b598cd3 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -13,6 +13,15 @@ CREATE TABLE blooms ( send_timestamp TIMESTAMP NOT NULL ); +CREATE TABLE reblooms ( + id BIGSERIAL NOT NULL PRIMARY KEY, + resender_name TEXT NOT NULL, + original_sender_name TEXT NOT NULL, + content TEXT NOT NULL, + send_timestamp TIMESTAMP NOT NULL, + times_rebloomed INT NOT NULL +); + CREATE TABLE follows ( id SERIAL PRIMARY KEY, follower INT NOT NULL REFERENCES users(id),