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
73 changes: 69 additions & 4 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,94 @@ class Bloom:
sender: User
content: str
sent_timestamp: datetime.datetime
original_bloom_id: Optional[int] = None
original_sender: Optional[User] = None
rebloom_count: int = 0

MAX_BLOOM_LENGTH = 280 # this is to ensure the extra safety

def add_bloom(*, sender: User, content: str) -> Bloom:
if len(content) > MAX_BLOOM_LENGTH:
raise ValueError(f"blooms content is too long(max {MAX_BLOOM_LENGTH})")

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:
# Let the database generate the id
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 (sender_id, content, send_timestamp)
VALUES (%(sender_id)s, %(content)s, %(timestamp)s)
RETURNING id
""",
dict(
bloom_id=bloom_id,
sender_id=sender.id,
content=content,
timestamp=datetime.datetime.now(datetime.UTC),
timestamp=now,
),
)
bloom_id = cur.fetchone()[0]

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,
)

def add_rebloom(*, rebloomer: User, original_bloom: Bloom) -> Bloom:
now = datetime.datetime.now(tz=datetime.UTC)

with db_cursor() as cur:
# Insert rebloom without id, DB generates it
cur.execute(
"""
INSERT INTO blooms (
sender_id, content, send_timestamp, original_bloom_id
)
VALUES (
%(sender_id)s, %(content)s, %(timestamp)s, %(original_bloom_id)s
)
RETURNING id
""",
dict(
sender_id=rebloomer.id,
content=original_bloom.content,
timestamp=now,
original_bloom_id=original_bloom.id,
),
)
new_id = cur.fetchone()[0]

# Increase rebloom_count on the original
cur.execute(
"""
UPDATE blooms
SET rebloom_count = COALESCE(rebloom_count, 0) + 1
WHERE id = %(id)s
""",
dict(id=original_bloom.id),
)

return Bloom(
id=new_id,
sender=rebloomer.username,
content=original_bloom.content,
sent_timestamp=now,
original_bloom_id=original_bloom.id,
original_sender=original_bloom.sender,
rebloom_count=(original_bloom.rebloom_count or 0) + 1,
)



def get_blooms_for_user(
username: str, *, before: Optional[int] = None, limit: Optional[int] = None
Expand Down
26 changes: 25 additions & 1 deletion backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,22 @@ def do_follow():
}
)


MAX_BLOOM_LENGTH = 280
@jwt_required()
def send_bloom():
type_check_error = verify_request_fields({"content": str})
if type_check_error is not None:
return type_check_error

content = request.json ["content"]

if len(content) > MAX_BLOOM_LENGTH:
return make_response(
({"success": False,
"message": f"blloms can't be longe than {MAX_BLOOM_LENGTH} symbols",
}, 400,)
)

user = get_current_user()

blooms.add_bloom(sender=user, content=request.json["content"])
Expand All @@ -166,6 +175,21 @@ def send_bloom():
}
)

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

original = blooms.get_bloom(bloom_id)
if original is None:
return make_response(("Bloom not found", 404))

user = get_current_user()
new_bloom = blooms.add_rebloom(rebloomer=user, original_bloom=original)

return jsonify({"success": True, "bloom": new_bloom})

def get_bloom(id_str):
try:
Expand Down
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
send_bloom,
suggested_follows,
user_blooms,
rebloom,
)

from dotenv import load_dotenv
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
34 changes: 32 additions & 2 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { apiService } from "../lib/api.mjs";

/**
* Create a bloom component
* @param {string} template - The ID of the template to clone
Expand All @@ -20,6 +22,34 @@ 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 rebloomCountEl = bloomFrag.querySelector("[data-rebloom-count]");
const rebloomButton = bloomFrag.querySelector("[data-action='rebloom']");

if (bloom.original_bloom_id && bloom.original_sender) {
// This is a rebloom
rebloomInfo.textContent =
`Re-bloomed by ${bloom.sender}, originally by ${bloom.original_sender}`;
} else {
// Original bloom: no rebloom info
rebloomInfo.textContent = "";//it gives an origianl bloom, no rebllom information
}

if (bloom.rebloom_count && bloom.rebloom_count > 0) {
rebloomCountEl.textContent = `Re-bloomed ${bloom.rebloom_count} times`;
} else {
rebloomCountEl.textContent = "";
}

rebloomButton?.addEventListener("click", async () => {
try {
await apiService.rebloom(bloom.id); // implement this in your API service
// TODO: refresh timeline / current view after rebloom
} catch (error) {
console.error("Failed to re-bloom", error);
}
});


bloomArticle.setAttribute("data-bloom-id", bloom.id);
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
Expand All @@ -37,8 +67,8 @@ const createBloom = (template, bloom) => {
function _formatHashtags(text) {
if (!text) return text;
return text.replace(
/\B#[^#]+/g,
(match) => `<a href="/hashtag/${match.slice(1)}">${match}</a>`
/\B#([a-zA-Z0-9_]+)/g,// gets SwizBiz, without # or trailing love!!.
(match, tag) => `[${match}](/hashtag/${tag})`
);
}

Expand Down
7 changes: 6 additions & 1 deletion front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ <h2 id="bloom-form-title" class="bloom-form__title">Share a Bloom</h2>
<a href="#" class="bloom__time"><time class="bloom__time" data-time>2m</time></a>
</div>
<div class="bloom__content" data-content></div>
<p data-rebloom-info class="bloom__rebloom-info"></p>
<p data-rebloom-count class="bloom__rebloom-count"></p>
<button type="button" data-action="rebloom" class="bloom__rebloom-button">
Re-bloom
</button>
</article>
</template>

Expand All @@ -256,6 +261,6 @@ <h2 id="bloom-form-title" class="bloom-form__title">Share a Bloom</h2>
<p>Please enable JavaScript in your browser.</p>
</noscript>

<script type="module" src="/index.mjs"></script>
<script type="module" src="./index.mjs"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions front-end/lib/api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,26 @@ async function postBloom(content) {
return {success: false};
}
}
async function rebloom(bloomId) {
const endpoint = `/bloom/${bloomId}/rebloom`;

try {
const data = await _apiRequest(endpoint, {
method: "POST",
});

if (data.success) {
// refresh main timeline so the new bloom appears
await getBlooms();
}

return data;
} catch (error) {
// Error already handled by _apiRequest
return { success: false };
}
}


// ======= USER methods
async function getProfile(username) {
Expand Down Expand Up @@ -292,6 +312,7 @@ const apiService = {
getBlooms,
postBloom,
getBloomsByHashtag,
rebloom,

// User methods
getProfile,
Expand Down
15 changes: 11 additions & 4 deletions front-end/views/hashtag.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ import {createHeading} from "../components/heading.mjs";

function hashtagView(hashtag) {
destroy();

apiService.getBloomsByHashtag(hashtag);

// it will show the basic structure or loading msg
renderOne(
state.isLoggedIn,
getLogoutContainer(),
Expand All @@ -28,6 +26,7 @@ function hashtagView(hashtag) {
document
.querySelector("[data-action='logout']")
?.addEventListener("click", handleLogout);

renderOne(
state.isLoggedIn,
getLoginContainer(),
Expand All @@ -39,17 +38,25 @@ function hashtagView(hashtag) {
?.addEventListener("click", handleLogin);

renderOne(
state.currentHashtag,
`#${hashtag}`,
getHeadingContainer(),
"heading-template",
createHeading
);
apiService.getBloomsByHashtag(hashtag).then(() => {
renderOne(
state.currentHashtag,
getHeadingContainer(),
"heading-template",
createHeading
);
renderEach(
state.hashtagBlooms || [],
getTimelineContainer(),
"bloom-template",
createBloom
);
});
}

export {hashtagView};
4 changes: 2 additions & 2 deletions front-end/views/profile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ function profileView(username) {
createLogin
);
document
.querySelector("[data-action='login']")
?.addEventListener("click", handleLogin);
.querySelector("[data-form='login']")
?.addEventListener("submit", handleLogin);

const profileData = state.profiles.find((p) => p.username === username);
if (profileData) {
Expand Down