Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/controllers/Ublog.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ final class Ublog(env: Env) extends LilaController(env):
WithBlogOf(createdBy): (user, blog) =>
canViewPost(user, blog)(post).so:
for
others <- env.ublog.api.otherPosts(UblogBlog.Id.User(user.id), post)
otherPosts <- env.ublog.api.recommend(UblogBlog.Id.User(user.id), post)
liked <- ctx.user.so(env.ublog.rank.liked(post))
followed <- ctx.userId.so(env.relation.api.fetchFollows(_, user.id))
prefFollowable <- ctx.isAuth.so(env.pref.api.followable(user.id))
Expand All @@ -58,7 +58,7 @@ final class Ublog(env: Env) extends LilaController(env):
markup <- env.ublog.markup(post)
viewedPost = env.ublog.viewCounter(post, ctx.ip)
page <- renderPage:
views.ublog.post.page(user, blog, viewedPost, markup, others, liked, followable, followed)
views.ublog.post.page(user, blog, viewedPost, markup, otherPosts, liked, followable, followed)
yield Ok(page)

def discuss(id: UblogPostId) = Open:
Expand Down
35 changes: 35 additions & 0 deletions bin/mongodb/ublog-similar-full.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* O(n) for ublog_post documents
* for 48k documents in the DB, it takes about 2 minutes to run
* and uses up to 600MB of memory.
*
* Should run only once, then be replaced with
* ublog-graph-incremental.js
*/
const nbSimilar = 6;
console.log('Full recompute');
const all = db.ublog_post.find({ live: true, 'likers.1': { $exists: true } }, { likers: 1 }).toArray();
console.log(`${all.length} posts to go.`);

console.log(`Computing likers...`);
const likers = new Map();
all.forEach(p => {
p.likers.forEach(l => {
if (!likers.has(l)) likers.set(l, []);
likers.get(l).push(p._id);
});
});
console.log(likers.size + ` likers found.`);

console.log(`Updating posts...`);
all.forEach(p => {
const similar = new Map();
p.likers.forEach(liker => {
(likers.get(liker) || []).forEach(id => {
if (id != p._id) similar.set(id, (similar.get(id) || 0) + 1);
});
});
const top3 = Array.from(similar)
.sort((a, b) => b[1] - a[1])
.slice(0, nbSimilar);
db.ublog_post.updateOne({ _id: p._id }, { $set: { similar: top3.map(([id, _]) => id) } });
});
37 changes: 37 additions & 0 deletions bin/mongodb/ublog-similar-incremental.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* O(1) assuming constant number of recently updated posts
* At the time of writing, with 48k ublog_post in the DB,
* it takes about 4 seconds to run and uses up to 300MB of memory.
*
* Should run periodically, e.g. every 1 hour.
*/
const nbSimilar = 6;
const since = new Date(Date.now() - 1000 * 60 * 60 * 24 * 15);
const updatable = db.ublog_post.find({ live: true, 'updated.at': { $gt: since } }, { likers: 1 }).toArray();
console.log(`${updatable.length} posts were updated since ${since}`);

const updatableLikers = new Set();
updatable.forEach(p => p.likers.forEach(l => updatableLikers.add(l)));
console.log(`They have ${updatableLikers.size} likers.`);

console.log(`Computing liker->ids...`);
const likerToIds = new Map();
db.ublog_post.find({ live: true, likers: { $in: Array.from(updatableLikers) } }, { likers: 1 }).forEach(p => {
updatableLikers.intersection(new Set(p.likers)).forEach(l => {
if (!likerToIds.has(l)) likerToIds.set(l, []);
likerToIds.get(l).push(p._id);
});
});
console.log(`Updating ${updatable.length} posts...`);

