Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d9ed15b
Add original_bloom_id to the blooms table to help us with the rebloom…
HassanOHOsman Aug 21, 2026
48db6f4
update the bloom class
HassanOHOsman Aug 21, 2026
397bca1
Create rebloom function
HassanOHOsman Aug 21, 2026
320d5c6
update rebloom function
HassanOHOsman Aug 21, 2026
df2ce91
update get_bloom function
HassanOHOsman Aug 21, 2026
532e6b3
add the missing comma inside the blooms table to avoid the syntactica…
HassanOHOsman Aug 21, 2026
62f68ec
update get_blooms_for_user function
HassanOHOsman Aug 21, 2026
64efa89
update get_bloom func
HassanOHOsman Aug 21, 2026
05ad727
add an endpoint for reblooming
HassanOHOsman Aug 21, 2026
db8601c
import rebloom handler function endpoint and add a router for it in t…
HassanOHOsman Aug 21, 2026
02f3358
setup the api.mjs for the rebloom endpoint
HassanOHOsman Aug 21, 2026
e0d739b
Update the html structure (template tag with "bloom-form-template" id…
HassanOHOsman Aug 21, 2026
fadd3e1
update create bloom
HassanOHOsman Aug 21, 2026
7280198
update bloom.mjs
HassanOHOsman Aug 21, 2026
d0f4bb3
update html to include span tag with the current rebloom count
HassanOHOsman Aug 21, 2026
2207ba9
Update the rebloom btn event listner to incoude count for reblooms
HassanOHOsman Aug 21, 2026
79bebde
add rebloom_count field to my Bloom class
HassanOHOsman Aug 21, 2026
6282c0d
fix rebloom count persistence
HassanOHOsman Aug 21, 2026
8e9fdf8
1. update add_bloom function
HassanOHOsman Aug 21, 2026
6d6ea81
re-write rebloom function to use only bloom table and remove the old …
HassanOHOsman Aug 24, 2026
44b8c03
update the rebloom_count subqueries (in both get_bloom and get_blooms…
HassanOHOsman Aug 24, 2026
5d5687f
Update create bloom to include logic about whether the post is a rebl…
HassanOHOsman Aug 24, 2026
fd6089b
Update createBloom to include what logic to display incase the poast …
HassanOHOsman Aug 24, 2026
f7053c5
UI now displays the clearly the original bloom from a rebloomed bloom.
HassanOHOsman Aug 24, 2026
2b47907
reposition the div tag containing the rebloomed bloom details for bet…
HassanOHOsman Aug 25, 2026
f5dd88c
1)fix broken other_profile endpoint
HassanOHOsman Aug 25, 2026
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
140 changes: 124 additions & 16 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class Bloom:
sender: User
content: str
sent_timestamp: datetime.datetime
original_bloom: Optional["Bloom"] = None
rebloom_count: int = 0


def add_bloom(*, sender: User, content: str) -> Bloom:
Expand All @@ -37,67 +39,173 @@ def add_bloom(*, sender: User, content: str) -> Bloom:
)


def rebloom(*, sender: User, original_bloom_id: int) -> Optional[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,
original_bloom_id
)
SELECT
%(bloom_id)s,
%(sender_id)s,
content,
%(timestamp)s,
COALESCE(original_bloom_id, id)
FROM blooms
WHERE id = %(original_bloom_id)s
RETURNING id
""",
{
"bloom_id": bloom_id,
"sender_id": sender.id,
"timestamp": now,
"original_bloom_id": original_bloom_id,
},
)
if cur.fetchone() is None:
return None

return get_bloom(bloom_id)


def get_blooms_for_user(
username: str, *, before: Optional[int] = None, limit: Optional[int] = None
) -> List[Bloom]:
with db_cursor() as cur:
kwargs = {
"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 = ""

limit_clause = make_limit_clause(limit, kwargs)

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
f"""
SELECT
blooms.id,
users.username,
blooms.content,
blooms.send_timestamp,
blooms.original_bloom_id,
(
SELECT COUNT(*)
FROM blooms AS reblooms_of_this
WHERE reblooms_of_this.original_bloom_id = COALESCE(
blooms.original_bloom_id,
blooms.id
)
) AS rebloom_count
FROM blooms
INNER JOIN users
ON users.id = blooms.sender_id
WHERE users.username = %(sender_username)s
{before_clause}
ORDER BY blooms.send_timestamp DESC
{limit_clause}
""",
kwargs,
)

rows = cur.fetchall()
blooms = []
blooms_list = []

for row in rows:
bloom_id, sender_username, content, timestamp = row
blooms.append(
(
bloom_id,
sender_username,
content,
timestamp,
original_bloom_id,
rebloom_count,
) = row

original_bloom = (
get_bloom(original_bloom_id)
if original_bloom_id is not None
else None
)

blooms_list.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
original_bloom=original_bloom,
rebloom_count=rebloom_count,
)
)
return blooms

return blooms_list


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,
blooms.content,
blooms.send_timestamp,
blooms.original_bloom_id,
(
SELECT COUNT(*)
FROM blooms AS reblooms_of_this
WHERE reblooms_of_this.original_bloom_id = COALESCE(
blooms.original_bloom_id,
blooms.id
)
) AS rebloom_count
FROM blooms
INNER JOIN users ON users.id = blooms.sender_id
WHERE blooms.id = %s
""",
(bloom_id,),
)

row = cur.fetchone()

if row is None:
return None
bloom_id, sender_username, content, timestamp = row

(
bloom_id,
sender_username,
content,
timestamp,
original_bloom_id,
rebloom_count,
) = row

