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
75 changes: 50 additions & 25 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -54,28 +63,44 @@ 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,
)
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

Expand Down
46 changes: 36 additions & 10 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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()
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
22 changes: 22 additions & 0 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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}`);
Expand All @@ -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;
};

Expand Down
1 change: 1 addition & 0 deletions front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ <h2 id="bloom-form-title" class="bloom-form__title">Share a Bloom</h2>
<a href="#" class="bloom__time"><time class="bloom__time" data-time>2m</time></a>
</div>
<div class="bloom__content" data-content></div>
<button type="button" data-action="rebloom">Re-bloom</button>
</article>
</template>

Expand Down
21 changes: 21 additions & 0 deletions front-end/lib/api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -292,6 +312,7 @@ const apiService = {
getBlooms,
postBloom,
getBloomsByHashtag,
rebloom,

// User methods
getProfile,
Expand Down