Skip to content

feat: add activity discussion/comments system with threaded replies#20

Open
ayesha1145 wants to merge 1 commit intoalphaonelabs:mainfrom
ayesha1145:feat/activity-comments
Open

feat: add activity discussion/comments system with threaded replies#20
ayesha1145 wants to merge 1 commit intoalphaonelabs:mainfrom
ayesha1145:feat/activity-comments

Conversation

@ayesha1145
Copy link
Copy Markdown
Contributor

@ayesha1145 ayesha1145 commented Mar 25, 2026

Adds a threaded discussion/comments system to activity pages, allowing enrolled users and hosts to discuss activities in real time.

Changes:

  • schema.sql: new comments table with parent_id for threaded replies, encrypted body, indexes on activity/user/parent
  • src/worker.py: 3 new API endpoints:
    • GET /api/activities/:id/comments — list all comments (public)
    • POST /api/activities/:id/comments — post a comment or reply (auth required, 2000 char limit)
    • DELETE /api/comments/:id — delete own comment or any comment as host/admin
    • DDL added to /api/init so comments table is created on initialization
  • public/course.html: Discussion section below activity details
    • Threaded replies with visual indentation
    • Reply button per comment (auth only)
    • Login CTA for unauthenticated users
    • Avatar initials, author name, date
    • XSS-safe rendering via escHtml()

Features:

  • Nested replies (one level deep rendered, unlimited depth stored)
  • Comment body encrypted at rest (matches platform security model)
  • Permission-based deletion: own comment, activity host, or admin
  • Input validation: body required, max 2000 characters, parent must exist in same activity

Pull Request Summary: Add Activity Discussion/Comments System with Threaded Replies

Purpose

Adds a threaded discussion/comments system to activity pages, enabling enrolled users and hosts to discuss activities in real time with hierarchical parent-child comment relationships.

Key Changes

Database (schema.sql)

  • Added comments table with support for threaded replies via optional parent_id self-reference and ON DELETE CASCADE
  • Encrypted body storage (at-rest encryption matching platform security model)
  • Added three indexes on activity_id, parent_id, and user_id for performance
  • Includes created_at and updated_at timestamps

Backend API (src/worker.py)

  • Implemented three comment handler functions:
    • api_get_comments: Fetches and decrypts all comments for an activity
    • api_post_comment: Creates new comments with 2000-character limit and optional parent_id for replies; requires authentication
    • api_delete_comment: Allows deletion by comment author, activity host, or admin
  • Updated _DDL schema to include comments table and indexes
  • Updated API documentation to describe the three new endpoints

Frontend (public/course.html)

  • Added "Discussion" UI section with comment form, reply indicator, and login CTA placeholder
  • Implements threaded reply rendering (one level deep visually; unlimited depth storage)
  • Loads comments via GET /api/activities/{actId}/comments and posts via POST with bearer token authentication
  • Reply flow: reply button per comment → focus textarea → post with parent_id
  • XSS-safe rendering via escHtml() and conditional form visibility based on authentication

Input Validation

  • Comment body required, max 2000 characters
  • Parent comment (if provided) must exist and belong to the same activity

⚠️ Critical Implementation Gap

The API routes are not wired into the dispatcher. While all three handler functions are fully implemented and the database schema is complete, no route matchers have been added to the _dispatch function. The _dispatch function will return 404 "API endpoint not found" for all comment requests because the routes checking for /api/activities/:id/comments and /api/comments/:id are missing. The feature is currently non-functional and requires routing to be added to _dispatch before the API endpoints can be accessed.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

Warning

Rate limit exceeded

@ayesha1145 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 8 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: alphaonelabs/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8d09b5eb-c88e-4e2a-be4f-2d005e462031

📥 Commits

Reviewing files that changed from the base of the PR and between 661e531 and f456156.

📒 Files selected for processing (3)
  • public/course.html
  • schema.sql
  • src/worker.py

Walkthrough

Adds a discussion/comments feature: DB schema for threaded comments, three new backend comment handlers (get/post/delete) implemented but not yet routed, and client-side UI + logic on the activity page to fetch, render, reply, and post comments.

Changes

