Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
183 changes: 167 additions & 16 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class Bloom:
sender: User
content: str
sent_timestamp: datetime.datetime
original_bloom: Optional["Bloom"] = None
rebloom_count: int = 0


def add_bloom(*, sender: User, content: str) -> Bloom:
Expand All @@ -36,6 +38,86 @@ def add_bloom(*, sender: User, content: str) -> Bloom:
dict(hashtag=hashtag, bloom_id=bloom_id),
)

def rebloom(
*, sender: User, original_bloom_id: int
) -> Optional[Bloom]:
now = datetime.datetime.now(tz=datetime.UTC)
bloom_id = int(now.timestamp() * 1000000)

with db_cursor() as cur:
cur.execute(
"""
SELECT COALESCE(original_bloom_id, id)
FROM blooms
WHERE id = %(original_bloom_id)s
""",
{"original_bloom_id": original_bloom_id},
)

row = cur.fetchone()

if row is None:
return None

root_bloom_id = row[0]

cur.execute(
"""
INSERT INTO blooms (
id,
sender_id,
content,
send_timestamp,
original_bloom_id
)
SELECT
%(bloom_id)s,
%(sender_id)s,
content,
%(timestamp)s,
%(root_bloom_id)s
FROM blooms
WHERE id = %(original_bloom_id)s
RETURNING id, sender_id, content, send_timestamp, original_bloom_id
""",
{
"bloom_id": bloom_id,
"sender_id": sender.id,
"timestamp": now,
"original_bloom_id": original_bloom_id,
"root_bloom_id": root_bloom_id,
},
)

row = cur.fetchone()

if row is None:
return None

cur.execute(
"""
INSERT INTO reblooms (
rebloomer_id,
bloom_id,
rebloom_timestamp
)
VALUES (
%(rebloomer_id)s,
%(bloom_id)s,
%(timestamp)s
)
""",
{
"rebloomer_id": sender.id,
"bloom_id": root_bloom_id,
"timestamp": now,
},
)

return get_bloom(bloom_id)




def get_blooms_for_user(
username: str, *, before: Optional[int] = None, limit: Optional[int] = None
Expand All @@ -44,60 +126,129 @@ def get_blooms_for_user(
kwargs = {
"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 = ""

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
f"""
SELECT
blooms.id,
users.username,
blooms.content,
blooms.send_timestamp,
blooms.original_bloom_id,
(
SELECT COUNT(*)
FROM reblooms
WHERE reblooms.bloom_id = COALESCE(
blooms.original_bloom_id,
blooms.id
)
) AS rebloom_count
FROM blooms
INNER JOIN users
ON users.id = blooms.sender_id
WHERE users.username = %(sender_username)s
{before_clause}
ORDER BY blooms.send_timestamp DESC
{limit_clause}
""",
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,
original_bloom_id,
rebloom_count,
) = row

original_bloom = (
get_bloom(original_bloom_id)
if original_bloom_id is not None
else None
)

blooms_list.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
original_bloom=original_bloom,
rebloom_count=rebloom_count,
)
)
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,
blooms.content,
blooms.send_timestamp,
blooms.original_bloom_id,
(
SELECT COUNT(*)
FROM reblooms
WHERE reblooms.bloom_id = COALESCE(
blooms.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,
original_bloom_id,
rebloom_count,
) = row

original_bloom = (
get_bloom(original_bloom_id)
if original_bloom_id is not None
else None
)

return Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
original_bloom=original_bloom,
rebloom_count=rebloom_count,
)


def get_blooms_with_hashtag(
hashtag_without_leading_hash: str, *, limit: int = None
) -> List[Bloom]:
Expand Down
22 changes: 22 additions & 0 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,25 @@ def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, No
)
)
return None


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

current_user = get_current_user()

original_bloom = blooms.get_bloom(original_bloom_id)

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

rebloomed = blooms.rebloom(
sender=current_user,
original_bloom_id=original_bloom_id,
)

return jsonify(rebloomed)
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 @@ -58,6 +59,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("/rebloom/<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)

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
31 changes: 31 additions & 0 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
* "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);
const bloomParser = new DOMParser();

Expand All @@ -20,17 +23,45 @@ 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 rebloomInfo = bloomFrag.querySelector("[data-rebloom-info]");
const rebloomButton = bloomFrag.querySelector("[data-action='rebloom']");
const rebloomCount = bloomFrag.querySelector("[data-rebloom-count]");

rebloomCount.textContent = bloom.rebloom_count ?? 0;

bloomArticle.setAttribute("data-bloom-id", bloom.id);
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
bloomUsername.textContent = bloom.sender;
bloomTime.textContent = _formatTimestamp(bloom.sent_timestamp);
bloomTimeLink.setAttribute("href", `/bloom/${bloom.id}`);

bloomContent.replaceChildren(
...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html")
.body.childNodes
);

if (bloom.original_bloom) {
rebloomInfo.hidden = false;
rebloomInfo.textContent = `Rebloomed from ${bloom.original_bloom.sender}`;
}

// Handle rebloom
rebloomButton.addEventListener("click", async () => {
try {
rebloomButton.disabled = true;
rebloomButton.textContent = "Reblooming...";

const rebloomedBloom = await apiService.rebloom(bloom.id);

rebloomCount.textContent = rebloomedBloom.rebloom_count;
} catch (error) {
console.error("Failed to rebloom:", error);
} finally {
rebloomButton.disabled = false;
rebloomButton.innerHTML = `Rebloom <span data-rebloom-count>${rebloomCount.textContent}</span>`;
}
});

return bloomFrag;
};

Expand Down
13 changes: 12 additions & 1 deletion front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,20 @@ <h2 id="bloom-form-title" class="bloom-form__title">Share a Bloom</h2>
<article class="bloom box" data-bloom data-bloom-id="">
<div class="bloom__header flex">
<a href="#" class="bloom__username" data-username>Username</a>
<a href="#" class="bloom__time"><time class="bloom__time" data-time>2m</time></a>
<a href="#" class="bloom__time">
<time class="bloom__time" data-time>2m</time>
</a>
</div>

<div class="bloom__rebloom-info" data-rebloom-info hidden></div>

<div class="bloom__content" data-content></div>

<div class="bloom__actions">
<button type="button" data-action="rebloom">
Rebloom <span data-rebloom-count>0</span>
</button>
</div>
</article>
</template>

Expand Down
Loading