Compare commits
No commits in common. "adb07bb40ecde1154a35323dc7b9188281ca3097" and "7cd7ce2f1062b2b35a46ead86f73ab3051cd79db" have entirely different histories.
adb07bb40e
...
7cd7ce2f10
@ -243,29 +243,6 @@ 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 []
|
||||
@ -275,7 +252,25 @@ 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 []
|
||||
cmap = _build_comment_items(raw, user)
|
||||
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"], []),
|
||||
}
|
||||
top = []
|
||||
for item in cmap.values():
|
||||
parent = item["comment"].get("parent_uid")
|
||||
@ -286,29 +281,6 @@ 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 []
|
||||
|
||||
@ -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_recent_comments_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_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,11 +48,8 @@ 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:
|
||||
uid = item["post"]["uid"]
|
||||
item["attachments"] = attachments_map.get(uid, [])
|
||||
item["recent_comments"] = recent_comments.get(uid, [])
|
||||
item["attachments"] = attachments_map.get(item["post"]["uid"], [])
|
||||
|
||||
seo_ctx = list_page_seo(
|
||||
request,
|
||||
|
||||
@ -374,14 +374,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.admin-select,
|
||||
.admin-btn-sm,
|
||||
.admin-btn {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.admin-toolbar {
|
||||
flex-direction: column;
|
||||
|
||||
@ -258,12 +258,3 @@
|
||||
height: 72px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.attachment-preview .attachment-remove {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 0.875rem;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -702,84 +702,6 @@ img {
|
||||
.comment-author { font-size: 0.8125rem; font-weight: 600; color: var(--text-primary); }
|
||||
.comment-author:hover { color: var(--accent); }
|
||||
|
||||
.comment-form {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comment-reply-form {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.comment-form > a {
|
||||
flex-shrink: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.comment-form .mention-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.comment-form textarea {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
.comment-form-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.comment-form-actions button {
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
color: var(--text-muted);
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.comment-form-actions button:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
button.comment-form-submit {
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
button.comment-form-submit:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.comment-form {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.comment-form > a {
|
||||
display: none;
|
||||
}
|
||||
.comment-form .mention-wrapper {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
@ -1034,27 +956,10 @@ button.comment-form-submit:hover {
|
||||
.profile-tab,
|
||||
.sidebar-link,
|
||||
.topnav-link,
|
||||
.topnav-mobile-link,
|
||||
.dropdown-item,
|
||||
.btn,
|
||||
.comment-form-submit {
|
||||
.topnav-mobile-link {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.topnav-icon,
|
||||
.emoji-toggle-btn {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-card input,
|
||||
.modal-card textarea,
|
||||
.modal-card select {
|
||||
@ -1074,10 +979,6 @@ button.comment-form-submit:hover {
|
||||
.comment-text {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.grid-2col {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.card-link-host {
|
||||
|
||||
@ -56,24 +56,3 @@
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.bugs-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.bugs-header h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.bug-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.bug-card-header {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,6 +340,33 @@
|
||||
box-shadow: 0 6px 20px rgba(229, 57, 53, 0.5);
|
||||
}
|
||||
|
||||
.feed-comment-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.feed-comment-form input {
|
||||
flex: 1;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
min-height: auto;
|
||||
}
|
||||
.feed-comment-submit {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-radius: var(--radius);
|
||||
font-weight: 600;
|
||||
font-size: 0.8125rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.feed-comment-submit:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.feed-layout {
|
||||
grid-template-columns: 1fr;
|
||||
@ -380,6 +407,14 @@
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
|
||||
.feed-comment-form {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.feed-comment-form input {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
@ -405,11 +440,6 @@
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.feed-fab {
|
||||
bottom: calc(1rem + env(safe-area-inset-bottom));
|
||||
right: calc(1rem + env(safe-area-inset-right));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
@ -566,12 +596,3 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@ -307,12 +307,4 @@
|
||||
.gist-detail-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.gist-code-block pre {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,13 +60,10 @@
|
||||
}
|
||||
|
||||
.rendered-content table {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0.75rem 0;
|
||||
font-size: 0.8125rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.rendered-content th,
|
||||
@ -150,24 +147,3 @@
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.rendered-content ul,
|
||||
.rendered-content ol {
|
||||
padding-left: 1.125rem;
|
||||
}
|
||||
|
||||
.rendered-content h1 { font-size: 1.125rem; }
|
||||
.rendered-content h2 { font-size: 1.0625rem; }
|
||||
.rendered-content h3 { font-size: 0.9375rem; }
|
||||
.rendered-content h4 { font-size: 0.875rem; }
|
||||
|
||||
.rendered-content blockquote {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.rendered-content pre code {
|
||||
padding: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -40,9 +40,3 @@
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.mention-dropdown-item {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,7 +141,6 @@
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message-bubble.mine {
|
||||
@ -265,10 +264,3 @@
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.messages-send-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,8 +8,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@ -100,14 +98,4 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.notification-dismiss {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -77,7 +77,6 @@
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 0;
|
||||
position: relative;
|
||||
margin-left: calc(var(--comment-depth, 0) * 1.5rem);
|
||||
}
|
||||
|
||||
.comment-replies {
|
||||
@ -188,6 +187,63 @@
|
||||
background: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comment-form > a {
|
||||
flex-shrink: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.comment-form textarea {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
.comment-form-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.comment-form-actions button {
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
color: var(--text-muted);
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.comment-form-actions button:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
button.comment-form-submit {
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
button.comment-form-submit:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.comment-thread-line {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
@ -217,7 +273,6 @@
|
||||
|
||||
.comment {
|
||||
gap: 0.5rem;
|
||||
margin-left: calc(var(--comment-depth, 0) * 0.5rem);
|
||||
}
|
||||
|
||||
.comment-votes {
|
||||
@ -228,3 +283,17 @@
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.comment-form {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.comment-form > a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment-form textarea {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@
|
||||
line-height: 1.5;
|
||||
color: var(--text-code, #e0e0e0);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
|
||||
@ -4,80 +4,28 @@ export class CommentManager {
|
||||
}
|
||||
|
||||
initCommentReply() {
|
||||
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`;
|
||||
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" });
|
||||
});
|
||||
}
|
||||
form.addEventListener("submit", () => {
|
||||
const btn = form.querySelector("button[type='submit']");
|
||||
if (btn) btn.disabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
{% macro render_comment(item, depth=0) %}
|
||||
<div class="comment" style="--comment-depth: {{ depth }};" data-depth="{{ depth }}">
|
||||
<div class="comment-votes">
|
||||
<form method="POST" action="/votes/comment/{{ item.comment['uid'] }}">
|
||||
<input type="hidden" name="value" value="1">
|
||||
<button type="submit" class="comment-vote-btn vote-up{% if item.my_vote == 1 %} voted{% endif %}">+</button>
|
||||
</form>
|
||||
<span class="comment-vote-count" data-vote-count="{{ item.comment['uid'] }}">{{ item.votes.up - item.votes.down }}</span>
|
||||
<form method="POST" action="/votes/comment/{{ item.comment['uid'] }}">
|
||||
<input type="hidden" name="value" value="-1">
|
||||
<button type="submit" class="comment-vote-btn vote-down{% if item.my_vote == -1 %} voted{% endif %}">-</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="comment-body" id="comment-{{ item.comment['uid'] }}" data-comment-uid="{{ item.comment['uid'] }}">
|
||||
<div class="comment-header">
|
||||
<a href="/profile/{{ item.author['username'] if item.author else '#' }}">
|
||||
<img src="{{ avatar_url('multiavatar', item.author['username'] if item.author else '?', 32) }}" class="avatar-img avatar-sm" alt="{{ item.author['username'] if item.author else '?' }}" loading="lazy">
|
||||
</a>
|
||||
{% set _user = item.author %}{% set _class = "comment-author" %}{% include "_user_link.html" %}
|
||||
<span class="comment-time">{{ item.time_ago }}</span>
|
||||
</div>
|
||||
<div class="comment-text rendered-content" data-render>{{ item.comment['content'] }}</div>
|
||||
{% set attachments = item.get('attachments', []) %}
|
||||
{% if attachments %}
|
||||
{% include "_attachment_display.html" %}
|
||||
{% endif %}
|
||||
<div class="comment-actions">
|
||||
<button class="comment-action-btn" data-action="reply"><span class="icon">💬</span> Reply</button>
|
||||
{% if user and item.comment['user_uid'] == user['uid'] %}
|
||||
<form method="POST" action="/comments/delete/{{ item.comment['uid'] }}" class="inline-form">
|
||||
<button type="submit" class="comment-action-btn"><span class="icon">🗑️</span> Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if item.children %}
|
||||
<div class="comment-replies">
|
||||
{% for child in item.children %}
|
||||
{{ render_comment(child, depth + 1) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@ -1,17 +0,0 @@
|
||||
{% if user %}
|
||||
<form class="comment-form" method="POST" action="/comments/create">
|
||||
<input type="hidden" name="target_uid" value="{{ _comment_target_uid }}">
|
||||
<input type="hidden" name="target_type" value="{{ _comment_target_type }}">
|
||||
<a href="/profile/{{ user['username'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ user['username'] }}" loading="lazy">
|
||||
</a>
|
||||
<textarea name="content" placeholder="Your opinion goes here..." required maxlength="1000" class="emoji-picker-target" data-mention></textarea>
|
||||
<div class="comment-form-actions">
|
||||
<div class="attachment-upload-container" data-attachment-upload
|
||||
data-max-size="{{ max_upload_size_mb() }}"
|
||||
data-max-files="{{ max_attachments_per_resource() }}"
|
||||
data-allowed-types="{{ allowed_file_types() }}"></div>
|
||||
<button type="submit" class="comment-form-submit"><span class="icon">📤</span> Post</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
@ -1,7 +1,52 @@
|
||||
<section class="comments-section">
|
||||
<h3>Comments</h3>
|
||||
|
||||
{% from "_comment.html" import render_comment with context %}
|
||||
{% macro render_comment(item, depth=0) %}
|
||||
<div class="comment" style="margin-left: {{ depth * 1.5 }}rem;" data-depth="{{ depth }}">
|
||||
<div class="comment-votes">
|
||||
<form method="POST" action="/votes/comment/{{ item.comment['uid'] }}">
|
||||
<input type="hidden" name="value" value="1">
|
||||
<button type="submit" class="comment-vote-btn vote-up{% if item.my_vote == 1 %} voted{% endif %}">+</button>
|
||||
</form>
|
||||
<span class="comment-vote-count" data-vote-count="{{ item.comment['uid'] }}">{{ item.votes.up - item.votes.down }}</span>
|
||||
<form method="POST" action="/votes/comment/{{ item.comment['uid'] }}">
|
||||
<input type="hidden" name="value" value="-1">
|
||||
<button type="submit" class="comment-vote-btn vote-down{% if item.my_vote == -1 %} voted{% endif %}">-</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="comment-body" id="comment-{{ item.comment['uid'] }}" data-comment-uid="{{ item.comment['uid'] }}">
|
||||
<div class="comment-header">
|
||||
<a href="/profile/{{ item.author['username'] if item.author else '#' }}">
|
||||
<img src="{{ avatar_url('multiavatar', item.author['username'] if item.author else '?', 32) }}" class="avatar-img avatar-sm" alt="{{ item.author['username'] if item.author else '?' }}" loading="lazy">
|
||||
</a>
|
||||
{% set _user = item.author %}{% set _class = "comment-author" %}{% include "_user_link.html" %}
|
||||
<span class="comment-time">{{ item.time_ago }}</span>
|
||||
</div>
|
||||
<div class="comment-text rendered-content" data-render>{{ item.comment['content'] }}</div>
|
||||
{% set attachments = item.get('attachments', []) %}
|
||||
{% if attachments %}
|
||||
{% include "_attachment_display.html" %}
|
||||
{% endif %}
|
||||
<div class="comment-actions">
|
||||
<button class="comment-action-btn" data-action="reply"><span class="icon">💬</span> Reply</button>
|
||||
{% if user and item.comment['user_uid'] == user['uid'] %}
|
||||
<form method="POST" action="/comments/delete/{{ item.comment['uid'] }}" class="inline-form">
|
||||
<button type="submit" class="comment-action-btn"><span class="icon">🗑️</span> Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if item.children %}
|
||||
<div class="comment-replies">
|
||||
{% for child in item.children %}
|
||||
{{ render_comment(child, depth + 1) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% for item in comments %}
|
||||
{{ render_comment(item, 0) }}
|
||||
@ -9,5 +54,21 @@
|
||||
<p class="no-comments-msg">No comments yet. Start the discussion.</p>
|
||||
{% endfor %}
|
||||
|
||||
{% set _comment_target_uid = target_uid %}{% set _comment_target_type = target_type %}{% include "_comment_form.html" %}
|
||||
{% if user %}
|
||||
<form class="comment-form" method="POST" action="/comments/create">
|
||||
<input type="hidden" name="target_uid" value="{{ target_uid }}">
|
||||
<input type="hidden" name="target_type" value="{{ target_type }}">
|
||||
<a href="/profile/{{ user['username'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ user['username'] }}" loading="lazy">
|
||||
</a>
|
||||
<textarea name="content" placeholder="Your opinion goes here..." required maxlength="1000" class="emoji-picker-target" data-mention></textarea>
|
||||
<div class="comment-form-actions">
|
||||
<div class="attachment-upload-container" data-attachment-upload
|
||||
data-max-size="{{ max_upload_size_mb() }}"
|
||||
data-max-files="{{ max_attachments_per_resource() }}"
|
||||
data-allowed-types="{{ allowed_file_types() }}"></div>
|
||||
<button type="submit" class="comment-form-submit"><span class="icon">📤</span> Post</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@ -31,16 +31,12 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if item.recent_comments %}
|
||||
{% from "_comment.html" import render_comment with context %}
|
||||
<div class="post-card-comments">
|
||||
{% for c in item.recent_comments %}
|
||||
{{ render_comment(c, 0) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if _show_comment_form %}
|
||||
{% set _comment_target_uid = item.post['uid'] %}{% set _comment_target_type = "post" %}{% include "_comment_form.html" %}
|
||||
{% if _show_comment_form and user %}
|
||||
<form class="feed-comment-form" method="POST" action="/comments/create">
|
||||
<input type="hidden" name="post_uid" value="{{ item.post['uid'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', user['username'], 24) }}" class="avatar-img avatar-24" alt="" loading="lazy">
|
||||
<input type="text" name="content" placeholder="Your opinion goes here..." maxlength="1000" autocomplete="off" data-mention>
|
||||
<button type="submit" class="feed-comment-submit">Post</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
||||
@ -167,12 +167,6 @@
|
||||
</footer>
|
||||
{% endblock %}
|
||||
|
||||
{% if user %}
|
||||
<template id="comment-reply-template">
|
||||
{% set _comment_target_uid = "" %}{% set _comment_target_type = "" %}{% include "_comment_form.html" %}
|
||||
</template>
|
||||
{% endif %}
|
||||
|
||||
<script defer src="/static/vendor/marked.umd.js"></script>
|
||||
<script defer src="/static/vendor/highlight.min.js"></script>
|
||||
<script defer src="/static/vendor/purify.min.js"></script>
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="/static/css/feed.css">
|
||||
<link rel="stylesheet" href="/static/css/sidebar.css">
|
||||
<link rel="stylesheet" href="/static/css/post.css">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1 class="sr-only">Feed</h1>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user