Skip to content

Commit

Permalink
Sticky forum posts
Browse files Browse the repository at this point in the history
Partial lichess-org#931, and also seen this a few times in Slack.

If you go to the page of a forum category, or to the forum page of a team, a sticky post will _appear on page 1_ and have a _green color and a info-icon_.
Who can make posts sticky? People with the ModerateForum role, and in case of a team, the team owner.
Posts can be un-stickified later.
  • Loading branch information
thomas-daniels committed Jul 7, 2017
1 parent 203b6ea commit 8639a12
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 6 deletions.
3 changes: 2 additions & 1 deletion app/controllers/ForumCateg.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ object ForumCateg extends LilaController with ForumController {
OptionFuOk(categApi.show(slug, page, ctx.troll)) {
case (categ, topics) => for {
canWrite <- isGrantedWrite(categ.slug)
stickyPosts <- categApi.stickyPosts(categ.slug, ctx.troll)
_ <- Env.user.lightUserApi preloadMany topics.currentPageResults.flatMap(_.lastPostUserId)
} yield html.forum.categ.show(categ, topics, canWrite)
} yield html.forum.categ.show(categ, topics, canWrite, (stickyPosts.nonEmpty && page == 1).fold(Some(stickyPosts), None))
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions app/controllers/ForumTopic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ object ForumTopic extends LilaController with ForumController {
}
}

def sticky(categSlug: String, slug: String) = Auth { implicit ctx => me =>
CategGrantMod(categSlug) {
OptionFuRedirect(topicApi.show(categSlug, slug, 1, ctx.troll)) {
case (categ, topic, pag) => topicApi.toggleSticky(categ, topic, me) inject
routes.ForumTopic.show(categSlug, slug, pag.nbPages)
}
}
}

/**
* Returns a list of the usernames of people participating in a forum topic conversation
*/
Expand Down
17 changes: 15 additions & 2 deletions app/views/forum/categ/show.scala.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@(categ: lila.forum.Categ, topics: Paginator[lila.forum.TopicView], canWrite: Boolean)(implicit ctx: Context)
@(categ: lila.forum.Categ, topics: Paginator[lila.forum.TopicView], canWrite: Boolean, stickyPosts: Option[List[lila.forum.TopicView]])(implicit ctx: Context)

