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
153 changes: 123 additions & 30 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,30 @@
@dataclass
class Bloom:
id: int
sender: User
sender: str
content: str
sent_timestamp: datetime.datetime
original_bloom_id: Optional[int] = None
# Additional metadata for reblooms
original_sender: Optional[str] = None
original_sent_timestamp: Optional[datetime.datetime] = None
rebloom_count: int = 0


def add_bloom(*, sender: User, content: str) -> Bloom:
def add_bloom(*, sender: User, content: str, original_bloom_id: Optional[int] = None) -> 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)
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),
original_bloom_id=original_bloom_id,
),
)
for hashtag in hashtags:
Expand All @@ -53,28 +59,55 @@ def get_blooms_for_user(
limit_clause = make_limit_clause(limit, kwargs)

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
{limit_clause}
""",
kwargs,
)
f"""SELECT
blooms.id,
users.username,
blooms.content,
blooms.send_timestamp,
blooms.original_bloom_id,
original_users.username AS original_sender,
original_blooms.send_timestamp AS original_sent_timestamp,
(
SELECT COUNT(*)
FROM blooms rb
WHERE rb.original_bloom_id = COALESCE(blooms.original_bloom_id, blooms.id)
) AS rebloom_count
FROM
blooms
INNER JOIN users ON users.id = blooms.sender_id
LEFT JOIN blooms AS original_blooms ON original_blooms.id = blooms.original_bloom_id
LEFT JOIN users AS original_users ON original_users.id = original_blooms.sender_id
WHERE
users.username = %(sender_username)s
{before_clause}
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,
original_sent_timestamp,
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,
original_sent_timestamp=original_sent_timestamp,
rebloom_count=rebloom_count or 0,
)
)
return blooms
Expand All @@ -83,18 +116,50 @@ 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,
blooms.original_bloom_id,
original_users.username AS original_sender,
original_blooms.send_timestamp AS original_sent_timestamp,
(
SELECT COUNT(*)
FROM blooms rb
WHERE rb.original_bloom_id = COALESCE(blooms.original_bloom_id, blooms.id)
) AS rebloom_count
FROM blooms
INNER JOIN users ON users.id = blooms.sender_id
LEFT JOIN blooms AS original_blooms ON original_blooms.id = blooms.original_bloom_id
LEFT JOIN users AS original_users ON original_users.id = original_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,
original_bloom_id,
original_sender,
original_sent_timestamp,
rebloom_count,
) = row
return Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
original_bloom_id=original_bloom_id,
original_sender=original_sender,
original_sent_timestamp=original_sent_timestamp,
rebloom_count=rebloom_count or 0,
)


