diff --git a/app/views/coach.scala b/app/views/coach.scala
index a46cf19534d6..831ecd31871c 100644
--- a/app/views/coach.scala
+++ b/app/views/coach.scala
@@ -22,7 +22,7 @@ def show(
)(using ctx: Context) = ui.show(
c,
studies = studies.map(s => st.article(cls := "study")(views.study.bits.widget(s, h3))),
- posts = posts.map(views.ublog.ui.card(_))
+ posts = posts.map(p => views.ublog.ui.card(p, showSticky = ~p.sticky))
)
def edit(c: lila.coach.Coach.WithUser, form: Form[?])(using ctx: Context) =
diff --git a/app/views/user/show/header.scala b/app/views/user/show/header.scala
index c64e8c426d0d..6ea5d2139677 100644
--- a/app/views/user/show/header.scala
+++ b/app/views/user/show/header.scala
@@ -256,7 +256,7 @@ object header:
,
(ctx.kid.no && info.ublog.so(_.latests).nonEmpty).option(
div(cls := "user-show__blog ublog-post-cards")(
- info.ublog.so(_.latests).map(views.ublog.ui.card(_))
+ info.ublog.so(_.latests).map(p => views.ublog.ui.card(p, showSticky = ~p.sticky))
)
),
div(cls := "angles number-menu number-menu--tabs menu-box-pop")(
diff --git a/modules/coreI18n/src/main/key.scala b/modules/coreI18n/src/main/key.scala
index 83b8ce92adbb..b1cc3dc3a245 100644
--- a/modules/coreI18n/src/main/key.scala
+++ b/modules/coreI18n/src/main/key.scala
@@ -2762,6 +2762,8 @@ object I18nKey:
val `createBlogDiscussionHelp`: I18nKey = "ublog:createBlogDiscussionHelp"
val `publishOnYourBlog`: I18nKey = "ublog:publishOnYourBlog"
val `publishHelp`: I18nKey = "ublog:publishHelp"
+ val `stickyPost`: I18nKey = "ublog:stickyPost"
+ val `stickyPostHelp`: I18nKey = "ublog:stickyPostHelp"
val `xPublishedY`: I18nKey = "ublog:xPublishedY"
val `thisPostIsPublished`: I18nKey = "ublog:thisPostIsPublished"
val `thisIsADraft`: I18nKey = "ublog:thisIsADraft"
diff --git a/modules/ublog/src/main/UblogApi.scala b/modules/ublog/src/main/UblogApi.scala
index 8f11b514a83c..38b2153ec141 100644
--- a/modules/ublog/src/main/UblogApi.scala
+++ b/modules/ublog/src/main/UblogApi.scala
@@ -79,7 +79,7 @@ final class UblogApi(
def latestPosts(blogId: UblogBlog.Id, nb: Int): Fu[List[UblogPost.PreviewPost]] =
colls.post
.find($doc("blog" -> blogId, "live" -> true), previewPostProjection.some)
- .sort($doc("lived.at" -> -1))
+ .sort($doc("sticky" -> -1, "lived.at" -> -1))
.cursor[UblogPost.PreviewPost](ReadPref.sec)
.list(nb)
diff --git a/modules/ublog/src/main/UblogBsonHandlers.scala b/modules/ublog/src/main/UblogBsonHandlers.scala
index 60610011e26b..cfff8d95824e 100644
--- a/modules/ublog/src/main/UblogBsonHandlers.scala
+++ b/modules/ublog/src/main/UblogBsonHandlers.scala
@@ -31,5 +31,6 @@ private object UblogBsonHandlers:
"image" -> true,
"created" -> true,
"lived" -> true,
- "topics" -> true
+ "topics" -> true,
+ "sticky" -> true
)
diff --git a/modules/ublog/src/main/UblogForm.scala b/modules/ublog/src/main/UblogForm.scala
index 4c56f015e577..c220f544c82f 100644
--- a/modules/ublog/src/main/UblogForm.scala
+++ b/modules/ublog/src/main/UblogForm.scala
@@ -22,6 +22,7 @@ final class UblogForm(val captcher: CaptchaApi, langList: LangList):
"topics" -> optional(text),
"live" -> boolean,
"discuss" -> boolean,
+ "sticky" -> boolean,
"gameId" -> of[GameId],
"move" -> text
)(UblogPostData.apply)(unapply)
@@ -40,6 +41,7 @@ final class UblogForm(val captcher: CaptchaApi, langList: LangList):
topics = post.topics.mkString(", ").some,
live = post.live,
discuss = ~post.discuss,
+ sticky = ~post.sticky,
gameId = GameId(""),
move = ""
)
@@ -56,6 +58,7 @@ object UblogForm:
topics: Option[String],
live: Boolean,
discuss: Boolean,
+ sticky: Boolean,
gameId: GameId,
move: String
) extends WithCaptcha:
@@ -72,6 +75,7 @@ object UblogForm:
image = none,
live = false,
discuss = Option(false),
+ sticky = Option(false),
created = UblogPost.Recorded(user.id, nowInstant),
updated = none,
lived = none,
@@ -92,6 +96,7 @@ object UblogForm:
topics = topics.so(UblogTopic.fromStrList),
live = live,
discuss = Option(discuss),
+ sticky = Option(sticky),
updated = UblogPost.Recorded(user.id, nowInstant).some,
lived = prev.lived.orElse(live.option(UblogPost.Recorded(user.id, nowInstant)))
)
diff --git a/modules/ublog/src/main/UblogPost.scala b/modules/ublog/src/main/UblogPost.scala
index 0f67a3505f14..4a8e05a6ce2d 100644
--- a/modules/ublog/src/main/UblogPost.scala
+++ b/modules/ublog/src/main/UblogPost.scala
@@ -17,6 +17,7 @@ case class UblogPost(
topics: List[UblogTopic],
live: Boolean,
discuss: Option[Boolean],
+ sticky: Option[Boolean],
created: UblogPost.Recorded,
updated: Option[UblogPost.Recorded],
lived: Option[UblogPost.Recorded],
@@ -73,6 +74,7 @@ object UblogPost:
created: Recorded,
updated: Option[Recorded],
lived: Option[Recorded],
+ sticky: Option[Boolean],
topics: List[UblogTopic]
) extends BasePost
diff --git a/modules/ublog/src/main/ui/UblogFormUi.scala b/modules/ublog/src/main/ui/UblogFormUi.scala
index 66cb4c902704..6e1ffa0d20ad 100644
--- a/modules/ublog/src/main/ui/UblogFormUi.scala
+++ b/modules/ublog/src/main/ui/UblogFormUi.scala
@@ -110,6 +110,13 @@ final class UblogFormUi(helpers: Helpers, ui: UblogUi)(
help = trans.ublog.publishHelp().some,
half = true
)
+ ),
+ form3.split(
+ form3.checkbox(
+ form("sticky"),
+ trans.ublog.stickyPost(),
+ help = trans.ublog.stickyPostHelp().some
+ )
)
)
,
diff --git a/modules/ublog/src/main/ui/UblogUi.scala b/modules/ublog/src/main/ui/UblogUi.scala
index 55bdadd1d64b..d5340e92ae1a 100644
--- a/modules/ublog/src/main/ui/UblogUi.scala
+++ b/modules/ublog/src/main/ui/UblogUi.scala
@@ -31,7 +31,8 @@ final class UblogUi(helpers: Helpers, atomUi: AtomUi)(picfitUrl: lila.core.misc.
post: UblogPost.BasePost,
makeUrl: UblogPost.BasePost => Call = urlOfPost,
showAuthor: ShowAt = ShowAt.none,
- showIntro: Boolean = true
+ showIntro: Boolean = true,
+ showSticky: Boolean = false
)(using Context) =
a(
cls := s"ublog-post-card ublog-post-card--link ublog-post-card--by-${post.created.by}",
@@ -41,7 +42,10 @@ final class UblogUi(helpers: Helpers, atomUi: AtomUi)(picfitUrl: lila.core.misc.
thumbnail(post, _.Size.Small)(cls := "ublog-post-card__image"),
post.lived.map { live => semanticDate(live.at)(cls := "ublog-post-card__over-image") },
showAuthor match
- case ShowAt.none => emptyFrag
+ case ShowAt.none =>
+ if showSticky then
+ span(dataIcon := Icon.Star, cls := "user-link ublog-post-card__over-image pos-top")
+ else emptyFrag
case showAt =>
userIdSpanMini(post.created.by)(cls := s"ublog-post-card__over-image pos-$showAt")
),
diff --git a/translation/source/ublog.xml b/translation/source/ublog.xml
index 0534d27c2e4a..e23bcad13f1e 100644
--- a/translation/source/ublog.xml
+++ b/translation/source/ublog.xml
@@ -21,6 +21,8 @@
A forum topic will be created for people to comment on your post
Publish on your blog
If checked, the post will be listed on your blog. If not, it will be private, in your draft posts
+ Sticky post
+ If checked, this post will be listed first in your profile recent posts.
- Published a blog post
- Published %s blog posts
diff --git a/ui/@types/lichess/i18n.d.ts b/ui/@types/lichess/i18n.d.ts
index 8701c5cd40dd..303970a32ce3 100644
--- a/ui/@types/lichess/i18n.d.ts
+++ b/ui/@types/lichess/i18n.d.ts
@@ -5455,6 +5455,10 @@ interface I18n {
saveDraft: string;
/** Select the topics your post is about */
selectPostTopics: string;
+ /** Sticky post */
+ stickyPost: string;
+ /** If checked, this post will be listed first in your profile recent posts. */
+ stickyPostHelp: string;
/** This is a draft */
thisIsADraft: string;
/** This post is published */