diff --git a/api/page.go b/api/page.go
index 4fda357..0a2fc92 100644
--- a/api/page.go
+++ b/api/page.go
@@ -7,4 +7,5 @@ type page struct {
Path string `json:"path"`
IsLocked bool `json:"isLocked"`
CommentCount int `json:"commentCount"`
+ StickyCommentHex string `json:"stickyCommentHex"`
}
diff --git a/api/page_get.go b/api/page_get.go
index c62d3d1..a7c2da4 100644
--- a/api/page_get.go
+++ b/api/page_get.go
@@ -11,20 +11,21 @@ func pageGet(domain string, path string) (page, error) {
}
statement := `
- SELECT isLocked, commentCount
+ SELECT isLocked, commentCount, stickyCommentHex
FROM pages
WHERE domain=$1 AND path=$2;
`
row := db.QueryRow(statement, domain, path)
p := page{Domain: domain, Path: path}
- if err := row.Scan(&p.IsLocked, &p.CommentCount); err != nil {
+ if err := row.Scan(&p.IsLocked, &p.CommentCount, &p.StickyCommentHex); err != nil {
if err == sql.ErrNoRows {
// If there haven't been any comments, there won't be a record for this
// page. The sane thing to do is return defaults.
// TODO: the defaults are hard-coded in two places: here and the schema
p.IsLocked = false
p.CommentCount = 0
+ p.StickyCommentHex = "none"
} else {
logger.Errorf("error scanning page: %v", err)
return page{}, errorInternal
diff --git a/api/page_update.go b/api/page_update.go
index 1930271..13416ad 100644
--- a/api/page_update.go
+++ b/api/page_update.go
@@ -13,12 +13,12 @@ func pageUpdate(p page) error {
// commentCount
statement := `
INSERT INTO
- pages (domain, path, isLocked)
- VALUES ($1, $2, $3 )
+ pages (domain, path, isLocked, stickyCommentHex)
+ VALUES ($1, $2, $3, $4 )
ON CONFLICT (domain, path) DO
- UPDATE SET isLocked = $3;
+ UPDATE SET isLocked = $3, stickyCommentHex = $4;
`
- _, err := db.Exec(statement, p.Domain, p.Path, p.IsLocked)
+ _, err := db.Exec(statement, p.Domain, p.Path, p.IsLocked, p.StickyCommentHex)
if err != nil {
logger.Errorf("error setting page attributes: %v", err)
return errorInternal
diff --git a/db/20181218183803-sticky-comments.sql b/db/20181218183803-sticky-comments.sql
new file mode 100644
index 0000000..911d79d
--- /dev/null
+++ b/db/20181218183803-sticky-comments.sql
@@ -0,0 +1,2 @@
+ALTER TABLE pages
+ ADD stickyCommentHex TEXT NOT NULL DEFAULT 'none';
diff --git a/frontend/js/commento.js b/frontend/js/commento.js
index 5b9fbf3..1d5f474 100644
--- a/frontend/js/commento.js
+++ b/frontend/js/commento.js
@@ -47,6 +47,7 @@
var ID_DOWNVOTE = "commento-comment-downvote-";
var ID_APPROVE = "commento-comment-approve-";
var ID_REMOVE = "commento-comment-remove-";
+ var ID_STICKY = "commento-comment-sticky-";
var ID_CONTENTS = "commento-comment-contents-";
var ID_SUBMIT_BUTTON = "commento-submit-button-";
var ID_FOOTER = "commento-footer";
@@ -67,6 +68,7 @@
var shownSubmitButton = {"root": false};
var chosenAnonymous = false;
var isLocked = false;
+ var stickyCommentHex = "none";
var shownReply = {};
var configuredOauths = [];
var loginBoxType = "signup";
@@ -341,6 +343,7 @@
isFrozen = resp.isFrozen;
isLocked = resp.attributes.isLocked;
+ stickyCommentHex = resp.attributes.stickyCommentHex;
comments = resp.comments;
commenters = resp.commenters;
@@ -449,10 +452,14 @@
commentsArea.innerHTML = "";
- if (!isLocked)
- append(mainArea, textareaCreate("root"));
+ if (isLocked) {
+ if (isAuthenticated)
+ append(mainArea, messageCreate("This thread is locked. You cannot add new comments."));
+ else
+ append(mainArea, textareaCreate("root"));
+ }
else
- append(mainArea, messageCreate("This thread is locked. You cannot create new comments."));
+ append(mainArea, textareaCreate("root"));
append(mainArea, commentsArea);
append(root, mainArea);
@@ -588,6 +595,10 @@
}
cur.sort(function(a, b) {
+ if (a.commentHex == stickyCommentHex)
+ return -Infinity;
+ if (b.commentHex == stickyCommentHex)
+ return Infinity;
return b.score - a.score;
});
@@ -608,6 +619,7 @@
var downvote = create("button");
var approve = create("button");
var remove = create("button");
+ var sticky = create("button");
var children = commentsRecurse(parentMap, comment.commentHex);
var contents = create("div");
var color = colorGet(commenter.name);
@@ -629,6 +641,7 @@
downvote.id = ID_DOWNVOTE + comment.commentHex;
approve.id = ID_APPROVE + comment.commentHex;
remove.id = ID_REMOVE + comment.commentHex;
+ sticky.id = ID_STICKY + comment.commentHex;
contents.id = ID_CONTENTS + comment.commentHex;
collapse.title = "Collapse";
@@ -638,6 +651,14 @@
reply.title = "Reply";
approve.title = "Approve";
remove.title = "Remove";
+ if (stickyCommentHex == comment.commentHex) {
+ if (isModerator)
+ sticky.title = "Unsticky";
+ else
+ sticky.title = "This comment has been stickied";
+ }
+ else
+ sticky.title = "Sticky";
card.style["borderLeft"] = "2px solid " + color;
name.innerText = commenter.name;
@@ -684,6 +705,11 @@
classAdd(approve, "option-approve");
classAdd(remove, "option-button");
classAdd(remove, "option-remove");
+ classAdd(sticky, "option-button");
+ if (stickyCommentHex == comment.commentHex)
+ classAdd(sticky, "option-unsticky");
+ else
+ classAdd(sticky, "option-sticky");
if (isAuthenticated) {
if (comment.direction > 0)
@@ -696,6 +722,7 @@
attrSet(collapse, "onclick", "commentCollapse('" + comment.commentHex + "')");
attrSet(approve, "onclick", "commentApprove('" + comment.commentHex + "')");
attrSet(remove, "onclick", "commentDelete('" + comment.commentHex + "')");
+ attrSet(sticky, "onclick", "commentSticky('" + comment.commentHex + "')");
if (isAuthenticated) {
if (comment.direction > 0) {
@@ -730,14 +757,19 @@
append(options, downvote);
append(options, upvote);
- if (!isLocked)
- append(options, reply);
+ append(options, reply);
if (isModerator) {
+ if (parentHex == "root")
+ append(options, sticky);
append(options, remove);
if (comment.state == "unapproved")
append(options, approve);
}
+ else {
+ if (stickyCommentHex == comment.commentHex)
+ append(options, sticky);
+ }
attrSet(options, "style", "width: " + ((options.childNodes.length+1)*32) + "px;");
for (var i = 0; i < options.childNodes.length; i++)
@@ -1278,6 +1310,7 @@
function pageUpdate(callback) {
var attributes = {
"isLocked": isLocked,
+ "stickyCommentHex": stickyCommentHex,
};
var json = {
@@ -1314,6 +1347,32 @@
}
+ global.commentSticky = function(commentHex) {
+ if (stickyCommentHex != "none") {
+ var sticky = $(ID_STICKY + stickyCommentHex);
+ classRemove(sticky, "option-unsticky");
+ classAdd(sticky, "option-sticky");
+ }
+
+ if (stickyCommentHex == commentHex)
+ stickyCommentHex = "none";
+ else
+ stickyCommentHex = commentHex;
+
+ pageUpdate(function(success) {
+ var sticky = $(ID_STICKY + commentHex);
+ if (stickyCommentHex == commentHex) {
+ classRemove(sticky, "option-sticky");
+ classAdd(sticky, "option-unsticky");
+ }
+ else {
+ classRemove(sticky, "option-unsticky");
+ classAdd(sticky, "option-sticky");
+ }
+ });
+ }
+
+
function mainAreaCreate() {
var mainArea = create("div");
diff --git a/frontend/sass/commento-buttons.scss b/frontend/sass/commento-buttons.scss
index 8831821..aaeaba7 100644
--- a/frontend/sass/commento-buttons.scss
+++ b/frontend/sass/commento-buttons.scss
@@ -83,6 +83,20 @@
background: $green-7;
}
+.commento-option-sticky,
+.commento-option-unsticky {
+ height: 14px;
+ width: 14px;
+@include mask-image('data:image/svg+xml;utf8,');
+ margin: 12px 6px 12px 6px;
+ background: $gray-5;
+}
+
+.commento-option-unsticky {
+ @include mask-image('data:image/svg+xml;utf8,');
+ background: $yellow-7;
+}
+
.commento-option-button:focus {
outline: none;
}