From 705c858aa65acbebe9ac4eed7ef9ed3ce22bcd09 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 4 Dec 2024 14:53:07 +0000 Subject: [PATCH 1/4] DEV: Add support for glimmer topic list 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. --- .../components/topic-list-thumbnail.gjs | 155 ++++++++++++ .../topic-list-masonry-styles.gjs | 61 +++++ .../topic-thumbnail.hbr | 1 + .../topic-thumbnail.hbr | 2 - .../topic-thumbnails-init-legacy.js | 79 ++++++ .../initializers/topic-thumbnails-init.gjs | 78 ++++++ .../initializers/topic-thumbnails-init.js | 232 ------------------ .../discourse/lib/masonry-calculator.js | 91 +++++++ .../discourse/services/topic-thumbnails.js | 3 + .../templates/topic-list-thumbnail.hbr | 1 + 10 files changed, 469 insertions(+), 234 deletions(-) create mode 100644 javascripts/discourse/components/topic-list-thumbnail.gjs create mode 100644 javascripts/discourse/connectors/before-topic-list-body/topic-list-masonry-styles.gjs delete mode 100644 javascripts/discourse/connectors/topic-list-before-link/topic-thumbnail.hbr create mode 100644 javascripts/discourse/initializers/topic-thumbnails-init-legacy.js create mode 100644 javascripts/discourse/initializers/topic-thumbnails-init.gjs delete mode 100644 javascripts/discourse/initializers/topic-thumbnails-init.js create mode 100644 javascripts/discourse/lib/masonry-calculator.js diff --git a/javascripts/discourse/components/topic-list-thumbnail.gjs b/javascripts/discourse/components/topic-list-thumbnail.gjs new file mode 100644 index 0000000..61dbff8 --- /dev/null +++ b/javascripts/discourse/components/topic-list-thumbnail.gjs @@ -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"); + } + + +} diff --git a/javascripts/discourse/connectors/before-topic-list-body/topic-list-masonry-styles.gjs b/javascripts/discourse/connectors/before-topic-list-body/topic-list-masonry-styles.gjs new file mode 100644 index 0000000..a45fcdc --- /dev/null +++ b/javascripts/discourse/connectors/before-topic-list-body/topic-list-masonry-styles.gjs @@ -0,0 +1,61 @@ +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; + } + + +} diff --git a/javascripts/discourse/connectors/topic-list-before-columns/topic-thumbnail.hbr b/javascripts/discourse/connectors/topic-list-before-columns/topic-thumbnail.hbr index 517d43a..e098181 100644 --- a/javascripts/discourse/connectors/topic-list-before-columns/topic-thumbnail.hbr +++ b/javascripts/discourse/connectors/topic-list-before-columns/topic-thumbnail.hbr @@ -1 +1,2 @@ +{{!-- has-modern-replacement --}} {{~raw "topic-list-thumbnail" topic=context.topic location="before-columns"}} diff --git a/javascripts/discourse/connectors/topic-list-before-link/topic-thumbnail.hbr b/javascripts/discourse/connectors/topic-list-before-link/topic-thumbnail.hbr deleted file mode 100644 index f59fe05..0000000 --- a/javascripts/discourse/connectors/topic-list-before-link/topic-thumbnail.hbr +++ /dev/null @@ -1,2 +0,0 @@ -{{~raw "topic-list-thumbnail" topic=context.topic location="before-link"}} - diff --git a/javascripts/discourse/initializers/topic-thumbnails-init-legacy.js b/javascripts/discourse/initializers/topic-thumbnails-init-legacy.js new file mode 100644 index 0000000..263fe7c --- /dev/null +++ b/javascripts/discourse/initializers/topic-thumbnails-init-legacy.js @@ -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; + } + } + ); + }, +}; diff --git a/javascripts/discourse/initializers/topic-thumbnails-init.gjs b/javascripts/discourse/initializers/topic-thumbnails-init.gjs new file mode 100644 index 0000000..d00995d --- /dev/null +++ b/javascripts/discourse/initializers/topic-thumbnails-init.gjs @@ -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", ); + + 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"), + }); + } +}); diff --git a/javascripts/discourse/initializers/topic-thumbnails-init.js b/javascripts/discourse/initializers/topic-thumbnails-init.js deleted file mode 100644 index 0e291cd..0000000 --- a/javascripts/discourse/initializers/topic-thumbnails-init.js +++ /dev/null @@ -1,232 +0,0 @@ -import { readOnly } from "@ember/object/computed"; -import { once } from "@ember/runloop"; -import { service } from "@ember/service"; -import { htmlSafe } from "@ember/template"; -import { withPluginApi } from "discourse/lib/plugin-api"; -import { - getResolverOption, - setResolverOption, -} from "discourse-common/resolver"; -import discourseComputed, { observes } from "discourse-common/utils/decorators"; - -export default { - name: "topic-thumbnails-init", - initialize() { - 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"), - }); - - 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"), - }); - } - - 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); - } - }, - }); - - ////////////////////////// - /////////// Masonry Layout - ////////////////////////// - api.modifyClass("component:topic-list", { - pluginId: "topic-thumbnails-masonry", - topicThumbnailsService: service("topic-thumbnails"), - - masonryTargetColumnWidth: 300, - masonryGridSpacingPixels: 10, - masonryTitleSpacePixels: 76, - masonryDefaultAspect: 1.3, - masonryMinAspect: 0.7, - - @discourseComputed("masonryContainerWidth") - masonryNumColumns(width) { - return Math.floor(width / this.masonryTargetColumnWidth); - }, - - @discourseComputed( - "masonryNumColumns", - "masonryContainerWidth", - "masonryGridSpacingPixels" - ) - masonryColumnWidth(numColumns, containerWidth, gridSpacing) { - return (containerWidth - (numColumns - 1) * gridSpacing) / numColumns; - }, - - didInsertElement() { - this._super(); - this.updateElementHeight(); - - if (window.ResizeObserver) { - const observer = new ResizeObserver(() => this.updateElementHeight()); - observer.observe(this.element); - this.set("resizeObserver", observer); - } - }, - - willDestroyElement() { - this._super(); - if (this.resizeObserver) { - this.resizeObserver.unobserve(this.element); - } - }, - - updateElementHeight() { - this.set( - "masonryContainerWidth", - this.element.getBoundingClientRect().width - ); - }, - - @observes("topics.[]", "masonryContainerWidth") - masonryTopicsChanged() { - if (!this.topicThumbnailsService.displayMasonry) { - return; - } - if (!this.masonryContainerWidth) { - return; - } - once(this, this.calculateMasonryLayout); - }, - - calculateMasonryLayout() { - const numColumns = this.masonryNumColumns; - const gridSpacingPixels = this.masonryGridSpacingPixels; - - const columnHeights = []; - for (let n = 0; n < numColumns; n++) { - columnHeights[n] = 0; - } - - this.filteredTopics.forEach((topic) => { - // Pick the column with the lowest height - const smallestColumn = columnHeights.indexOf( - Math.min(...columnHeights) - ); - - // Get the height of this topic - let aspect = this.masonryDefaultAspect; - if (topic.thumbnails) { - aspect = topic.thumbnails[0].width / topic.thumbnails[0].height; - } - aspect = Math.max(aspect, this.masonryMinAspect); - const thisHeight = - this.masonryColumnWidth / aspect + this.masonryTitleSpacePixels; - - topic.set("masonryData", { - columnIndex: smallestColumn, - height: thisHeight, - heightAbove: columnHeights[smallestColumn], - }); - - columnHeights[smallestColumn] += thisHeight + gridSpacingPixels; - }); - - this.set("masonryTallestColumn", Math.max(...columnHeights)); - }, - - attributeBindings: ["masonryStyle:style"], - - @discourseComputed( - "topicThumbnailsService.displayMasonry", - "masonryNumColumns", - "masonryGridSpacingPixels", - "masonryTallestColumn", - "masonryColumnWidth" - ) - masonryStyle( - useMasonry, - numColumns, - gridSpacingPixels, - tallestColumn, - columnWidth - ) { - if (!useMasonry) { - return; - } - - return htmlSafe( - `--masonry-num-columns: ${Math.round(numColumns)}; ` + - `--masonry-grid-spacing: ${gridSpacingPixels}px; ` + - `--masonry-tallest-column: ${Math.round(tallestColumn)}px; ` + - `--masonry-column-width: ${Math.round(columnWidth)}px; ` - ); - }, - }); - - api.modifyClass("component:topic-list-item", { - pluginId: "topic-thumbnails-masonry", - attributeBindings: ["masonryStyle:style"], - - @discourseComputed("topic.masonryData") - masonryStyle(masonryData) { - if (!masonryData) { - return; - } - - return htmlSafe( - `--masonry-height: ${Math.round(masonryData.height)}px; ` + - `--masonry-height-above: ${Math.round( - masonryData.heightAbove - )}px; ` + - `--masonry-column-index: ${masonryData.columnIndex};` - ); - }, - }); - }, -}; diff --git a/javascripts/discourse/lib/masonry-calculator.js b/javascripts/discourse/lib/masonry-calculator.js new file mode 100644 index 0000000..5c01771 --- /dev/null +++ b/javascripts/discourse/lib/masonry-calculator.js @@ -0,0 +1,91 @@ +import { htmlSafe } from "@ember/template"; + +export default class MasonryCalculator { + masonryTargetColumnWidth = 300; + gridSpacingPixels = 10; + masonryTitleSpacePixels = 76; + masonryDefaultAspect = 1.3; + masonryMinAspect = 0.7; + + topics; + masonryContainerWidth; + + constructor(topicThumbnails, topics, masonryContainerWidth) { + this.topicThumbnails = topicThumbnails; + this.topics = topics; + this.masonryContainerWidth = masonryContainerWidth; + } + + get numColumns() { + return Math.floor( + this.masonryContainerWidth / this.masonryTargetColumnWidth + ); + } + + get columnWidth() { + return ( + (this.masonryContainerWidth - + (this.numColumns - 1) * this.gridSpacingPixels) / + this.numColumns + ); + } + + calculateMasonryLayout() { + const numColumns = this.numColumns; + const gridSpacingPixels = this.gridSpacingPixels; + + const columnHeights = []; + for (let n = 0; n < numColumns; n++) { + columnHeights[n] = 0; + } + + this.topicData = this.topics.map((topic) => { + // Pick the column with the lowest height + const smallestColumn = columnHeights.indexOf(Math.min(...columnHeights)); + + // Get the height of this topic + let aspect = this.masonryDefaultAspect; + if (topic.thumbnails) { + aspect = topic.thumbnails[0].width / topic.thumbnails[0].height; + } + aspect = Math.max(aspect, this.masonryMinAspect); + const thisHeight = + this.columnWidth / aspect + this.masonryTitleSpacePixels; + + const dataForTopic = { + columnIndex: smallestColumn, + height: thisHeight, + heightAbove: columnHeights[smallestColumn], + }; + + columnHeights[smallestColumn] += thisHeight + gridSpacingPixels; + + return dataForTopic; + }); + + this.tallestColumn = Math.max(...columnHeights); + } + + get masonryStyle() { + return htmlSafe( + [ + `.topic-list {`, + `--masonry-num-columns: ${Math.round(this.numColumns)};`, + `--masonry-grid-spacing: ${this.gridSpacingPixels}px;`, + `--masonry-tallest-column: ${Math.round(this.tallestColumn)}px;`, + `--masonry-column-width: ${Math.round(this.columnWidth)}px;`, + `}`, + + ...this.topicData.map((topicData, index) => { + return [ + `.masonry-${index} {`, + `--masonry-column-index: ${topicData.columnIndex};`, + `--masonry-height: ${Math.round(topicData.height)}px;`, + `--masonry-height-above: ${Math.round(topicData.heightAbove)}px;`, + `}`, + ].join("\n"); + }), + ].join("\n") + ); + } +} diff --git a/javascripts/discourse/services/topic-thumbnails.js b/javascripts/discourse/services/topic-thumbnails.js index 2f3e041..e546b3d 100644 --- a/javascripts/discourse/services/topic-thumbnails.js +++ b/javascripts/discourse/services/topic-thumbnails.js @@ -1,3 +1,4 @@ +import { tracked } from "@glimmer/tracking"; import { dependentKeyCompat } from "@ember/object/compat"; import Service, { service } from "@ember/service"; import Site from "discourse/models/site"; @@ -33,6 +34,8 @@ export default class TopicThumbnailService extends Service { @service router; @service discovery; + @tracked masonryContainerWidth; + @dependentKeyCompat get isTopicListRoute() { return this.discovery.onDiscoveryRoute; diff --git a/javascripts/discourse/templates/topic-list-thumbnail.hbr b/javascripts/discourse/templates/topic-list-thumbnail.hbr index 6361365..2407f96 100644 --- a/javascripts/discourse/templates/topic-list-thumbnail.hbr +++ b/javascripts/discourse/templates/topic-list-thumbnail.hbr @@ -1,3 +1,4 @@ +{{!-- has-modern-replacement --}} {{! template-lint-disable deprecated-inline-view-helper }} {{#if view.shouldDisplay}} {{#if view.hasThumbnail}} From 334c838a51567eb2e09f3dd0f7442c78e20b512f Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 5 Dec 2024 17:05:43 +0000 Subject: [PATCH 2/4] Add spec --- spec/system/topic_thumbnails_spec.rb | 68 ++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 spec/system/topic_thumbnails_spec.rb diff --git a/spec/system/topic_thumbnails_spec.rb b/spec/system/topic_thumbnails_spec.rb new file mode 100644 index 0000000..fc94aa0 --- /dev/null +++ b/spec/system/topic_thumbnails_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +RSpec.describe "Topic Thumbnails", type: :system do + fab!(:theme) { upload_theme_component } + fab!(:topics) { Fabricate.times(5, :topic) } + fab!(:user) + + before { sign_in user } + + RSpec.shared_examples "topic thumbnails" do + { + "grid" => "grid", + "minimal-grid" => "minimal", + "list" => "list", + "blog-style" => "blog-style-grid", + }.each do |style, class_name| + it "renders topic thumbnails in #{style} style" do + theme.update_setting(:default_thumbnail_mode, style) + theme.save! + + visit "/latest" + + expect(page).to have_css(".topic-list.topic-thumbnails-#{class_name}") + expect(page).to have_css(".topic-list-thumbnail", count: 5) + end + end + + it "renders topic thumbnails in masonry style" do + theme.update_setting(:default_thumbnail_mode, "masonry") + theme.save! + + visit "/latest" + + expect(page).to have_css(".topic-list.topic-thumbnails-masonry") + expect(page).to have_css(".topic-list-thumbnail", count: 5) + + expect(page).to have_css(".topic-list-item.masonry-0") + end + end + + context "with default raw-hbs topic list" do + it "is using legacy topic list" do + visit "/latest" + expect(page).to have_css(".topic-list") + enabled = + page.evaluate_script("Discourse.lookup('service:current-user').canUseGlimmerTopicList") + expect(enabled).to eq(false) + end + + it_behaves_like "topic thumbnails" + end + + context "with glimmer topic list" do + before do + SiteSetting.experimental_glimmer_topic_list_groups = Group::AUTO_GROUPS[:everyone].to_s + end + + it "is using glimmer topic list" do + visit "/latest" + expect(page).to have_css(".topic-list") + enabled = + page.evaluate_script("Discourse.lookup('service:current-user').canUseGlimmerTopicList") + expect(enabled).to eq(true) + end + + it_behaves_like "topic thumbnails" + end +end From 835fa6980c5c6521e68934b030165ed26287c605 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 5 Dec 2024 23:02:56 +0000 Subject: [PATCH 3/4] fixup activity column --- .../components/topic-list-thumbnail.gjs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/javascripts/discourse/components/topic-list-thumbnail.gjs b/javascripts/discourse/components/topic-list-thumbnail.gjs index 61dbff8..e6a7f9a 100644 --- a/javascripts/discourse/components/topic-list-thumbnail.gjs +++ b/javascripts/discourse/components/topic-list-thumbnail.gjs @@ -1,6 +1,8 @@ import Component from "@glimmer/component"; import { service } from "@ember/service"; +import coldAgeClass from "discourse/helpers/cold-age-class"; import concatClass from "discourse/helpers/concat-class"; +import formatDate from "discourse/helpers/format-date"; import dIcon from "discourse-common/helpers/d-icon"; export default class TopicListThumbnail extends Component { @@ -142,13 +144,20 @@ export default class TopicListThumbnail extends Component { {{this.topic.reply_count}} - {{! TODO }} - {{!-- {{raw - "list/activity-column" - topic=this.topic - class="topic-thumbnail-blog-data-activity" - tagName="div" - }} --}} + {{/if}} From e2a1f3cec3915f5db71767069dd545d5c396f15d Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 16 Dec 2024 13:10:45 +0000 Subject: [PATCH 4/4] Update javascripts/discourse/components/topic-list-thumbnail.gjs Co-authored-by: Jarek Radosz --- javascripts/discourse/components/topic-list-thumbnail.gjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/javascripts/discourse/components/topic-list-thumbnail.gjs b/javascripts/discourse/components/topic-list-thumbnail.gjs index e6a7f9a..22aca65 100644 --- a/javascripts/discourse/components/topic-list-thumbnail.gjs +++ b/javascripts/discourse/components/topic-list-thumbnail.gjs @@ -77,8 +77,8 @@ export default class TopicListThumbnail extends Component { } get url() { - return this.topic.linked_post_number - ? this.topic.urlForPostNumber(this.topic.linked_post_number) + return this.topic.get("linked_post_number") + ? this.topic.urlForPostNumber(this.topic.get("linked_post_number")) : this.topic.get("lastUnreadUrl"); }