Cohort / File(s) Summary
Database Schema
schema.sql
Added comments table (id, activity_id, user_id, body, parent_id, created_at, updated_at) with self-referential parent_id (ON DELETE CASCADE) and indexes: idx_comments_activity, idx_comments_parent, idx_comments_user.
Backend API handlers
src/worker.py
Added async handlers: api_get_comments, api_post_comment, api_delete_comment. Handlers validate activity existence/auth, encrypt/decrypt comment bodies, enforce parent/activity consistency, and enforce owner/host deletion rules. DDL updated in-file, but routing/dispatch wiring not updated.
Frontend UI & Logic
public/course.html
Added comments-section UI, hidden comment form and login CTA, client-side state (replyToId), functions to load and render nested comments from /api/activities/{actId}/comments, reply flow (start/cancel), and POST submission including Authorization header when token present.

Sequence Diagram

sequenceDiagram
    participant Client as Browser Client
    participant Server as Worker (API)
    participant DB as Database

    rect rgba(0, 100, 200, 0.5)
    Note over Client,DB: Load Comments Thread
    Client->>Server: GET /api/activities/{actId}/comments
    Server->>DB: Query comments by activity_id (+ users join)
    DB-->>Server: Return encrypted comments + author data
    Server->>Server: Decrypt comment bodies
    Server-->>Client: Return comment thread (parent/replies)
    Client->>Client: Render nested comments list
    end

    rect rgba(100, 150, 50, 0.5)
    Note over Client,DB: Post New Comment
    Client->>Server: POST /api/activities/{actId}/comments<br/>{body, parent_id?, Authorization: Bearer token}
    Server->>Server: Validate auth, activity exists, body length
    alt parent_id provided
      Server->>DB: Verify parent_id belongs to same activity
    end
    Server->>Server: Encrypt body
    Server->>DB: Insert comment row
    DB-->>Server: Confirm insert
    Server-->>Client: Return created comment metadata
    Client->>Client: Re-fetch and re-render thread
    end

    rect rgba(200, 100, 0, 0.5)
    Note over Client,DB: Delete Comment
    Client->>Server: DELETE /api/comments/{commentId}<br/>{Authorization: Bearer token}
    Server->>DB: Query comment + activity + user verification
    Server->>Server: Check: comment owner OR activity host
    alt Permission granted
      Server->>DB: Delete comment
      DB-->>Server: Confirm deletion
      Server-->>Client: Success response
    else Permission denied
      Server-->>Client: 403 Forbidden
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main addition: a threaded discussion/comments system for activities.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@public/course.html`:
- Around line 308-313: The inline script references DOM elements ('comment-form'
and 'comment-login-cta') before the comments markup exists, causing
getElementById to return null and abort execution; wrap the block that checks
token/actId and calls loadComments() (and any initialization that depends on DOM
nodes, e.g., loadActivity()) in a DOMContentLoaded listener or defer execution
until the DOM is ready, so element accesses
(document.getElementById('comment-form') and
document.getElementById('comment-login-cta')) are safe; alternatively move the
comments section markup above the script so the elements exist before the script
runs.
- Around line 326-333: The composer lacks an accessible label and the buttons
miss explicit types: add a programmatic label for the textarea (e.g., create a
<label> or use aria-label/aria-labelledby referencing the element with
id="comment-input" or the reply indicator id="reply-indicator") so the textarea
with id="comment-input" is properly announced, and set type="button" on both the
cancel button (which calls cancelReply()) and the Post Comment button (which
calls postComment()) to prevent accidental form submission if wrapped in a form.
- Around line 255-257: Replace the inline onclick reply construction (replyBtn)
with a safe data-attribute approach and a delegated click handler: stop emitting
"onclick" that calls startReply(...) and instead render the button with data
attributes like data-comment-id and data-comment-author (use escHtml for
HTML-escaping only), then add a single delegated click listener that reads
event.target.dataset and calls startReply(id, author). Also fix the DOM timing
errors by deferring code that uses document.getElementById('#comment-form') and
'#comment-login-cta' — either move the script to just before </body> or wrap
those calls in DOMContentLoaded (or a similar defer) so they never run on null.
Finally, add a proper <label> for the comment textarea and ensure all
form-action buttons explicitly include type="button" (or type="submit" where
appropriate) to prevent unintended submissions.

In `@schema.sql`:
- Around line 103-108: The comments.parent_id FK currently self-references
comments(id) without delete semantics, causing either FK errors or orphaned
replies when the delete API removes only one row; pick and apply a consistent
policy: either add "ON DELETE CASCADE" to the FOREIGN KEY (parent_id) REFERENCES
comments(id) in the schema and ensure the comment-deletion code (the delete
comment handler that currently deletes a single row) performs one-row deletes
(the DB will cascade the subtree), or add "ON DELETE SET NULL" in the schema and
update the delete handler to set child comments' parent_id to NULL (or run an
update query to nullify children) before deleting the parent; also update any
migration SQL and the corresponding logic in the comment-deletion function so
the DB schema and worker deletion semantics match.

In `@src/worker.py`:
- Around line 1318-1323: The current authorization grants any user with
user.get("role") == "host" global delete rights (is_admin), which duplicates the
activity host check and allows unrelated hosts to delete comments; remove the
is_admin branch or change it to check for an explicit admin role (e.g.,
user.get("role") == "admin") and ensure tokens actually mint that admin role;
update the authorization logic around is_owner, is_host, is_admin (and the
err("Permission denied", 403) return) so only the comment owner, the activity
host, or a true admin can delete comments.
- Around line 1233-1326: Dispatch currently doesn't route the new comment
endpoints, so add matches in _dispatch() to call api_get_comments,
api_post_comment, and api_delete_comment. Specifically: when the path matches
GET /api/activities/:id/comments call api_get_comments(req, env, activity_id,
enc_key); when it matches POST /api/activities/:id/comments call
api_post_comment(req, env, activity_id, user, enc_key); and when it matches
DELETE /api/comments/:id call api_delete_comment(req, env, comment_id, user).
Ensure you extract activity_id/comment_id from the route, and pass the same env,
req, enc_key and user objects used elsewhere in _dispatch() so the functions
api_get_comments, api_post_comment and api_delete_comment are reachable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: alphaonelabs/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ab0765c9-c3e2-4d12-b671-ed470c6367ed

📥 Commits

Reviewing files that changed from the base of the PR and between a08bafc and bd5e152.

📒 Files selected for processing (3)
  • public/course.html
  • schema.sql
  • src/worker.py

@ayesha1145 ayesha1145 force-pushed the feat/activity-comments branch from bd5e152 to 661e531 Compare March 25, 2026 19:43
@ayesha1145
Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit feedback:

  • Fixed critical: API routes now wired into _dispatch() — GET/POST /api/activities/:id/comments and DELETE /api/comments/:id are properly routed using re.fullmatch()
  • Fixed critical: DOM timing — comments form/login CTA initialization wrapped in DOMContentLoaded listener
  • Fixed critical: XSS in reply button — replaced inline onclick with data-id/data-author attributes and delegated click handler on document
  • Fixed critical: removed global host role bypass (is_admin) — only comment owner or activity host can delete
  • Fixed major: added ON DELETE CASCADE to parent_id FK in schema.sql — deleting a parent comment cascades to its replies
  • Fixed minor: added type="button" to cancel and post buttons, added sr-only label for textarea accessibility

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (3)
public/course.html (2)

255-257: ⚠️ Potential issue | 🔴 Critical

The XSS vulnerability in the reply button remains unfixed.

The escHtml() function escapes HTML entities (<, >, &, ") but does not escape single quotes. When an author's name contains a single quote (e.g., "O'Brien"), the inline onclick breaks:

<button onclick="startReply('123', 'O'Brien')">

This can be exploited for script injection. The delegated click handler at lines 308-312 was added but never used—the buttons don't have the reply-btn class.

🔧 Suggested fix: Use data attributes with delegated handler
   function renderComment(c, byParent) {
     const replies = (byParent[c.id] || []).map(r => renderComment(r, byParent)).join('');
     const initial = (c.author || '?')[0].toUpperCase();
     const date = new Date(c.created_at).toLocaleDateString();
     const replyBtn = token
-      ? '<button onclick="startReply(\'' + c.id + '\', \'' + escHtml(c.author) + '\')" class="text-xs text-indigo-500 hover:underline">Reply</button>'
+      ? '<button type="button" class="reply-btn text-xs text-indigo-500 hover:underline" data-id="' + escHtml(c.id) + '" data-author="' + escHtml(c.author) + '">Reply</button>'
       : '';

The existing delegated handler at lines 308-312 will then work correctly.

As per coding guidelines, **/*.html: Review HTML templates for XSS risks from unescaped user content.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public/course.html` around lines 255 - 257, The reply button uses inline
onclick with escHtml for the author which doesn't escape single quotes, causing
an XSS vector; instead render the button without inline JS and put the author/id
into data attributes (e.g., data-comment-id and data-author) and add the
"reply-btn" class so the existing delegated handler (the click listener that
expects ".reply-btn") can read dataset.commentId/dataset.author and call
startReply safely; update where the button is created (the expression that
currently builds the '<button onclick="startReply(...)"...'> string) to emit
data- attributes and class="reply-btn" and remove the inline onclick, keeping
escHtml for HTML content where needed.

