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
95 changes: 89 additions & 6 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@ class Bloom:
sender: User
content: str
sent_timestamp: datetime.datetime
activity_timestamp: datetime.datetime
rebloom_count: int

@dataclass
class RebloomView:
id: int
sender: str
content: str
sent_timestamp: datetime.datetime
rebloomer: str
activity_timestamp: datetime.datetime
rebloom_count: int

def add_bloom(*, sender: User, content: str) -> Bloom:
hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")]
Expand All @@ -36,6 +47,17 @@ def add_bloom(*, sender: User, content: str) -> Bloom:
dict(hashtag=hashtag, bloom_id=bloom_id),
)

def add_rebloom(*, user_id: int, bloom_id: int) -> bool:
with db_cursor() as cur:
try:
cur.execute(
"INSERT INTO reblooms (user_id, bloom_id) VALUES (%(user_id)s, %(bloom_id)s) ON CONFLICT (user_id, bloom_id) DO NOTHING", dict(user_id=user_id, bloom_id=bloom_id)
)
return True
except Exception as e:
print(f"cannot rebloom bloom: {bloom_id}")
return False


def get_blooms_for_user(
username: str, *, before: Optional[int] = None, limit: Optional[int] = None
Expand All @@ -54,7 +76,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.bloom_id = blooms.id) AS rebloom_count
FROM
blooms INNER JOIN users ON users.id = blooms.sender_id
WHERE
Expand All @@ -68,33 +91,90 @@ 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_count = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
activity_timestamp=timestamp,
rebloom_count=rebloom_count
)
)
return blooms

