From d96bf4a4bc6c77476b0a421d086de80a0c7687e8 Mon Sep 17 00:00:00 2001 From: delmorallopez <124817272+delmorallopez@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:36:38 +0100 Subject: [PATCH] Feat-Rebloom --- backend/data/blooms.py | 75 ++++++++++++++++++++++------------ backend/endpoints.py | 46 ++++++++++++++++----- db/schema.sql | 3 +- front-end/components/bloom.mjs | 22 ++++++++++ front-end/index.html | 1 + front-end/lib/api.mjs | 21 ++++++++++ 6 files changed, 132 insertions(+), 36 deletions(-) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 7e280cf..2870e7c 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -13,28 +13,37 @@ class Bloom: sender: User content: str sent_timestamp: datetime.datetime + original_bloom_id: Optional[int] = None + original_sender: Optional[str] = None # helpful for UI -def add_bloom(*, sender: User, content: str) -> Bloom: - hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] - +def add_bloom(*, sender: User, content: str = None, original_bloom_id: int = None) -> Bloom: now = datetime.datetime.now(tz=datetime.UTC) bloom_id = int(now.timestamp() * 1000000) + with db_cursor() as cur: cur.execute( - "INSERT INTO blooms (id, sender_id, content, send_timestamp) VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s)", + """ + INSERT INTO blooms (id, sender_id, content, send_timestamp, original_bloom_id) + VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s, %(original_bloom_id)s) + """, dict( bloom_id=bloom_id, sender_id=sender.id, content=content, - timestamp=datetime.datetime.now(datetime.UTC), + timestamp=now, + original_bloom_id=original_bloom_id, ), ) - for hashtag in hashtags: - cur.execute( - "INSERT INTO hashtags (hashtag, bloom_id) VALUES (%(hashtag)s, %(bloom_id)s)", - dict(hashtag=hashtag, bloom_id=bloom_id), - ) + + # Only extract hashtags if it's a normal bloom + if content: + hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] + for hashtag in hashtags: + cur.execute( + "INSERT INTO hashtags (hashtag, bloom_id) VALUES (%(hashtag)s, %(bloom_id)s)", + dict(hashtag=hashtag, bloom_id=bloom_id), + ) def get_blooms_for_user( @@ -54,13 +63,19 @@ 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 - WHERE - username = %(sender_username)s - {before_clause} - ORDER BY send_timestamp DESC + b.id, + u.username, + b.content, + b.send_timestamp, + b.original_bloom_id, + ou.username AS original_sender, + ob.content AS original_content + FROM blooms b + JOIN users u ON u.id = b.sender_id + LEFT JOIN blooms ob ON b.original_bloom_id = ob.id + LEFT JOIN users ou ON ob.sender_id = ou.id + WHERE u.username = %(sender_username)s + ORDER BY b.send_timestamp DESC {limit_clause} """, kwargs, @@ -68,14 +83,24 @@ def get_blooms_for_user( rows = cur.fetchall() blooms = [] for row in rows: - bloom_id, sender_username, content, timestamp = row - blooms.append( - Bloom( - id=bloom_id, - sender=sender_username, - content=content, - sent_timestamp=timestamp, - ) + ( + bloom_id, + sender_username, + content, + timestamp, + original_bloom_id, + original_sender, + original_content, + ) = row + blooms.append( + Bloom( + id=bloom_id, + sender=sender_username, + content=content, + sent_timestamp=timestamp, + original_bloom_id=original_bloom_id, + original_sender=original_sender, + ) ) return blooms diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..93b4089 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -152,19 +152,28 @@ def do_follow(): @jwt_required() def send_bloom(): - type_check_error = verify_request_fields({"content": str}) - if type_check_error is not None: - return type_check_error - user = get_current_user() + data = request.json - blooms.add_bloom(sender=user, content=request.json["content"]) + if "content" in data: + content = data["content"] - return jsonify( - { - "success": True, - } - ) + elif "original_bloom_id" in data: + original = blooms.get_bloom(data["original_bloom_id"]) + if not original: + return jsonify({"error": "Bloom not found"}), 404 + + content = f"🔁 {user.username} rebloomed: {original.content}" + + else: + return jsonify({"error": "Missing content"}), 400 + + if len(content) > 280: + return jsonify({"error": "Too long"}), 400 + + blooms.add_bloom(sender=user, content=content) + + return jsonify({"success": True}) def get_bloom(id_str): @@ -178,6 +187,23 @@ def get_bloom(id_str): return jsonify(bloom) +@jwt_required() +def rebloom(): + user = get_current_user() + + bloom_id = request.json.get("bloom_id") + original = blooms.get_bloom(bloom_id) + + if not original: + return jsonify({"error": "Not found"}), 404 + + return blooms.add_bloom( + sender=user, + content=original.content + ) + + + @jwt_required() def home_timeline(): current_user = get_current_user() diff --git a/db/schema.sql b/db/schema.sql index 61e7580..1351eb2 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -10,7 +10,8 @@ CREATE TABLE blooms ( id BIGSERIAL NOT NULL PRIMARY KEY, sender_id INT NOT NULL REFERENCES users(id), content TEXT NOT NULL, - send_timestamp TIMESTAMP NOT NULL + send_timestamp TIMESTAMP NOT NULL, + original_bloom_id BIGINT REFERENCES blooms(id) ); CREATE TABLE follows ( diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c..d80de66 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -10,6 +10,9 @@ * "sent_timestamp": "datetime as ISO 8601 formatted string"} */ + +import { apiService } from "../index.mjs"; + const createBloom = (template, bloom) => { if (!bloom) return; const bloomFrag = document.getElementById(template).content.cloneNode(true); @@ -20,6 +23,13 @@ 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-action='rebloom']"); + + if (rebloomButton) { + rebloomButton.addEventListener("click", () => { + apiService.rebloom(bloom.id); + }); + } bloomArticle.setAttribute("data-bloom-id", bloom.id); bloomUsername.setAttribute("href", `/profile/${bloom.sender}`); @@ -31,6 +41,18 @@ const createBloom = (template, bloom) => { .body.childNodes ); + if (bloom.original_bloom_id) { + const header = bloomFrag.querySelector(".bloom__header"); + + const rebloomInfo = document.createElement("div"); + rebloomInfo.textContent = `${bloom.sender} re-bloomed`; + + header.prepend(rebloomInfo); + + // Show original author instead + bloomUsername.textContent = bloom.original_sender; + } + return bloomFrag; }; diff --git a/front-end/index.html b/front-end/index.html index 89d6b13..d476d60 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 f4b5339..fa39e1c 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -173,6 +173,26 @@ async function getBlooms(username) { } } + +async function rebloom(bloomId) { + try { + const data = await _apiRequest("/bloom", { + method: "POST", + body: JSON.stringify({ + original_bloom_id: bloomId, + }), + }); + + if (data.success) { + await getBlooms(); + } + + return data; + } catch (error) { + return { success: false }; + } +} + /** * Fetches blooms containing a specific hashtag */ @@ -292,6 +312,7 @@ const apiService = { getBlooms, postBloom, getBloomsByHashtag, + rebloom, // User methods getProfile,