339-341: 🧹 Nitpick | 🔵 Trivial

Place the label before the textarea for conventional accessibility.

While placing a <label> after its associated input is valid HTML, conventional practice (and many screen readers' expectations) is to place labels before inputs. Consider moving line 341 above line 339.

📝 Suggested reordering
     <div id="reply-indicator" class="hidden text-xs text-indigo-600 mb-2">
       Replying to a comment &mdash; <button type="button" onclick="cancelReply()" class="underline">cancel</button>
     </div>
+    <label for="comment-input" class="sr-only">Write a comment</label>
     <textarea id="comment-input" rows="3" placeholder="Write a comment..."
       class="w-full border border-slate-200 rounded-xl px-4 py-2 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-indigo-300 resize-none"></textarea>
-    <label for="comment-input" class="sr-only">Write a comment</label>

As per coding guidelines, **/*.html: Review HTML templates for accessibility (ARIA attributes, semantic elements).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public/course.html` around lines 339 - 341, Move the visually hidden label
for the comment input so it precedes the textarea element to follow conventional
accessibility order: place the <label for="comment-input" class="sr-only">Write
a comment</label> before the <textarea id="comment-input" ...> element
(referencing the id "comment-input" and the label's for attribute) and ensure
the for/id pairing remains unchanged.
src/worker.py (1)

1225-1322: ⚠️ Potential issue | 🔴 Critical

These handlers are unreachable—routing is missing from _dispatch().

The three comment handlers (api_get_comments, api_post_comment, api_delete_comment) are defined but never called. The _dispatch() function (lines 1155-1222) doesn't include routes for:

  • GET /api/activities/:id/comments
  • POST /api/activities/:id/comments
  • DELETE /api/comments/:id

Frontend calls to these endpoints will receive {"error": "API endpoint not found"} (404).

🔧 Add routing in `_dispatch()` before line 1220
        # Comments
        m = re.fullmatch(r"/api/activities/([A-Za-z0-9_-]+)/comments", path)
        if m and method == "GET":
            return await api_get_comments(request, env, m.group(1), env.ENCRYPTION_KEY)
        if m and method == "POST":
            user = verify_token(request.headers.get("Authorization"), env.JWT_SECRET)
            return await api_post_comment(request, env, m.group(1), user, env.ENCRYPTION_KEY)

        m = re.fullmatch(r"/api/comments/([A-Za-z0-9_-]+)", path)
        if m and method == "DELETE":
            user = verify_token(request.headers.get("Authorization"), env.JWT_SECRET)
            return await api_delete_comment(request, env, m.group(1), user)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/worker.py` around lines 1225 - 1322, The comment handlers
api_get_comments, api_post_comment, and api_delete_comment are never routed
because _dispatch() lacks matching routes; add route checks in _dispatch() to
match GET /api/activities/:id/comments -> call api_get_comments(request, env,
activity_id, env.ENCRYPTION_KEY), POST /api/activities/:id/comments ->
verify_token(...) then call api_post_comment(request, env, activity_id, user,
env.ENCRYPTION_KEY), and DELETE /api/comments/:id -> verify_token(...) then call
api_delete_comment(request, env, comment_id, user); place these checks alongside
the other API route matches in _dispatch() (use the same regex style used
elsewhere to extract the id).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@public/course.html`:
- Around line 308-312: The delegated click listener on document checks for
elements with class 'reply-btn' and calls startReply(id, author), but
renderComment() currently renders reply buttons with inline onclicks and no
'reply-btn' class or dataset attributes; update renderComment() to remove the
inline onclick, add class="reply-btn", and include data-id and data-author
attributes on the reply button so the existing
document.addEventListener('click', ...) can find
e.target.classList.contains('reply-btn') and call startReply using
e.target.dataset.id and e.target.dataset.author.

In `@src/worker.py`:
- Around line 367-382: The _DDL definition for the comments table is missing the
ON DELETE CASCADE on the parent_id foreign key, causing schema drift versus
schema.sql; update the comments table DDL in the _DDL variable so the FOREIGN
KEY (parent_id) REFERENCES comments(id) clause includes ON DELETE CASCADE (i.e.,
change the parent_id FK in the comments CREATE TABLE statement inside _DDL to
"FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE") to ensure
child replies are removed when a parent comment is deleted.
- Line 1230: The req parameter is unused in api_get_comments and
api_delete_comment; to silence linters and clarify intent, rename the parameter
to _req in both function signatures (api_get_comments and api_delete_comment) or
remove it if the handler interface permits—ensure you only change the parameter
name in the function definitions and not any internal references (there are
none), and run tests/lint to confirm no callers rely on the original name.
- Around line 1293-1296: Wrap the database insert (the await
env.DB.prepare(...).bind(cid, activity_id, user["id"], encrypt(text, enc_key),
parent_id).run() call) in a try/except block, call capture_exception(e) in the
except to log the error, and return or raise the same kind of graceful error
response your other handlers use (e.g., the pattern used in api_register /
api_create_activity) so callers get a consistent error path; ensure
capture_exception is imported where needed and preserve the existing variables
cid, activity_id, user["id"], encrypt(text, enc_key), and parent_id when moving
the call into the try block.
- Around line 1248-1258: Replace the imperative loop that builds comments with a
list comprehension for clarity and concision: construct comments = [ { "id":
r["id"], "body": decrypt(r["body"], enc_key), "parent_id": r["parent_id"],
"created_at": r["created_at"], "updated_at": r["updated_at"], "user_id":
r["user_id"], "author": decrypt(r["author_name"], enc_key) } for r in
(rows.results or []) ], preserving use of decrypt and enc_key and the same keys
from each r.

---

Duplicate comments:
In `@public/course.html`:
- Around line 255-257: The reply button uses inline onclick with escHtml for the
author which doesn't escape single quotes, causing an XSS vector; instead render
the button without inline JS and put the author/id into data attributes (e.g.,
data-comment-id and data-author) and add the "reply-btn" class so the existing
delegated handler (the click listener that expects ".reply-btn") can read
dataset.commentId/dataset.author and call startReply safely; update where the
button is created (the expression that currently builds the '<button
onclick="startReply(...)"...'> string) to emit data- attributes and
class="reply-btn" and remove the inline onclick, keeping escHtml for HTML
content where needed.
- Around line 339-341: Move the visually hidden label for the comment input so
it precedes the textarea element to follow conventional accessibility order:
place the <label for="comment-input" class="sr-only">Write a comment</label>
before the <textarea id="comment-input" ...> element (referencing the id
"comment-input" and the label's for attribute) and ensure the for/id pairing
remains unchanged.

In `@src/worker.py`:
- Around line 1225-1322: The comment handlers api_get_comments,
api_post_comment, and api_delete_comment are never routed because _dispatch()
lacks matching routes; add route checks in _dispatch() to match GET
/api/activities/:id/comments -> call api_get_comments(request, env, activity_id,
env.ENCRYPTION_KEY), POST /api/activities/:id/comments -> verify_token(...) then
call api_post_comment(request, env, activity_id, user, env.ENCRYPTION_KEY), and
DELETE /api/comments/:id -> verify_token(...) then call
api_delete_comment(request, env, comment_id, user); place these checks alongside
the other API route matches in _dispatch() (use the same regex style used
elsewhere to extract the id).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: alphaonelabs/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1f69f08e-5771-4740-a019-6f98e9380b8e

📥 Commits

Reviewing files that changed from the base of the PR and between bd5e152 and 661e531.

📒 Files selected for processing (3)
  • public/course.html
  • schema.sql
  • src/worker.py

@ayesha1145 ayesha1145 force-pushed the feat/activity-comments branch from 661e531 to 1e53385 Compare March 25, 2026 20:05
@ayesha1145 ayesha1145 force-pushed the feat/activity-comments branch from 1e53385 to f456156 Compare March 25, 2026 20:05
@ayesha1145
Copy link
Copy Markdown
Contributor Author

Addressed latest CodeRabbit feedback:

  • Fixed critical: ON DELETE CASCADE added to parent_id FK in _DDL (src/worker.py) — now matches schema.sql; deleting a parent comment cascades to its replies in both schema and in-memory DDL
  • Fixed: reply button now uses reply-btn class with data-id/data-author attributes — delegated click handler on document correctly triggers startReply()
  • Fixed: renamed unused req params to _req in api_get_comments and api_delete_comment (PEP 8)
  • Fixed: replaced imperative loop with list comprehension in api_get_comments (more Pythonic)
  • Fixed: wrapped INSERT in try/except with capture_exception() logging — consistent with rest of codebase

@ayesha1145
Copy link
Copy Markdown
Contributor Author

@A1L13N This PR is ready for review — all feedback has been addressed ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant