Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
91 changes: 85 additions & 6 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class Bloom:
sender: User
content: str
sent_timestamp: datetime.datetime
rebloom_from: Optional[str] = None
rebloomed_by: Optional[any] = None
rebloom_count: int = 0


def add_bloom(*, sender: User, content: str) -> Bloom:
Expand All @@ -36,6 +39,62 @@ def add_bloom(*, sender: User, content: str) -> Bloom:
dict(hashtag=hashtag, bloom_id=bloom_id),
)

def add_rebloom(*, rebloomer: User, original_bloom_id: int) -> Optional[Bloom]:
# 1. Fetch original bloom from DB

with db_cursor() as cur:
cur.execute(
"""
SELECT blooms.id, users.id, users.username, content, send_timestamp
FROM blooms
INNER JOIN users ON users.id = blooms.sender_id
WHERE blooms.id =%s
""",
(original_bloom_id,)
)
row = cur.fetchone()

if row is None:
return None

original_id, original_sender_id, original_sender_username, original_content, original_timestamp = row

# 2. New bloom id & timestamp
now = datetime.datetime.now(tz=datetime.UTC)
new_bloom_id = int(now.timestamp() * 1000000)

# 3. Insert new bloom as a "repost" of the original
with db_cursor() as cur:
cur.execute(
"""
INSERT INTO blooms (id, sender_id, content, send_timestamp, rebloom_from, rebloom_by)
VALUES (%(id)s, %(sender_id)s, %(content)s, %(timestamp)s, %(rebloom_from)s, %(rebloom_by)s)
""",
dict(
id=new_bloom_id,
sender_id=rebloomer.id,
content=original_content,
timestamp=now,
rebloom_from=original_bloom_id,
rebloom_by=rebloomer.id,
),
)

# 4. Increment rebloom count on the original
cur.execute(
"UPDATE blooms SET rebloom_count = rebloom_count + 1 WHERE id = %(original)s",
dict(original=original_bloom_id),
)

# 5. Return the new repost bloom
return Bloom(
id=new_bloom_id,
sender=rebloomer,
content=original_content,
sent_timestamp=now,
rebloomed_by=rebloomer,
rebloom_count=0,
)

def get_blooms_for_user(
username: str, *, before: Optional[int] = None, limit: Optional[int] = None
Expand All @@ -54,11 +113,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.rebloom_from, rebloomer.username, original_sender.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 rebloomer ON rebloomer.id = blooms.rebloom_by
LEFT JOIN blooms AS original_bloom ON original_bloom.id = blooms.rebloom_from
LEFT JOIN users AS original_sender ON original_sender.id = original_bloom.sender_id
WHERE
username = %(sender_username)s
users.username = %(sender_username)s
{before_clause}
ORDER BY send_timestamp DESC
{limit_clause}
Expand All @@ -68,13 +132,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, rebloom_from_id, rebloomed_by_username, original_sender_username, rebloom_count = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloom_from=original_sender_username,
rebloomed_by=rebloomed_by_username,
rebloom_count= rebloom_count or 0
)
)
return blooms
Expand All @@ -83,18 +150,30 @@ 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, blooms.content, blooms.send_timestamp, original_sender.username, blooms.rebloom_count
FROM
blooms
INNER JOIN users ON users.id = blooms.sender_id
LEFT JOIN users AS rebloomer ON rebloomer.id = blooms.rebloom_by
LEFT JOIN blooms AS original_bloom on original_bloom.id = blooms.rebloom_from
LEFT JOIN users AS original_sender on original_sender.id = original_bloom.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, rebloom_from_id, rebloomed_by_username, original_sender_username, rebloom_count = row
return Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloom_from=original_sender_username,
rebloomed_by=rebloomed_by_username,
rebloom_count= rebloom_count or 0,
)


Expand Down
20 changes: 20 additions & 0 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ def send_bloom():
)


@jwt_required()
def send_rebloom():
type_check_error = verify_request_fields({"bloom_id": int})
if type_check_error is not None:
return type_check_error

current_user = get_current_user()
bloom_id = request.json["bloom_id"]

new_bloom = blooms.add_rebloom(
rebloomer=current_user,
original_bloom_id=bloom_id
)
if new_bloom is None:
return make_response(({"success": False, "message": "Original bloom not found"}, 400))

return jsonify({
"success": True,
})

def get_bloom(id_str):
try:
id_int = int(id_str)
Expand Down
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
send_bloom,
suggested_follows,
user_blooms,
send_rebloom,
)

from dotenv import load_dotenv
Expand Down Expand Up @@ -59,6 +60,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("/blooms/<profile_username>", view_func=user_blooms)
app.add_url_rule("/rebloom", methods=["POST"], view_func= send_rebloom)
app.add_url_rule("/hashtag/<hashtag>", view_func=hashtag)

