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
87 changes: 72 additions & 15 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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]:
Expand All @@ -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 = ""
Expand All @@ -54,27 +88,33 @@ 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,
)
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
Expand All @@ -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(
Expand All @@ -108,26 +158,33 @@ 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,
)
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
Expand All @@ -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
14 changes: 14 additions & 0 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion 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,
rebloom
)

from dotenv import load_dotenv
Expand Down Expand Up @@ -58,9 +59,10 @@ 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/<bloom_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)

app.run(host="0.0.0.0", port="3000", debug=True)


Expand Down
3 changes: 3 additions & 0 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
128 changes: 84 additions & 44 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => `<a href="/hashtag/${match.slice(1)}">${match}</a>`
);
}
// function _formatHashtags(text) {
// if (!text) return text;
// return text.replace(
// /\B#[^#]+/g,
// (match) => `<a href="/hashtag/${match.slice(1)}">${match}</a>`
// );
// }

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;
};