updatable.forEach(p => {
const similar = new Map();
p.likers.forEach(liker => {
(likerToIds.get(liker) || []).forEach(id => {
if (id != p._id) similar.set(id, (similar.get(id) || 0) + 1);
});
});
const top3 = Array.from(similar)
.sort((a, b) => b[1] - a[1])
.slice(0, nbSimilar);
db.ublog_post.updateOne({ _id: p._id }, { $set: { similar: top3.map(([id, _]) => id) } });
});
1 change: 0 additions & 1 deletion modules/coreI18n/src/main/key.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2798,7 +2798,6 @@ object I18nKey:
val `xPublishedY`: I18nKey = "ublog:xPublishedY"
val `thisPostIsPublished`: I18nKey = "ublog:thisPostIsPublished"
val `thisIsADraft`: I18nKey = "ublog:thisIsADraft"
val `moreBlogPostsBy`: I18nKey = "ublog:moreBlogPostsBy"
val `noPostsInThisBlogYet`: I18nKey = "ublog:noPostsInThisBlogYet"
val `noDrafts`: I18nKey = "ublog:noDrafts"
val `latestBlogPosts`: I18nKey = "ublog:latestBlogPosts"
Expand Down
23 changes: 16 additions & 7 deletions modules/ublog/src/main/UblogApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import lila.core.shutup.{ PublicSource, ShutupApi }
import lila.core.timeline as tl
import lila.db.dsl.{ *, given }
import lila.memo.PicfitApi
import lila.core.user.KidMode

final class UblogApi(
colls: UblogColls,
Expand Down Expand Up @@ -115,16 +116,24 @@ final class UblogApi(
.cursor[UblogPost.PreviewPost](ReadPref.sec)
.list(nb)

def otherPosts(blog: UblogBlog.Id, post: UblogPost, nb: Int = 4): Fu[List[UblogPost.PreviewPost]] =
colls.post
.find($doc("blog" -> blog, "live" -> true, "_id".$ne(post.id)), previewPostProjection.some)
.sort($doc("lived.at" -> -1))
.cursor[UblogPost.PreviewPost](ReadPref.sec)
.list(nb)

def postPreview(id: UblogPostId) =
colls.post.byId[UblogPost.PreviewPost](id, previewPostProjection)

private def postPreviews(ids: List[UblogPostId]) = ids.nonEmpty.so:
colls.post.byIdsProj[UblogPost.PreviewPost, UblogPostId](ids, previewPostProjection, _.sec)

def recommend(blog: UblogBlog.Id, post: UblogPost)(using kid: KidMode): Fu[List[UblogPost.PreviewPost]] =
for
sameAuthor <- colls.post
.find($doc("blog" -> blog, "live" -> true, "_id".$ne(post.id)), previewPostProjection.some)
.sort($doc("lived.at" -> -1))
.cursor[UblogPost.PreviewPost](ReadPref.sec)
.list(3)
similarIds = post.similar.filterNot(sameAuthor.map(_.id).contains)
similar <- postPreviews(post.similar)
mix = (similar ++ sameAuthor).filter(_.isLichess || kid.no)
yield scala.util.Random.shuffle(mix).take(6)

object image:
private def rel(post: UblogPost) = s"ublog:${post.id}"

Expand Down
1 change: 1 addition & 0 deletions modules/ublog/src/main/UblogForm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ object UblogForm:
lived = none,
likes = UblogPost.Likes(1),
views = UblogPost.Views(0),
similar = Nil,
rankAdjustDays = none,
pinned = none
)
Expand Down
1 change: 1 addition & 0 deletions modules/ublog/src/main/UblogPost.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ case class UblogPost(
lived: Option[UblogPost.Recorded],
likes: UblogPost.Likes,
views: UblogPost.Views,
similar: List[UblogPostId],
rankAdjustDays: Option[Int],
pinned: Option[Boolean]
) extends UblogPost.BasePost
Expand Down
11 changes: 9 additions & 2 deletions modules/ublog/src/main/ui/UblogPostUi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,15 @@ final class UblogPostUi(helpers: Helpers, ui: UblogUi)(
followable.option(followButton(user, followed))
)
),
h2(a(href := routes.Ublog.index(user.username))(trans.ublog.moreBlogPostsBy(user.username))),
(others.size > 0).option(div(cls := "ublog-post-cards")(others.map(ui.card(_))))
(others.length > 0).option(
div(
h2("You may also like"),
div(cls := "ublog-post-cards")(
others.map:
ui.card(_, showAuthor = ui.ShowAt.top, showIntro = true)
)
)
)
)
)
)
Expand Down
2 changes: 1 addition & 1 deletion project/BuildSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ object BuildSettings {
"-source:3.7",
"-indent",
// "-explaintypes",
// "-explain",
// "-explain-cyclic",
"-feature",
"-language:postfixOps",
"-language:implicitConversions",
Expand Down
1 change: 0 additions & 1 deletion translation/source/ublog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
<string name="xPublishedY">%1$s published %2$s</string>
<string name="thisPostIsPublished">This post is published</string>
<string name="thisIsADraft">This is a draft</string>
<string name="moreBlogPostsBy">More blog posts by %s</string>
<string name="noPostsInThisBlogYet">No posts in this blog, yet.</string>
<string name="noDrafts">No drafts to show.</string>
<string name="latestBlogPosts">Latest blog posts</string>
Expand Down
3 changes: 3 additions & 0 deletions ui/bits/css/ublog/_post.scss
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@
display: block;
}
}
.ublog-post-card--link:hover {
box-shadow: none;
}
}

.ublog-post__mod-tools {
Expand Down
3 changes: 1 addition & 2 deletions ui/lib/css/layout/_page-menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
&__content {
grid-area: content;
height: 100%;

// overflow: hidden; /* fixes crazy text overflow on Fx */
min-width: 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

}

&__content.box {
Expand Down
67 changes: 67 additions & 0 deletions ui/lib/src/carousel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { frag } from './common';

// it is an abomination to have more than one of these per page, so it's not supported

type CarouselOpts = {
selector: string;
itemWidth: number; // this is a suggestion
pauseFor: Seconds;
slideFor?: Seconds;
};

export function makeCarousel({ selector, itemWidth, pauseFor, slideFor = 0.6 }: CarouselOpts): void {
let timer: number | undefined = undefined;

requestIdleCallback(() => {
const el = document.querySelector<HTMLElement>(selector)!;
if (!el) return;

const track = frag<HTMLElement>('<div class="track"></div>');
track.append(...el.children);
el.innerHTML = '';
el.append(track);
el.style.visibility = 'visible';

layoutChanged();
window.addEventListener('resize', layoutChanged);

function layoutChanged() {
const kids = [...track.children].filter((k): k is HTMLElement => k instanceof HTMLElement);
const styleGap = toPx('gap', el);
const gap = Number.isNaN(styleGap) ? 0 : styleGap;
const visible = Math.floor((el.clientWidth + gap) / (itemWidth + gap));
const itemW = Math.floor((el.clientWidth - gap * (visible - 1)) / visible);

kids.forEach(k => (k.style.width = `${itemW}px`));
kids.forEach(k => (k.style.marginRight = `${gap}px`));

const rotateInner = () => {
kids.forEach(k => (k.style.transition = `transform ${slideFor}s ease`));
kids.forEach(k => (k.style.transform = `translateX(-${itemW + gap}px)`));
setTimeout(() => {
track.append(track.firstChild!);
fix();
}, slideFor * 1000);
};

const fix = () => {
kids.forEach(k => (k.style.transition = ''));
kids.forEach(k => (k.style.transform = ''));
};
requestAnimationFrame(fix);
clearInterval(timer);
if (kids.length <= visible) return;
timer = setInterval(rotateInner, pauseFor * 1000);
}
});
}

function toPx(key: keyof CSSStyleDeclaration, contextEl: HTMLElement = document.body): number {
// must be simple units like vw, em, and %. things like 'auto' will return NaN
const style = window.getComputedStyle(contextEl);
const el = frag<HTMLElement>(`<div style="position:absolute;visibility:hidden;width:${style[key]}"/>`);
contextEl.append(el);
const pixels = parseFloat(window.getComputedStyle(el).width);
el.remove();
return pixels;
}
33 changes: 0 additions & 33 deletions ui/lobby/src/view/blog.ts

This file was deleted.

Loading