Skip to content

Commit

Permalink
DEV: Add support for glimmer topic list
Browse files Browse the repository at this point in the history
This makes the theme component compatible with core's glimmer topic list, while maintaining compatibility with the raw-hbs topic list.

Some parts of the logic (e.g. the masonry layout system, and the topic-list-before-link outlet) are reimplemented in a way which works in both old and new topic-lists.

Other parts are re-implemented separately for the new topic list. For those parts, the legacy versions are isolated in the `topic-thumbnails-init-legacy.js` initializer for future removal.
  • Loading branch information
davidtaylorhq committed Dec 4, 2024
1 parent 85ad071 commit a66b7bd
Show file tree
Hide file tree
Showing 10 changed files with 468 additions and 234 deletions.
155 changes: 155 additions & 0 deletions javascripts/discourse/components/topic-list-thumbnail.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import concatClass from "discourse/helpers/concat-class";
import dIcon from "discourse-common/helpers/d-icon";

export default class TopicListThumbnail extends Component {
@service topicThumbnails;

responsiveRatios = [1, 1.5, 2];

// Make sure to update about.json thumbnail sizes if you change these variables
get displayWidth() {
return this.topicThumbnails.displayList
? settings.list_thumbnail_size
: 400;
}

get topic() {
return this.args.topic;
}

get hasThumbnail() {
return !!this.topic.thumbnails;
}

get srcSet() {
const srcSetArray = [];

this.responsiveRatios.forEach((ratio) => {
const target = ratio * this.displayWidth;
const match = this.topic.thumbnails.find(
(t) => t.url && t.max_width === target
);
if (match) {
srcSetArray.push(`${match.url} ${ratio}x`);
}
});

if (srcSetArray.length === 0) {
srcSetArray.push(`${this.original.url} 1x`);
}

return srcSetArray.join(",");
}

get original() {
return this.topic.thumbnails[0];
}

get width() {
return this.original.width;
}

get isLandscape() {
return this.original.width >= this.original.height;
}

get height() {
return this.original.height;
}

get fallbackSrc() {
const largeEnough = this.topic.thumbnails.filter((t) => {
if (!t.url) {
return false;
}
return t.max_width > this.displayWidth * this.responsiveRatios.lastObject;
});

if (largeEnough.lastObject) {
return largeEnough.lastObject.url;
}

return this.original.url;
}

get url() {
return this.topic.linked_post_number
? this.topic.urlForPostNumber(this.topic.linked_post_number)
: this.topic.get("lastUnreadUrl");
}

<template>
<div
class={{concatClass
"topic-list-thumbnail"
(if this.hasThumbnail "has-thumbnail" "no-thumbnail")
}}
>
<a href={{this.url}}>
{{#if this.hasThumbnail}}
<img
class="background-thumbnail"
src={{this.fallbackSrc}}
srcset={{this.srcSet}}
width={{this.width}}
height={{this.height}}
loading="lazy"
/>
<img
class="main-thumbnail"
src={{this.fallbackSrc}}
srcset={{this.srcSet}}
width={{this.width}}
height={{this.height}}
loading="lazy"
/>
{{else}}
<div class="thumbnail-placeholder">
{{dIcon settings.placeholder_icon}}
</div>
{{/if}}
</a>
</div>

{{#if this.topicThumbnails.showLikes}}
<div class="topic-thumbnail-likes">
{{dIcon "heart"}}
<span class="number">
{{this.topic.like_count}}
</span>
</div>
{{/if}}

{{#if this.topicThumbnails.displayBlogStyle}}
<div class="topic-thumbnail-blog-data">
<div class="topic-thumbnail-blog-data-views">
{{dIcon "eye"}}
<span class="number">
{{this.topic.views}}
</span>
</div>
<div class="topic-thumbnail-blog-data-likes">
{{dIcon "heart"}}
<span class="number">
{{this.topic.like_count}}
</span>
</div>
<div class="topic-thumbnail-blog-data-comments">
{{dIcon "comment"}}
<span class="number">
{{this.topic.reply_count}}
</span>
</div>
{{! TODO }}
{{!-- {{raw
"list/activity-column"
topic=this.topic
class="topic-thumbnail-blog-data-activity"
tagName="div"
}} --}}
</div>
{{/if}}
</template>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import { modifier } from "ember-modifier";
import MasonryCalculator from "../../lib/masonry-calculator";

export default class TopicListMasonryStyles extends Component {
@service topicThumbnails;

attachResizeObserver = modifier((element) => {
const topicList = element.closest(".topic-list");

if (!topicList) {
// eslint-disable-next-line no-console
console.error(
"topic-list-thumbnails resize-observer must be inside a topic-list"
);
return;
}

this.topicThumbnails.masonryContainerWidth =
topicList.getBoundingClientRect().width;

const observer = new ResizeObserver(() => {
this.topicThumbnails.masonryContainerWidth =
topicList.getBoundingClientRect().width;
});
observer.observe(topicList);

return () => {
observer.disconnect();
this.topicThumbnails.masonryContainerWidth = null;
};
});

get masonryStyle() {
if (!this.topicThumbnails.displayMasonry) {
return;
}

if (!this.topicThumbnails.masonryContainerWidth) {
return;
}

const calculator = new MasonryCalculator(
this.topicThumbnails,
this.args.outletArgs.topics,
this.topicThumbnails.masonryContainerWidth
);
calculator.calculateMasonryLayout();
return calculator.masonryStyle;
}

<template>
{{#if this.topicThumbnails.displayMasonry}}
<style {{this.attachResizeObserver}}>

Check failure on line 55 in javascripts/discourse/connectors/before-topic-list-body/topic-list-masonry-styles.gjs

View workflow job for this annotation

GitHub Actions / ci / linting

Use of <style> detected. Do not use forbidden elements.
{{this.masonryStyle}}
</style>
{{/if}}
</template>
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
{{!-- has-modern-replacement --}}
{{~raw "topic-list-thumbnail" topic=context.topic location="before-columns"}}

This file was deleted.

79 changes: 79 additions & 0 deletions javascripts/discourse/initializers/topic-thumbnails-init-legacy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { readOnly } from "@ember/object/computed";
import { service } from "@ember/service";
import { withPluginApi } from "discourse/lib/plugin-api";
import { withSilencedDeprecations } from "discourse-common/lib/deprecated";
import {
getResolverOption,
setResolverOption,
} from "discourse-common/resolver";
import { observes } from "discourse-common/utils/decorators";
export default {
name: "topic-thumbnails-init",
initialize() {
withSilencedDeprecations("discourse.hbr-topic-list-overrides", () => {
withPluginApi("0.8.7", (api) => this.initWithApi(api));
});
},

initWithApi(api) {
api.modifyClass("component:topic-list", {
pluginId: "topic-thumbnails",
topicThumbnailsService: service("topic-thumbnails"),
classNameBindings: [
"isMinimalGrid:topic-thumbnails-minimal",
"isThumbnailGrid:topic-thumbnails-grid",
"isThumbnailList:topic-thumbnails-list",
"isMasonryList:topic-thumbnails-masonry",
"isBlogStyleGrid:topic-thumbnails-blog-style-grid",
],
isMinimalGrid: readOnly("topicThumbnailsService.displayMinimalGrid"),
isThumbnailGrid: readOnly("topicThumbnailsService.displayGrid"),
isThumbnailList: readOnly("topicThumbnailsService.displayList"),
isMasonryList: readOnly("topicThumbnailsService.displayMasonry"),
isBlogStyleGrid: readOnly("topicThumbnailsService.displayBlogStyle"),
});

api.modifyClass("component:topic-list-item", {
pluginId: "topic-thumbnails",
topicThumbnailsService: service("topic-thumbnails"),

// Hack to disable the mobile topic-list-item template
// Our grid styling is responsive, and uses the desktop HTML structure
@observes("topic.pinned")
renderTopicListItem() {
const wasMobileView = getResolverOption("mobileView");
if (
wasMobileView &&
(this.topicThumbnailsService.displayGrid ||
this.topicThumbnailsService.displayMasonry ||
this.topicThumbnailsService.displayMinimalGrid ||
this.topicThumbnailsService.displayBlogStyle)
) {
setResolverOption("mobileView", false);
}

this._super();

if (wasMobileView) {
setResolverOption("mobileView", true);
}
},
});

api.modifyClass(
"component:topic-list-item",
(Superclass) =>
class extends Superclass {
@service topicThumbnails;

get classNames() {
const result = super.classNames;
if (this.topicThumbnails.displayMasonry) {
return [...result, `masonry-${this.index}`];
}
return result;
}
}
);
},
};
78 changes: 78 additions & 0 deletions javascripts/discourse/initializers/topic-thumbnails-init.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { readOnly } from "@ember/object/computed";
import { service } from "@ember/service";
import { apiInitializer } from "discourse/lib/api";
import TopicListThumbnail from "../components/topic-list-thumbnail";

export default apiInitializer("0.8", (api) => {
const ttService = api.container.lookup("service:topic-thumbnails");

api.registerValueTransformer("topic-list-class", ({ value }) => {
if (ttService.displayMinimalGrid) {
value.push("topic-thumbnails-minimal");
} else if (ttService.displayGrid) {
value.push("topic-thumbnails-grid");
} else if (ttService.displayList) {
value.push("topic-thumbnails-list");
} else if (ttService.displayMasonry) {
value.push("topic-thumbnails-masonry");
} else if (ttService.displayBlogStyle) {
value.push("topic-thumbnails-blog-style-grid");
}
return value;
});

api.registerValueTransformer("topic-list-columns", ({ value: columns }) => {
if (ttService.enabledForRoute && !ttService.displayList) {
columns.add(
"thumbnail",
{ item: TopicListThumbnail },
{ before: "topic" }
);
}
return columns;
});

api.renderInOutlet("topic-list-before-link", <template>
{{#if ttService.displayList}}
<TopicListThumbnail @topic={{@outletArgs.topic}} />
{{/if}}
</template>);

api.registerValueTransformer("topic-list-item-mobile-layout", ({ value }) => {
if (ttService.enabledForRoute && !ttService.displayList) {
// Force the desktop layout
return false;
}
return value;
});

api.registerValueTransformer(
"topic-list-item-class",
({ value, context: { index } }) => {
if (ttService.displayMasonry) {
value.push(`masonry-${index}`);
}
return value;
}
);

const siteSettings = api.container.lookup("service:site-settings");
if (settings.docs_thumbnail_mode !== "none" && siteSettings.docs_enabled) {
api.modifyClass("component:docs-topic-list", {
pluginId: "topic-thumbnails",
topicThumbnailsService: service("topic-thumbnails"),
classNameBindings: [
"isMinimalGrid:topic-thumbnails-minimal",
"isThumbnailGrid:topic-thumbnails-grid",
"isThumbnailList:topic-thumbnails-list",
"isMasonryList:topic-thumbnails-masonry",
"isBlogStyleGrid:topic-thumbnails-blog-style-grid",
],
isMinimalGrid: readOnly("topicThumbnailsService.displayMinimalGrid"),
isThumbnailGrid: readOnly("topicThumbnailsService.displayGrid"),
isThumbnailList: readOnly("topicThumbnailsService.displayList"),
isMasonryList: readOnly("topicThumbnailsService.displayMasonry"),
isBlogStyleGrid: readOnly("topicThumbnailsService.displayBlogStyle"),
});
}
});
Loading

0 comments on commit a66b7bd

Please sign in to comment.