diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 7e280cf..fd8cc6e 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -1,6 +1,6 @@ import datetime -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Dict, List, Optional from data.connection import db_cursor @@ -13,6 +13,9 @@ class Bloom: sender: User content: str sent_timestamp: datetime.datetime + original_bloom_id: Optional[int] = None + original_sender: Optional[str] = None + rebloom_count: int = 0 def add_bloom(*, sender: User, content: str) -> Bloom: @@ -37,6 +40,37 @@ def add_bloom(*, sender: User, content: str) -> Bloom: ) +def add_rebloom(*, sender: User, original_bloom_id: int) -> Bloom: + """Create a rebloom of an existing bloom.""" + original = get_bloom(original_bloom_id) + if original is None: + raise ValueError(f"Bloom {original_bloom_id} not found") + + now = datetime.datetime.now(tz=datetime.UTC) + bloom_id = int(now.timestamp() * 1000000) + + with db_cursor() as cur: + # Insert the rebloom + cur.execute( + """INSERT INTO blooms + (id, sender_id, content, send_timestamp, original_bloom_id, original_sender_id) + VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s, %(original_bloom_id)s, %(original_sender_id)s)""", + dict( + bloom_id=bloom_id, + sender_id=sender.id, + content=original.content, + timestamp=now, + original_bloom_id=original_bloom_id, + original_sender_id=original.sender_id, + ), + ) + # Increment rebloom count on original + cur.execute( + "UPDATE blooms SET rebloom_count = rebloom_count + 1 WHERE id = %(id)s", + dict(id=original_bloom_id), + ) + + def get_blooms_for_user( username: str, *, before: Optional[int] = None, limit: Optional[int] = None ) -> List[Bloom]: @@ -45,7 +79,7 @@ def get_blooms_for_user( "sender_username": username, } if before is not None: - before_clause = "AND send_timestamp < %(before_limit)s" + before_clause = "AND blooms.send_timestamp < %(before_limit)s" kwargs["before_limit"] = before else: before_clause = "" @@ -54,13 +88,16 @@ def get_blooms_for_user( cur.execute( f"""SELECT - blooms.id, users.username, content, send_timestamp + blooms.id, users.username, blooms.content, blooms.send_timestamp, + blooms.original_bloom_id, original_senders.username, blooms.rebloom_count FROM - blooms INNER JOIN users ON users.id = blooms.sender_id + blooms + INNER JOIN users ON users.id = blooms.sender_id + LEFT JOIN users AS original_senders ON original_senders.id = blooms.original_sender_id WHERE - username = %(sender_username)s + users.username = %(sender_username)s {before_clause} - ORDER BY send_timestamp DESC + ORDER BY blooms.send_timestamp DESC {limit_clause} """, kwargs, @@ -68,13 +105,16 @@ 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, original_bloom_id, original_sender, rebloom_count = 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, + rebloom_count=rebloom_count or 0, ) ) return blooms @@ -83,19 +123,29 @@ def get_blooms_for_user( def get_bloom(bloom_id: int) -> Optional[Bloom]: with db_cursor() as cur: cur.execute( - "SELECT blooms.id, users.username, content, send_timestamp FROM blooms INNER JOIN users ON users.id = blooms.sender_id WHERE blooms.id = %s", + """SELECT blooms.id, users.username, users.id, blooms.content, blooms.send_timestamp, + blooms.original_bloom_id, original_senders.username, blooms.rebloom_count + FROM blooms + INNER JOIN users ON users.id = blooms.sender_id + LEFT JOIN users AS original_senders ON original_senders.id = blooms.original_sender_id + WHERE blooms.id = %s""", (bloom_id,), ) row = cur.fetchone() if row is None: return None - bloom_id, sender_username, content, timestamp = row - return Bloom( + bloom_id, sender_username, sender_id, content, timestamp, original_bloom_id, original_sender, rebloom_count = row + bloom = Bloom( id=bloom_id, sender=sender_username, content=content, sent_timestamp=timestamp, + original_bloom_id=original_bloom_id, + original_sender=original_sender, + rebloom_count=rebloom_count or 0, ) + bloom.sender_id = sender_id + return bloom def get_blooms_with_hashtag( @@ -108,12 +158,16 @@ def get_blooms_with_hashtag( with db_cursor() as cur: cur.execute( f"""SELECT - blooms.id, users.username, content, send_timestamp + blooms.id, users.username, blooms.content, blooms.send_timestamp, + blooms.original_bloom_id, original_senders.username, blooms.rebloom_count FROM - blooms INNER JOIN hashtags ON blooms.id = hashtags.bloom_id INNER JOIN users ON blooms.sender_id = users.id + blooms + INNER JOIN hashtags ON blooms.id = hashtags.bloom_id + INNER JOIN users ON blooms.sender_id = users.id + LEFT JOIN users AS original_senders ON original_senders.id = blooms.original_sender_id WHERE hashtag = %(hashtag_without_leading_hash)s - ORDER BY send_timestamp DESC + ORDER BY blooms.send_timestamp DESC {limit_clause} """, kwargs, @@ -121,13 +175,16 @@ def get_blooms_with_hashtag( rows = cur.fetchall() blooms = [] for row in rows: - bloom_id, sender_username, content, timestamp = row + bloom_id, sender_username, content, timestamp, original_bloom_id, original_sender, rebloom_count = 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, + rebloom_count=rebloom_count or 0, ) ) return blooms @@ -139,4 +196,4 @@ def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str: kwargs["limit"] = limit else: limit_clause = "" - return limit_clause + return limit_clause \ No newline at end of file diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..a2ea939 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -165,7 +165,21 @@ def send_bloom(): "success": True, } ) +@jwt_required() +def rebloom(bloom_id_str): + try: + bloom_id_int = int(bloom_id_str) + except ValueError: + return make_response({"success": False, "message": "Invalid bloom id"}, 400) + + user = get_current_user() + + try: + blooms.add_rebloom(sender=user, original_bloom_id=bloom_id_int) + except ValueError as e: + return make_response({"success": False, "message": str(e)}, 404) + return jsonify({"success": True}) def get_bloom(id_str): try: diff --git a/backend/main.py b/backend/main.py index 7ba155f..3a46480 100644 --- a/backend/main.py +++ b/backend/main.py @@ -14,6 +14,7 @@ send_bloom, suggested_follows, user_blooms, + rebloom ) from dotenv import load_dotenv @@ -58,9 +59,10 @@ 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("/rebloom/", methods=["POST"], view_func=rebloom) app.add_url_rule("/blooms/", view_func=user_blooms) app.add_url_rule("/hashtag/", view_func=hashtag) - + app.run(host="0.0.0.0", port="3000", debug=True) diff --git a/db/schema.sql b/db/schema.sql index 61e7580..0b3a80d 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -11,6 +11,9 @@ CREATE TABLE blooms ( sender_id INT NOT NULL REFERENCES users(id), content TEXT NOT NULL, send_timestamp TIMESTAMP NOT NULL + original_bloom_id BIGINT REFERENCES blooms(id), + original_sender_id INT REFERENCES users(id), + rebloom_count INT DEFAULT 0 ); CREATE TABLE follows ( diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c..645376f 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -31,57 +31,97 @@ const createBloom = (template, bloom) => { .body.childNodes ); - return bloomFrag; -}; + // return bloomFrag; +// }; -function _formatHashtags(text) { - if (!text) return text; - return text.replace( - /\B#[^#]+/g, - (match) => `${match}` - ); -} +// function _formatHashtags(text) { +// if (!text) return text; +// return text.replace( +// /\B#[^#]+/g, +// (match) => `${match}` +// ); +// } -function _formatTimestamp(timestamp) { - if (!timestamp) return ""; +// function _formatTimestamp(timestamp) { +// if (!timestamp) return ""; - try { - const date = new Date(timestamp); - const now = new Date(); - const diffSeconds = Math.floor((now - date) / 1000); +// try { +// const date = new Date(timestamp); +// const now = new Date(); +// const diffSeconds = Math.floor((now - date) / 1000); - // Less than a minute - if (diffSeconds < 60) { - return `${diffSeconds}s`; - } +// // Less than a minute +// if (diffSeconds < 60) { +// return `${diffSeconds}s`; +// } - // Less than an hour - const diffMinutes = Math.floor(diffSeconds / 60); - if (diffMinutes < 60) { - return `${diffMinutes}m`; - } +// // Less than an hour +// const diffMinutes = Math.floor(diffSeconds / 60); +// if (diffMinutes < 60) { +// return `${diffMinutes}m`; +// } - // Less than a day - const diffHours = Math.floor(diffMinutes / 60); - if (diffHours < 24) { - return `${diffHours}h`; - } +// // Less than a day +// const diffHours = Math.floor(diffMinutes / 60); +// if (diffHours < 24) { +// return `${diffHours}h`; +// } - // Less than a week - const diffDays = Math.floor(diffHours / 24); - if (diffDays < 7) { - return `${diffDays}d`; - } +// // Less than a week +// const diffDays = Math.floor(diffHours / 24); +// if (diffDays < 7) { +// return `${diffDays}d`; +// } + +// // Format as month and day for older dates +// return new Intl.DateTimeFormat("en-US", { +// month: "short", +// day: "numeric", +// }).format(date); +// } catch (error) { +// console.error("Failed to format timestamp:", error); +// return ""; +// } +// } - // Format as month and day for older dates - return new Intl.DateTimeFormat("en-US", { - month: "short", - day: "numeric", - }).format(date); - } catch (error) { - console.error("Failed to format timestamp:", error); - return ""; +// export {createBloom}; +if (bloom.original_sender) { + const rebloomed = document.createElement("p"); + rebloomed.textContent = `🔁 Rebloomed from @${bloom.original_sender}`; + rebloomed.style.color = "green"; + rebloomed.style.fontSize = "0.85em"; + bloomArticle.prepend(rebloomed); } -} -export {createBloom}; + // Show rebloom count if > 0 + if (bloom.rebloom_count > 0) { + const count = document.createElement("p"); + count.textContent = `🔁 ${bloom.rebloom_count} rebloom${bloom.rebloom_count > 1 ? "s" : ""}`; + count.style.fontSize = "0.85em"; + bloomArticle.appendChild(count); + } + + // Add rebloom button + const rebloombtn = document.createElement("button"); + rebloombtn.textContent = "🔁 Rebloom"; + rebloombtn.addEventListener("click", async () => { + const token = localStorage.getItem("token"); + if (!token) { + alert("You must be logged in to rebloom"); + return; + } + const res = await fetch(`/api/rebloom/${bloom.id}`, { + method: "POST", + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await res.json(); + if (data.success) { + alert("Rebloomed!"); + } else { + alert("Failed to rebloom"); + } + }); + bloomArticle.appendChild(rebloombtn); + + return bloomFrag; +}; \ No newline at end of file