From c0bb5313c2b95a97722110e6ee8af94edd0e2bb8 Mon Sep 17 00:00:00 2001 From: ZabihollahNamazi Date: Mon, 25 May 2026 13:32:58 +0100 Subject: [PATCH] rebloom --- backend/.gitignore | 2 +- backend/data/blooms.py | 45 +++++++++++++++++++++++++---- backend/endpoints.py | 23 +++++++++++++++ backend/main.py | 3 ++ db/schema.sql | 4 ++- front-end/components/bloom.mjs | 52 ++++++++++++++++++++++++++++++++++ front-end/index.html | 8 ++++++ 7 files changed, 130 insertions(+), 7 deletions(-) diff --git a/backend/.gitignore b/backend/.gitignore index 30a3427..ac6bb9d 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,3 +1,3 @@ /.env -/.venv/ +/venv/ *.pyc diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 7e280cf..a3a0062 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -13,6 +13,8 @@ class Bloom: sender: User content: str sent_timestamp: datetime.datetime + rebloomed_by: Optional[str] = None # Tracks if someone shared it + rebloom_count: int = 0 # Tracks share count def add_bloom(*, sender: User, content: str) -> Bloom: @@ -36,6 +38,29 @@ def add_bloom(*, sender: User, content: str) -> Bloom: dict(hashtag=hashtag, bloom_id=bloom_id), ) +def add_rebloom(*, rebloomer: User, original_bloom_id: int) -> None: + original = get_bloom(original_bloom_id) + if not original: + return + + now = datetime.datetime.now(tz=datetime.UTC) + new_bloom_id = int(now.timestamp() * 1000000) + + with db_cursor() as cur: + # 1. Insert the shared copy as a new timeline entry attributed to the rebloomer + cur.execute( + """INSERT INTO blooms + (id, sender_id, content, send_timestamp, rebloomed_by_id) + VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s, %(rebloomed_by_id)s)""", + dict( + bloom_id=new_bloom_id, + sender_id=original.sender_id, # Keep original author tracking if needed, or mapping structure + content=original.content, + timestamp=now, + rebloomed_by_id=rebloomer.id + ), + ) + # 2. Increment a counter system or handle via a tracking table aggregation def get_blooms_for_user( username: str, *, before: Optional[int] = None, limit: Optional[int] = None @@ -54,13 +79,21 @@ def get_blooms_for_user( cur.execute( f"""SELECT - blooms.id, users.username, content, send_timestamp + b.id, + u.username AS sender_username, + b.content, + b.send_timestamp, + rb_u.username AS rebloomed_by_username, + (SELECT COUNT(*) FROM blooms WHERE rebloomed_from_id = b.id) AS rebloom_count FROM - blooms INNER JOIN users ON users.id = blooms.sender_id + blooms b + INNER JOIN users u ON u.id = b.sender_id + LEFT JOIN users rb_u ON rb_u.id = b.rebloomed_by_id WHERE - username = %(sender_username)s + (u.username = %(sender_username)s AND b.rebloomed_by_id IS NULL) + OR rb_u.username = %(sender_username)s {before_clause} - ORDER BY send_timestamp DESC + ORDER BY b.send_timestamp DESC {limit_clause} """, kwargs, @@ -68,13 +101,15 @@ 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, rebloomed_by, rebloom_count = row blooms.append( Bloom( id=bloom_id, sender=sender_username, content=content, sent_timestamp=timestamp, + rebloomed_by=rebloomed_by, + rebloom_count=rebloom_count ) ) return blooms diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..fb79e7b 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -177,6 +177,29 @@ def get_bloom(id_str): return make_response((f"Bloom not found", 404)) return jsonify(bloom) +@jwt_required() +def do_rebloom(id_str): + """ + Endpoint to handle reblooming an existing bloom post + """ + # 1. Parse the incoming original bloom ID + try: + original_id_int = int(id_str) + except ValueError: + return make_response(("Invalid bloom id", 400)) + + # 2. Grab the logged-in user who clicked the rebloom button + current_user = get_current_user() + + # 3. Call your core dataclass layer function to perform the clone insert + try: + blooms.add_rebloom(rebloomer=current_user, original_bloom_id=original_id_int) + return jsonify({ + "success": True, + "message": "Rebloomed successfully!" + }) + except Exception as error: + return make_response(({"success": False, "message": str(error)}, 500)) @jwt_required() def home_timeline(): diff --git a/backend/main.py b/backend/main.py index 7ba155f..74f0885 100644 --- a/backend/main.py +++ b/backend/main.py @@ -14,6 +14,7 @@ send_bloom, suggested_follows, user_blooms, + do_rebloom ) from dotenv import load_dotenv @@ -61,6 +62,8 @@ def main(): app.add_url_rule("/blooms/", view_func=user_blooms) app.add_url_rule("/hashtag/", view_func=hashtag) + app.add_url_rule("/api/blooms//rebloom", view_func=do_rebloom, methods=["POST"]) + app.run(host="0.0.0.0", port="3000", debug=True) diff --git a/db/schema.sql b/db/schema.sql index 61e7580..a23b51b 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -10,7 +10,9 @@ 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, + rebloomed_from_id BIGINT REFERENCES blooms(id) ON DELETE CASCADE, + rebloomed_by_id INT REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE follows ( diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c..d478f4c 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -21,6 +21,27 @@ const createBloom = (template, bloom) => { const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])"); const bloomContent = bloomFrag.querySelector("[data-content]"); + // REBLOOM UI EXTENSIONS + // Look for a top notification banner anchor inside your HTML template template + const rebloomHeader = bloomFrag.querySelector("[data-rebloom-header]"); + if (bloom.rebloomed_by) { + if (rebloomHeader) { + rebloomHeader.textContent = `🔄 ${bloom.rebloomed_by} re-bloomed`; + rebloomHeader.setAttribute("href", `/profile/${bloom.rebloomed_by}`); + rebloomHeader.classList.remove("hidden"); // Ensure it's visible + } + // Visual indicator to distinctively dim or border wrap the shared block card + bloomArticle.classList.add("rebloom-card-style"); + } else if (rebloomHeader) { + rebloomHeader.classList.add("hidden"); + } + + // Look for your counter UI node element + const rebloomCounter = bloomFrag.querySelector("[data-rebloom-count]"); + if (rebloomCounter) { + rebloomCounter.textContent = bloom.rebloom_count > 0 ? `🔄 ${bloom.rebloom_count}` : "🔄"; + } + bloomArticle.setAttribute("data-bloom-id", bloom.id); bloomUsername.setAttribute("href", `/profile/${bloom.sender}`); bloomUsername.textContent = bloom.sender; @@ -31,6 +52,37 @@ const createBloom = (template, bloom) => { .body.childNodes ); + const rebloomButton = bloomFrag.querySelector("[data-rebloom-button]"); + if (rebloomButton) { + rebloomButton.addEventListener("click", async (event) => { + // Prevent standard browser button click bubbles or form submissions + event.preventDefault(); + + try { + // Send a POST network ping request straight to your Flask Endpoint.py + const response = await fetch(`/api/blooms/${bloom.id}/rebloom`, { + method: "POST", + headers: { + "Authorization": `Bearer ${localStorage.getItem("token")}`, + "Content-Type": "application/json" + } + }); + + const result = await response.json(); + + if (response.ok && result.success) { + // Instantly refresh the timeline viewport so the newly shared item renders at the top + window.location.reload(); + } else { + alert(`Could not rebloom: ${result.message || "Unknown error"}`); + } + } catch (error) { + console.error("Network error executing rebloom:", error); + alert("A network connection problem occurred. Please try again."); + } + }); + } + return bloomFrag; }; diff --git a/front-end/index.html b/front-end/index.html index 89d6b13..38d478f 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -234,11 +234,19 @@

Share a Bloom