app.run(host="0.0.0.0", port="3000", debug=True)
Expand Down
5 changes: 5 additions & 0 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ CREATE TABLE blooms (
send_timestamp TIMESTAMP NOT NULL
);

ALTER TABLE blooms
ADD COLUMN rebloom_from BIGINT REFERENCES blooms(id),
ADD COLUMN rebloom_by INT REFERENCES users(id),
ADD COLUMN rebloom_count INT DEFAULT 0;

CREATE TABLE follows (
id SERIAL PRIMARY KEY,
follower INT NOT NULL REFERENCES users(id),
Expand Down
55 changes: 55 additions & 0 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {apiService} from "../index.mjs"
/**
* Create a bloom component
* @param {string} template - The ID of the template to clone
Expand All @@ -21,6 +22,10 @@ const createBloom = (template, bloom) => {
const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])");
const bloomContent = bloomFrag.querySelector("[data-content]");

const rebloomEl = bloomFrag.querySelector("[data-rebloom]")
const rebloomCount = bloomFrag.querySelector("[data-rebloom-count]");
const rebloomBtn = bloomFrag.querySelector("[data-rebloom-button]");

bloomArticle.setAttribute("data-bloom-id", bloom.id);
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
bloomUsername.textContent = bloom.sender;
Expand All @@ -31,6 +36,34 @@ const createBloom = (template, bloom) => {
.body.childNodes
);

rebloomBtn.addEventListener("click", async (e) => handleRebloomClick(e, bloom));
// Show "originally bloomed by"
if (bloom.rebloomed_by) {
rebloomEl.replaceChildren();

const originalBloomerLink = document.createElement("a");
originalBloomerLink.href = `/profile/${bloom.rebloom_from}`;
originalBloomerLink.textContent = bloom.rebloom_from;

originalBloomerLink.style.fontWeight = "bold";

rebloomEl.append( "Originally bloomed by ", originalBloomerLink );
} else {
rebloomEl.hidden = true;
}

// Count how many times it has been rebloomed
const count = Number(bloom.rebloom_count || 0);

if (rebloomCount) {
if (count > 0) {
rebloomCount.hidden = false;
rebloomCount.textContent = `Rebloomed ${String(count)} times`;
} else {
rebloomCount.hidden = true;
}
}

return bloomFrag;
};

Expand Down Expand Up @@ -84,4 +117,26 @@ function _formatTimestamp(timestamp) {
}
}

async function handleRebloomClick(event, bloom) {
event.preventDefault();
const button = event.currentTarget;
const originalText = button.textContent;

try {
// Make button inert while calling backend
button.inert = true;
button.textContent = "Reblooming...";

await apiService.postRebloom(bloom);

} catch (error) {
console.error("Rebloom failed:", error);
} finally {
// Restore UI state
button.textContent = originalText;
button.inert = false;
}
}


export {createBloom};
14 changes: 14 additions & 0 deletions front-end/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,17 @@ dialog {
[hidden] {
display: none !important;
}

/* rebloom info line */
[data-rebloom] {
font-size: 0.8rem;
color: var(--accent);
}

/* rebloom count */
.rebloom-count {
font-size: 0.8rem;
color: var(--accent);
margin-left: auto;
margin-right: 2em;
}
3 changes: 3 additions & 0 deletions front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,12 @@ <h2 id="bloom-form-title" class="bloom-form__title">Share a Bloom</h2>
<article class="bloom box" data-bloom data-bloom-id="">
<div class="bloom__header flex">
<a href="#" class="bloom__username" data-username>Username</a>
<span data-rebloom-count class="rebloom-count">count</span>
<a href="#" class="bloom__time"><time class="bloom__time" data-time>2m</time></a>
</div>
<div class="bloom__content" data-content></div>
<div data-rebloom></div>
<button type="button" data-rebloom-button>Rebloom</button>
</article>
</template>

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

async function postRebloom(bloom){
try{
const data = await _apiRequest("/rebloom", {
method: "POST",
body: JSON.stringify({ bloom_id: bloom.id })
});
if(data.success){
await getBlooms();
await getProfile(state.currentUser);
}
return data;
} catch(error){
console.error("Rebloom failed:", error);
return {success: false};
}
}

// ======= USER methods
async function getProfile(username) {
const endpoint = username ? `/profile/${username}` : "/profile";
Expand Down Expand Up @@ -292,6 +309,7 @@ const apiService = {
getBlooms,
postBloom,
getBloomsByHashtag,
postRebloom,

// User methods
getProfile,
Expand Down