@newTopicButton = {
@if(canWrite) {
Expand Down Expand Up @@ -36,7 +36,6 @@
@forum.pagination(routes.ForumCateg.show(categ.slug, 1), topics, showPost = false)
@newTopicButton
</div>

<table class="forum_table forum_topics_list">
<thead>
<tr class="thead">
Expand All @@ -47,6 +46,20 @@
</tr>
</thead>
<tbody>
@stickyPosts.map { postList =>
@postList.map { topic =>
<tr>
<td class="subject"><a href="@routes.ForumTopic.show(categ.slug, topic.slug)" class="sticky"><span data-icon="&#xe005;"></span>@topic.name</a></td>
<td class="right">@topic.views.localize</td>
<td class="right">@topic.nbReplies.localize</td>
<td class="last_post">
@topic.lastPost.map { post =>
<a href="@routes.ForumTopic.show(categ.slug, topic.slug, topic.lastPage)#@post.number">@momentFromNow(post.createdAt)</a><br />@trans.by(authorLink(post))
}
</td>
</tr>
}
}
@topics.currentPageResults.map { topic =>
<tr>
<td class="subject"><a href="@routes.ForumTopic.show(categ.slug, topic.slug)">@topic.name</a></td>
Expand Down
3 changes: 3 additions & 0 deletions app/views/forum/topic/show.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@
<form class="mod" method="post" action="@routes.ForumTopic.close(categ.slug, topic.slug)">
<button class="button">@topic.closed.fold("Reopen", "Close")</button>
</form>
<form class="mod" method="post" action="@routes.ForumTopic.sticky(categ.slug, topic.slug)">
<button class="button">@topic.sticky.fold("Un-sticky", "Sticky")</button>
</form>
}

<div class="topicReply">
Expand Down
1 change: 1 addition & 0 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ GET /forum/participants/:topicId controllers.ForumTopic.participants(topic
GET /forum/:categSlug/:slug controllers.ForumTopic.show(categSlug: String, slug: String, page: Int ?= 1)
POST /forum/:categSlug/:slug/close controllers.ForumTopic.close(categSlug: String, slug: String)
POST /forum/:categSlug/:slug/hide controllers.ForumTopic.hide(categSlug: String, slug: String)
POST /forum/:categSlug/:slug/sticky controllers.ForumTopic.sticky(categSlug: String, slug: String)
POST /forum/:categSlug/:slug/new controllers.ForumPost.create(categSlug: String, slug: String, page: Int ?= 1)
POST /forum/:categSlug/delete/:id controllers.ForumPost.delete(categSlug: String, id: String)
POST /forum/post/:id controllers.ForumPost.edit(id: String)
Expand Down
44 changes: 43 additions & 1 deletion modules/forum/src/main/BSONHandlers.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package lila.forum

import lila.db.BSON
import lila.db.BSON.LoggingHandler
import lila.db.dsl._
import reactivemongo.bson._
import org.joda.time.DateTime

private object BSONHandlers {

Expand All @@ -11,5 +13,45 @@ private object BSONHandlers {
implicit val PostEditBSONHandler = Macros.handler[OldVersion]
implicit val PostBSONHandler = Macros.handler[Post]

implicit val TopicBSONHandler = LoggingHandler(logger)(Macros.handler[Topic])
implicit val TopicBSONHandler = LoggingHandler(logger)(new BSONHandler[BSONDocument, Topic] {
import Topic.BSONFields._

def read(doc: BSONDocument): Topic = Topic(
_id = doc.getAs[String](id).get,
categId = doc.getAs[String](categId).get,
slug = doc.getAs[String](slug).get,
name = doc.getAs[String](name).get,
views = doc.getAs[Int](views).get,
createdAt = doc.getAs[DateTime](createdAt).get,
updatedAt = doc.getAs[DateTime](updatedAt).get,
nbPosts = doc.getAs[Int](nbPosts).get,
lastPostId = doc.getAs[String](lastPostId).get,
updatedAtTroll = doc.getAs[DateTime](updatedAtTroll).get,
nbPostsTroll = doc.getAs[Int](nbPostsTroll).get,
lastPostIdTroll = doc.getAs[String](lastPostIdTroll).get,
troll = doc.getAs[Boolean](troll).get,
closed = doc.getAs[Boolean](closed).get,
hidden = doc.getAs[Boolean](hidden).get,
sticky = doc.getAs[Boolean](sticky) getOrElse false
)

def write(o: Topic): BSONDocument = BSONDocument(
id -> o.id,
categId -> o.categId,
slug -> o.slug,
name -> o.name,
views -> o.views,
createdAt -> o.createdAt,
updatedAt -> o.updatedAt,
nbPosts -> o.nbPosts,
lastPostId -> o.lastPostId,
updatedAtTroll -> o.updatedAtTroll,
nbPostsTroll -> o.nbPostsTroll,
lastPostIdTroll -> o.lastPostIdTroll,
troll -> o.troll,
closed -> o.closed,
hidden -> o.hidden,
sticky -> o.sticky
)
})
}
5 changes: 5 additions & 0 deletions modules/forum/src/main/CategApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ private[forum] final class CategApi(env: Env) {
optionT(env.topicApi.paginator(categ, page, troll) map { (categ, _).some })
}

def stickyPosts(slug: String, troll: Boolean): Fu[List[TopicView]] =
(CategRepo bySlug slug) flatMap { categ =>
env.topicApi.getSticky(categ.get, troll)
}

def denormalize(categ: Categ): Funit = for {
topics TopicRepo byCateg categ
topicIds = topics map (_.id)
Expand Down
25 changes: 23 additions & 2 deletions modules/forum/src/main/Topic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ case class Topic(
lastPostIdTroll: String,
troll: Boolean,
closed: Boolean,
hidden: Boolean
hidden: Boolean,
sticky: Boolean
) {

def id = _id
Expand Down Expand Up @@ -73,6 +74,26 @@ object Topic {
lastPostIdTroll = "",
troll = troll,
closed = false,
hidden = hidden
hidden = hidden,
sticky = false
)

object BSONFields {
val id = "_id"
val categId = "categId"
val slug = "slug"
val name = "name"
val views = "views"
val createdAt = "createdAt"
val updatedAt = "updatedAt"
val nbPosts = "nbPosts"
val lastPostId = "lastPostId"
val updatedAtTroll = "updatedAtTroll"
val nbPostsTroll = "nbPostsTroll"
val lastPostIdTroll = "lastPostIdTroll"
val troll = "troll"
val closed = "closed"
val hidden = "hidden"
val sticky = "sticky"
}
}
18 changes: 18 additions & 0 deletions modules/forum/src/main/TopicApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ private[forum] final class TopicApi(
)
}

def getSticky(categ: Categ, troll: Boolean): Fu[List[TopicView]] =
TopicRepo.stickyByCateg(categ) flatMap { topics =>
scala.concurrent.Future.sequence(topics map {
topic =>
{
env.postColl.byId[Post](topic lastPostId troll) map { post =>
TopicView(categ, topic, post, env.postApi lastPageOf topic, troll)
}
}
})
}

def delete(categ: Categ, topic: Topic): Funit =
PostRepo.idsByTopicId(topic.id) flatMap { postIds =>
(PostRepo removeByTopic topic.id zip env.topicColl.remove($id(topic.id))) >>
Expand All @@ -125,6 +137,12 @@ private[forum] final class TopicApi(
} >>- env.recent.invalidate
}

def toggleSticky(categ: Categ, topic: Topic, mod: User): Funit =
TopicRepo.sticky(topic.id, !topic.sticky) >> {
MasterGranter(_.ModerateForum)(mod) ??
modLog.toggleStickyTopic(mod.id, categ.name, topic.name, topic.sticky)
}

def denormalize(topic: Topic): Funit = for {
nbPosts PostRepo countByTopics List(topic.id)
lastPost PostRepo lastByTopics List(topic.id)
Expand Down
7 changes: 7 additions & 0 deletions modules/forum/src/main/TopicRepo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,18 @@ sealed abstract class TopicRepo(troll: Boolean) {
def hide(id: String, value: Boolean): Funit =
coll.updateField($id(id), "hidden", value).void

def sticky(id: String, value: Boolean): Funit =
coll.updateField($id(id), "sticky", value).void

def byCateg(categ: Categ): Fu[List[Topic]] =
coll.list[Topic](byCategQuery(categ))

def byTree(categSlug: String, slug: String): Fu[Option[Topic]] =
coll.uno[Topic]($doc("categId" -> categSlug, "slug" -> slug) ++ trollFilter)

def stickyByCateg(categ: Categ): Fu[List[Topic]] =
coll.list[Topic](byCategQuery(categ) ++ byStickyQuery())

def nextSlug(categ: Categ, name: String, it: Int = 1): Fu[String] = {
val slug = Topic.nameToId(name) + ~(it != 1).option("-" + it)
// also take troll topic into accounts
Expand All @@ -45,4 +51,5 @@ sealed abstract class TopicRepo(troll: Boolean) {
coll.incFieldUnchecked($id(topic.id), "views")

def byCategQuery(categ: Categ) = $doc("categId" -> categ.slug) ++ trollFilter
def byStickyQuery() = $doc("sticky" -> true)
}
4 changes: 4 additions & 0 deletions modules/mod/src/main/Modlog.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ case class Modlog(
case Modlog.closeTopic => "close topic"
case Modlog.showTopic => "show topic"
case Modlog.hideTopic => "unfeature topic"
case Modlog.stickyTopic => "sticky topic"
case Modlog.unstickyTopic => "un-sticky topic"
case Modlog.setTitle => "set FIDE title"
case Modlog.removeTitle => "remove FIDE title"
case Modlog.setEmail => "set email address"
Expand Down Expand Up @@ -68,6 +70,8 @@ object Modlog {
val closeTopic = "closeTopic"
val showTopic = "showTopic"
val hideTopic = "hideTopic"
val stickyTopic = "stickyTopic"
val unstickyTopic = "unstickyTopic"
val setTitle = "setTitle"
val removeTitle = "removeTitle"
val setEmail = "setEmail"
Expand Down
6 changes: 6 additions & 0 deletions modules/mod/src/main/ModlogApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ final class ModlogApi(coll: Coll) {
))
}

def toggleStickyTopic(mod: String, categ: String, topic: String, sticky: Boolean) = add {
Modlog(mod, none, sticky ? Modlog.stickyTopic | Modlog.unstickyTopic, details = Some(
categ + " / " + topic
))
}

def deleteQaQuestion(mod: String, user: String, title: String) = add {
Modlog(mod, user.some, Modlog.deleteQaQuestion, details = Some(title take 140))
}
Expand Down
8 changes: 8 additions & 0 deletions public/stylesheets/forum.css
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,11 @@ body.dark .textcomplete-dropdown li:hover,
body.dark .textcomplete-dropdown .active {
background-color: #3e3e3e;
}

a.sticky {
color: green;
}

a.sticky > span {
margin-right: 0.6em;
}

0 comments on commit 8639a12

Please sign in to comment.