def get_reblooms_for_user(username: str, *, before: Optional[int] = None, limit: Optional[int] = None) -> List[Bloom]:
with db_cursor() as cur:
kwargs = {
"username": username,
}
if before is not None:
before_clause = "AND 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 AS sender, blooms.content, blooms.send_timestamp, rebloomer.username AS rebloomer, reblooms.rebloom_timestamp AS activity_time,
(SELECT COUNT(*) FROM reblooms WHERE reblooms.bloom_id = blooms.id) AS rebloom_count
FROM
reblooms
INNER JOIN blooms ON blooms.id = reblooms.bloom_id
INNER JOIN users ON users.id = blooms.sender_id
INNER JOIN users AS rebloomer ON rebloomer.id = reblooms.user_id
WHERE
rebloomer.username = %(username)s
{before_clause}
ORDER BY reblooms.rebloom_timestamp DESC
{limit_clause}
""",
kwargs
)
rows = cur.fetchall()

reblooms = []
for row in rows:
bloom_id, send_username, content, timestamp, rebloomer_name, activity_time, rebloom_count = row
reblooms.append(
RebloomView(
id=bloom_id,
sender=send_username,
content=content,
sent_timestamp=timestamp,
rebloomer=rebloomer_name,
activity_timestamp=activity_time,
rebloom_count=rebloom_count
)
)
return reblooms



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.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,
activity_timestamp=timestamp,
rebloom_count=rebloom_count
)


Expand All @@ -108,7 +188,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.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 @@ -121,13 +202,15 @@ def get_blooms_with_hashtag(
rows = cur.fetchall()
blooms = []
for row in rows:
bloom_id, sender_username, content, timestamp = row
bloom_id, sender_username, content, timestamp, rebloom_count = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
activity_timestamp=timestamp,
rebloom_count=rebloom_count
)
)
return blooms
Expand Down
44 changes: 35 additions & 9 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,16 @@ def other_profile(profile_username):
current_user = get_current_user()

followers = get_inverse_followed_usernames(profile_user)
all_blooms = blooms.get_blooms_for_user(profile_username)
all_blooms.reverse()
own_blooms = blooms.get_blooms_for_user(profile_username)
shared_blooms = blooms.get_reblooms_for_user(profile_username)
all_blooms = own_blooms + shared_blooms

sorted_blooms = list(sorted(all_blooms, key=lambda bloom: bloom.activity_timestamp, reverse=True))

return jsonify(
{
"username": profile_username,
"recent_blooms": all_blooms[:10],
"recent_blooms": sorted_blooms[:10],
"follows": get_followed_usernames(profile_user),
"followers": list(followers),
"is_following": current_user is not None
Expand Down Expand Up @@ -177,6 +181,21 @@ def get_bloom(id_str):
return make_response((f"Bloom not found", 404))
return jsonify(bloom)

@jwt_required()
def rebloom(bloom_id):
try:
id_int = int(bloom_id)
except ValueError:
return make_response(jsonify({"error": "Invalid bloom id"}), 400)

current_user = get_current_user()

success = blooms.add_rebloom(user_id=current_user.id, bloom_id=id_int)

if success:
return make_response(jsonify({"success": True, "message": "Rebloom successfully"}), 200)
else:
return make_response(jsonify({"success": False, "message": "Failed to rebloom"}), 500)

@jwt_required()
def home_timeline():
Expand All @@ -195,22 +214,29 @@ def home_timeline():
# Get the current user's own blooms
own_blooms = blooms.get_blooms_for_user(current_user.username, limit=50)

# Get the current user's shared blooms
shared_blooms = blooms.get_reblooms_for_user(current_user.username, limit=50)

# Combine own blooms with followed blooms
all_blooms = followed_blooms + own_blooms
all_blooms = followed_blooms + own_blooms + shared_blooms

# Sort by timestamp (newest first)
sorted_blooms = list(
sorted(all_blooms, key=lambda bloom: bloom.sent_timestamp, reverse=True)
sorted(all_blooms, key=lambda bloom: bloom.activity_timestamp, reverse=True)
)

return jsonify(sorted_blooms)

def user_timeline(profile_username):
own_blooms = blooms.get_blooms_for_user(profile_username)

shared_blooms = blooms.get_reblooms_for_user(profile_username)

combined_timeline = own_blooms + shared_blooms

def user_blooms(profile_username):
user_blooms = blooms.get_blooms_for_user(profile_username)
user_blooms.reverse()
return jsonify(user_blooms)
sorted_timeline = list(sorted(combined_timeline, key=lambda bloom: bloom.activity_timestamp, reverse=True))

return jsonify(sorted_timeline)

@jwt_required()
def suggested_follows(limit_str):
Expand Down
6 changes: 4 additions & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from endpoints import (
do_follow,
get_bloom,
rebloom,
hashtag,
home_timeline,
login,
Expand All @@ -13,7 +14,7 @@
self_profile,
send_bloom,
suggested_follows,
user_blooms,
user_timeline,
)

from dotenv import load_dotenv
Expand Down Expand Up @@ -58,7 +59,8 @@ 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("/blooms/<int:bloom_id>/rebloom", methods=["POST"], view_func=rebloom)
app.add_url_rule("/blooms/<profile_username>", view_func=user_timeline)
app.add_url_rule("/hashtag/<hashtag>", view_func=hashtag)

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,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
bloom_id BIGINT NOT NULL REFERENCES blooms(id) ON DELETE CASCADE,
rebloom_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, bloom_id)
);
42 changes: 42 additions & 0 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { apiService } from "../index.mjs";
import { state } from "../lib/state.mjs";

/**
* Create a bloom component
* @param {string} template - The ID of the template to clone
Expand All @@ -20,8 +23,36 @@ 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 rebloomBtn = bloomFrag.querySelector("[data-action='rebloom']");
const rebloomHeader = bloomFrag.querySelector("[data-rebloom-header]");
const rebloomBy = bloomFrag.querySelector("[data-rebloom-by]");
const rebloomCountLabel = bloomFrag.querySelector("[data-rebloom-count]");

bloomArticle.setAttribute("data-bloom-id", bloom.id);

bloomArticle.removeAttribute("data-is-rebloom");
if (rebloomHeader) rebloomHeader.classList.add("is-hidden");

if (bloom.rebloomer) {
bloomArticle.setAttribute("data-is-rebloom", "true");

if (rebloomHeader && rebloomBy) {
rebloomHeader.classList.remove("is-hidden");
rebloomBy.setAttribute("href", `/profile/${bloom.rebloomer}`);
rebloomBy.textContent = bloom.rebloomer;

}
}

if (rebloomCountLabel) {
if (bloom.rebloom_count > 0) {
rebloomCountLabel.textContent = `(${bloom.rebloom_count})`;
rebloomCountLabel.classList.remove("is-hidden");
} else {
rebloomCountLabel.classList.add("is-hidden");
}
}

bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
bloomUsername.textContent = bloom.sender;
bloomTime.textContent = _formatTimestamp(bloom.sent_timestamp);
Expand All @@ -31,6 +62,17 @@ const createBloom = (template, bloom) => {
.body.childNodes
);

if (rebloomBtn) {
rebloomBtn.addEventListener("click", async (e) => {
e.preventDefault();
rebloomBtn.disabled = true;
console.log(bloom)
const response = await apiService.postRebloom(bloom.id);
console.log("API service responded with", response);
rebloomBtn.disabled = false;
})
}

return bloomFrag;
};

Expand Down
30 changes: 29 additions & 1 deletion front-end/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,34 @@ dialog {
grid-area: char-count;
}

/* REBLOOM */

.bloom.box[data-is-rebloom="true"] {
border-left: 4px solid #7149c6 !important;
background-color: #f9f6ff;
box-shadow: inset 0 0 8px rgba(113, 73, 198, 0.02);
}

.bloom__rebloom-header {
font-size: 0.85rem;
font-weight: 500;
color: #657786;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 4px;
}

.bloom__rebloom-author {
font-weight: 700;
color: #7149c6;
text-decoration: none;
}

.bloom__rebloom-author:hover {
text-decoration: underline;
}

/* LOGIN */
.login__container {
margin: 1em 0 auto auto;
Expand Down Expand Up @@ -264,6 +292,6 @@ dialog {
}

/* We always want attribute hidden to sync with display:none, no matter what */
[hidden] {
.is-hidden, [hidden] {
display: none !important;
}
Loading