DEV: Drop legacy topic-list and raw-handlebars compilation system (#32081)

This commit is contained in:
David Taylor 2025-04-14 10:42:40 +01:00 committed by GitHub
parent 13cb472ec8
commit f0057c7353
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
127 changed files with 156 additions and 3420 deletions

View File

@ -1,9 +1,6 @@
import { htmlSafe } from "@ember/template";
import { registerRawHelper } from "discourse/lib/helpers";
import { renderIcon } from "discourse/lib/icon-library";
registerRawHelper("check-icon", checkIcon);
export default function checkIcon(value) {
let icon = value ? "check" : "xmark";
return htmlSafe(renderIcon("string", icon));

View File

@ -1 +0,0 @@
engine-strict = true

View File

@ -1,20 +0,0 @@
{
"name": "discourse-hbr",
"version": "1.0.0",
"description": "Support for Discourse's raw Handlebars templates (hbr)",
"author": "Discourse",
"license": "GPL-2.0-only",
"dependencies": {
"handlebars": "^4.7.8",
"broccoli-filter": "^1.3.0"
},
"engines": {
"node": ">= 18",
"npm": "please-use-pnpm",
"yarn": "please-use-pnpm",
"pnpm": "^9"
},
"exports": {
"./raw-handlebars-compiler": "./raw-handlebars-compiler.js"
}
}

View File

@ -1,174 +0,0 @@
"use strict";
const Filter = require("broccoli-filter");
const Handlebars = require("handlebars");
const RawHandlebars = Handlebars.create();
function buildPath(blk, args) {
let result = {
type: "PathExpression",
data: false,
depth: blk.path.depth,
loc: blk.path.loc,
};
// Server side precompile doesn't have jquery.extend
Object.keys(args).forEach(function (a) {
result[a] = args[a];
});
return result;
}
function replaceGet(ast) {
let visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.MustacheStatement = function (mustache) {
if (!(mustache.params.length || mustache.hash)) {
mustache.params[0] = mustache.path;
mustache.path = buildPath(mustache, {
parts: ["get"],
original: "get",
strict: true,
falsy: true,
});
}
return Handlebars.Visitor.prototype.MustacheStatement.call(this, mustache);
};
// rewrite `each x as |y|` as each y in x`
// This allows us to use the same syntax in all templates
visitor.BlockStatement = function (block) {
if (block.path.original === "each" && block.params.length === 1) {
let paramName = block.program.blockParams[0];
block.params = [
buildPath(block, { original: paramName }),
{ type: "CommentStatement", value: "in" },
block.params[0],
];
delete block.program.blockParams;
}
return Handlebars.Visitor.prototype.BlockStatement.call(this, block);
};
visitor.accept(ast);
}
RawHandlebars.Compiler = function () {};
RawHandlebars.Compiler.prototype = Object.create(Handlebars.Compiler.prototype);
RawHandlebars.Compiler.prototype.compiler = RawHandlebars.Compiler;
RawHandlebars.JavaScriptCompiler = function () {};
RawHandlebars.JavaScriptCompiler.prototype = Object.create(
Handlebars.JavaScriptCompiler.prototype
);
RawHandlebars.JavaScriptCompiler.prototype.compiler =
RawHandlebars.JavaScriptCompiler;
RawHandlebars.JavaScriptCompiler.prototype.namespace = "RawHandlebars";
RawHandlebars.precompile = function (value, asObject) {
let ast = Handlebars.parse(value);
replaceGet(ast);
let options = {
knownHelpers: {
get: true,
},
data: true,
stringParams: true,
};
asObject = asObject === undefined ? true : asObject;
let environment = new RawHandlebars.Compiler().compile(ast, options);
return new RawHandlebars.JavaScriptCompiler().compile(
environment,
options,
undefined,
asObject
);
};
RawHandlebars.compile = function (string) {
let ast = Handlebars.parse(string);
replaceGet(ast);
// this forces us to rewrite helpers
let options = { data: true, stringParams: true };
let environment = new RawHandlebars.Compiler().compile(ast, options);
let templateSpec = new RawHandlebars.JavaScriptCompiler().compile(
environment,
options,
undefined,
true
);
let t = RawHandlebars.template(templateSpec);
t.isMethod = false;
return t;
};
function TemplateCompiler(inputTree, options) {
if (!(this instanceof TemplateCompiler)) {
return new TemplateCompiler(inputTree, options);
}
Filter.call(this, inputTree, options); // this._super()
this.options = options || {};
this.inputTree = inputTree;
}
TemplateCompiler.prototype = Object.create(Filter.prototype);
TemplateCompiler.prototype.constructor = TemplateCompiler;
TemplateCompiler.prototype.extensions = ["hbr"];
TemplateCompiler.prototype.targetExtension = "js";
TemplateCompiler.prototype.registerPlugins = function registerPlugins() {};
TemplateCompiler.prototype.initializeFeatures =
function initializeFeatures() {};
TemplateCompiler.prototype.processString = function (string, relativePath) {
let filename;
const pluginName = relativePath.match(/^discourse\/plugins\/([^\/]+)\//)?.[1];
if (pluginName) {
filename = relativePath
.replace(`discourse/plugins/${pluginName}/`, "")
.replace(/^(discourse\/)?raw-templates\//, "javascripts/");
} else {
filename = relativePath.replace(/^raw-templates\//, "");
}
filename = filename.replace(/\.hbr$/, "");
const hasModernReplacement = string.includes(
"{{!-- has-modern-replacement --}}"
);
return `
import { template as compiler } from "discourse/lib/raw-handlebars";
import { addRawTemplate } from "discourse/lib/raw-templates";
let template = compiler(${this.precompile(string, false)});
addRawTemplate("${filename}", template, {
core: ${!pluginName},
pluginName: ${JSON.stringify(pluginName)},
hasModernReplacement: ${hasModernReplacement},
});
export default template;
`;
};
TemplateCompiler.prototype.precompile = function (value, asObject) {
return RawHandlebars.precompile(value, asObject);
};
module.exports = TemplateCompiler;

View File

@ -6,7 +6,6 @@ const Funnel = require("broccoli-funnel");
const mergeTrees = require("broccoli-merge-trees");
const fs = require("fs");
const concat = require("broccoli-concat");
const RawHandlebarsCompiler = require("discourse-hbr/raw-handlebars-compiler");
const DiscoursePluginColocatedTemplateProcessor = require("./colocated-template-compiler");
const EmberApp = require("ember-cli/lib/broccoli/ember-app");
@ -15,19 +14,6 @@ function fixLegacyExtensions(tree) {
getDestinationPath: function (relativePath) {
if (relativePath.endsWith(".es6")) {
return relativePath.slice(0, -4);
} else if (relativePath.endsWith(".raw.hbs")) {
relativePath = relativePath.replace(".raw.hbs", ".hbr");
}
if (relativePath.endsWith(".hbr")) {
if (relativePath.includes("/templates/")) {
relativePath = relativePath.replace("/templates/", "/raw-templates/");
} else if (relativePath.includes("/connectors/")) {
relativePath = relativePath.replace(
"/connectors/",
"/raw-templates/connectors/"
);
}
}
return relativePath;
@ -201,8 +187,6 @@ module.exports = {
tree = unColocateConnectors(tree);
tree = namespaceModules(tree, pluginName);
tree = RawHandlebarsCompiler(tree);
const colocateBase = `discourse/plugins/${pluginName}`;
tree = new DiscoursePluginColocatedTemplateProcessor(
tree,

View File

@ -10,7 +10,6 @@
"dependencies": {
"@babel/core": "^7.26.10",
"deprecation-silencer": "workspace:1.0.0",
"discourse-hbr": "workspace:1.0.0",
"discourse-widget-hbs": "workspace:1.0.0",
"ember-cli-babel": "^8.2.0",
"ember-cli-htmlbars": "^6.3.0",

View File

@ -17,8 +17,7 @@
"@babel/core": "^7.26.10",
"ember-auto-import": "^2.10.0",
"ember-cli-babel": "^8.2.0",
"ember-cli-htmlbars": "^6.3.0",
"handlebars": "^4.7.8"
"ember-cli-htmlbars": "^6.3.0"
},
"devDependencies": {
"@ember/optional-features": "^2.2.0",

View File

@ -4,7 +4,6 @@ import { service } from "@ember/service";
import { observes } from "@ember-decorators/object";
import $ from "jquery";
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
import TopicList from "discourse/components/topic-list";
import List from "discourse/components/topic-list/list";
import discourseComputed, { bind } from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
@ -122,35 +121,19 @@ export default class BasicTopicList extends Component {
<template>
<ConditionalLoadingSpinner @condition={{this.loading}}>
{{#if this.topics}}
{{#if this.site.useGlimmerTopicList}}
<List
@showPosters={{this.showPosters}}
@hideCategory={{this.hideCategory}}
@topics={{this.topics}}
@expandExcerpts={{this.expandExcerpts}}
@bulkSelectHelper={{this.bulkSelectHelper}}
@canBulkSelect={{this.canBulkSelect}}
@tagsForUser={{this.tagsForUser}}
@changeSort={{this.changeSort}}
@order={{this.order}}
@ascending={{this.ascending}}
@focusLastVisitedTopic={{this.focusLastVisitedTopic}}
/>
{{else}}
<TopicList
@showPosters={{this.showPosters}}
@hideCategory={{this.hideCategory}}
@topics={{this.topics}}
@expandExcerpts={{this.expandExcerpts}}
@bulkSelectHelper={{this.bulkSelectHelper}}
@canBulkSelect={{this.canBulkSelect}}
@tagsForUser={{this.tagsForUser}}
@changeSort={{this.changeSort}}
@order={{this.order}}
@ascending={{this.ascending}}
@focusLastVisitedTopic={{this.focusLastVisitedTopic}}
/>
{{/if}}
<List
@showPosters={{this.showPosters}}
@hideCategory={{this.hideCategory}}
@topics={{this.topics}}
@expandExcerpts={{this.expandExcerpts}}
@bulkSelectHelper={{this.bulkSelectHelper}}
@canBulkSelect={{this.canBulkSelect}}
@tagsForUser={{this.tagsForUser}}
@changeSort={{this.changeSort}}
@order={{this.order}}
@ascending={{this.ascending}}
@focusLastVisitedTopic={{this.focusLastVisitedTopic}}
/>
{{else}}
{{#unless this.loadingMore}}
<div class="alert alert-info">

View File

@ -19,11 +19,7 @@ export default class CategoriesTopicList extends Component {
{{#if this.topics}}
{{#each this.topics as |t|}}
{{#if this.site.useGlimmerTopicList}}
<LatestTopicListItem @topic={{t}} />
{{else}}
<LatestTopicListItem @topic={{t}} />
{{/if}}
<LatestTopicListItem @topic={{t}} />
{{/each}}
<div class="more-topics">

View File

@ -14,7 +14,6 @@ import NewListHeaderControlsWrapper from "discourse/components/new-list-header-c
import PluginOutlet from "discourse/components/plugin-outlet";
import TopPeriodButtons from "discourse/components/top-period-buttons";
import TopicDismissButtons from "discourse/components/topic-dismiss-buttons";
import TopicList from "discourse/components/topic-list";
import List from "discourse/components/topic-list/list";
import basePath from "discourse/helpers/base-path";
import hideApplicationFooter from "discourse/helpers/hide-application-footer";
@ -209,27 +208,15 @@ export default class DiscoveryTopics extends Component {
{{/if}}
{{#if @model.sharedDrafts}}
{{#if this.site.useGlimmerTopicList}}
<List
@listTitle="shared_drafts.title"
@top={{this.top}}
@hideCategory="true"
@category={{@category}}
@topics={{@model.sharedDrafts}}
@discoveryList={{true}}
class="shared-drafts"
/>
{{else}}
<TopicList
@listTitle="shared_drafts.title"
@top={{this.top}}
@hideCategory="true"
@category={{@category}}
@topics={{@model.sharedDrafts}}
@discoveryList={{true}}
class="shared-drafts"
/>
{{/if}}
<List
@listTitle="shared_drafts.title"
@top={{this.top}}
@hideCategory="true"
@category={{@category}}
@topics={{@model.sharedDrafts}}
@discoveryList={{true}}
class="shared-drafts"
/>
{{/if}}
<DiscoveryTopicsList
@ -290,57 +277,30 @@ export default class DiscoveryTopics extends Component {
</span>
{{#if this.hasTopics}}
{{#if this.site.useGlimmerTopicList}}
<List
@highlightLastVisited={{true}}
@top={{this.top}}
@hot={{this.hot}}
@showTopicPostBadges={{this.showTopicPostBadges}}
@showPosters={{true}}
@canBulkSelect={{@canBulkSelect}}
@bulkSelectHelper={{@bulkSelectHelper}}
@changeSort={{@changeSort}}
@hideCategory={{@model.hideCategory}}
@order={{this.order}}
@ascending={{this.ascending}}
@expandGloballyPinned={{this.expandGloballyPinned}}
@expandAllPinned={{this.expandAllPinned}}
@category={{@category}}
@topics={{@model.topics}}
@discoveryList={{true}}
@focusLastVisitedTopic={{true}}
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
@newListSubset={{@model.params.subset}}
@changeNewListSubset={{@changeNewListSubset}}
@newRepliesCount={{this.newRepliesCount}}
@newTopicsCount={{this.newTopicsCount}}
/>
{{else}}
<TopicList
@highlightLastVisited={{true}}
@top={{this.top}}
@hot={{this.hot}}
@showTopicPostBadges={{this.showTopicPostBadges}}
@showPosters={{true}}
@canBulkSelect={{@canBulkSelect}}
@bulkSelectHelper={{@bulkSelectHelper}}
@changeSort={{@changeSort}}
@hideCategory={{@model.hideCategory}}
@order={{this.order}}
@ascending={{this.ascending}}
@expandGloballyPinned={{this.expandGloballyPinned}}
@expandAllPinned={{this.expandAllPinned}}
@category={{@category}}
@topics={{@model.topics}}
@discoveryList={{true}}
@focusLastVisitedTopic={{true}}
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
@newListSubset={{@model.params.subset}}
@changeNewListSubset={{@changeNewListSubset}}
@newRepliesCount={{this.newRepliesCount}}
@newTopicsCount={{this.newTopicsCount}}
/>
{{/if}}
<List
@highlightLastVisited={{true}}
@top={{this.top}}
@hot={{this.hot}}
@showTopicPostBadges={{this.showTopicPostBadges}}
@showPosters={{true}}
@canBulkSelect={{@canBulkSelect}}
@bulkSelectHelper={{@bulkSelectHelper}}
@changeSort={{@changeSort}}
@hideCategory={{@model.hideCategory}}
@order={{this.order}}
@ascending={{this.ascending}}
@expandGloballyPinned={{this.expandGloballyPinned}}
@expandAllPinned={{this.expandAllPinned}}
@category={{@category}}
@topics={{@model.topics}}
@discoveryList={{true}}
@focusLastVisitedTopic={{true}}
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
@newListSubset={{@model.params.subset}}
@changeNewListSubset={{@changeNewListSubset}}
@newRepliesCount={{this.newRepliesCount}}
@newTopicsCount={{this.newTopicsCount}}
/>
{{/if}}
<span class="after-topic-list-plugin-outlet-wrapper">

View File

@ -1,127 +0,0 @@
import Component from "@ember/component";
import { hash } from "@ember/helper";
import {
attributeBindings,
classNameBindings,
} from "@ember-decorators/component";
import PluginOutlet from "discourse/components/plugin-outlet";
import ItemRepliesCell from "discourse/components/topic-list/item/replies-cell";
import {
navigateToTopic,
showEntrance,
} from "discourse/components/topic-list-item";
import TopicPostBadges from "discourse/components/topic-post-badges";
import TopicStatus from "discourse/components/topic-status";
import UserAvatarFlair from "discourse/components/user-avatar-flair";
import UserLink from "discourse/components/user-link";
import avatar from "discourse/helpers/avatar";
import categoryLink from "discourse/helpers/category-link";
import discourseTags from "discourse/helpers/discourse-tags";
import formatDate from "discourse/helpers/format-date";
import topicFeaturedLink from "discourse/helpers/topic-featured-link";
import topicLink from "discourse/helpers/topic-link";
import discourseComputed from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
@attributeBindings("topic.id:data-topic-id")
@classNameBindings(":latest-topic-list-item", "unboundClassNames")
export default class LatestTopicListItem extends Component {
showEntrance = showEntrance;
navigateToTopic = navigateToTopic;
click(e) {
// for events undefined has a different meaning than false
if (this.showEntrance(e) === false) {
return false;
}
return this.unhandledRowClick(e, this.topic);
}
// Can be overwritten by plugins to handle clicks on other parts of the row
unhandledRowClick() {}
@discourseComputed("topic")
unboundClassNames(topic) {
let classes = [];
if (topic.get("category")) {
classes.push("category-" + topic.get("category.fullSlug"));
}
if (topic.get("tags")) {
topic.get("tags").forEach((tagName) => classes.push("tag-" + tagName));
}
["liked", "archived", "bookmarked", "pinned", "closed", "visited"].forEach(
(name) => {
if (topic.get(name)) {
classes.push(name);
}
}
);
return classes.join(" ");
}
<template>
<PluginOutlet
@name="above-latest-topic-list-item"
@connectorTagName="div"
@outletArgs={{hash topic=this.topic}}
/>
<div class="topic-poster">
<UserLink
@user={{this.topic.lastPosterUser}}
aria-label={{if
this.topic.lastPosterUser.username
(i18n
"latest_poster_link" username=this.topic.lastPosterUser.username
)
}}
>
{{avatar this.topic.lastPosterUser imageSize="large"}}
</UserLink>
<UserAvatarFlair @user={{this.topic.lastPosterUser}} />
</div>
<div class="main-link">
<div class="top-row">
<TopicStatus @topic={{this.topic}} />
{{topicLink this.topic}}
{{~#if this.topic.featured_link}}
&nbsp;{{topicFeaturedLink this.topic}}
{{/if}}{{! intentionally inline
to avoid whitespace}}<TopicPostBadges
@unreadPosts={{this.topic.unread_posts}}
@unseen={{this.topic.unseen}}
@url={{this.topic.lastUnreadUrl}}
/>
</div>
<div class="bottom-row">
{{categoryLink this.topic.category}}{{discourseTags
this.topic
mode="list"
}}{{! intentionally inline to avoid whitespace}}
<PluginOutlet
@name="below-latest-topic-list-item-bottom-row"
@connectorTagName="span"
@outletArgs={{hash topic=this.topic}}
/>
</div>
</div>
<div class="topic-stats">
<PluginOutlet
@name="above-latest-topic-list-item-post-count"
@connectorTagName="div"
@outletArgs={{hash topic=this.topic}}
/>
<ItemRepliesCell @topic={{this.topic}} @tagName="div" />
<div class="topic-last-activity">
<a
href={{this.topic.lastPostUrl}}
title={{this.topic.bumpedAtTitle}}
>{{formatDate this.topic.bumpedAt format="tiny" noTitle="true"}}</a>
</div>
</div>
</template>
}

View File

@ -1,13 +1,32 @@
import Component from "@ember/component";
import { classNameBindings, tagName } from "@ember-decorators/component";
import $ from "jquery";
import PostCountOrBadges from "discourse/components/topic-list/post-count-or-badges";
import { showEntrance } from "discourse/components/topic-list-item";
import TopicStatus from "discourse/components/topic-status";
import coldAgeClass from "discourse/helpers/cold-age-class";
import formatAge from "discourse/helpers/format-age";
import rawDate from "discourse/helpers/raw-date";
import topicLink from "discourse/helpers/topic-link";
export function showEntrance(e) {
let target = $(e.target);
if (target.hasClass("posts-map") || target.parents(".posts-map").length > 0) {
if (target.prop("tagName") !== "A") {
target = target.find("a");
if (target.length === 0) {
target = target.end();
}
}
this.appEvents.trigger("topic-entrance:show", {
topic: this.topic,
position: target.offset(),
});
return false;
}
}
@tagName("tr")
@classNameBindings(":category-topic-link", "topic.archived", "topic.visited")
export default class MobileCategoryTopic extends Component {

View File

@ -1,9 +1,7 @@
import Component from "@glimmer/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import NewListHeaderControls from "discourse/components/topic-list/new-list-header-controls";
import raw from "discourse/helpers/raw";
export default class NewListHeaderControlsWrapper extends Component {
@service site;
@ -21,30 +19,14 @@ export default class NewListHeaderControlsWrapper extends Component {
}
<template>
{{#if this.site.useGlimmerTopicList}}
<div class="topic-replies-toggle-wrapper">
<NewListHeaderControls
@current={{@current}}
@newRepliesCount={{@newRepliesCount}}
@newTopicsCount={{@newTopicsCount}}
@noStaticLabel={{true}}
@changeNewListSubset={{@changeNewListSubset}}
/>
</div>
{{else}}
<div
{{! template-lint-disable no-invalid-interactive }}
{{on "click" this.click}}
class="topic-replies-toggle-wrapper"
>
{{raw
"list/new-list-header-controls"
current=@current
newRepliesCount=@newRepliesCount
newTopicsCount=@newTopicsCount
noStaticLabel=true
}}
</div>
{{/if}}
<div class="topic-replies-toggle-wrapper">
<NewListHeaderControls
@current={{@current}}
@newRepliesCount={{@newRepliesCount}}
@newTopicsCount={{@newTopicsCount}}
@noStaticLabel={{true}}
@changeNewListSubset={{@changeNewListSubset}}
/>
</div>
</template>
}

View File

@ -222,11 +222,7 @@ export default class ParentCategoryRow extends CategoryListItem {
{{#if this.showTopics}}
<td class="latest">
{{#each this.category.featuredTopics as |t|}}
{{#if this.site.useGlimmerTopicList}}
<FeaturedTopic @topic={{t}} />
{{else}}
<FeaturedTopic @topic={{t}} />
{{/if}}
<FeaturedTopic @topic={{t}} />
{{/each}}
</td>
<PluginOutlet

View File

@ -1,418 +0,0 @@
import Component from "@ember/component";
import { hash } from "@ember/helper";
import { alias } from "@ember/object/computed";
import { getOwner } from "@ember/owner";
import { schedule } from "@ember/runloop";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import {
attributeBindings,
classNameBindings,
tagName,
} from "@ember-decorators/component";
import { observes, on } from "@ember-decorators/object";
import $ from "jquery";
import PluginOutlet from "discourse/components/plugin-outlet";
import discourseComputed, { bind } from "discourse/lib/decorators";
import deprecated from "discourse/lib/deprecated";
import { wantsNewWindow } from "discourse/lib/intercept-click";
import { RAW_TOPIC_LIST_DEPRECATION_OPTIONS } from "discourse/lib/plugin-api";
import { RUNTIME_OPTIONS } from "discourse/lib/raw-handlebars-helpers";
import { findRawTemplate } from "discourse/lib/raw-templates";
import { applyValueTransformer } from "discourse/lib/transformer";
import DiscourseURL, { groupPath } from "discourse/lib/url";
import { i18n } from "discourse-i18n";
export function showEntrance(e) {
let target = $(e.target);
if (target.hasClass("posts-map") || target.parents(".posts-map").length > 0) {
if (target.prop("tagName") !== "A") {
target = target.find("a");
if (target.length === 0) {
target = target.end();
}
}
this.appEvents.trigger("topic-entrance:show", {
topic: this.topic,
position: target.offset(),
});
return false;
}
}
export function navigateToTopic(topic, href) {
const historyStore = getOwner(this).lookup("service:history-store");
historyStore.set("lastTopicIdViewed", topic.id);
DiscourseURL.routeTo(href || topic.get("url"));
return false;
}
@tagName("tr")
@classNameBindings(":topic-list-item", "unboundClassNames", "topic.visited")
@attributeBindings("dataTopicId:data-topic-id", "role", "ariaLevel:aria-level")
export default class TopicListItem extends Component {
static reopen() {
deprecated(
"Modifying topic-list-item with `reopen` is deprecated. Use the value transformer `topic-list-columns` and other new topic-list plugin APIs instead.",
RAW_TOPIC_LIST_DEPRECATION_OPTIONS
);
return super.reopen(...arguments);
}
static reopenClass() {
deprecated(
"Modifying topic-list-item with `reopenClass` is deprecated. Use the value transformer `topic-list-columns` and other new topic-list plugin APIs instead.",
RAW_TOPIC_LIST_DEPRECATION_OPTIONS
);
return super.reopenClass(...arguments);
}
@service router;
@service historyStore;
@alias("topic.id") dataTopicId;
didReceiveAttrs() {
super.didReceiveAttrs(...arguments);
this.renderTopicListItem();
}
// Already-rendered topic is marked as highlighted
// Ideally this should be a modifier... but we can't do that
// until this component has its tagName removed.
@observes("topic.highlight")
topicHighlightChanged() {
if (this.topic.highlight) {
this._highlightIfNeeded();
}
}
@observes("topic.pinned", "expandGloballyPinned", "expandAllPinned")
renderTopicListItem() {
const template = findRawTemplate("list/topic-list-item");
if (template) {
this.set(
"topicListItemContents",
htmlSafe(template(this, RUNTIME_OPTIONS))
);
schedule("afterRender", () => {
if (this.isDestroyed || this.isDestroying) {
return;
}
if (this.selected && this.selected.includes(this.topic)) {
this.element.querySelector("input.bulk-select").checked = true;
}
if (this._shouldFocusLastVisited()) {
const title = this._titleElement();
if (title) {
title.addEventListener("focus", this._onTitleFocus);
title.addEventListener("blur", this._onTitleBlur);
}
}
});
}
}
didInsertElement() {
super.didInsertElement(...arguments);
if (this.includeUnreadIndicator) {
this.messageBus.subscribe(this.unreadIndicatorChannel, this.onMessage);
}
}
willDestroyElement() {
super.willDestroyElement(...arguments);
this.messageBus.unsubscribe(this.unreadIndicatorChannel, this.onMessage);
if (this._shouldFocusLastVisited()) {
const title = this._titleElement();
if (title) {
title.removeEventListener("focus", this._onTitleFocus);
title.removeEventListener("blur", this._onTitleBlur);
}
}
}
@bind
onMessage(data) {
const nodeClassList = document.querySelector(
`.indicator-topic-${data.topic_id}`
).classList;
nodeClassList.toggle("read", !data.show_indicator);
}
@discourseComputed("topic.participant_groups")
participantGroups(groupNames) {
if (!groupNames) {
return [];
}
return groupNames.map((name) => {
return { name, url: groupPath(name) };
});
}
@discourseComputed("topic.id")
unreadIndicatorChannel(topicId) {
return `/private-messages/unread-indicator/${topicId}`;
}
@discourseComputed("topic.unread_by_group_member")
unreadClass(unreadByGroupMember) {
return unreadByGroupMember ? "" : "read";
}
@discourseComputed("topic.unread_by_group_member")
includeUnreadIndicator(unreadByGroupMember) {
return typeof unreadByGroupMember !== "undefined";
}
@discourseComputed
newDotText() {
return this.currentUser && this.currentUser.trust_level > 0
? ""
: i18n("filters.new.lower_title");
}
@discourseComputed("topic", "lastVisitedTopic")
unboundClassNames(topic, lastVisitedTopic) {
let classes = [];
if (topic.get("category")) {
classes.push("category-" + topic.get("category.fullSlug"));
}
if (topic.get("tags")) {
topic.get("tags").forEach((tag) => classes.push("tag-" + tag));
}
if (topic.get("hasExcerpt")) {
classes.push("has-excerpt");
}
if (topic.get("unseen")) {
classes.push("unseen-topic");
}
if (topic.unread_posts) {
classes.push("unread-posts");
}
["liked", "archived", "bookmarked", "pinned", "closed"].forEach((name) => {
if (topic.get(name)) {
classes.push(name);
}
});
if (topic === lastVisitedTopic) {
classes.push("last-visit");
}
return classes.join(" ");
}
hasLikes() {
return this.get("topic.like_count") > 0;
}
hasOpLikes() {
return this.get("topic.op_like_count") > 0;
}
@discourseComputed
expandPinned() {
return applyValueTransformer(
"topic-list-item-expand-pinned",
this._expandPinned,
{
topic: this.topic,
mobileView: this.site.mobileView,
}
);
}
get _expandPinned() {
const pinned = this.get("topic.pinned");
if (!pinned) {
return false;
}
if (this.site.mobileView) {
if (!this.siteSettings.show_pinned_excerpt_mobile) {
return false;
}
} else {
if (!this.siteSettings.show_pinned_excerpt_desktop) {
return false;
}
}
if (this.expandGloballyPinned && this.get("topic.pinned_globally")) {
return true;
}
if (this.expandAllPinned) {
return true;
}
return false;
}
showEntrance() {
return showEntrance.call(this, ...arguments);
}
click(e) {
const result = this.showEntrance(e);
if (result === false) {
return result;
}
const topic = this.topic;
const target = e.target;
const classList = target.classList;
if (classList.contains("bulk-select")) {
const selected = this.selected;
if (target.checked) {
selected.addObject(topic);
if (this.lastChecked && e.shiftKey) {
const bulkSelects = Array.from(
document.querySelectorAll("input.bulk-select")
),
from = bulkSelects.indexOf(target),
to = bulkSelects.findIndex((el) => el.id === this.lastChecked.id),
start = Math.min(from, to),
end = Math.max(from, to);
bulkSelects
.slice(start, end)
.filter((el) => el.checked !== true)
.forEach((checkbox) => {
checkbox.click();
});
}
this.set("lastChecked", target);
} else {
selected.removeObject(topic);
this.set("lastChecked", null);
}
}
if (
classList.contains("raw-topic-link") ||
classList.contains("post-activity")
) {
if (wantsNewWindow(e)) {
return true;
}
e.preventDefault();
return this.navigateToTopic(topic, target.getAttribute("href"));
}
// make full row click target on mobile, due to size constraints
if (
this.site.mobileView &&
e.target.matches(
".topic-list-data, .main-link, .right, .topic-item-stats, .topic-item-stats__category-tags, .discourse-tags"
)
) {
if (wantsNewWindow(e)) {
return true;
}
e.preventDefault();
return this.navigateToTopic(topic, topic.lastUnreadUrl);
}
return this.unhandledRowClick(e, topic);
}
unhandledRowClick() {}
keyDown(e) {
if (e.key === "Enter" && e.target.classList.contains("post-activity")) {
e.preventDefault();
return this.navigateToTopic(this.topic, e.target.getAttribute("href"));
}
}
navigateToTopic() {
return navigateToTopic.call(this, ...arguments);
}
highlight(opts = { isLastViewedTopic: false }) {
schedule("afterRender", () => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
this.element.classList.add("highlighted");
this.element.setAttribute(
"data-is-last-viewed-topic",
opts.isLastViewedTopic
);
this.element.addEventListener("animationend", () => {
this.element.classList.remove("highlighted");
});
if (opts.isLastViewedTopic && this._shouldFocusLastVisited()) {
this._titleElement()?.focus();
}
});
}
@on("didInsertElement")
_highlightIfNeeded() {
// highlight the last topic viewed
const lastViewedTopicId = this.historyStore.get("lastTopicIdViewed");
const isLastViewedTopic = lastViewedTopicId === this.topic.id;
if (isLastViewedTopic) {
this.historyStore.delete("lastTopicIdViewed");
this.highlight({ isLastViewedTopic: true });
} else if (this.get("topic.highlight")) {
// highlight new topics that have been loaded from the server or the one we just created
this.set("topic.highlight", false);
this.highlight();
}
}
@bind
_onTitleFocus() {
if (this.element && !this.isDestroying && !this.isDestroyed) {
this.element.classList.add("selected");
}
}
@bind
_onTitleBlur() {
if (this.element && !this.isDestroying && !this.isDestroyed) {
this.element.classList.remove("selected");
}
}
_shouldFocusLastVisited() {
return this.site.desktopView && this.focusLastVisitedTopic;
}
_titleElement() {
return this.element.querySelector(".main-link .title");
}
<template>
<PluginOutlet
@name="above-topic-list-item"
@outletArgs={{hash topic=this.topic}}
/>
{{this.topicListItemContents}}
</template>
}

View File

@ -1,339 +1,20 @@
import Component from "@ember/component";
import { hash } from "@ember/helper";
import { dependentKeyCompat } from "@ember/object/compat";
import { alias } from "@ember/object/computed";
import { service } from "@ember/service";
import {
classNameBindings,
classNames,
tagName,
} from "@ember-decorators/component";
import { observes, on } from "@ember-decorators/object";
import PluginOutlet from "discourse/components/plugin-outlet";
import TopicListItem from "discourse/components/topic-list-item";
import raw from "discourse/helpers/raw";
import discourseComputed from "discourse/lib/decorators";
import Component from "@glimmer/component";
import curryComponent from "ember-curry-component";
import List from "discourse/components/topic-list/list";
import deprecated from "discourse/lib/deprecated";
import { RAW_TOPIC_LIST_DEPRECATION_OPTIONS } from "discourse/lib/plugin-api";
import LoadMore from "discourse/mixins/load-more";
import { i18n } from "discourse-i18n";
@tagName("table")
@classNames("topic-list")
@classNameBindings("bulkSelectEnabled:sticky-header")
export default class TopicList extends Component.extend(LoadMore) {
static reopen() {
export default class TopicListShim extends Component {
constructor() {
super(...arguments);
deprecated(
"Modifying topic-list with `reopen` is deprecated. Use the value transformer `topic-list-columns` and other new topic-list plugin APIs instead.",
RAW_TOPIC_LIST_DEPRECATION_OPTIONS
`components/topic-list is deprecated, and should be replaced with components/topics-list/list`,
{ id: "discourse.legacy-topic-list" }
);
return super.reopen(...arguments);
}
static reopenClass() {
deprecated(
"Modifying topic-list with `reopenClass` is deprecated. Use the value transformer `topic-list-columns` and other new topic-list plugin APIs instead.",
RAW_TOPIC_LIST_DEPRECATION_OPTIONS
);
return super.reopenClass(...arguments);
}
@service modal;
@service router;
@service siteSettings;
showTopicPostBadges = true;
listTitle = "topic.title";
lastCheckedElementId = null;
// Overwrite this to perform client side filtering of topics, if desired
@alias("topics") filteredTopics;
get canDoBulkActions() {
return (
this.currentUser?.canManageTopic && this.bulkSelectHelper?.selected.length
);
}
@on("init")
_init() {
this.addObserver("hideCategory", this.rerender);
this.addObserver("order", this.rerender);
this.addObserver("ascending", this.rerender);
this.refreshLastVisited();
}
get selected() {
return this.bulkSelectHelper?.selected;
}
// for the classNameBindings
@dependentKeyCompat
get bulkSelectEnabled() {
return (
this.get("canBulkSelect") && this.bulkSelectHelper?.bulkSelectEnabled
);
}
get toggleInTitle() {
return (
!this.bulkSelectHelper?.bulkSelectEnabled && this.get("canBulkSelect")
);
}
@discourseComputed
sortable() {
return !!this.changeSort;
}
@discourseComputed("order")
showLikes(order) {
return order === "likes";
}
@discourseComputed("order")
showOpLikes(order) {
return order === "op_likes";
}
@observes("topics.[]")
topicsAdded() {
// special case so we don't keep scanning huge lists
if (!this.lastVisitedTopic) {
this.refreshLastVisited();
}
}
@observes("topics", "order", "ascending", "category", "top", "hot")
lastVisitedTopicChanged() {
this.refreshLastVisited();
}
scrolled() {
super.scrolled(...arguments);
let onScroll = this.onScroll;
if (!onScroll) {
return;
}
onScroll.call(this);
}
_updateLastVisitedTopic(topics, order, ascending, top, hot) {
this.set("lastVisitedTopic", null);
if (!this.highlightLastVisited) {
return;
}
if (order && order !== "activity") {
return;
}
if (top || hot) {
return;
}
if (!topics || topics.length === 1) {
return;
}
if (ascending) {
return;
}
let user = this.currentUser;
if (!user || !user.previous_visit_at) {
return;
}
let lastVisitedTopic, topic;
let prevVisit = user.get("previousVisitAt");
// this is more efficient cause we keep appending to list
// work backwards
let start = 0;
while (topics[start] && topics[start].get("pinned")) {
start++;
}
let i;
for (i = topics.length - 1; i >= start; i--) {
if (topics[i].get("bumpedAt") > prevVisit) {
lastVisitedTopic = topics[i];
break;
}
topic = topics[i];
}
if (!lastVisitedTopic || !topic) {
return;
}
// end of list that was scanned
if (topic.get("bumpedAt") > prevVisit) {
return;
}
this.set("lastVisitedTopic", lastVisitedTopic);
}
refreshLastVisited() {
this._updateLastVisitedTopic(
this.topics,
this.order,
this.ascending,
this.top,
this.hot
);
}
click(e) {
const onClick = (sel, callback) => {
let target = e.target.closest(sel);
if (target) {
callback(target);
}
};
onClick("button.bulk-select", () => {
this.bulkSelectHelper.toggleBulkSelect();
this.rerender();
});
onClick("button.bulk-select-all", () => {
this.bulkSelectHelper.autoAddTopicsToBulkSelect = true;
document
.querySelectorAll("input.bulk-select:not(:checked)")
.forEach((el) => el.click());
});
onClick("button.bulk-clear-all", () => {
this.bulkSelectHelper.autoAddTopicsToBulkSelect = false;
document
.querySelectorAll("input.bulk-select:checked")
.forEach((el) => el.click());
});
onClick("th.sortable", (element) => {
this.changeSort(element.dataset.sortOrder);
this.rerender();
});
onClick("button.topics-replies-toggle", (element) => {
if (element.classList.contains("--all")) {
this.changeNewListSubset(null);
} else if (element.classList.contains("--topics")) {
this.changeNewListSubset("topics");
} else if (element.classList.contains("--replies")) {
this.changeNewListSubset("replies");
}
this.rerender();
});
}
keyDown(e) {
if (e.key === "Enter" || e.key === " ") {
let onKeyDown = (sel, callback) => {
let target = e.target.closest(sel);
if (target) {
callback.call(this, target);
}
};
onKeyDown("th.sortable", (element) => {
e.preventDefault();
this.changeSort(element.dataset.sortOrder);
this.rerender();
});
}
}
<template>
<caption class="sr-only">{{i18n "sr_topic_list_caption"}}</caption>
<thead class="topic-list-header">
{{raw
"topic-list-header"
canBulkSelect=this.canBulkSelect
toggleInTitle=this.toggleInTitle
hideCategory=this.hideCategory
showPosters=this.showPosters
showLikes=this.showLikes
showOpLikes=this.showOpLikes
order=this.order
ascending=this.ascending
sortable=this.sortable
listTitle=this.listTitle
bulkSelectEnabled=this.bulkSelectEnabled
bulkSelectHelper=this.bulkSelectHelper
canDoBulkActions=this.canDoBulkActions
showTopicsAndRepliesToggle=this.showTopicsAndRepliesToggle
newListSubset=this.newListSubset
newRepliesCount=this.newRepliesCount
newTopicsCount=this.newTopicsCount
}}
</thead>
<PluginOutlet
@name="before-topic-list-body"
@outletArgs={{hash
topics=this.topics
selected=this.selected
bulkSelectEnabled=this.bulkSelectEnabled
lastVisitedTopic=this.lastVisitedTopic
discoveryList=this.discoveryList
hideCategory=this.hideCategory
}}
/>
<tbody class="topic-list-body">
{{#each this.filteredTopics as |topic index|}}
<TopicListItem
@topic={{topic}}
@bulkSelectEnabled={{this.bulkSelectEnabled}}
@showTopicPostBadges={{this.showTopicPostBadges}}
@hideCategory={{this.hideCategory}}
@showPosters={{this.showPosters}}
@showLikes={{this.showLikes}}
@showOpLikes={{this.showOpLikes}}
@expandGloballyPinned={{this.expandGloballyPinned}}
@expandAllPinned={{this.expandAllPinned}}
@lastVisitedTopic={{this.lastVisitedTopic}}
@selected={{this.selected}}
@lastChecked={{this.lastChecked}}
@tagsForUser={{this.tagsForUser}}
@focusLastVisitedTopic={{this.focusLastVisitedTopic}}
@index={{index}}
/>
{{raw
"list/visited-line"
lastVisitedTopic=this.lastVisitedTopic
topic=topic
}}
<PluginOutlet
@name="after-topic-list-item"
@outletArgs={{hash topic=topic index=index}}
@connectorTagName="tr"
/>
{{/each}}
</tbody>
<PluginOutlet
@name="after-topic-list-body"
@outletArgs={{hash
topics=this.topics
selected=this.selected
bulkSelectEnabled=this.bulkSelectEnabled
lastVisitedTopic=this.lastVisitedTopic
discoveryList=this.discoveryList
hideCategory=this.hideCategory
}}
/>
{{#let (curryComponent List this.args) as |CurriedComponent|}}
<CurriedComponent />
{{/let}}
</template>
}

View File

@ -1,5 +1,5 @@
import Component from "@glimmer/component";
import { concat, get, hash } from "@ember/helper";
import { hash } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
@ -7,7 +7,6 @@ import { and } from "truth-helpers";
import PluginOutlet from "discourse/components/plugin-outlet";
import icon from "discourse/helpers/d-icon";
import element from "discourse/helpers/element";
import TopicStatusIcons from "discourse/helpers/topic-status-icons";
import { i18n } from "discourse-i18n";
export default class TopicStatus extends Component {
@ -108,22 +107,10 @@ export default class TopicStatus extends Component {
class="topic-status"
>{{icon "far-eye-slash"}}</span>
{{~/if~}}
{{~#if this.site.useGlimmerTopicList~}}
<PluginOutlet
@name="after-topic-status"
@outletArgs={{hash topic=@topic context=@context}}
/>
{{~else~}}
{{~#each TopicStatusIcons.entries as |entry|~}}
{{~#if (get @topic entry.attribute)~}}
<span
title={{i18n (concat "topic_statuses." entry.titleKey ".help")}}
class="topic-status"
>{{icon entry.iconName}}</span>
{{~/if~}}
{{~/each~}}
{{~/if~}}
<PluginOutlet
@name="after-topic-status"
@outletArgs={{hash topic=@topic context=@context}}
/>
{{~! no whitespace ~}}
</this.wrapperElement>
{{~! no whitespace ~}}

View File

@ -84,15 +84,6 @@ loaderShim("discourse-common/lib/object", () =>
loaderShim("discourse-common/lib/popular-themes", () =>
importSync("discourse/lib/popular-themes")
);
loaderShim("discourse-common/lib/raw-handlebars-helpers", () =>
importSync("discourse/lib/raw-handlebars-helpers")
);
loaderShim("discourse-common/lib/raw-handlebars", () =>
importSync("discourse/lib/raw-handlebars")
);
loaderShim("discourse-common/lib/raw-templates", () =>
importSync("discourse/lib/raw-templates")
);
loaderShim("discourse-common/lib/suffix-trie", () =>
importSync("discourse/lib/suffix-trie")
);

View File

@ -1,8 +1,5 @@
import { htmlSafe } from "@ember/template";
import { autoUpdatingRelativeAge } from "discourse/lib/formatter";
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("age-with-tooltip", ageWithTooltip);
export default function ageWithTooltip(dt, params = {}) {
return htmlSafe(

View File

@ -1,7 +1,6 @@
import { get } from "@ember/object";
import { htmlSafe } from "@ember/template";
import { avatarImg } from "discourse/lib/avatar-utils";
import { registerRawHelper } from "discourse/lib/helpers";
import { prioritizeNameInUx } from "discourse/lib/settings";
import { formatUsername } from "discourse/lib/utilities";
import { i18n } from "discourse-i18n";
@ -81,7 +80,6 @@ export function renderAvatar(user, options) {
}
}
registerRawHelper("avatar", avatar);
export default function avatar(user, params) {
return htmlSafe(renderAvatar.call(this, user, params));
}

View File

@ -1,7 +1,4 @@
import getUrl from "discourse/lib/get-url";
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("base-path", basePath);
export default function basePath() {
return getUrl("");

View File

@ -1,8 +1,5 @@
import deprecated from "discourse/lib/deprecated";
import getUrl from "discourse/lib/get-url";
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("base-url", baseUrl);
export default function baseUrl() {
deprecated("Use `{{base-path}}` instead of `{{base-url}}`", {

View File

@ -1,8 +1,5 @@
import { isPresent } from "@ember/utils";
import { categoryLinkHTML } from "discourse/helpers/category-link";
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("category-badge", categoryBadge);
export default function categoryBadge(cat, options = {}) {
return categoryLinkHTML(cat, {

View File

@ -3,7 +3,7 @@ import { htmlSafe } from "@ember/template";
import categoryVariables from "discourse/helpers/category-variables";
import replaceEmoji from "discourse/helpers/replace-emoji";
import getURL from "discourse/lib/get-url";
import { helperContext, registerRawHelper } from "discourse/lib/helpers";
import { helperContext } from "discourse/lib/helpers";
import { iconHTML } from "discourse/lib/icon-library";
import { applyValueTransformer } from "discourse/lib/transformer";
import { escapeExpression } from "discourse/lib/utilities";
@ -116,7 +116,6 @@ export function categoryLinkHTML(category, options) {
}
export default categoryLinkHTML;
registerRawHelper("category-link", categoryLinkHTML);
function buildTopicCount(count) {
return `<span class="topic-count" aria-label="${i18n(

View File

@ -1,12 +1,10 @@
import { helperContext, registerRawHelper } from "discourse/lib/helpers";
import { helperContext } from "discourse/lib/helpers";
function daysSinceEpoch(dt) {
// 1000 * 60 * 60 * 24 = days since epoch
return dt.getTime() / 86400000;
}
registerRawHelper("cold-age-class", coldAgeClass);
export default function coldAgeClass(dt, params = {}) {
let className = params["class"] || "age";

View File

@ -1,7 +1,3 @@
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("component-for-collection", componentForCollection);
export default function componentForCollection(
collectionIdentifier,
selectKit

View File

@ -1,7 +1,3 @@
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("component-for-row", componentForRow);
export default function componentForRow(
collectionForIdentifier,
item,

View File

@ -1,9 +1,6 @@
import { htmlSafe } from "@ember/template";
import { registerRawHelper } from "discourse/lib/helpers";
import { renderIcon } from "discourse/lib/icon-library";
export default function icon(id, options = {}) {
return htmlSafe(renderIcon("string", id, options));
}
registerRawHelper("d-icon", icon);

View File

@ -1,5 +1,4 @@
import { htmlSafe } from "@ember/template";
import { registerRawHelper } from "discourse/lib/helpers";
let usernameDecorators = [];
export function addUsernameSelectorDecorator(decorator) {
@ -20,8 +19,6 @@ export function decorateUsername(username) {
return decorations.length ? htmlSafe(decorations.join("")) : "";
}
registerRawHelper("decorate-username-selector", decorateUsernameSelector);
export default function decorateUsernameSelector(username) {
return decorateUsername(username);
}

View File

@ -1,5 +1,5 @@
import { htmlSafe } from "@ember/template";
import { helperContext, registerRawHelper } from "discourse/lib/helpers";
import { helperContext } from "discourse/lib/helpers";
import { escapeExpression } from "discourse/lib/utilities";
function setDir(text) {
@ -9,8 +9,6 @@ function setDir(text) {
return `<span ${mixed ? 'dir="auto"' : ""}>${content}</span>`;
}
registerRawHelper("dir-span", dirSpan);
export default function dirSpan(str, params = {}) {
let isHtmlSafe = false;
if (params.htmlSafe) {

View File

@ -1,8 +1,6 @@
import { htmlSafe } from "@ember/template";
import { registerRawHelper } from "discourse/lib/helpers";
import renderTag from "discourse/lib/render-tag";
registerRawHelper("discourse-tag", discourseTag);
export default function discourseTag(name, params) {
return htmlSafe(renderTag(name, params));
}

View File

@ -1,8 +1,6 @@
import { htmlSafe } from "@ember/template";
import { registerRawHelper } from "discourse/lib/helpers";
import renderTags from "discourse/lib/render-tags";
registerRawHelper("discourse-tags", discourseTags);
export default function discourseTags(topic, params) {
return htmlSafe(renderTags(topic, params));
}

View File

@ -1,9 +1,7 @@
import { htmlSafe } from "@ember/template";
import { registerRawHelper } from "discourse/lib/helpers";
import { emojiUnescape } from "discourse/lib/text";
import { escapeExpression } from "discourse/lib/utilities";
registerRawHelper("emoji", emoji);
export default function emoji(code, options) {
const escaped = escapeExpression(`:${code}:`);
return htmlSafe(emojiUnescape(escaped, options));

View File

@ -1,13 +1,11 @@
import { htmlSafe } from "@ember/template";
import deprecated from "discourse/lib/deprecated";
import { registerRawHelper } from "discourse/lib/helpers";
import { renderIcon } from "discourse/lib/icon-library";
export function iconHTML(id, params) {
return renderIcon("string", id, params);
}
registerRawHelper("fa-icon", faIcon);
export default function faIcon(icon, params) {
deprecated("Use `{{d-icon}}` instead of `{{fa-icon}}", {
id: "discourse.fa-icon",

View File

@ -1,8 +1,6 @@
import { htmlSafe } from "@ember/template";
import { autoUpdatingRelativeAge } from "discourse/lib/formatter";
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("format-age", formatAge);
export default function formatAge(dt) {
dt = new Date(dt);
return htmlSafe(autoUpdatingRelativeAge(dt));

View File

@ -1,13 +1,11 @@
import { htmlSafe } from "@ember/template";
import { autoUpdatingRelativeAge } from "discourse/lib/formatter";
import { registerRawHelper } from "discourse/lib/helpers";
/**
Display logic for dates. It is unbound in Ember but will use jQuery to
update the dates on a regular interval.
**/
registerRawHelper("format-date", formatDate);
export default function formatDate(val, params = {}) {
let leaveAgo,
format = "medium",

View File

@ -1,8 +1,6 @@
import { htmlSafe } from "@ember/template";
import { durationTiny } from "discourse/lib/formatter";
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("format-duration", formatDuration);
export default function formatDuration(seconds) {
return htmlSafe(durationTiny(seconds));
}

View File

@ -1,5 +1,3 @@
import { registerRawHelper } from "discourse/lib/helpers";
import { formatUsername } from "discourse/lib/utilities";
export default formatUsername;
registerRawHelper("format-username", formatUsername);

View File

@ -1,7 +1,4 @@
import { default as emberGetUrl } from "discourse/lib/get-url";
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("get-url", getUrl);
export default function getUrl(value) {
return emberGetUrl(value);

View File

@ -1,7 +1,4 @@
import { htmlSafe as emberHtmlSafe } from "@ember/template";
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("html-safe", htmlSafe);
export default function htmlSafe(string) {
return emberHtmlSafe(string);

View File

@ -1,8 +1,5 @@
import { registerRawHelper } from "discourse/lib/helpers";
import { i18n } from "discourse-i18n";
registerRawHelper("i18n-yes-no", i18nYesNo);
export default function i18nYesNo(value, params) {
return i18n(value ? "yes_value" : "no_value", params);
}

View File

@ -1,6 +1,3 @@
import { registerRawHelper } from "discourse/lib/helpers";
import { i18n } from "discourse-i18n";
registerRawHelper("i18n", i18n);
export default i18n;

View File

@ -1,11 +1,8 @@
import { htmlSafe } from "@ember/template";
import { number as numberFormatter } from "discourse/lib/formatter";
import { registerRawHelper } from "discourse/lib/helpers";
import { escapeExpression } from "discourse/lib/utilities";
import I18n, { i18n } from "discourse-i18n";
registerRawHelper("number", number);
export default function number(orig, params = {}) {
orig = Math.round(parseFloat(orig));
if (isNaN(orig)) {

View File

@ -1,28 +0,0 @@
import { htmlSafe } from "@ember/template";
import PluginOutlet from "discourse/components/plugin-outlet";
import { connectorsExist } from "discourse/lib/plugin-connectors";
import RawHandlebars from "discourse/lib/raw-handlebars";
import rawRenderGlimmer from "discourse/lib/raw-render-glimmer";
const GlimmerPluginOutletWrapper = <template>
{{~! no whitespace ~}}
<PluginOutlet @name={{@data.name}} @outletArgs={{@data.outletArgs}} />
{{~! no whitespace ~}}
</template>;
RawHandlebars.registerHelper("plugin-outlet", function (options) {
const { name, tagName, outletArgs } = options.hash;
if (!connectorsExist(name)) {
return htmlSafe("");
}
return htmlSafe(
rawRenderGlimmer(
this,
`${tagName || "span"}.hbr-ember-outlet`,
GlimmerPluginOutletWrapper,
{ name, outletArgs }
)
);
});

View File

@ -1,8 +1,5 @@
import { htmlSafe } from "@ember/template";
import { longDate } from "discourse/lib/formatter";
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("raw-date", rawDate);
export default function rawDate(dt) {
return htmlSafe(longDate(new Date(dt)));

View File

@ -1,5 +0,0 @@
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("raw-hash", function (params) {
return params;
});

View File

@ -1,11 +0,0 @@
import { htmlSafe } from "@ember/template";
import { rawConnectorsFor } from "discourse/lib/plugin-connectors";
import RawHandlebars from "discourse/lib/raw-handlebars";
RawHandlebars.registerHelper("raw-plugin-outlet", function (args) {
const connectors = rawConnectorsFor(args.hash.name);
if (connectors.length) {
const output = connectors.map((c) => c.template({ context: this }));
return htmlSafe(output.join(""));
}
});

View File

@ -1,59 +0,0 @@
import Helper from "@ember/component/helper";
import { registerDestructor } from "@ember/destroyable";
import { getOwner, setOwner } from "@ember/owner";
import { schedule } from "@ember/runloop";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { bind } from "discourse/lib/decorators";
import { helperContext, registerRawHelper } from "discourse/lib/helpers";
import { RUNTIME_OPTIONS } from "discourse/lib/raw-handlebars-helpers";
import { findRawTemplate } from "discourse/lib/raw-templates";
function renderRaw(ctx, template, templateName, params) {
params = { ...params };
params.parent ||= ctx;
let context = helperContext();
if (!params.view) {
const viewClass = context.registry.resolve(`raw-view:${templateName}`);
if (viewClass) {
setOwner(params, getOwner(context));
params.view = viewClass.create(params, context);
}
if (!params.view) {
params = { ...params, ...context };
}
}
return htmlSafe(template(params, RUNTIME_OPTIONS));
}
const helperFunction = function (templateName, params) {
templateName = templateName.replace(".", "/");
const template = findRawTemplate(templateName);
if (!template) {
// eslint-disable-next-line no-console
console.warn("Could not find raw template: " + templateName);
return;
}
return renderRaw(this, template, templateName, params);
};
registerRawHelper("raw", helperFunction);
export default class RawHelper extends Helper {
@service renderGlimmer;
compute(args, params) {
registerDestructor(this, this.cleanup);
return helperFunction(...args, params);
}
@bind
cleanup() {
schedule("afterRender", () => this.renderGlimmer.cleanup());
}
}

View File

@ -1,10 +1,7 @@
import { htmlSafe, isHTMLSafe } from "@ember/template";
import { registerRawHelper } from "discourse/lib/helpers";
import { emojiUnescape } from "discourse/lib/text";
import { escapeExpression } from "discourse/lib/utilities";
registerRawHelper("replace-emoji", replaceEmoji);
export default function replaceEmoji(text, options) {
text = isHTMLSafe(text) ? text.toString() : escapeExpression(text);
return htmlSafe(emojiUnescape(text, options));

View File

@ -1,6 +1,3 @@
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("shorten-url", shortenUrl);
export default function shortenUrl(url) {
let matches = url.match(/\//g);

View File

@ -1,7 +1,5 @@
import { registerRawHelper } from "discourse/lib/helpers";
import { i18n } from "discourse-i18n";
registerRawHelper("theme-i18n", themeI18n);
export default function themeI18n(themeId, key, params) {
if (typeof themeId !== "number") {
throw new Error(

View File

@ -1,6 +1,3 @@
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("theme-prefix", themePrefix);
export default function themePrefix(themeId, key) {
return `theme_translations.${themeId}.${key}`;
}

View File

@ -1,7 +1,5 @@
import { registerRawHelper } from "discourse/lib/helpers";
import { getSetting as getThemeSetting } from "discourse/lib/theme-settings-store";
registerRawHelper("theme-setting", themeSetting);
export default function themeSetting(themeId, key) {
if (typeof themeId !== "number") {
throw new Error(

View File

@ -1,8 +1,6 @@
import { htmlSafe } from "@ember/template";
import { registerRawHelper } from "discourse/lib/helpers";
import renderTopicFeaturedLink from "discourse/lib/render-topic-featured-link";
registerRawHelper("topic-featured-link", topicFeaturedLink);
export default function topicFeaturedLink(topic, params) {
return htmlSafe(renderTopicFeaturedLink(topic, params));
}

View File

@ -1,7 +1,5 @@
import { htmlSafe } from "@ember/template";
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("topic-link", topicLink);
export default function topicLink(topic, args = {}) {
const title = topic.get("fancyTitle");

View File

@ -1,18 +0,0 @@
import deprecated from "discourse/lib/deprecated";
import { RAW_TOPIC_LIST_DEPRECATION_OPTIONS } from "discourse/lib/plugin-api";
const TopicStatusIcons = new (class {
entries = [];
addObject(entry) {
deprecated(
"TopicStatusIcons is deprecated. Use 'after-topic-status' plugin outlet instead.",
RAW_TOPIC_LIST_DEPRECATION_OPTIONS
);
const [attribute, iconName, titleKey] = entry;
this.entries.push({ attribute, iconName, titleKey });
}
})();
export default TopicStatusIcons;

View File

@ -1,6 +1,3 @@
import { registerRawHelper } from "discourse/lib/helpers";
registerRawHelper("value-entered", valueEntered);
export default function valueEntered(value) {
if (!value) {
return "";

View File

@ -1,8 +1,5 @@
import { setOwner } from "@ember/owner";
import Handlebars from "handlebars";
import { createHelperContext, registerHelpers } from "discourse/lib/helpers";
import RawHandlebars from "discourse/lib/raw-handlebars";
import { registerRawHelpers } from "discourse/lib/raw-handlebars-helpers";
function isThemeOrPluginHelper(path) {
return (
@ -38,7 +35,6 @@ export function autoLoadModules(owner, registry) {
createHelperContext(context);
registerHelpers(registry);
registerRawHelpers(RawHandlebars, Handlebars, owner);
}
export default {

View File

@ -2,15 +2,12 @@ import * as GlimmerManager from "@glimmer/manager";
import ClassicComponent from "@ember/component";
import deprecated from "discourse/lib/deprecated";
import DiscourseTemplateMap from "discourse/lib/discourse-template-map";
import { RAW_TOPIC_LIST_DEPRECATION_OPTIONS } from "discourse/lib/plugin-api";
import { getThemeInfo } from "discourse/lib/source-identifier";
// We're using a patched version of Ember with a modified GlimmerManager to make the code below work.
// This patch is not ideal, but Ember does not allow us to change a component template after initial association
// https://github.com/glimmerjs/glimmer-vm/blob/03a4b55c03/packages/%40glimmer/manager/lib/public/template.ts#L14-L20
const LEGACY_TOPIC_LIST_OVERRIDES = ["topic-list", "topic-list-item"];
function sourceForModuleName(name) {
const pluginMatch = name.match(/^discourse\/plugins\/([^\/]+)\//)?.[1];
if (pluginMatch) {
@ -74,25 +71,14 @@ export default {
const originalTemplate = GlimmerManager.getComponentTemplate(component);
if (originalTemplate) {
if (LEGACY_TOPIC_LIST_OVERRIDES.includes(componentName)) {
// Special handling for these, with a different deprecation id, so the auto-feature-flag works correctly
deprecated(
`Overriding '${componentName}' template is deprecated. Use the value transformer 'topic-list-columns' and other new topic-list plugin APIs instead.`,
{
...RAW_TOPIC_LIST_DEPRECATION_OPTIONS,
source: sourceForModuleName(finalOverrideModuleName),
}
);
} else {
deprecated(
`Overriding component templates is deprecated, and will soon be disabled. Use plugin outlets, CSS, or other customization APIs instead. [${finalOverrideModuleName}]`,
{
id: "discourse.component-template-overrides",
url: "https://meta.discourse.org/t/355668",
source: sourceForModuleName(finalOverrideModuleName),
}
);
}
deprecated(
`Overriding component templates is deprecated, and will soon be disabled. Use plugin outlets, CSS, or other customization APIs instead. [${finalOverrideModuleName}]`,
{
id: "discourse.component-template-overrides",
url: "https://meta.discourse.org/t/355668",
source: sourceForModuleName(finalOverrideModuleName),
}
);
const overrideTemplate = require(finalOverrideModuleName).default;

View File

@ -1,7 +0,0 @@
import { eagerLoadRawTemplateModules } from "discourse/lib/raw-templates";
export default {
initialize() {
eagerLoadRawTemplateModules();
},
};

View File

@ -1,14 +0,0 @@
import { registerDeprecationHandler } from "discourse/lib/deprecated";
import { needsHbrTopicList } from "discourse/lib/raw-templates";
export default {
before: "inject-objects",
initialize() {
registerDeprecationHandler((message, opts) => {
if (opts?.id === "discourse.hbr-topic-list-overrides") {
needsHbrTopicList(true);
}
});
},
};

View File

@ -1,9 +1,7 @@
import Helper from "@ember/component/helper";
import { get } from "@ember/object";
import { dasherize } from "@ember/string";
import { htmlSafe } from "@ember/template";
import deprecated from "discourse/lib/deprecated";
import RawHandlebars from "discourse/lib/raw-handlebars";
export function makeArray(obj) {
if (obj === null || obj === undefined) {
@ -27,17 +25,6 @@ export function htmlHelper(fn) {
const _helpers = {};
function rawGet(ctx, property, options) {
if (options.types && options.data.view) {
let view = options.data.view;
return view.getStream
? view.getStream(property).value()
: view.getAttr(property);
} else {
return get(ctx, property);
}
}
export function registerHelper(name, fn) {
_helpers[name] = Helper.helper(fn);
}
@ -63,31 +50,6 @@ export function helperContext() {
return _helperContext;
}
function resolveParams(ctx, options) {
let params = {};
const hash = options.hash;
if (hash) {
if (options.hashTypes) {
Object.keys(hash).forEach(function (k) {
const type = options.hashTypes[k];
if (
type === "STRING" ||
type === "StringLiteral" ||
type === "SubExpression"
) {
params[k] = hash[k];
} else if (type === "ID" || type === "PathExpression") {
params[k] = rawGet(ctx, hash[k], options);
}
});
} else {
params = hash;
}
}
return params;
}
/**
* Register a helper for Ember and raw-hbs. This exists for
* legacy reasons, and should be avoided in new code. Instead, you should
@ -95,7 +57,7 @@ function resolveParams(ctx, options) {
*/
export function registerUnbound(name, fn) {
deprecated(
`[registerUnbound ${name}] registerUnbound is deprecated. Instead, you should export a default function from 'discourse/helpers/${name}.js'. If the helper is also used in raw-hbs, you can register it using 'registerRawHelper'.`,
`[registerUnbound ${name}] registerUnbound is deprecated. Instead, you should export a default function from 'discourse/helpers/${name}.js'.`,
{ id: "discourse.register-unbound" }
);
@ -104,29 +66,14 @@ export function registerUnbound(name, fn) {
return fn(...params, args);
}
};
registerRawHelper(name, fn);
}
/**
* Register a helper for raw-hbs only
*/
export function registerRawHelper(name, fn) {
const func = function (...args) {
const options = args.pop();
const properties = args;
for (let i = 0; i < properties.length; i++) {
if (
options.types &&
(options.types[i] === "ID" || options.types[i] === "PathExpression")
) {
properties[i] = rawGet(this, properties[i], options);
}
}
return fn.call(this, ...properties, resolveParams(this, options));
};
RawHandlebars.registerHelper(name, func);
export function registerRawHelper(name) {
deprecated(
`[registerRawHelper ${name}] the raw handlebars system has been removed, so calls to registerRawHelper should be removed.`,
{ id: "discourse.register-raw-helper" }
);
}

View File

@ -199,11 +199,7 @@ const POST_STREAM_DEPRECATION_OPTIONS = {
// url: "", // TODO (glimmer-post-stream) uncomment when the topic is created on meta
};
export const RAW_TOPIC_LIST_DEPRECATION_OPTIONS = {
since: "v3.4.0.beta4-dev",
id: "discourse.hbr-topic-list-overrides",
url: "https://meta.discourse.org/t/343404",
};
const blockedModifications = ["component:topic-list"];
const appliedModificationIds = new WeakMap();
@ -295,7 +291,11 @@ class PluginApi {
return;
}
const klass = this.container.factoryFor(normalized);
let klass;
if (!blockedModifications.includes(normalized)) {
klass = this.container.factoryFor(normalized);
}
if (!klass) {
if (!opts.ignoreMissing) {
// eslint-disable-next-line no-console
@ -328,17 +328,6 @@ class PluginApi {
* ```
**/
modifyClass(resolverName, changes, opts) {
if (
resolverName === "component:topic-list" ||
resolverName === "component:topic-list-item" ||
resolverName === "raw-view:topic-status"
) {
deprecated(
`Modifying '${resolverName}' with 'modifyClass' is deprecated. Use the value transformer 'topic-list-columns' and other new topic-list plugin APIs instead.`,
RAW_TOPIC_LIST_DEPRECATION_OPTIONS
);
}
const klass = this._resolveClass(resolverName, opts);
if (!klass) {
return;
@ -376,17 +365,6 @@ class PluginApi {
* ```
**/
modifyClassStatic(resolverName, changes, opts) {
if (
resolverName === "component:topic-list" ||
resolverName === "component:topic-list-item" ||
resolverName === "raw-view:topic-status"
) {
deprecated(
`Modifying '${resolverName}' with 'modifyClass' is deprecated. Use the value transformer 'topic-list-columns' and other new topic-list plugin APIs instead.`,
RAW_TOPIC_LIST_DEPRECATION_OPTIONS
);
}
const klass = this._resolveClass(resolverName, opts);
if (!klass) {
return;

View File

@ -6,10 +6,8 @@ import {
import templateOnly from "@ember/component/template-only";
import { isDeprecatedOutletArgument } from "discourse/helpers/deprecated-outlet-argument";
import deprecated, { withSilencedDeprecations } from "discourse/lib/deprecated";
import { buildRawConnectorCache } from "discourse/lib/raw-templates";
let _connectorCache;
let _rawConnectorCache;
let _extraConnectorClasses = {};
let _extraConnectorComponents = {};
let debugOutletCallback;
@ -64,7 +62,6 @@ function findOutlets(keys, callback) {
export function clearCache() {
_connectorCache = null;
_rawConnectorCache = null;
}
/**
@ -235,13 +232,6 @@ export function renderedConnectorsFor(outletName, args, context, owner) {
});
}
export function rawConnectorsFor(outletName) {
if (!_rawConnectorCache) {
_rawConnectorCache = buildRawConnectorCache();
}
return _rawConnectorCache[outletName] || [];
}
export function buildArgsWithDeprecations(args, deprecatedArgs, opts = {}) {
const output = {};

View File

@ -1,112 +0,0 @@
import { get } from "@ember/object";
export const RUNTIME_OPTIONS = {
allowProtoPropertiesByDefault: true,
};
export function registerRawHelpers(hbs, handlebarsClass, owner) {
if (!hbs.helpers) {
hbs.helpers = Object.create(handlebarsClass.helpers);
}
lazyLoadHelpers(hbs, owner);
if (hbs.__helpers_registered) {
return;
}
hbs.__helpers_registered = true;
hbs.helpers["get"] = function (context, options) {
if (!context || !options.contexts) {
return;
}
if (typeof context !== "string") {
return context;
}
let firstContext = options.contexts[0];
let val = firstContext[context];
if (context.toString().startsWith("controller.")) {
context = context.slice(context.indexOf(".") + 1);
}
return val === undefined ? get(firstContext, context) : val;
};
// #each .. in support (as format is transformed to this)
hbs.registerHelper(
"each",
function (localName, inKeyword, contextName, options) {
if (typeof contextName === "undefined") {
return;
}
let list = get(this, contextName);
let output = [];
let innerContext = options.contexts[0];
for (let i = 0; i < list.length; i++) {
innerContext[localName] = list[i];
output.push(options.fn(innerContext));
}
delete innerContext[localName];
return output.join("");
}
);
function stringCompatHelper(fn) {
const old = hbs.helpers[fn];
hbs.helpers[fn] = function (context, options) {
return old.apply(this, [hbs.helpers.get(context, options), options]);
};
}
// HACK: Ensure that the variable is resolved only once.
// The "get" function will be called twice because both `if` and `unless`
// helpers are patched to resolve the variable and `unless` is implemented
// as not `if`. For example, for {{#unless var}} will generate a stack
// trace like:
//
// - patched-unless("var") "var" is resolved to its value, val
// - unless(val) unless is implemented as !if
// - !patched-if(val) val is already resolved, but it is resolved again
// - !if(???) at this point, ??? usually stands for undefined
//
// The following code ensures that patched-unless will call `if` directly,
// `patched-unless("var")` will return `!if(val)`.
const oldIf = hbs.helpers["if"];
hbs.helpers["unless"] = function (context, options) {
return oldIf.apply(this, [
hbs.helpers.get(context, options),
{
fn: options.inverse,
inverse: options.fn,
hash: options.hash,
},
]);
};
stringCompatHelper("if");
stringCompatHelper("with");
}
function lazyLoadHelpers(hbs, owner) {
// Reimplements `helperMissing` so that it triggers a lookup() for
// a helper of that name. Means we don't need to eagerly load all
// helpers/* files during boot.
hbs.registerHelper("helperMissing", function (...args) {
const opts = args[args.length - 1];
if (opts?.name) {
// Lookup and evaluate the relevant module. Raw helpers may be registered as a side effect
owner.lookup(`helper:${opts.name}`);
if (hbs.helpers[opts.name]) {
// Helper now exists, invoke it
return hbs.helpers[opts.name]?.call(this, ...arguments);
} else {
// Not a helper, treat as property
return hbs.helpers["get"].call(this, ...arguments);
}
}
});
}

View File

@ -1,134 +0,0 @@
import Handlebars from "handlebars";
// This is a mechanism for quickly rendering templates which is Ember aware
// templates are highly compatible with Ember so you don't need to worry about calling "get"
// and discourseComputed properties function, additionally it uses stringParams like Ember does
const RawHandlebars = Handlebars.create();
function buildPath(blk, args) {
let result = {
type: "PathExpression",
data: false,
depth: blk.path.depth,
loc: blk.path.loc,
};
// Server side precompile doesn't have jquery.extend
Object.keys(args).forEach(function (a) {
result[a] = args[a];
});
return result;
}
function replaceGet(ast) {
let visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.MustacheStatement = function (mustache) {
if (!(mustache.params.length || mustache.hash)) {
mustache.params[0] = mustache.path;
mustache.path = buildPath(mustache, {
parts: ["get"],
original: "get",
strict: true,
falsy: true,
});
}
return Handlebars.Visitor.prototype.MustacheStatement.call(this, mustache);
};
// rewrite `each x as |y|` as each y in x`
// This allows us to use the same syntax in all templates
visitor.BlockStatement = function (block) {
if (block.path.original === "each" && block.params.length === 1) {
let paramName = block.program.blockParams[0];
block.params = [
buildPath(block, { original: paramName }),
{ type: "CommentStatement", value: "in" },
block.params[0],
];
delete block.program.blockParams;
}
return Handlebars.Visitor.prototype.BlockStatement.call(this, block);
};
visitor.accept(ast);
}
if (Handlebars.Compiler) {
RawHandlebars.Compiler = function () {};
RawHandlebars.Compiler.prototype = Object.create(
Handlebars.Compiler.prototype
);
RawHandlebars.Compiler.prototype.compiler = RawHandlebars.Compiler;
RawHandlebars.JavaScriptCompiler = function () {};
RawHandlebars.JavaScriptCompiler.prototype = Object.create(
Handlebars.JavaScriptCompiler.prototype
);
RawHandlebars.JavaScriptCompiler.prototype.compiler =
RawHandlebars.JavaScriptCompiler;
RawHandlebars.JavaScriptCompiler.prototype.namespace = "RawHandlebars";
RawHandlebars.precompile = function (value, asObject, { plugins = [] } = {}) {
let ast = Handlebars.parse(value);
replaceGet(ast);
plugins.forEach((plugin) => plugin(ast));
let options = {
knownHelpers: {
get: true,
},
data: true,
stringParams: true,
};
asObject = asObject === undefined ? true : asObject;
let environment = new RawHandlebars.Compiler().compile(ast, options);
return new RawHandlebars.JavaScriptCompiler().compile(
environment,
options,
undefined,
asObject
);
};
RawHandlebars.compile = function (string, { plugins = [] } = {}) {
let ast = Handlebars.parse(string);
replaceGet(ast);
plugins.forEach((plugin) => plugin(ast));
// this forces us to rewrite helpers
let options = { data: true, stringParams: true };
let environment = new RawHandlebars.Compiler().compile(ast, options);
let templateSpec = new RawHandlebars.JavaScriptCompiler().compile(
environment,
options,
undefined,
true
);
let t = RawHandlebars.template(templateSpec);
t.isMethod = false;
return t;
};
}
export function template() {
return RawHandlebars.template.apply(this, arguments);
}
export function precompile() {
return RawHandlebars.precompile.apply(this, arguments);
}
export function compile() {
return RawHandlebars.compile.apply(this, arguments);
}
export default RawHandlebars;

View File

@ -1,55 +0,0 @@
import { getOwner } from "@ember/owner";
import { schedule } from "@ember/runloop";
let counter = 0;
/**
* Generate HTML which can be inserted into a raw-hbs template to render a Glimmer component.
* The result of this function must be rendered immediately, so that an `afterRender` hook
* can access the element in the DOM and attach the glimmer component.
*
* Example usage:
*
* ```hbs
* {{! raw-templates/something-cool.hbr }}
* {{html-safe view.html}}
* ```
*
* ```gjs
* // raw-views/something-cool.gjs
* import EmberObject from "@ember/object";
* import rawRenderGlimmer from "discourse/lib/raw-render-glimmer";
*
* export default class SomethingCool extends EmberObject {
* get html() {
* return rawRenderGlimmer(this, "div", <template>Hello {{@data.name}}</template>, { name: this.name });
* }
* ```
*
* And then this can be invoked from any other raw view (including raw plugin outlets) like:
*
* ```hbs
* {{raw "something-cool" name="david"}}
* ```
*/
export default function rawRenderGlimmer(owner, renderInto, component, data) {
const renderGlimmerService = getOwner(owner).lookup("service:render-glimmer");
counter++;
const id = `_render_glimmer_${counter}`;
const [type, ...classNames] = renderInto.split(".");
schedule("afterRender", () => {
const element = document.getElementById(id);
if (element) {
const componentInfo = {
element,
component,
data,
};
renderGlimmerService.add(componentInfo);
}
});
return `<${type} id="${id}" class="${classNames.join(" ")}"></${type}>`;
}

View File

@ -1,108 +0,0 @@
import require from "require";
import deprecated from "discourse/lib/deprecated";
import { RAW_TOPIC_LIST_DEPRECATION_OPTIONS } from "discourse/lib/plugin-api";
import { getResolverOption } from "discourse/resolver";
export const __DISCOURSE_RAW_TEMPLATES = {};
let _needsHbrTopicList = false;
export function needsHbrTopicList(value) {
if (value === undefined) {
return _needsHbrTopicList;
} else {
_needsHbrTopicList = value;
}
}
export function resetNeedsHbrTopicList() {
_needsHbrTopicList = false;
}
const TOPIC_LIST_TEMPLATE_NAMES = [
"list/action-list",
"list/activity-column",
"list/category-column",
"list/new-list-header-controls",
"list/participant-groups",
"list/post-count-or-badges",
"list/posters-column",
"list/posts-count-column",
"list/topic-excerpt",
"list/topic-list-item",
"list/unread-indicator",
"list/visited-line",
"mobile/list/topic-list-item",
"topic-bulk-select-dropdown",
"topic-list-header-column",
"topic-list-header",
"topic-post-badges",
"topic-status",
];
export function addRawTemplate(name, template, opts = {}) {
const cleanName = name.replace(/^javascripts\//, "");
if (
(TOPIC_LIST_TEMPLATE_NAMES.includes(cleanName) ||
name.includes("/connectors/")) &&
!opts.core &&
!opts.hasModernReplacement
) {
const message = `[${name}] hbr topic-list template overrides and connectors are deprecated. Use the value transformer \`topic-list-columns\` and other new topic-list plugin APIs instead.`;
// NOTE: addRawTemplate is called too early for deprecation handlers to process this:
deprecated(message, RAW_TOPIC_LIST_DEPRECATION_OPTIONS);
needsHbrTopicList(true);
}
// Core templates should never overwrite themes / plugins
if (opts.core && __DISCOURSE_RAW_TEMPLATES[name]) {
return;
}
__DISCOURSE_RAW_TEMPLATES[name] = template;
}
export function removeRawTemplate(name) {
delete __DISCOURSE_RAW_TEMPLATES[name];
}
export function findRawTemplate(name) {
if (getResolverOption("mobileView")) {
return (
__DISCOURSE_RAW_TEMPLATES[`javascripts/mobile/${name}`] ||
__DISCOURSE_RAW_TEMPLATES[`javascripts/${name}`] ||
__DISCOURSE_RAW_TEMPLATES[`mobile/${name}`] ||
__DISCOURSE_RAW_TEMPLATES[name]
);
}
return (
__DISCOURSE_RAW_TEMPLATES[`javascripts/${name}`] ||
__DISCOURSE_RAW_TEMPLATES[name]
);
}
export function buildRawConnectorCache() {
let result = {};
Object.keys(__DISCOURSE_RAW_TEMPLATES).forEach((resource) => {
const segments = resource.split("/");
const connectorIndex = segments.indexOf("connectors");
if (connectorIndex >= 0) {
const outletName = segments[connectorIndex + 1];
result[outletName] ??= [];
result[outletName].push({
template: __DISCOURSE_RAW_TEMPLATES[resource],
});
}
});
return result;
}
export function eagerLoadRawTemplateModules() {
for (const key of Object.keys(requirejs.entries)) {
if (key.includes("/raw-templates/")) {
require(key);
}
}
}

View File

@ -1,4 +1,3 @@
import Handlebars from "handlebars";
import $ from "jquery";
import * as AvatarUtils from "discourse/lib/avatar-utils";
import deprecated from "discourse/lib/deprecated";
@ -41,11 +40,6 @@ export function escapeExpression(string) {
return "";
}
// don't escape SafeStrings, since they're already safe
if (string instanceof Handlebars.SafeString) {
return string.toString();
}
return escape(string);
}

View File

@ -27,7 +27,6 @@ loaderShim("a11y-dialog", () => importSync("a11y-dialog"));
loaderShim("discourse-i18n", () => importSync("discourse-i18n"));
loaderShim("ember-modifier", () => importSync("ember-modifier"));
loaderShim("ember-route-template", () => importSync("ember-route-template"));
loaderShim("handlebars", () => importSync("handlebars"));
loaderShim("jquery", () => importSync("jquery"));
loaderShim("js-yaml", () => importSync("js-yaml"));
loaderShim("message-bus-client", () => importSync("message-bus-client"));

View File

@ -89,7 +89,6 @@ export default class Site extends RestModel {
@sort("categories", "topicCountDesc") categoriesByCount;
#glimmerPostStreamEnabled;
#glimmerTopicDecision;
init() {
super.init(...arguments);
@ -161,51 +160,6 @@ export default class Site extends RestModel {
return enabled;
}
get useGlimmerTopicList() {
if (this.#glimmerTopicDecision !== undefined) {
// Caches the decision after the first call, and avoids re-printing the same message
return this.#glimmerTopicDecision;
}
let decision;
const { needsHbrTopicList } = require("discourse/lib/raw-templates");
/* eslint-disable no-console */
const settingValue = this.siteSettings.glimmer_topic_list_mode;
if (settingValue === "enabled") {
if (needsHbrTopicList()) {
console.log(
"⚠️ Using the new 'glimmer' topic list, even though some themes/plugins are not ready"
);
} else {
console.log("✅ Using the new 'glimmer' topic list");
}
decision = true;
} else if (settingValue === "disabled") {
decision = false;
} else {
// auto
if (needsHbrTopicList()) {
console.log(
"⚠️ Detected themes/plugins which are incompatible with the new 'glimmer' topic-list. Falling back to old implementation."
);
decision = false;
} else {
if (!isTesting() && !isRailsTesting()) {
console.log("✅ Using the new 'glimmer' topic list");
}
decision = true;
}
}
/* eslint-enable no-console */
this.#glimmerTopicDecision = decision;
return decision;
}
@computed("categories.[]")
get categoriesById() {
const map = new Map();

View File

@ -1,3 +0,0 @@
<button class='btn-transparent {{class}}' title='{{i18n "topics.bulk.toggle"}}'>
{{d-icon icon}}
</button>

View File

@ -1,8 +0,0 @@
{{#if postNumbers}}
<div class='post-actions {{className}}'>
{{d-icon icon}}
{{#each postNumbers as |postNumber|}}
<a href='{{topic.url}}/{{postNumber}}'>#{{postNumber}}</a>
{{/each}}
</div>
{{/if}}

View File

@ -1,7 +0,0 @@
<{{tagName}} class="{{class}} {{cold-age-class topic.createdAt startDate=topic.bumpedAt class=""}} activity" title="{{html-safe topic.bumpedAtTitle}}">
<a class="post-activity" href="{{topic.lastPostUrl}}">
{{~raw-plugin-outlet name="topic-list-before-relative-date"~}}
{{~plugin-outlet name="topic-list-before-relative-date" outletArgs=(raw-hash topic=topic)~}}
{{~format-date topic.bumpedAt format="tiny" noTitle="true"~}}
</a>
</{{tagName}}>

View File

@ -1 +0,0 @@
<td class='category topic-list-data'>{{category-link category}}</td>

View File

@ -1,13 +0,0 @@
{{#if view.staticLabel}}
<span class="static-label">{{view.staticLabel}}</span>
{{else}}
<button class="topics-replies-toggle --all{{#if view.allActive}} active{{/if}}">
{{i18n "filters.new.all"}}
</button>
<button class="topics-replies-toggle --topics{{#if view.topicsActive}} active{{/if}}">
{{view.topicsButtonLabel}}
</button>
<button class="topics-replies-toggle --replies{{#if view.repliesActive}} active{{/if}}">
{{view.repliesButtonLabel}}
</button>
{{/if}}

View File

@ -1,18 +0,0 @@
<div
class="participant-group-wrapper"
role="list"
aria-label="{{i18n 'topic.participant_groups'}}"
>
{{#each groups as |group|}}
<div class="participant-group">
<a
class="user-group trigger-group-card"
href="{{group.url}}"
data-group-card="{{group.name}}"
>
{{d-icon "users"}}
{{group.name}}
</a>
</div>
{{/each}}
</div>

View File

@ -1,5 +0,0 @@
{{#if view.showBadges}}
{{raw "topic-post-badges" unreadPosts=topic.unread_posts unseen=topic.unseen url=topic.lastUnreadUrl newDotText=newDotText}}
{{else}}
{{raw "list/posts-count-column" topic=topic tagName="div"}}
{{/if}}

View File

@ -1,9 +0,0 @@
<td class='posters topic-list-data'>
{{#each posters as |poster|}}
{{#if poster.moreCount}}
<a class="posters-more-count">{{poster.moreCount}}</a>
{{else}}
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extraClasses}}" aria-label="{{i18n "user.profile_possessive" username=poster.user.username}}">{{avatar poster avatarTemplatePath="user.avatar_template" usernamePath="user.username" namePath="user.name" imageSize="small"}}</a>
{{/if}}
{{/each}}
</td>

View File

@ -1,7 +0,0 @@
<{{view.tagName}} class='num posts-map posts {{view.likesHeat}} topic-list-data'>
<button class="btn-link posts-map badge-posts {{view.likesHeat}}" title="{{view.title}}" aria-label="{{view.title}}">
{{raw-plugin-outlet name="topic-list-before-reply-count"}}
{{plugin-outlet name="topic-list-before-reply-count" outletArgs=(raw-hash topic=topic)}}
{{number topic.replyCount noTitle="true"}}
</button>
</{{view.tagName}}>

View File

@ -1,8 +0,0 @@
{{#if topic.hasExcerpt}}
<a href="{{topic.url}}" class="topic-excerpt">
{{dir-span topic.escapedExcerpt htmlSafe="true"}}
{{#if topic.excerptTruncated}}
<span class="topic-excerpt-more">{{i18n 'read_more'}}</span>
{{/if}}
</a>
{{/if}}

View File

@ -1,97 +0,0 @@
{{~raw-plugin-outlet name="topic-list-before-columns"}}
{{#if bulkSelectEnabled}}
<td class="bulk-select topic-list-data">
<label for="bulk-select-{{topic.id}}">
<input type="checkbox" class="bulk-select" id="bulk-select-{{topic.id}}">
</label>
</td>
{{/if}}
{{!--
The `~` syntax strip spaces between the elements, making it produce
`<a class=topic-post-badges>Some text</a><span class=topic-post-badges>`,
with no space between them.
This causes the topic-post-badge to be considered the same word as "text"
at the end of the link, preventing it from line wrapping onto its own line.
--}}
<td class='main-link clearfix topic-list-data' colspan="1">
{{~raw-plugin-outlet name="topic-list-before-link"}}
{{~plugin-outlet name="topic-list-before-link" outletArgs=(raw-hash topic=topic)}}
<span class="link-top-line" role="heading" aria-level="2">
{{~raw-plugin-outlet name="topic-list-before-status"}}
{{~plugin-outlet name="topic-list-before-status" outletArgs=(raw-hash topic=topic)}}
{{~raw "topic-status" topic=topic}}
{{~topic-link topic class="raw-link raw-topic-link"}}
{{~#if topic.featured_link}}
&nbsp;{{~topic-featured-link topic}}
{{~/if}}
{{~raw-plugin-outlet name="topic-list-after-title"}}
{{~plugin-outlet name="topic-list-after-title" outletArgs=(raw-hash topic=topic)}}
{{~raw "list/unread-indicator" includeUnreadIndicator=includeUnreadIndicator
topicId=topic.id
unreadClass=unreadClass~}}
{{~#if showTopicPostBadges}}
{{~raw "topic-post-badges" unreadPosts=topic.unread_posts unseen=topic.unseen url=topic.lastUnreadUrl newDotText=newDotText}}
{{~/if}}
</span>
<div class="link-bottom-line">
{{#unless hideCategory}}
{{#unless topic.isPinnedUncategorized}}
{{~raw-plugin-outlet name="topic-list-before-category"}}
{{~plugin-outlet name="topic-list-before-category" outletArgs=(raw-hash topic=topic)}}
{{category-link topic.category}}
{{/unless}}
{{/unless}}
{{discourse-tags topic mode="list" tagsForUser=tagsForUser}}
{{#if participantGroups}}
{{raw "list/participant-groups" groups=participantGroups}}
{{/if}}
{{raw "list/action-list" topic=topic postNumbers=topic.liked_post_numbers className="likes" icon="heart"}}
</div>
{{#if expandPinned}}
{{raw "list/topic-excerpt" topic=topic}}
{{/if}}
{{~raw-plugin-outlet name="topic-list-main-link-bottom"}}
{{~plugin-outlet name="topic-list-main-link-bottom" outletArgs=(raw-hash topic=topic)}}
</td>
{{~raw-plugin-outlet name="topic-list-after-main-link"}}
{{~plugin-outlet name="topic-list-after-main-link" outletArgs=(raw-hash topic=topic)}}
{{#if showPosters}}
{{raw "list/posters-column" posters=topic.featuredUsers}}
{{/if}}
{{raw "list/posts-count-column" topic=topic}}
{{#if showLikes}}
<td class="num likes topic-list-data">
{{#if hasLikes}}
<a href='{{topic.summaryUrl}}'>
{{number topic.like_count}} {{d-icon "heart"}}
</a>
{{/if}}
</td>
{{/if}}
{{#if showOpLikes}}
<td class="num likes">
{{#if hasOpLikes}}
<a href='{{topic.summaryUrl}}'>
{{number topic.op_like_count}} {{d-icon "heart"}}
</a>
{{/if}}
</td>
{{/if}}
<td class="num views {{topic.viewsHeat}} topic-list-data">
{{raw-plugin-outlet name="topic-list-before-view-count"}}
{{plugin-outlet name="topic-list-before-view-count" outletArgs=(raw-hash topic=topic)}}
{{number topic.views numberKey="views_long"}}
</td>
{{raw "list/activity-column" topic=topic class="num topic-list-data" tagName="td"}}
{{~raw-plugin-outlet name="topic-list-after-columns"}}

View File

@ -1,5 +0,0 @@
{{~#if includeUnreadIndicator~}}
&nbsp;<span class='badge badge-notification unread-indicator indicator-topic-{{topicId}} {{unreadClass}}' title='{{i18n "topic.unread_indicator"}}'>
{{~d-icon "asterisk"}}
</span>
{{~/if}}

View File

@ -1,9 +0,0 @@
{{#if view.isLastVisited}}
<tr class='topic-list-item-separator'>
<td class="topic-list-data" colspan="6">
<span>
{{i18n 'topics.new_messages_marker'}}
</span>
</td>
</tr>
{{/if}}

View File

@ -1,63 +0,0 @@
<td class="topic-list-data">
{{~raw-plugin-outlet name="topic-list-before-columns"}}
<div class='pull-left'>
{{#if bulkSelectEnabled}}
<label for="bulk-select-{{topic.id}}">
<input type="checkbox" class="bulk-select" id="bulk-select-{{topic.id}}">
</label>
{{else}}
<a href="{{topic.lastPostUrl}}" aria-label="{{i18n 'latest_poster_link' username=topic.lastPosterUser.username}}" data-user-card="{{topic.lastPosterUser.username}}">{{avatar topic.lastPosterUser imageSize="large"}}</a>
{{/if}}
</div>
<div class='topic-item-metadata right'>
{{!--
The `~` syntax strip spaces between the elements, making it produce
`<a class=topic-post-badges>Some text</a><span class=topic-post-badges>`,
with no space between them.
This causes the topic-post-badge to be considered the same word as "text"
at the end of the link, preventing it from line wrapping onto its own line.
--}}
{{~raw-plugin-outlet name="topic-list-before-link"}}
{{~plugin-outlet name="topic-list-before-link" outletArgs=(raw-hash topic=topic)}}
<div class='main-link'>
{{~raw-plugin-outlet name="topic-list-before-status"}}
{{~plugin-outlet name="topic-list-before-status" outletArgs=(raw-hash topic=topic)}}
{{~raw "topic-status" topic=topic~}}
{{~topic-link topic class="raw-link raw-topic-link"}}
{{~#if topic.featured_link~}}
&nbsp;{{~topic-featured-link topic~}}
{{~/if~}}
{{~raw-plugin-outlet name="topic-list-after-title"}}
{{~plugin-outlet name="topic-list-after-title" outletArgs=(raw-hash topic=topic)}}
{{~#if topic.unseen~}}
<span class="topic-post-badges">&nbsp;<span class="badge-notification new-topic"></span></span>
{{~/if~}}
{{~#if expandPinned~}}
{{~raw "list/topic-excerpt" topic=topic~}}
{{~/if~}}
{{~raw-plugin-outlet name="topic-list-main-link-bottom"}}
{{~plugin-outlet name="topic-list-main-link-bottom" outletArgs=(raw-hash topic=topic)}}
</div>
{{~raw-plugin-outlet name="topic-list-after-main-link"}}
{{~plugin-outlet name="topic-list-after-main-link" outletArgs=(raw-hash topic=topic)}}
<div class='pull-right'>
{{raw "list/post-count-or-badges" topic=topic postBadgesEnabled=showTopicPostBadges}}
</div>
<div class="topic-item-stats clearfix">
<span class="topic-item-stats__category-tags">
{{#unless hideCategory}}
{{~raw-plugin-outlet name="topic-list-before-category"}}
{{~plugin-outlet name="topic-list-before-category" outletArgs=(raw-hash topic=topic)}}
{{category-link topic.category~}}
{{~/unless}}
{{~discourse-tags topic mode="list"}}
</span>
<div class='num activity last'>
<span class="age activity" title="{{topic.bumpedAtTitle}}"><a
href="{{topic.lastPostUrl}}">{{format-date topic.bumpedAt format="tiny" noTitle="true"}}</a>
</span>
</div>
{{~plugin-outlet name="topic-list-after-topic-item-stats" outletArgs=(raw-hash topic=topic)}}
</div>
{{~raw-plugin-outlet name="topic-list-after-columns"}}
</td>

View File

@ -1,35 +0,0 @@
<th data-sort-order='{{order}}' class='{{view.className}} topic-list-data' scope="col" {{#if view.ariaSort}}aria-sort='{{view.ariaSort}}'{{/if}}>
{{~#if canBulkSelect}}
{{~#if showBulkToggle}}
{{raw "flat-button" class="bulk-select" icon="list-check" title="topics.bulk.toggle"}}
{{/if ~}}
{{~#if bulkSelectEnabled}}
<span class='bulk-select-topics'>
{{~#if canDoBulkActions}}
{{raw "topic-bulk-select-dropdown" bulkSelectHelper=bulkSelectHelper}}
{{/if ~}}
<button class='btn btn-default bulk-select-all'>{{i18n "topics.bulk.select_all"}}</button>
<button class='btn btn-default bulk-clear-all'>{{i18n "topics.bulk.clear_all"}}</button>
</span>
{{/if ~}}
{{/if ~}}
{{~#unless bulkSelectEnabled}}
{{~#if view.showTopicsAndRepliesToggle}}
{{raw "list/new-list-header-controls" current=newListSubset newRepliesCount=newRepliesCount newTopicsCount=newTopicsCount}}
{{else}}
{{#if sortable}}
<button aria-pressed='{{view.ariaPressed}}'>
{{view.localizedName}}
{{~#if view.isSorting}}
{{d-icon view.sortIcon}}
{{/if ~}}
</button>
{{else}}
<span {{#if view.screenreaderOnly}}class="sr-only"{{/if}}>
{{view.localizedName}}
</span>
{{/if}}
{{/if ~}}
{{/unless ~}}
{{~plugin-outlet name="topic-list-heading-bottom" outletArgs=(raw-hash name=view.name bulkSelectEnabled=bulkSelectEnabled)~}}
</th>

View File

@ -1,26 +0,0 @@
{{~raw-plugin-outlet name="topic-list-header-before"~}}
{{~plugin-outlet name="topic-list-header-before"~}}
{{#if bulkSelectEnabled}}
<th class="bulk-select topic-list-data">
{{#if canBulkSelect}}
{{raw "flat-button" class="bulk-select" icon="list-check" title="topics.bulk.toggle"}}
{{/if}}
</th>
{{/if}}
{{raw "topic-list-header-column" order='default' name=listTitle bulkSelectEnabled=bulkSelectEnabled showBulkToggle=toggleInTitle canBulkSelect=canBulkSelect canDoBulkActions=canDoBulkActions showTopicsAndRepliesToggle=showTopicsAndRepliesToggle newListSubset=newListSubset newRepliesCount=newRepliesCount newTopicsCount=newTopicsCount bulkSelectHelper=bulkSelectHelper }}
{{raw-plugin-outlet name="topic-list-header-after-main-link"}}
{{plugin-outlet name="topic-list-header-after-main-link"}}
{{#if showPosters}}
{{raw "topic-list-header-column" order='posters' name='posters' screenreaderOnly='true'}}
{{/if}}
{{raw "topic-list-header-column" sortable=sortable number='true' order='posts' name='replies'}}
{{#if showLikes}}
{{raw "topic-list-header-column" sortable=sortable number='true' order='likes' name='likes'}}
{{/if}}
{{#if showOpLikes}}
{{raw "topic-list-header-column" sortable=sortable number='true' order='op_likes' name='likes'}}
{{/if}}
{{raw "topic-list-header-column" sortable=sortable number='true' order='views' name='views'}}
{{raw "topic-list-header-column" sortable=sortable number='true' order='activity' name='activity'}}
{{~raw-plugin-outlet name="topic-list-header-after"~}}
{{~plugin-outlet name="topic-list-header-after"~}}

View File

@ -1,26 +0,0 @@
<span class="topic-post-badges">
{{~#if newPosts~}}
&nbsp;<a
href="{{url}}"
class="badge badge-notification unread-posts"
title="{{i18n 'topic.unread_posts' count=newPosts}}"
aria-label="{{i18n 'topic.unread_posts' count=newPosts}}"
>{{newPosts}}</a>
{{~/if}}
{{~#if unreadPosts~}}
&nbsp;<a
href="{{url}}"
class="badge badge-notification unread-posts"
title="{{i18n 'topic.unread_posts' count=unreadPosts}}"
aria-label="{{i18n 'topic.unread_posts' count=unreadPosts}}"
>{{unreadPosts}}</a>
{{~/if}}
{{~#if unseen~}}
&nbsp;<a
href="{{url}}"
class="badge badge-notification new-topic"
title="{{i18n 'topic.new'}}"
aria-label="{{i18n 'topic.new'}}"
>{{newDotText}}</a>
{{~/if}}
</span>

View File

@ -1,14 +0,0 @@
{{~#if view.renderDiv ~}}
<div class='topic-statuses'>
{{/if ~}}
{{~#each view.statuses as |status|~}}
{{~#if status.href ~}}
<a href='{{status.href}}' title='{{status.title}}' class='topic-status {{status.extraClasses}}'>{{d-icon status.icon}}</a>
{{~else ~}}
<{{status.openTag}} title='{{status.title}}' class='topic-status'>{{d-icon status.icon class=status.key}}</{{status.closeTag}}>
{{~/if ~}}
{{~/each}}
{{~#if view.showDefault~}}{{d-icon view.showDefault}}{{~/if ~}}
{{~#if view.renderDiv ~}}
</div>
{{/if ~}}

View File

@ -1,57 +0,0 @@
import EmberObject from "@ember/object";
import discourseComputed from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
export default class NewListHeaderControls extends EmberObject {
@discourseComputed
topicsActive() {
return this.current === "topics";
}
@discourseComputed
repliesActive() {
return this.current === "replies";
}
@discourseComputed
allActive() {
return !this.topicsActive && !this.repliesActive;
}
@discourseComputed
repliesButtonLabel() {
if (this.newRepliesCount > 0) {
return i18n("filters.new.replies_with_count", {
count: this.newRepliesCount,
});
} else {
return i18n("filters.new.replies");
}
}
@discourseComputed
topicsButtonLabel() {
if (this.newTopicsCount > 0) {
return i18n("filters.new.topics_with_count", {
count: this.newTopicsCount,
});
} else {
return i18n("filters.new.topics");
}
}
@discourseComputed
staticLabel() {
if (this.noStaticLabel) {
return null;
}
if (this.newTopicsCount > 0 && this.newRepliesCount > 0) {
return null;
}
if (this.newTopicsCount > 0) {
return this.topicsButtonLabel;
} else {
return this.repliesButtonLabel;
}
}
}

View File

@ -1,15 +0,0 @@
import EmberObject from "@ember/object";
import { and } from "@ember/object/computed";
import discourseComputed from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
export default class PostCountOrBadges extends EmberObject {
@and("postBadgesEnabled", "topic.unread_posts") showBadges;
@discourseComputed
newDotText() {
return this.currentUser && this.currentUser.trust_level > 0
? ""
: i18n("filters.new.lower_title");
}
}

View File

@ -1,49 +0,0 @@
import EmberObject from "@ember/object";
import discourseComputed from "discourse/lib/decorators";
import I18n from "discourse-i18n";
export default class PostsCountColumn extends EmberObject {
tagName = "td";
@discourseComputed("topic.like_count", "topic.posts_count")
ratio(likeCount, postCount) {
const likes = parseFloat(likeCount);
const posts = parseFloat(postCount);
if (posts < 10) {
return 0;
}
return (likes || 0) / posts;
}
@discourseComputed("topic.replyCount", "ratioText")
title(count, ratio) {
return I18n.messageFormat("posts_likes_MF", {
count,
ratio,
});
}
@discourseComputed("ratio")
ratioText(ratio) {
const settings = this.siteSettings;
if (ratio > settings.topic_post_like_heat_high) {
return "high";
}
if (ratio > settings.topic_post_like_heat_medium) {
return "med";
}
if (ratio > settings.topic_post_like_heat_low) {
return "low";
}
return "";
}
@discourseComputed("ratioText")
likesHeat(ratioText) {
if (ratioText && ratioText.length) {
return `heatmap-${ratioText}`;
}
}
}

View File

@ -1,9 +0,0 @@
import EmberObject from "@ember/object";
import discourseComputed from "discourse/lib/decorators";
export default class VisitedLine extends EmberObject {
@discourseComputed
isLastVisited() {
return this.lastVisitedTopic === this.topic;
}
}

View File

@ -1,41 +0,0 @@
import EmberObject, { action } from "@ember/object";
import { service } from "@ember/service";
import BulkSelectTopicsDropdown from "discourse/components/bulk-select-topics-dropdown";
import rawRenderGlimmer from "discourse/lib/raw-render-glimmer";
import { i18n } from "discourse-i18n";
const BulkSelectGlimmerWrapper = <template>
<span class="bulk-select-topic-dropdown__count">
{{i18n "topics.bulk.selected_count" count=@data.selectedCount}}
</span>
<BulkSelectTopicsDropdown
@bulkSelectHelper={{@data.bulkSelectHelper}}
@afterBulkActionComplete={{@data.afterBulkAction}}
/>
</template>;
export default class extends EmberObject {
@service router;
get selectedCount() {
return this.bulkSelectHelper.selected.length;
}
@action
afterBulkAction() {
return this.router.refresh();
}
get html() {
return rawRenderGlimmer(
this,
"div.bulk-select-topics-dropdown",
BulkSelectGlimmerWrapper,
{
bulkSelectHelper: this.bulkSelectHelper,
selectedCount: this.selectedCount,
afterBulkAction: this.afterBulkAction,
}
);
}
}

View File

@ -1,72 +0,0 @@
import EmberObject from "@ember/object";
import { and } from "@ember/object/computed";
import discourseComputed from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
export default class TopicListHeaderColumn extends EmberObject {
sortable = null;
@and("sortable", "isSorting") ariaPressed;
@discourseComputed
localizedName() {
if (this.forceName) {
return this.forceName;
}
return this.name ? i18n(this.name) : "";
}
@discourseComputed
sortIcon() {
const isAscending =
(
this.parent.ascending ||
this.parent.context?.ascending ||
""
).toString() === "true";
return `chevron-${isAscending ? "up" : "down"}`;
}
@discourseComputed
isSorting() {
return (
this.sortable &&
(this.parent.order === this.order ||
this.parent.context?.order === this.order)
);
}
@discourseComputed
className() {
const name = [];
if (this.order) {
name.push(this.order);
}
if (this.sortable) {
name.push("sortable");
if (this.isSorting) {
name.push("sorting");
}
}
if (this.number) {
name.push("num");
}
return name.join(" ");
}
@discourseComputed
ariaSort() {
if (this.isSorting) {
return this.parent.ascending ? "ascending" : "descending";
} else {
return false;
}
}
}

View File

@ -1,119 +0,0 @@
import EmberObject from "@ember/object";
import discourseComputed from "discourse/lib/decorators";
import deprecated from "discourse/lib/deprecated";
import { RAW_TOPIC_LIST_DEPRECATION_OPTIONS } from "discourse/lib/plugin-api";
import { i18n } from "discourse-i18n";
export default class TopicStatus extends EmberObject {
static reopen() {
deprecated(
"Modifying raw-view:topic-status with `reopen` is deprecated. Use the value transformer `topic-list-columns` and other new topic-list plugin APIs instead.",
RAW_TOPIC_LIST_DEPRECATION_OPTIONS
);
return super.reopen(...arguments);
}
static reopenClass() {
deprecated(
"Modifying raw-view:topic-status with `reopenClass` is deprecated. Use the value transformer `topic-list-columns` and other new topic-list plugin APIs instead.",
RAW_TOPIC_LIST_DEPRECATION_OPTIONS
);
return super.reopenClass(...arguments);
}
showDefault = null;
@discourseComputed("defaultIcon")
renderDiv(defaultIcon) {
return (defaultIcon || this.statuses.length > 0) && !this.noDiv;
}
@discourseComputed
statuses() {
const topic = this.topic;
const results = [];
// TODO, custom statuses? via override?
if (topic.is_warning) {
results.push({ icon: "envelope", key: "warning" });
}
if (topic.bookmarked) {
const postNumbers = topic.bookmarked_post_numbers;
let url = topic.url;
let extraClasses = "";
if (postNumbers && postNumbers[0] > 1) {
url += "/" + postNumbers[0];
} else {
extraClasses = "op-bookmark";
}
results.push({
extraClasses,
icon: "bookmark",
key: "bookmarked",
href: url,
});
}
if (topic.closed && topic.archived) {
results.push({ icon: "lock", key: "locked_and_archived" });
} else if (topic.closed) {
results.push({ icon: "lock", key: "locked" });
} else if (topic.archived) {
results.push({ icon: "lock", key: "archived" });
}
if (topic.pinned) {
results.push({ icon: "thumbtack", key: "pinned" });
}
if (topic.unpinned) {
results.push({ icon: "thumbtack", key: "unpinned" });
}
if (topic.invisible) {
results.push({ icon: "far-eye-slash", key: "unlisted" });
}
if (
this.showPrivateMessageIcon &&
topic.isPrivateMessage &&
!topic.is_warning
) {
results.push({ icon: "envelope", key: "personal_message" });
}
results.forEach((result) => {
const translationParams = {};
if (result.key === "unlisted") {
translationParams.unlistedReason = topic.visibilityReasonTranslated;
}
result.title = i18n(
`topic_statuses.${result.key}.help`,
translationParams
);
if (
this.currentUser &&
(result.key === "pinned" || result.key === "unpinned")
) {
result.openTag = "a href";
result.closeTag = "a";
} else {
result.openTag = "span";
result.closeTag = "span";
}
});
let defaultIcon = this.defaultIcon;
if (results.length === 0 && defaultIcon) {
this.set("showDefault", defaultIcon);
}
return results;
}
}

View File

@ -28,7 +28,6 @@ export const CRITICAL_DEPRECATIONS = [
"discourse.qunit.acceptance-function",
"discourse.qunit.global-exists",
"discourse.post-stream.trigger-new-post",
"discourse.hbr-topic-list-overrides",
"discourse.mobile-templates",
"discourse.mobile-view",
"discourse.mobile-templates",

View File

@ -1,31 +0,0 @@
import Service from "@ember/service";
import { TrackedSet } from "@ember-compat/tracked-built-ins";
/**
* This service is responsible for rendering glimmer components into HTML generated
* by raw-hbs. It is not intended to be used directly.
*
* See discourse/lib/raw-render-glimmer.js for usage instructions.
*/
export default class RenderGlimmerService extends Service {
_registrations = new TrackedSet();
add(info) {
this._registrations.add(info);
}
remove(info) {
this._registrations.delete(info);
}
/**
* Removes registrations for elements which are no longer in the DOM.
*/
cleanup() {
this._registrations.forEach((info) => {
if (!document.body.contains(info.element)) {
this.remove(info);
}
});
}
}

View File

@ -13,7 +13,6 @@ const { Webpack } = require("@embroider/webpack");
const { StatsWriterPlugin } = require("webpack-stats-plugin");
const { RetryChunkLoadPlugin } = require("webpack-retry-chunk-load-plugin");
const withSideWatch = require("./lib/with-side-watch");
const RawHandlebarsCompiler = require("discourse-hbr/raw-handlebars-compiler");
const crypto = require("crypto");
const commonBabelConfig = require("./lib/common-babel-config");
const TerserPlugin = require("terser-webpack-plugin");
@ -65,11 +64,9 @@ module.exports = function (defaults) {
...commonBabelConfig(),
trees: {
app: RawHandlebarsCompiler(
withSideWatch("app", {
watching: ["../discourse-markdown-it", "../truth-helpers"],
})
),
app: withSideWatch("app", {
watching: ["../discourse-markdown-it", "../truth-helpers"],
}),
},
});

Some files were not shown because too many files have changed in this diff Show More