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
142 changes: 128 additions & 14 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,43 @@
@dataclass
class Bloom:
id: int
sender: User
sender: str
content: str
sent_timestamp: datetime.datetime
original_sender: Optional[str] = None
rebloomer: Optional[str] = None
rebloom_count: int = 0


def add_bloom(*, sender: User, content: str) -> Bloom:
hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")]

now = datetime.datetime.now(tz=datetime.UTC)
bloom_id = int(now.timestamp() * 1000000)
timestamp = datetime.datetime.now(datetime.UTC)
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)",
dict(
bloom_id=bloom_id,
sender_id=sender.id,
content=content,
timestamp=datetime.datetime.now(datetime.UTC),
timestamp=timestamp,
),
)
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),
)

return Bloom(
id=bloom_id,
sender=sender.username,
content=content,
sent_timestamp=timestamp,
rebloom_count=0,
)


def get_blooms_for_user(
Expand All @@ -54,7 +66,8 @@ def get_blooms_for_user(

cur.execute(
f"""SELECT
blooms.id, users.username, content, send_timestamp
blooms.id, users.username, content, send_timestamp,
(SELECT COUNT(*) FROM reblooms WHERE reblooms.original_bloom_id = blooms.id) as rebloom_count
FROM
blooms INNER JOIN users ON users.id = blooms.sender_id
WHERE
Expand All @@ -66,35 +79,39 @@ def get_blooms_for_user(
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, rebloom_count = row
blooms_list.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloom_count=rebloom_count or 0,
)
)
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, content, send_timestamp,
(SELECT COUNT(*) FROM reblooms WHERE reblooms.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, rebloom_count = row
return Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloom_count=rebloom_count or 0,
)


Expand All @@ -108,7 +125,8 @@ 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, content, send_timestamp,
(SELECT COUNT(*) FROM reblooms WHERE reblooms.original_bloom_id = blooms.id) as rebloom_count
FROM
blooms INNER JOIN hashtags ON blooms.id = hashtags.bloom_id INNER JOIN users ON blooms.sender_id = users.id
WHERE
Expand All @@ -119,18 +137,19 @@ def get_blooms_with_hashtag(
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, rebloom_count = row
blooms_list.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloom_count=rebloom_count or 0,
)
)
return blooms
return blooms_list


def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str:
Expand All @@ -140,3 +159,98 @@ def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str:
else:
limit_clause = ""
return limit_clause


def add_rebloom(*, rebloomer: User, original_bloom_id: int) -> Bloom:
with db_cursor() as cur:
cur.execute(
"SELECT blooms.id, blooms.sender_id, blooms.content, blooms.send_timestamp FROM blooms WHERE blooms.id = %s",
(original_bloom_id,),
)
row = cur.fetchone()
if row is None:
raise ValueError(f"Bloom {original_bloom_id} does not exist")

bloom_id, original_sender_id, content, _ = row

cur.execute(
"SELECT id FROM reblooms WHERE rebloomer_id = %s AND original_bloom_id = %s",
(rebloomer.id, original_bloom_id),
)
if cur.fetchone() is not None:
raise ValueError("You have already rebloomed this bloom")

# Add rebloom record
rebloom_timestamp = datetime.datetime.now(datetime.UTC)
cur.execute(
"INSERT INTO reblooms (rebloomer_id, original_bloom_id, rebloom_timestamp) VALUES (%s, %s, %s)",
(rebloomer.id, original_bloom_id, rebloom_timestamp),
)

cur.execute("SELECT username FROM users WHERE id = %s", (original_sender_id,))
original_sender_row = cur.fetchone()
original_sender_username = original_sender_row[0] if original_sender_row else None

# Return a Bloom object representing the rebloom
# For reblooms, we use the original bloom's data but mark it as rebloomed
return Bloom(
id=bloom_id, # Keep original bloom ID
sender=rebloomer.username, # Show rebloomer as sender in feed
content=content,
sent_timestamp=rebloom_timestamp, # Use rebloom timestamp for feed ordering
original_sender=original_sender_username,
rebloomer=rebloomer.username,
rebloom_count=0, # Will be calculated separately
)


def get_rebloom_count(bloom_id: int) -> int:
with db_cursor() as cur:
cur.execute(
"SELECT COUNT(*) FROM reblooms WHERE original_bloom_id = %s",
(bloom_id,),
)
row = cur.fetchone()
return row[0] if row else 0


def get_reblooms_for_user(username: str, *, limit: Optional[int] = None) -> List[Bloom]:
"""Get all reblooms made by a user (for their feed)"""
with db_cursor() as cur:
kwargs = {"username": username}
limit_clause = make_limit_clause(limit, kwargs)

cur.execute(
f"""SELECT
blooms.id,
rebloomer_user.username as rebloomer_username,
original_sender_user.username as original_sender_username,
blooms.content,
reblooms.rebloom_timestamp,
(SELECT COUNT(*) FROM reblooms r2 WHERE r2.original_bloom_id = blooms.id) as rebloom_count
FROM reblooms
INNER JOIN blooms ON reblooms.original_bloom_id = blooms.id
INNER JOIN users rebloomer_user ON reblooms.rebloomer_id = rebloomer_user.id
INNER JOIN users original_sender_user ON blooms.sender_id = original_sender_user.id
WHERE rebloomer_user.username = %(username)s
ORDER BY reblooms.rebloom_timestamp DESC
{limit_clause}
""",
kwargs,
)
rows = cur.fetchall()
reblooms_list = []
for row in rows:
bloom_id, rebloomer_username, original_sender_username, content, rebloom_timestamp, rebloom_count = row
reblooms_list.append(
Bloom(
id=bloom_id,
sender=rebloomer_username,
content=content,
sent_timestamp=rebloom_timestamp,
original_sender=original_sender_username,
rebloomer=rebloomer_username,
rebloom_count=rebloom_count or 0,
)
)
return reblooms_list
43 changes: 41 additions & 2 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,35 @@ def send_bloom():
)


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

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

try:
rebloomed_bloom = blooms.add_rebloom(rebloomer=user, original_bloom_id=bloom_id)
return jsonify(
{
"success": True,
"bloom": rebloomed_bloom,
}
)
except ValueError as e:
return make_response(
(
{
"success": False,
"message": str(e),
},
400,
)
)


def get_bloom(id_str):
try:
id_int = int(id_str)
Expand All @@ -192,11 +221,21 @@ def home_timeline():
# Flatten list of blooms from followed users
followed_blooms = [bloom for blooms in nested_user_blooms for bloom in blooms]

# Get reblooms from followed users
nested_user_reblooms = [
blooms.get_reblooms_for_user(followed_user, limit=50)
for followed_user in followed_users
]
followed_reblooms = [bloom for blooms in nested_user_reblooms for bloom in blooms]

# Get the current user's own blooms
own_blooms = blooms.get_blooms_for_user(current_user.username, limit=50)

# Get the current user's own reblooms
own_reblooms = blooms.get_reblooms_for_user(current_user.username, limit=50)

# Combine own blooms with followed blooms
all_blooms = followed_blooms + own_blooms
# Combine own blooms with followed blooms and reblooms
all_blooms = followed_blooms + followed_reblooms + own_blooms + own_reblooms

# Sort by timestamp (newest first)
sorted_blooms = list(
Expand Down
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
login,
other_profile,
register,
rebloom,
self_profile,
send_bloom,
suggested_follows,
Expand Down Expand Up @@ -60,6 +61,7 @@ def main():
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("/hashtag/<hashtag>", view_func=hashtag)
app.add_url_rule("/rebloom", methods=["POST"], view_func=rebloom)

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

Expand Down
8 changes: 8 additions & 0 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ CREATE TABLE hashtags (
bloom_id BIGINT NOT NULL REFERENCES blooms(id),
UNIQUE(hashtag, bloom_id)
);

CREATE TABLE reblooms (
id SERIAL PRIMARY KEY,
rebloomer_id INT NOT NULL REFERENCES users(id),
original_bloom_id BIGINT NOT NULL REFERENCES blooms(id),
rebloom_timestamp TIMESTAMP NOT NULL,
UNIQUE(rebloomer_id, original_bloom_id)
);
37 changes: 25 additions & 12 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
/**
* Create a bloom component
* @param {string} template - The ID of the template to clone
* @param {Object} bloom - The bloom data
* @returns {DocumentFragment} - The bloom fragment of UI, for items in the Timeline
* btw a bloom object is composed thus
* {"id": Number,
* "sender": username,
* "content": "string from textarea",
* "sent_timestamp": "datetime as ISO 8601 formatted string"}

*/
import {apiService} from "../lib/api.mjs";

const createBloom = (template, bloom) => {
if (!bloom) return;
const bloomFrag = document.getElementById(template).content.cloneNode(true);
Expand All @@ -20,6 +10,10 @@ 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 rebloomHeader = bloomFrag.querySelector("[data-rebloom-header]");
const rebloomIndicator = bloomFrag.querySelector("[data-rebloom-indicator]");
const rebloomBtn = bloomFrag.querySelector("[data-rebloom-btn]");
const rebloomCount = bloomFrag.querySelector("[data-rebloom-count]");

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

if (bloom.rebloomer && bloom.original_sender) {
rebloomHeader.style.display = "block";
rebloomIndicator.textContent = `${bloom.sender} rebloomed ${bloom.original_sender}`;
bloomArticle.classList.add("bloom--rebloomed");
}

if (bloom.rebloom_count > 0) {
rebloomCount.style.display = "inline";
rebloomCount.textContent = `${bloom.rebloom_count} rebloom${bloom.rebloom_count !== 1 ? 's' : ''}`;
}

if (rebloomBtn) {
rebloomBtn.addEventListener("click", () => handleRebloom(bloom.id));
}

return bloomFrag;
};

async function handleRebloom(bloomId) {
await apiService.rebloom(bloomId);
}

function _formatHashtags(text) {
if (!text) return text;
return text.replace(
Expand Down
Loading