diff --git a/devplacepy/database.py b/devplacepy/database.py index e3bd1f6..bfd5e34 100644 --- a/devplacepy/database.py +++ b/devplacepy/database.py @@ -243,6 +243,29 @@ def get_user_votes(user_uid, target_uids): return {r["target_uid"]: r["value"] for r in rows} +def _build_comment_items(raw, user=None): + uids = [c["user_uid"] for c in raw] + cids = [c["uid"] for c in raw] + users = get_users_by_uids(uids) + ups, downs = get_vote_counts(cids) + my_votes = get_user_votes(user["uid"], cids) if user else {} + from devplacepy.utils import time_ago + from devplacepy.attachments import get_attachments_batch as _gab + atts_map = _gab("comment", cids) if "attachments" in db.tables else {} + items = {} + for c in raw: + items[c["uid"]] = { + "comment": c, + "author": users.get(c["user_uid"]), + "time_ago": time_ago(c["created_at"]), + "votes": {"up": ups.get(c["uid"], 0), "down": downs.get(c["uid"], 0)}, + "my_vote": my_votes.get(c["uid"], 0), + "children": [], + "attachments": atts_map.get(c["uid"], []), + } + return items + + def load_comments(target_type, target_uid, user=None): if "comments" not in db.tables: return [] @@ -252,25 +275,7 @@ def load_comments(target_type, target_uid, user=None): raw = list(comments_table.find(post_uid=target_uid, order_by=["created_at"])) if not raw: return [] - uids = [c["user_uid"] for c in raw] - cids = [c["uid"] for c in raw] - users = get_users_by_uids(uids) - ups, downs = get_vote_counts(cids) - my_votes = get_user_votes(user["uid"], cids) if user else {} - from devplacepy.utils import time_ago - from devplacepy.attachments import get_attachments_batch as _gab - atts_map = _gab("comment", cids) if "attachments" in db.tables else {} - cmap = {} - for c in raw: - cmap[c["uid"]] = { - "comment": c, - "author": users.get(c["user_uid"]), - "time_ago": time_ago(c["created_at"]), - "votes": {"up": ups.get(c["uid"], 0), "down": downs.get(c["uid"], 0)}, - "my_vote": my_votes.get(c["uid"], 0), - "children": [], - "attachments": atts_map.get(c["uid"], []), - } + cmap = _build_comment_items(raw, user) top = [] for item in cmap.values(): parent = item["comment"].get("parent_uid") @@ -281,6 +286,29 @@ def load_comments(target_type, target_uid, user=None): return top +def get_recent_comments_by_post_uids(post_uids, limit=3, user=None): + if not post_uids or "comments" not in db.tables: + return {} + placeholders, params = _in_clause(post_uids) + params["lim"] = limit + raw = list(db.query( + f"SELECT * FROM (" + f" SELECT *, ROW_NUMBER() OVER (" + f" PARTITION BY target_uid ORDER BY created_at DESC, id DESC" + f" ) AS rn FROM comments" + f" WHERE target_type='post' AND target_uid IN ({placeholders})" + f") WHERE rn <= :lim ORDER BY target_uid, created_at ASC", + **params, + )) + if not raw: + return {} + items = _build_comment_items(raw, user) + result = defaultdict(list) + for c in raw: + result[c["target_uid"]].append(items[c["uid"]]) + return dict(result) + + def get_attachments(resource_type: str, resource_uid: str) -> list: if "attachments" not in db.tables: return [] diff --git a/devplacepy/routers/feed.py b/devplacepy/routers/feed.py index 29f1f2f..9ef4e0f 100644 --- a/devplacepy/routers/feed.py +++ b/devplacepy/routers/feed.py @@ -1,7 +1,7 @@ import logging from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse -from devplacepy.database import get_table, get_daily_topic, get_users_by_uids, get_comment_counts_by_post_uids, get_site_stats, get_top_authors, paginate +from devplacepy.database import get_table, get_daily_topic, get_users_by_uids, get_comment_counts_by_post_uids, get_recent_comments_by_post_uids, get_site_stats, get_top_authors, paginate from devplacepy.attachments import get_attachments_batch from devplacepy.content import enrich_items from devplacepy.templating import templates @@ -48,8 +48,11 @@ async def feed_page(request: Request, tab: str = "all", topic: str = None, befor post_uids_list = [item["post"]["uid"] for item in posts] attachments_map = get_attachments_batch("post", post_uids_list) + recent_comments = get_recent_comments_by_post_uids(post_uids_list, 3, user) for item in posts: - item["attachments"] = attachments_map.get(item["post"]["uid"], []) + uid = item["post"]["uid"] + item["attachments"] = attachments_map.get(uid, []) + item["recent_comments"] = recent_comments.get(uid, []) seo_ctx = list_page_seo( request, diff --git a/devplacepy/static/css/base.css b/devplacepy/static/css/base.css index e8f4503..411961c 100644 --- a/devplacepy/static/css/base.css +++ b/devplacepy/static/css/base.css @@ -711,6 +711,10 @@ img { align-items: center; } +.comment-reply-form { + margin-top: 0.75rem; +} + .comment-form > a { flex-shrink: 0; line-height: 0; diff --git a/devplacepy/static/css/feed.css b/devplacepy/static/css/feed.css index 12a00a5..15f1fb3 100644 --- a/devplacepy/static/css/feed.css +++ b/devplacepy/static/css/feed.css @@ -566,3 +566,12 @@ color: var(--text-muted); font-size: 0.875rem; } + +.post-card-comments { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 0.75rem; +} diff --git a/devplacepy/static/js/CommentManager.js b/devplacepy/static/js/CommentManager.js index 10533a4..4bb68d7 100644 --- a/devplacepy/static/js/CommentManager.js +++ b/devplacepy/static/js/CommentManager.js @@ -4,28 +4,80 @@ export class CommentManager { } initCommentReply() { - document.querySelectorAll("[data-action='reply']").forEach((btn) => { - btn.addEventListener("click", (e) => { - e.preventDefault(); - const comment = btn.closest(".comment"); - const commentForm = document.querySelector(".comment-form"); - if (!comment || !commentForm) return; - const textarea = commentForm.querySelector("textarea"); - if (!textarea) return; - let parentInput = commentForm.querySelector('input[name="parent_uid"]'); - if (!parentInput) { - parentInput = document.createElement("input"); - parentInput.type = "hidden"; - parentInput.name = "parent_uid"; - commentForm.appendChild(parentInput); - } - const commentBody = comment.querySelector(".comment-body"); - if (commentBody && commentBody.dataset.commentUid) { - parentInput.value = commentBody.dataset.commentUid; - } - textarea.focus(); - textarea.scrollIntoView({ behavior: "smooth" }); + document.addEventListener("click", (e) => { + const btn = e.target.closest("[data-action='reply']"); + if (!btn) return; + e.preventDefault(); + this.toggleReplyForm(btn); + }); + } + + toggleReplyForm(btn) { + const comment = btn.closest(".comment"); + if (!comment) return; + const body = comment.querySelector(".comment-body"); + if (!body) return; + + const existing = body.querySelector(":scope > .comment-reply-form"); + if (existing) { + existing.remove(); + return; + } + + const template = document.getElementById("comment-reply-template"); + if (!template) return; + + const container = comment.closest(".post-card, .comments-section"); + const source = container && container.querySelector(".comment-form:not(.comment-reply-form)"); + if (!source) return; + const targetUid = source.querySelector('input[name="target_uid"]').value; + const targetType = source.querySelector('input[name="target_type"]').value; + + const fragment = template.content.cloneNode(true); + const form = fragment.querySelector(".comment-form"); + if (!form) return; + form.classList.add("comment-reply-form"); + form.querySelector('input[name="target_uid"]').value = targetUid; + form.querySelector('input[name="target_type"]').value = targetType; + + const parentInput = document.createElement("input"); + parentInput.type = "hidden"; + parentInput.name = "parent_uid"; + parentInput.value = body.dataset.commentUid || ""; + form.appendChild(parentInput); + + const cancel = document.createElement("button"); + cancel.type = "button"; + cancel.className = "comment-action-btn comment-reply-cancel"; + cancel.textContent = "Cancel"; + cancel.addEventListener("click", () => form.remove()); + form.querySelector(".comment-form-actions").appendChild(cancel); + + const actions = body.querySelector(":scope > .comment-actions"); + actions.insertAdjacentElement("afterend", form); + + this.enhanceForm(form); + const textarea = form.querySelector("textarea"); + if (textarea) textarea.focus(); + } + + enhanceForm(form) { + const enhancer = window.app && window.app.content; + if (enhancer) { + enhancer.initEmojiPickers(); + enhancer.initMentionInputs(); + enhancer.initAttachmentManagers(); + } + const textarea = form.querySelector("textarea"); + if (textarea) { + textarea.addEventListener("input", () => { + textarea.style.height = "auto"; + textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`; }); + } + form.addEventListener("submit", () => { + const btn = form.querySelector("button[type='submit']"); + if (btn) btn.disabled = true; }); } } diff --git a/devplacepy/templates/_comment_section.html b/devplacepy/templates/_comment_section.html index 74f2826..6527df9 100644 --- a/devplacepy/templates/_comment_section.html +++ b/devplacepy/templates/_comment_section.html @@ -1,52 +1,7 @@

Comments

- {% macro render_comment(item, depth=0) %} -
-
-
- - -
- {{ item.votes.up - item.votes.down }} -
- - -
-
- -
-
- - {{ item.author['username'] if item.author else '?' }} - - {% set _user = item.author %}{% set _class = "comment-author" %}{% include "_user_link.html" %} - {{ item.time_ago }} -
-
{{ item.comment['content'] }}
- {% set attachments = item.get('attachments', []) %} - {% if attachments %} - {% include "_attachment_display.html" %} - {% endif %} -
- - {% if user and item.comment['user_uid'] == user['uid'] %} -
- -
- {% endif %} -
- - {% if item.children %} -
- {% for child in item.children %} - {{ render_comment(child, depth + 1) }} - {% endfor %} -
- {% endif %} -
-
- {% endmacro %} + {% from "_comment.html" import render_comment with context %} {% for item in comments %} {{ render_comment(item, 0) }} diff --git a/devplacepy/templates/_post_card.html b/devplacepy/templates/_post_card.html index e8e6307..9ea7c2d 100644 --- a/devplacepy/templates/_post_card.html +++ b/devplacepy/templates/_post_card.html @@ -31,6 +31,15 @@ {% endif %} + {% if item.recent_comments %} + {% from "_comment.html" import render_comment with context %} +
+ {% for c in item.recent_comments %} + {{ render_comment(c, 0) }} + {% endfor %} +
+ {% endif %} + {% if _show_comment_form %} {% set _comment_target_uid = item.post['uid'] %}{% set _comment_target_type = "post" %}{% include "_comment_form.html" %} {% endif %} diff --git a/devplacepy/templates/base.html b/devplacepy/templates/base.html index d3a3efc..7d05a6f 100644 --- a/devplacepy/templates/base.html +++ b/devplacepy/templates/base.html @@ -167,6 +167,12 @@ {% endblock %} + {% if user %} + + {% endif %} + diff --git a/devplacepy/templates/feed.html b/devplacepy/templates/feed.html index ce42ddd..b3639c9 100644 --- a/devplacepy/templates/feed.html +++ b/devplacepy/templates/feed.html @@ -3,6 +3,7 @@ {% block extra_head %} + {% endblock %} {% block content %}

Feed