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
151 changes: 100 additions & 51 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,71 @@
@dataclass
class Bloom:
id: int
sender: User
sender: str
content: str
sent_timestamp: datetime.datetime


def add_bloom(*, sender: User, content: str) -> Bloom:
original_bloom_id: Optional[int] = None
original_sender: Optional[str] = None
original_sent_timestamp: Optional[datetime.datetime] = None
rebloom_count: int = 0


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),
timestamp=now,
original_bloom_id=original_bloom_id,
),
)
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=now,
original_bloom_id=original_bloom_id,
)


def _row_to_bloom(row):
(
bloom_id,
sender_username,
content,
timestamp,
original_bloom_id,
original_send_timestamp,
original_sender_username,
rebloom_count,
) = row
return Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
original_bloom_id=original_bloom_id,
original_sent_timestamp=original_send_timestamp,
original_sender=original_sender_username,
rebloom_count=rebloom_count,
)


def get_blooms_for_user(
Expand All @@ -45,7 +85,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 b.send_timestamp < %(before_limit)s"
kwargs["before_limit"] = before
else:
before_clause = ""
Expand All @@ -54,48 +94,57 @@ def get_blooms_for_user(

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
b.id,
sender.username,
b.content,
b.send_timestamp,
b.original_bloom_id,
original_bloom.send_timestamp AS original_send_timestamp,
original_sender.username AS original_sender_username,
COUNT(rebloom_child.id) AS rebloom_count
FROM blooms b
INNER JOIN users sender ON sender.id = b.sender_id
LEFT JOIN blooms original_bloom ON original_bloom.id = b.original_bloom_id
LEFT JOIN users original_sender ON original_sender.id = original_bloom.sender_id
LEFT JOIN blooms rebloom_child ON rebloom_child.original_bloom_id = b.id
WHERE sender.username = %(sender_username)s
{before_clause}
ORDER BY send_timestamp DESC
GROUP BY b.id, sender.username, b.content, b.send_timestamp, b.original_bloom_id, original_bloom.send_timestamp, original_sender.username
ORDER BY b.send_timestamp DESC
{limit_clause}
""",
kwargs,
)
rows = cur.fetchall()
blooms = []
for row in rows:
bloom_id, sender_username, content, timestamp = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
)
)
return blooms
return [_row_to_bloom(row) for row in rows]


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
b.id,
sender.username,
b.content,
b.send_timestamp,
b.original_bloom_id,
original_bloom.send_timestamp AS original_send_timestamp,
original_sender.username AS original_sender_username,
COUNT(rebloom_child.id) AS rebloom_count
FROM blooms b
INNER JOIN users sender ON sender.id = b.sender_id
LEFT JOIN blooms original_bloom ON original_bloom.id = b.original_bloom_id
LEFT JOIN users original_sender ON original_sender.id = original_bloom.sender_id
LEFT JOIN blooms rebloom_child ON rebloom_child.original_bloom_id = b.id
WHERE b.id = %s
GROUP BY b.id, sender.username, b.content, b.send_timestamp, b.original_bloom_id, original_bloom.send_timestamp, original_sender.username
""",
(bloom_id,),
)
row = cur.fetchone()
if row is None:
return None
bloom_id, sender_username, content, timestamp = row
return Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
)
return _row_to_bloom(row)


def get_blooms_with_hashtag(
Expand All @@ -108,29 +157,29 @@ def get_blooms_with_hashtag(
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
b.id,
sender.username,
b.content,
b.send_timestamp,
b.original_bloom_id,
original_bloom.send_timestamp AS original_send_timestamp,
original_sender.username AS original_sender_username,
COUNT(rebloom_child.id) AS rebloom_count
FROM blooms b
INNER JOIN hashtags ON b.id = hashtags.bloom_id
INNER JOIN users sender ON b.sender_id = sender.id
LEFT JOIN blooms original_bloom ON original_bloom.id = b.original_bloom_id
LEFT JOIN users original_sender ON original_sender.id = original_bloom.sender_id
LEFT JOIN blooms rebloom_child ON rebloom_child.original_bloom_id = b.id
WHERE hashtag = %(hashtag_without_leading_hash)s
GROUP BY b.id, sender.username, b.content, b.send_timestamp, b.original_bloom_id, original_bloom.send_timestamp, original_sender.username
ORDER BY b.send_timestamp DESC
{limit_clause}
""",
kwargs,
)
rows = cur.fetchall()
blooms = []
for row in rows:
bloom_id, sender_username, content, timestamp = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
)
)
return blooms
return [_row_to_bloom(row) for row in rows]


def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str:
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 @@ -57,6 +58,7 @@ def main():
app.add_url_rule("/suggested-follows/<limit_str>", view_func=suggested_follows)

app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom)
app.add_url_rule("/bloom/<id_str>/rebloom", methods=["POST"], view_func=rebloom)
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)
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
23 changes: 23 additions & 0 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const createBloom = (template, bloom) => {
const bloomUsername = bloomFrag.querySelector("[data-username]");
const bloomTime = bloomFrag.querySelector("[data-time]");
const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])");
const rebloomInfo = bloomFrag.querySelector("[data-rebloom-info]");
const rebloomCount = bloomFrag.querySelector("[data-rebloom-count]");
const rebloomButton = bloomFrag.querySelector("[data-action='rebloom']");
const bloomContent = bloomFrag.querySelector("[data-content]");

bloomArticle.setAttribute("data-bloom-id", bloom.id);
Expand All @@ -31,6 +34,26 @@ const createBloom = (template, bloom) => {
.body.childNodes
);

if (bloom.original_sender) {
rebloomInfo.hidden = false;
const originalTime = bloom.original_sent_timestamp
? ` • ${_formatTimestamp(bloom.original_sent_timestamp)}`
: "";
rebloomInfo.innerHTML = `Rebloomed from <a href="/profile/${bloom.original_sender}">@${bloom.original_sender}</a>${originalTime}`;
}

if (bloom.rebloom_count && bloom.rebloom_count > 0) {
rebloomCount.hidden = false;
rebloomCount.textContent =
bloom.rebloom_count === 1
? "1 rebloom"
: `${bloom.rebloom_count} reblooms`;
}

if (rebloomButton) {
rebloomButton.dataset.bloomId = bloom.id;
}

return bloomFrag;
};

Expand Down
7 changes: 7 additions & 0 deletions front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,13 @@ <h2 id="bloom-form-title" class="bloom-form__title">Share a Bloom</h2>
<a href="#" class="bloom__username" data-username>Username</a>
<a href="#" class="bloom__time"><time class="bloom__time" data-time>2m</time></a>
</div>
<div class="bloom__meta flex">
<span class="bloom__rebloom-info" data-rebloom-info hidden></span>
<span class="bloom__count" data-rebloom-count hidden></span>
<button type="button" class="bloom__action" data-action="rebloom">
Rebloom
</button>
</div>
<div class="bloom__content" data-content></div>
</article>
</template>
Expand Down
12 changes: 12 additions & 0 deletions front-end/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ async function init() {

handleRouteChange();

const timelineContainer = getTimelineContainer();
timelineContainer?.addEventListener("click", async (event) => {
const rebloomButton = event.target.closest("[data-action='rebloom']");
if (!rebloomButton) return;

const bloomArticle = rebloomButton.closest("[data-bloom]");
const bloomId = bloomArticle?.getAttribute("data-bloom-id");
if (!bloomId) return;

await apiService.rebloom(bloomId);
});

document.addEventListener("state-change", () => {
handleRouteChange();
});
Expand Down
18 changes: 18 additions & 0 deletions front-end/lib/api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,23 @@ async function postBloom(content) {
}
}

async function rebloom(bloomId) {
try {
const data = await _apiRequest(`/bloom/${bloomId}/rebloom`, {
method: "POST",
});

if (data.success) {
await getBlooms();
await getProfile(state.currentUser);
}

return data;
} catch (error) {
return {success: false};
}
}

// ======= USER methods
async function getProfile(username) {
const endpoint = username ? `/profile/${username}` : "/profile";
Expand Down Expand Up @@ -291,6 +308,7 @@ const apiService = {
getBloom,
getBlooms,
postBloom,
rebloom,
getBloomsByHashtag,

// User methods
Expand Down