original_bloom = (
get_bloom(original_bloom_id)
if original_bloom_id is not None
else None
)

return Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
original_bloom=original_bloom,
rebloom_count=rebloom_count,
)


def get_blooms_with_hashtag(
hashtag_without_leading_hash: str, *, limit: int = None
) -> List[Bloom]:
Expand Down
23 changes: 22 additions & 1 deletion backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ def other_profile(profile_username):

followers = get_inverse_followed_usernames(profile_user)
all_blooms = blooms.get_blooms_for_user(profile_username)
all_blooms.reverse()
return jsonify(
{
"username": profile_username,
Expand Down Expand Up @@ -245,3 +244,25 @@ def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, No
)
)
return None


@jwt_required()
def rebloom(id_str):
try:
original_bloom_id = int(id_str)
except ValueError:
return make_response(("Invalid bloom id", 400))

current_user = get_current_user()

original_bloom = blooms.get_bloom(original_bloom_id)

if original_bloom is None:
return make_response(("Bloom not found", 404))

rebloomed = blooms.rebloom(
sender=current_user,
original_bloom_id=original_bloom_id,
)

return jsonify(rebloomed)
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
home_timeline,
login,
other_profile,
rebloom,
register,
self_profile,
send_bloom,
Expand Down Expand Up @@ -58,6 +59,7 @@ def main():

app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom)
app.add_url_rule("/bloom/<id_str>", methods=["GET"], view_func=get_bloom)
app.add_url_rule("/rebloom/<id_str>", methods=["POST"], view_func=rebloom)
app.add_url_rule("/blooms/<profile_username>", view_func=user_blooms)
app.add_url_rule("/hashtag/<hashtag>", view_func=hashtag)

Expand Down
3 changes: 2 additions & 1 deletion db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
48 changes: 43 additions & 5 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
* "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);
const bloomParser = new DOMParser();

Expand All @@ -20,17 +23,52 @@ 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 rebloomInfo = bloomFrag.querySelector("[data-rebloom-info]");
const rebloomButton = bloomFrag.querySelector("[data-action='rebloom']");
const rebloomCount = bloomFrag.querySelector("[data-rebloom-count]");

const isRebloom = Boolean(bloom.original_bloom);
const displayAuthor = isRebloom ? bloom.original_bloom.sender : bloom.sender;
const displayTimestamp = isRebloom ? bloom.original_bloom.sent_timestamp : bloom.sent_timestamp;
const displayContent = isRebloom ? bloom.original_bloom.content : bloom.content;
const displayBloomId = isRebloom ? bloom.original_bloom.id : bloom.id;

rebloomCount.textContent = bloom.rebloom_count ?? 0;

bloomArticle.setAttribute("data-bloom-id", bloom.id);
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
bloomUsername.textContent = bloom.sender;
bloomTime.textContent = _formatTimestamp(bloom.sent_timestamp);
bloomTimeLink.setAttribute("href", `/bloom/${bloom.id}`);
bloomArticle.classList.toggle("bloom--rebloom", isRebloom);
bloomUsername.setAttribute("href", `/profile/${displayAuthor}`);
bloomUsername.textContent = displayAuthor;
bloomTime.textContent = _formatTimestamp(displayTimestamp);
bloomTimeLink.setAttribute("href", `/bloom/${displayBloomId}`);

bloomContent.replaceChildren(
...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html")
...bloomParser.parseFromString(_formatHashtags(displayContent), "text/html")
.body.childNodes
);

if (isRebloom) {
rebloomInfo.hidden = false;
rebloomInfo.textContent = `🔁 Rebloomed by ${bloom.sender}`;
}

// Handle rebloom
rebloomButton.addEventListener("click", async () => {
try {
rebloomButton.disabled = true;
rebloomButton.textContent = "Reblooming...";

const rebloomedBloom = await apiService.rebloom(bloom.id);

rebloomCount.textContent = rebloomedBloom.rebloom_count;
} catch (error) {
console.error("Failed to rebloom:", error);
} finally {
rebloomButton.disabled = false;
rebloomButton.innerHTML = `Rebloom <span data-rebloom-count>${rebloomCount.textContent}</span>`;
}
});

return bloomFrag;
};

Expand Down
12 changes: 11 additions & 1 deletion front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,21 @@ <h2 id="bloom-form-title" class="bloom-form__title">Share a Bloom</h2>
<!-- Bloom Template -->
<template id="bloom-template">
<article class="bloom box" data-bloom data-bloom-id="">
<div class="bloom__rebloom-info" data-rebloom-info hidden></div>
<div class="bloom__header flex">
<a href="#" class="bloom__username" data-username>Username</a>
<a href="#" class="bloom__time"><time class="bloom__time" data-time>2m</time></a>
<a href="#" class="bloom__time">
<time class="bloom__time" data-time>2m</time>
</a>
</div>

<div class="bloom__content" data-content></div>

<div class="bloom__actions">
<button type="button" data-action="rebloom">
Rebloom <span data-rebloom-count>0</span>
</button>
</div>
</article>
</template>

Expand Down
19 changes: 19 additions & 0 deletions front-end/lib/api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,24 @@ async function postBloom(content) {
}
}

async function rebloom(bloomId) {
try {
const data = await _apiRequest(`/rebloom/${bloomId}`, {
method: "POST",
});

if (data.success !== false) {
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";
Expand Down Expand Up @@ -291,6 +309,7 @@ const apiService = {
getBloom,
getBlooms,
postBloom,
rebloom,
getBloomsByHashtag,

// User methods
Expand Down