Expand All @@ -107,27 +172,55 @@ def get_blooms_with_hashtag(
limit_clause = make_limit_clause(limit, kwargs)
with db_cursor() as cur:
cur.execute(
f"""SELECT
blooms.id, users.username, content, send_timestamp
FROM
blooms INNER JOIN hashtags ON blooms.id = hashtags.bloom_id INNER JOIN users ON blooms.sender_id = users.id
WHERE
hashtag = %(hashtag_without_leading_hash)s
ORDER BY send_timestamp DESC
{limit_clause}
""",
kwargs,
)
f"""SELECT
blooms.id,
users.username,
blooms.content,
blooms.send_timestamp,
blooms.original_bloom_id,
original_users.username AS original_sender,
original_blooms.send_timestamp AS original_sent_timestamp,
(
SELECT COUNT(*)
FROM blooms rb
WHERE rb.original_bloom_id = COALESCE(blooms.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
LEFT JOIN blooms AS original_blooms ON original_blooms.id = blooms.original_bloom_id
LEFT JOIN users AS original_users ON original_users.id = original_blooms.sender_id
WHERE
hashtag = %(hashtag_without_leading_hash)s
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,
original_sent_timestamp,
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,
original_sent_timestamp=original_sent_timestamp,
rebloom_count=rebloom_count or 0,
)
)
return blooms
Expand Down
24 changes: 24 additions & 0 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,30 @@ def send_bloom():
)


@jwt_required()
@jwt_required()
def rebloom(bloom_id_str):
try:
bloom_id = int(bloom_id_str)
except ValueError:
return make_response(("Invalid bloom id", 400))

user = get_current_user()
original_bloom = blooms.get_bloom(bloom_id)

if not original_bloom:
return make_response(("Bloom not found", 404))

# Create a new bloom with the same content, linking to original
blooms.add_bloom(
sender=user,
content=original_bloom.content,
original_bloom_id=bloom_id
)

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 @@ -9,6 +9,7 @@
home_timeline,
login,
other_profile,
rebloom,
register,
self_profile,
send_bloom,
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/<bloom_id_str>", methods=["POST"], view_func=rebloom)
app.add_url_rule("/hashtag/<hashtag>", view_func=hashtag)

app.run(host="0.0.0.0", port="3000", debug=True)
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
47 changes: 46 additions & 1 deletion front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {apiService, state} from "../index.mjs";

/**
* Create a bloom component
* @param {string} template - The ID of the template to clone
Expand All @@ -7,7 +9,8 @@
* {"id": Number,
* "sender": username,
* "content": "string from textarea",
* "sent_timestamp": "datetime as ISO 8601 formatted string"}
* "sent_timestamp": "datetime as ISO 8601 formatted string",
* "original_bloom_id": Number or null}

*/
const createBloom = (template, bloom) => {
Expand All @@ -20,6 +23,14 @@ 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 rebloomSymbol = bloomFrag.querySelector("[data-rebloom-symbol]");
const originalAuthorInfo = bloomFrag.querySelector("[data-original-author-info]");
const originalAuthorLink = bloomFrag.querySelector("[data-original-author-link]");
const rebloomBadge = bloomFrag.querySelector("[data-rebloom-badge]");
const rebloomerEl = bloomFrag.querySelector("[data-rebloomer]");
const rebloomCountBadge = bloomFrag.querySelector("[data-rebloom-count-badge]");
const rebloomCountEl = bloomFrag.querySelector("[data-rebloom-count]");
const rebloomBtn = bloomFrag.querySelector("[data-action='rebloom']");

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

// Show rebloom metadata
const isRebloom = Boolean(bloom.original_bloom_id);
const hasRebloomCount = Number(bloom.rebloom_count) > 0;

if (isRebloom) {
bloomArticle.classList.add("bloom--rebloom");
// rebloomSymbol.style.display = "inline";
rebloomBadge.style.display = "block";
if (rebloomerEl) rebloomerEl.textContent = bloom.sender;

// Show original author
if (originalAuthorInfo && originalAuthorLink && bloom.original_sender) {
originalAuthorInfo.style.display = "block";
originalAuthorLink.textContent = bloom.original_sender;
originalAuthorLink.setAttribute("href", `/profile/${bloom.original_sender}`);
}
}

if (hasRebloomCount && rebloomCountBadge && rebloomCountEl) {
rebloomCountBadge.style.display = "inline";
rebloomCountEl.textContent = String(bloom.rebloom_count);
}

// Wire up rebloom button
if (rebloomBtn) {
rebloomBtn.addEventListener("click", async (e) => {
e.preventDefault();
if (state.isLoggedIn) {
const targetId = bloom.original_bloom_id || bloom.id;
await apiService.postRebloom(targetId);
}
});
}

return bloomFrag;
};

Expand Down
30 changes: 30 additions & 0 deletions front-end/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,36 @@ dialog {
display: grid;
gap: var(--space);
}

.bloom--rebloom {
border-left: 5px solid var(--brand);
background: hsl(var(--key), 45%, 85%);
}

.bloom__user-info {
flex: 1;
}

.bloom__original-author {
font-style: italic;
}

.bloom__original-author-link {
color: inherit !important;
text-decoration: underline;
}

.bloom__rebloom-badge {
background: hsl(var(--key), 60%, 70%);
color: var(--ink);
font-weight: 700;
padding: calc(var(--space) / 2);
border-radius: var(--corner);
font-size: 0.95em;
margin: 0 calc(var(--space) * -1) 0 calc(var(--space) * -1);
padding-left: var(--space);
padding-right: var(--space);
}
.is-invisible {
clip: rect(0 0 0 0);
clip-path: inset(50%);
Expand Down
Loading