mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 20:43:19 +08:00
DEV: Add more-topics plugin API (#29143)
From plugin-api comment: Registers a new tab to be displayed in "more topics" area at the bottom of a topic page. ```gjs api.registerMoreTopicsTab({ id: "other-topics", name: i18n("other_topics.tab"), component: <template>tbd</template>, condition: ({ topic }) => topic.otherTopics?.length > 0, }); ``` You can additionally use more-topics-tabs value transformer to conditionally show/hide specific tabs. ```js api.registerValueTransformer("more-topics-tabs", ({ value, context }) => { if (context.user?.aFeatureFlag) { // Remove "suggested" from the topics page return value.filter( (tab) => context.currentContext !== "topic" || tab.id !== "suggested-topics" ); } }); ```
This commit is contained in:
parent
7a936da05c
commit
9c5fc6f1df
103
app/assets/javascripts/discourse/app/components/more-topics.gjs
Normal file
103
app/assets/javascripts/discourse/app/components/more-topics.gjs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { cached, tracked } from "@glimmer/tracking";
|
||||||
|
import { fn } from "@ember/helper";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { eq, gt } from "truth-helpers";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import BrowseMore from "discourse/components/more-topics/browse-more";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import { applyValueTransformer } from "discourse/lib/transformer";
|
||||||
|
|
||||||
|
export let registeredTabs = [];
|
||||||
|
|
||||||
|
export function clearRegisteredTabs() {
|
||||||
|
registeredTabs.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MoreTopics extends Component {
|
||||||
|
@service currentUser;
|
||||||
|
@service keyValueStore;
|
||||||
|
|
||||||
|
@tracked selectedTab = this.initialTab;
|
||||||
|
|
||||||
|
get initialTab() {
|
||||||
|
let savedId = this.keyValueStore.get(
|
||||||
|
`more-topics-preference-${this.context}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fallback to the old setting
|
||||||
|
savedId ||= this.keyValueStore.get("more-topics-list-preference");
|
||||||
|
|
||||||
|
return (
|
||||||
|
(savedId && this.tabs.find((tab) => tab.id === savedId)) || this.tabs[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeTab() {
|
||||||
|
return this.tabs.find((tab) => tab === this.selectedTab) || this.tabs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
get context() {
|
||||||
|
return this.args.topic.get("isPrivateMessage") ? "pm" : "topic";
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached
|
||||||
|
get tabs() {
|
||||||
|
const defaultTabs = registeredTabs.filter((tab) =>
|
||||||
|
tab.condition({ topic: this.args.topic, context: this.context })
|
||||||
|
);
|
||||||
|
|
||||||
|
return applyValueTransformer("more-topics-tabs", defaultTabs, {
|
||||||
|
currentContext: this.context,
|
||||||
|
user: this.currentUser,
|
||||||
|
topic: this.args.topic,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
selectTab(tab) {
|
||||||
|
this.selectedTab = tab;
|
||||||
|
this.keyValueStore.set({
|
||||||
|
key: `more-topics-preference-${this.context}`,
|
||||||
|
value: tab.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="more-topics__container">
|
||||||
|
{{#if (gt this.tabs.length 1)}}
|
||||||
|
<div class="row">
|
||||||
|
<ul class="nav nav-pills">
|
||||||
|
{{#each this.tabs as |tab|}}
|
||||||
|
<li>
|
||||||
|
<DButton
|
||||||
|
@action={{fn this.selectTab tab}}
|
||||||
|
@translatedLabel={{tab.name}}
|
||||||
|
@translatedTitle={{tab.name}}
|
||||||
|
@icon={{tab.icon}}
|
||||||
|
class={{if (eq tab.id this.activeTab.id) "active"}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.activeTab}}
|
||||||
|
<div
|
||||||
|
class={{concatClass
|
||||||
|
"more-topics__lists"
|
||||||
|
(if (eq this.tabs.length 1) "single-list")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<this.activeTab.component @topic={{@topic}} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if @topic.suggestedTopics.length}}
|
||||||
|
<BrowseMore @topic={{@topic}} />
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -1,49 +0,0 @@
|
||||||
<div class="more-topics__container">
|
|
||||||
{{#unless this.singleList}}
|
|
||||||
<div class="row">
|
|
||||||
<ul class="nav nav-pills">
|
|
||||||
{{#each this.availableTabs as |tab|}}
|
|
||||||
<li>
|
|
||||||
<DButton
|
|
||||||
@translatedTitle={{tab.name}}
|
|
||||||
@translatedLabel={{tab.name}}
|
|
||||||
@action={{fn this.rememberTopicListPreference tab.id}}
|
|
||||||
@icon={{tab.icon}}
|
|
||||||
class={{if (eq tab.id this.selectedTab) "active"}}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{{/unless}}
|
|
||||||
|
|
||||||
<div class="more-topics__lists {{if this.singleList 'single-list'}}">
|
|
||||||
{{#if @topic.relatedMessages.length}}
|
|
||||||
<RelatedMessages @topic={{@topic}} />
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if @topic.suggestedTopics.length}}
|
|
||||||
<SuggestedTopics @topic={{@topic}} />
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<PluginOutlet
|
|
||||||
@name="below-suggested-topics"
|
|
||||||
@connectorTagName="div"
|
|
||||||
@outletArgs={{hash topic=@topic}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<PluginOutlet
|
|
||||||
@name="topic-more-content"
|
|
||||||
@outletArgs={{hash model=@topic}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if @topic.suggestedTopics.length}}
|
|
||||||
<h3 class="more-topics__browse-more">
|
|
||||||
{{html-safe this.browseMoreMessage}}
|
|
||||||
</h3>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
|
@ -1,52 +1,25 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { action, computed } from "@ember/object";
|
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
import { categoryBadgeHTML } from "discourse/helpers/category-link";
|
import { categoryBadgeHTML } from "discourse/helpers/category-link";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
export default class MoreTopics extends Component {
|
export default class BrowseMore extends Component {
|
||||||
@service site;
|
|
||||||
@service moreTopicsPreferenceTracking;
|
|
||||||
@service pmTopicTrackingState;
|
|
||||||
@service topicTrackingState;
|
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
|
@service pmTopicTrackingState;
|
||||||
|
@service site;
|
||||||
|
@service topicTrackingState;
|
||||||
|
|
||||||
@action
|
groupLink(groupName) {
|
||||||
rememberTopicListPreference(value) {
|
return `<a class="group-link" href="${getURL(
|
||||||
this.moreTopicsPreferenceTracking.updatePreference(value);
|
`/u/${this.currentUser.username}/messages/group/${groupName}`
|
||||||
|
)}">${iconHTML("users")} ${groupName}</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("moreTopicsPreferenceTracking.topicLists")
|
get privateMessageBrowseMoreMessage() {
|
||||||
get singleList() {
|
const suggestedGroupName = this.args.topic.get("suggested_group_name");
|
||||||
return this.availableTabs.length === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed("moreTopicsPreferenceTracking.selectedTab")
|
|
||||||
get selectedTab() {
|
|
||||||
return this.moreTopicsPreferenceTracking.selectedTab;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed("moreTopicsPreferenceTracking.topicLists")
|
|
||||||
get availableTabs() {
|
|
||||||
return this.moreTopicsPreferenceTracking.topicLists;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed(
|
|
||||||
"pmTopicTrackingState.isTracking",
|
|
||||||
"pmTopicTrackingState.statesModificationCounter",
|
|
||||||
"topicTrackingState.messageCount"
|
|
||||||
)
|
|
||||||
get browseMoreMessage() {
|
|
||||||
return this.args.topic.isPrivateMessage
|
|
||||||
? this._privateMessageBrowseMoreMessage()
|
|
||||||
: this._topicBrowseMoreMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
_privateMessageBrowseMoreMessage() {
|
|
||||||
const username = this.currentUser.username;
|
|
||||||
const suggestedGroupName = this.args.topic.suggested_group_name;
|
|
||||||
const inboxFilter = suggestedGroupName ? "group" : "user";
|
const inboxFilter = suggestedGroupName ? "group" : "user";
|
||||||
|
|
||||||
const unreadCount = this.pmTopicTrackingState.lookupCount("unread", {
|
const unreadCount = this.pmTopicTrackingState.lookupCount("unread", {
|
||||||
|
@ -67,9 +40,9 @@ export default class MoreTopics extends Component {
|
||||||
HAS_UNREAD_AND_NEW: hasBoth,
|
HAS_UNREAD_AND_NEW: hasBoth,
|
||||||
UNREAD: unreadCount,
|
UNREAD: unreadCount,
|
||||||
NEW: newCount,
|
NEW: newCount,
|
||||||
username,
|
username: this.currentUser.username,
|
||||||
groupName: suggestedGroupName,
|
groupName: suggestedGroupName,
|
||||||
groupLink: this._groupLink(username, suggestedGroupName),
|
groupLink: this.groupLink(suggestedGroupName),
|
||||||
basePath: getURL(""),
|
basePath: getURL(""),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -77,24 +50,24 @@ export default class MoreTopics extends Component {
|
||||||
HAS_UNREAD_AND_NEW: hasBoth,
|
HAS_UNREAD_AND_NEW: hasBoth,
|
||||||
UNREAD: unreadCount,
|
UNREAD: unreadCount,
|
||||||
NEW: newCount,
|
NEW: newCount,
|
||||||
username,
|
username: this.currentUser.username,
|
||||||
basePath: getURL(""),
|
basePath: getURL(""),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (suggestedGroupName) {
|
} else if (suggestedGroupName) {
|
||||||
return I18n.t("user.messages.read_more_in_group", {
|
return I18n.t("user.messages.read_more_in_group", {
|
||||||
groupLink: this._groupLink(username, suggestedGroupName),
|
groupLink: this.groupLink(suggestedGroupName),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return I18n.t("user.messages.read_more", {
|
return I18n.t("user.messages.read_more", {
|
||||||
basePath: getURL(""),
|
basePath: getURL(""),
|
||||||
username,
|
username: this.currentUser.username,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_topicBrowseMoreMessage() {
|
get topicBrowseMoreMessage() {
|
||||||
let category = this.args.topic.category;
|
let category = this.args.topic.get("category");
|
||||||
|
|
||||||
if (category && category.id === this.site.uncategorized_category_id) {
|
if (category && category.id === this.site.uncategorized_category_id) {
|
||||||
category = null;
|
category = null;
|
||||||
|
@ -113,7 +86,7 @@ export default class MoreTopics extends Component {
|
||||||
HAS_UNREAD_AND_NEW: unreadTopics > 0 && newTopics > 0,
|
HAS_UNREAD_AND_NEW: unreadTopics > 0 && newTopics > 0,
|
||||||
UNREAD: unreadTopics,
|
UNREAD: unreadTopics,
|
||||||
NEW: newTopics,
|
NEW: newTopics,
|
||||||
HAS_CATEGORY: category ? true : false,
|
HAS_CATEGORY: !!category,
|
||||||
categoryLink: category ? categoryBadgeHTML(category) : null,
|
categoryLink: category ? categoryBadgeHTML(category) : null,
|
||||||
basePath: getURL(""),
|
basePath: getURL(""),
|
||||||
});
|
});
|
||||||
|
@ -130,9 +103,13 @@ export default class MoreTopics extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_groupLink(username, groupName) {
|
<template>
|
||||||
return `<a class="group-link" href="${getURL(
|
<h3 class="more-topics__browse-more">
|
||||||
`/u/${username}/messages/group/${groupName}`
|
{{#if @topic.isPrivateMessage}}
|
||||||
)}">${iconHTML("users")} ${groupName}</a>`;
|
{{htmlSafe this.privateMessageBrowseMoreMessage}}
|
||||||
}
|
{{else}}
|
||||||
|
{{htmlSafe this.topicBrowseMoreMessage}}
|
||||||
|
{{/if}}
|
||||||
|
</h3>
|
||||||
|
</template>
|
||||||
}
|
}
|
|
@ -1,56 +1,33 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { cached } from "@glimmer/tracking";
|
import { cached } from "@glimmer/tracking";
|
||||||
import { action } from "@ember/object";
|
|
||||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
|
||||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import BasicTopicList from "discourse/components/basic-topic-list";
|
import BasicTopicList from "discourse/components/basic-topic-list";
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
|
||||||
import i18n from "discourse-common/helpers/i18n";
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import I18n from "discourse-i18n";
|
|
||||||
|
|
||||||
const LIST_ID = "related-Messages";
|
|
||||||
|
|
||||||
export default class RelatedMessages extends Component {
|
export default class RelatedMessages extends Component {
|
||||||
@service moreTopicsPreferenceTracking;
|
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
|
|
||||||
get hidden() {
|
|
||||||
return this.moreTopicsPreferenceTracking.get("selectedTab") !== LIST_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
registerList() {
|
|
||||||
this.moreTopicsPreferenceTracking.registerTopicList({
|
|
||||||
name: I18n.t("related_messages.pill"),
|
|
||||||
id: LIST_ID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
removeList() {
|
|
||||||
this.moreTopicsPreferenceTracking.removeTopicList(LIST_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
get targetUser() {
|
get targetUser() {
|
||||||
const topic = this.args.topic;
|
const { topic } = this.args;
|
||||||
|
|
||||||
if (!topic || !topic.isPrivateMessage) {
|
if (!topic || !topic.isPrivateMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedUsers = topic.details.allowed_users;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
topic.relatedMessages?.length >= 5 &&
|
topic.relatedMessages?.length >= 5 &&
|
||||||
allowedUsers.length === 2 &&
|
|
||||||
topic.details.allowed_groups.length === 0 &&
|
topic.details.allowed_groups.length === 0 &&
|
||||||
allowedUsers.find((u) => u.username === this.currentUser.username)
|
topic.details.allowed_users.length === 2 &&
|
||||||
|
topic.details.allowed_users.find(
|
||||||
|
(u) => u.username === this.currentUser.username
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return allowedUsers.find((u) => u.username !== this.currentUser.username);
|
return topic.details.allowed_users.find(
|
||||||
|
(u) => u.username !== this.currentUser.username
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,12 +39,10 @@ export default class RelatedMessages extends Component {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="related-messages"
|
|
||||||
class={{concatClass "more-topics__list" (if this.hidden "hidden")}}
|
|
||||||
role="complementary"
|
role="complementary"
|
||||||
aria-labelledby="related-messages-title"
|
aria-labelledby="related-messages-title"
|
||||||
{{didInsert this.registerList}}
|
id="related-messages"
|
||||||
{{willDestroy this.removeList}}
|
class="more-topics__list"
|
||||||
>
|
>
|
||||||
<h3 id="related-messages-title" class="more-topics__list-title">
|
<h3 id="related-messages-title" class="more-topics__list-title">
|
||||||
{{i18n "related_messages.title"}}
|
{{i18n "related_messages.title"}}
|
||||||
|
@ -75,9 +50,9 @@ export default class RelatedMessages extends Component {
|
||||||
|
|
||||||
<div class="topics">
|
<div class="topics">
|
||||||
<BasicTopicList
|
<BasicTopicList
|
||||||
@hideCategory="true"
|
|
||||||
@showPosters="true"
|
|
||||||
@topics={{@topic.relatedMessages}}
|
@topics={{@topic.relatedMessages}}
|
||||||
|
@hideCategory={{true}}
|
||||||
|
@showPosters={{true}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import BasicTopicList from "discourse/components/basic-topic-list";
|
||||||
|
import UserTip from "discourse/components/user-tip";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
|
export default class SuggestedTopics extends Component {
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
get suggestedTitle() {
|
||||||
|
const href = this.currentUser?.pmPath(this.args.topic);
|
||||||
|
if (href && this.args.topic.isPrivateMessage) {
|
||||||
|
return i18n("suggested_topics.pm_title");
|
||||||
|
} else {
|
||||||
|
return i18n("suggested_topics.title");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
role="complementary"
|
||||||
|
aria-labelledby="suggested-topics-title"
|
||||||
|
id="suggested-topics"
|
||||||
|
class="more-topics__list"
|
||||||
|
>
|
||||||
|
<UserTip
|
||||||
|
@id="suggested_topics"
|
||||||
|
@titleText={{i18n "user_tips.suggested_topics.title"}}
|
||||||
|
@contentText={{i18n "user_tips.suggested_topics.content"}}
|
||||||
|
@placement="top-start"
|
||||||
|
@priority={{700}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 id="suggested-topics-title" class="more-topics__list-title">
|
||||||
|
{{this.suggestedTitle}}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="topics">
|
||||||
|
{{#if @topic.isPrivateMessage}}
|
||||||
|
<BasicTopicList
|
||||||
|
@topics={{@topic.suggestedTopics}}
|
||||||
|
@hideCategory={{true}}
|
||||||
|
@showPosters={{true}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<BasicTopicList @topics={{@topic.suggestedTopics}} />
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -1,34 +0,0 @@
|
||||||
<div
|
|
||||||
id="suggested-topics"
|
|
||||||
class="more-topics__list {{if this.hidden 'hidden'}}"
|
|
||||||
role="complementary"
|
|
||||||
aria-labelledby="suggested-topics-title"
|
|
||||||
{{did-insert this.registerList}}
|
|
||||||
{{will-destroy this.removeList}}
|
|
||||||
>
|
|
||||||
{{#unless this.hidden}}
|
|
||||||
<UserTip
|
|
||||||
@id="suggested_topics"
|
|
||||||
@titleText={{i18n "user_tips.suggested_topics.title"}}
|
|
||||||
@contentText={{i18n "user_tips.suggested_topics.content"}}
|
|
||||||
@placement="top-start"
|
|
||||||
@priority={{700}}
|
|
||||||
/>
|
|
||||||
{{/unless}}
|
|
||||||
|
|
||||||
<h3 id="suggested-topics-title" class="more-topics__list-title">
|
|
||||||
{{i18n this.suggestedTitleLabel}}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="topics">
|
|
||||||
{{#if @topic.isPrivateMessage}}
|
|
||||||
<BasicTopicList
|
|
||||||
@hideCategory="true"
|
|
||||||
@showPosters="true"
|
|
||||||
@topics={{@topic.suggestedTopics}}
|
|
||||||
/>
|
|
||||||
{{else}}
|
|
||||||
<BasicTopicList @topics={{@topic.suggestedTopics}} />
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,38 +0,0 @@
|
||||||
import Component from "@glimmer/component";
|
|
||||||
import { action, computed } from "@ember/object";
|
|
||||||
import { service } from "@ember/service";
|
|
||||||
import I18n from "discourse-i18n";
|
|
||||||
|
|
||||||
export default class SuggestedTopics extends Component {
|
|
||||||
@service moreTopicsPreferenceTracking;
|
|
||||||
@service currentUser;
|
|
||||||
|
|
||||||
listId = "suggested-topics";
|
|
||||||
|
|
||||||
get suggestedTitleLabel() {
|
|
||||||
const href = this.currentUser && this.currentUser.pmPath(this.args.topic);
|
|
||||||
if (this.args.topic.isPrivateMessage && href) {
|
|
||||||
return "suggested_topics.pm_title";
|
|
||||||
} else {
|
|
||||||
return "suggested_topics.title";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed("moreTopicsPreferenceTracking.selectedTab")
|
|
||||||
get hidden() {
|
|
||||||
return this.moreTopicsPreferenceTracking.selectedTab !== this.listId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
registerList() {
|
|
||||||
this.moreTopicsPreferenceTracking.registerTopicList({
|
|
||||||
name: I18n.t("suggested_topics.pill"),
|
|
||||||
id: this.listId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
removeList() {
|
|
||||||
this.moreTopicsPreferenceTracking.removeTopicList(this.listId);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import RelatedMessages from "discourse/components/related-messages";
|
||||||
|
import SuggestedTopics from "discourse/components/suggested-topics";
|
||||||
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
initialize() {
|
||||||
|
withPluginApi("1.37.2", (api) => {
|
||||||
|
api.registerMoreTopicsTab({
|
||||||
|
id: "related-messages",
|
||||||
|
name: i18n("related_messages.pill"),
|
||||||
|
component: RelatedMessages,
|
||||||
|
condition: ({ context, topic }) =>
|
||||||
|
context === "pm" && topic.relatedMessages?.length > 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
api.registerMoreTopicsTab({
|
||||||
|
id: "suggested-topics",
|
||||||
|
name: i18n("suggested_topics.pill"),
|
||||||
|
component: SuggestedTopics,
|
||||||
|
condition: ({ topic }) => topic.suggestedTopics?.length > 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -3,7 +3,7 @@
|
||||||
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
|
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
|
||||||
// using the format described at https://keepachangelog.com/en/1.0.0/.
|
// using the format described at https://keepachangelog.com/en/1.0.0/.
|
||||||
|
|
||||||
export const PLUGIN_API_VERSION = "1.37.3";
|
export const PLUGIN_API_VERSION = "1.38.0";
|
||||||
|
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import { h } from "virtual-dom";
|
import { h } from "virtual-dom";
|
||||||
|
@ -23,6 +23,7 @@ import { forceDropdownForMenuPanels as glimmerForceDropdownForMenuPanels } from
|
||||||
import { addGlobalNotice } from "discourse/components/global-notice";
|
import { addGlobalNotice } from "discourse/components/global-notice";
|
||||||
import { headerButtonsDAG } from "discourse/components/header";
|
import { headerButtonsDAG } from "discourse/components/header";
|
||||||
import { headerIconsDAG } from "discourse/components/header/icons";
|
import { headerIconsDAG } from "discourse/components/header/icons";
|
||||||
|
import { registeredTabs } from "discourse/components/more-topics";
|
||||||
import { addWidgetCleanCallback } from "discourse/components/mount-widget";
|
import { addWidgetCleanCallback } from "discourse/components/mount-widget";
|
||||||
import { addPluginOutletDecorator } from "discourse/components/plugin-connector";
|
import { addPluginOutletDecorator } from "discourse/components/plugin-connector";
|
||||||
import {
|
import {
|
||||||
|
@ -3286,6 +3287,50 @@ class PluginApi {
|
||||||
registerPluginHeaderActionComponent(pluginId, componentClass);
|
registerPluginHeaderActionComponent(pluginId, componentClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a new tab to be displayed in "more topics" area at the bottom of a topic page.
|
||||||
|
*
|
||||||
|
* ```gjs
|
||||||
|
* api.registerMoreTopicsTab({
|
||||||
|
* id: "other-topics",
|
||||||
|
* name: i18n("other_topics.tab"),
|
||||||
|
* component: <template>tbd</template>,
|
||||||
|
* condition: ({ topic }) => topic.otherTopics?.length > 0,
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* You can additionally use more-topics-tabs value transformer to conditionally show/hide
|
||||||
|
* specific tabs.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* api.registerValueTransformer("more-topics-tabs", ({ value, context }) => {
|
||||||
|
* if (context.user?.aFeatureFlag) {
|
||||||
|
* // Remove "suggested" from the topics page
|
||||||
|
* return value.filter(
|
||||||
|
* (tab) =>
|
||||||
|
* context.currentContext !== "topic" ||
|
||||||
|
* tab.id !== "suggested-topics"
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @callback tabCondition
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {"topic"|"pm"} opts.context - the type of the current page
|
||||||
|
* @param {Topic} opts.topic - the current topic
|
||||||
|
*
|
||||||
|
* @param {Object} tab
|
||||||
|
* @param {string} tab.id - an identifier used in more-topics-tabs value transformer
|
||||||
|
* @param {string} tab.name - a name displayed on the tab
|
||||||
|
* @param {string} tab.icon - an optional icon displayed on the tab
|
||||||
|
* @param {Class} tab.component - contents of the tab
|
||||||
|
* @param {tabCondition} tab.condition - an optional callback to conditionally show the tab
|
||||||
|
*/
|
||||||
|
registerMoreTopicsTab(tab) {
|
||||||
|
registeredTabs.push(tab);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
#deprecatedWidgetOverride(widgetName, override) {
|
#deprecatedWidgetOverride(widgetName, override) {
|
||||||
// insert here the code to handle widget deprecations, e.g. for the header widgets we used:
|
// insert here the code to handle widget deprecations, e.g. for the header widgets we used:
|
||||||
|
|
|
@ -7,8 +7,9 @@ export const VALUE_TRANSFORMERS = Object.freeze([
|
||||||
// use only lowercase names
|
// use only lowercase names
|
||||||
"category-description-text",
|
"category-description-text",
|
||||||
"category-display-name",
|
"category-display-name",
|
||||||
"mentions-class",
|
|
||||||
"header-notifications-avatar-size",
|
"header-notifications-avatar-size",
|
||||||
"home-logo-href",
|
"home-logo-href",
|
||||||
"home-logo-image-url",
|
"home-logo-image-url",
|
||||||
|
"mentions-class",
|
||||||
|
"more-topics-tabs",
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { cached } from "@glimmer/tracking";
|
||||||
import EmberObject, { computed } from "@ember/object";
|
import EmberObject, { computed } from "@ember/object";
|
||||||
|
import { dependentKeyCompat } from "@ember/object/compat";
|
||||||
import { alias, and, equal, notEmpty, or } from "@ember/object/computed";
|
import { alias, and, equal, notEmpty, or } from "@ember/object/computed";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
|
@ -430,18 +432,20 @@ export default class Topic extends RestModel {
|
||||||
return newTags;
|
return newTags;
|
||||||
}
|
}
|
||||||
|
|
||||||
@discourseComputed("related_messages")
|
@dependentKeyCompat
|
||||||
relatedMessages(relatedMessages) {
|
@cached
|
||||||
if (relatedMessages) {
|
get relatedMessages() {
|
||||||
return relatedMessages.map((st) => this.store.createRecord("topic", st));
|
return this.get("related_messages")?.map((st) =>
|
||||||
}
|
this.store.createRecord("topic", st)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@discourseComputed("suggested_topics")
|
@dependentKeyCompat
|
||||||
suggestedTopics(suggestedTopics) {
|
@cached
|
||||||
if (suggestedTopics) {
|
get suggestedTopics() {
|
||||||
return suggestedTopics.map((st) => this.store.createRecord("topic", st));
|
return this.get("suggested_topics")?.map((st) =>
|
||||||
}
|
this.store.createRecord("topic", st)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@discourseComputed("posts_count")
|
@discourseComputed("posts_count")
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
import Service, { service } from "@ember/service";
|
|
||||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
|
||||||
|
|
||||||
const TOPIC_LIST_PREFERENCE_KEY = "more-topics-list-preference";
|
|
||||||
|
|
||||||
@disableImplicitInjections
|
|
||||||
export default class MoreTopicsPreferenceTracking extends Service {
|
|
||||||
@service keyValueStore;
|
|
||||||
|
|
||||||
@tracked selectedTab = null;
|
|
||||||
@tracked topicLists = [];
|
|
||||||
|
|
||||||
memoryTab = null;
|
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init(...arguments);
|
|
||||||
this.memoryTab = this.keyValueStore.get(TOPIC_LIST_PREFERENCE_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePreference(value) {
|
|
||||||
// Don't change the preference when selecting related PMs.
|
|
||||||
// It messes with the topics pref.
|
|
||||||
const rememberPref = value !== "related-messages";
|
|
||||||
|
|
||||||
if (rememberPref) {
|
|
||||||
this.keyValueStore.set({ key: TOPIC_LIST_PREFERENCE_KEY, value });
|
|
||||||
this.memoryTab = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedTab = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerTopicList(item) {
|
|
||||||
// We have a preference stored and the list exists.
|
|
||||||
if (this.memoryTab && this.memoryTab === item.id) {
|
|
||||||
this.selectedTab = item.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the first list as a default. Future lists may override this
|
|
||||||
// if they match the stored preference.
|
|
||||||
if (!this.selectedTab) {
|
|
||||||
this.selectedTab = item.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.topicLists = [...this.topicLists, item];
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTopicList(itemId) {
|
|
||||||
this.topicLists = this.topicLists.filter((item) => item.id !== itemId);
|
|
||||||
|
|
||||||
if (this.selectedTab === itemId) {
|
|
||||||
this.selectedTab = this.topicLists[0]?.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { click, visit } from "@ember/test-helpers";
|
||||||
|
import { test } from "qunit";
|
||||||
|
import { PLUGIN_API_VERSION, withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
|
||||||
|
acceptance("More topics - Plugin API", function (needs) {
|
||||||
|
needs.user();
|
||||||
|
|
||||||
|
test("displays the tabs", async function (assert) {
|
||||||
|
withPluginApi(PLUGIN_API_VERSION, (api) => {
|
||||||
|
api.registerMoreTopicsTab({
|
||||||
|
id: "my-tab",
|
||||||
|
name: "News",
|
||||||
|
component: <template>hello there!</template>,
|
||||||
|
condition: ({ context, topic }) =>
|
||||||
|
context === "topic" && topic.id === 280,
|
||||||
|
});
|
||||||
|
|
||||||
|
api.registerMoreTopicsTab({
|
||||||
|
id: "my-pm-tab",
|
||||||
|
name: "Other",
|
||||||
|
component: <template>hi!</template>,
|
||||||
|
condition: ({ context }) => context === "pm",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await visit("/t/-/280");
|
||||||
|
assert.dom(".more-topics__container li").exists({ count: 2 });
|
||||||
|
assert.dom(".more-topics__container li:last-of-type").hasText("News");
|
||||||
|
|
||||||
|
await click(`.more-topics__container button[title="News"]`);
|
||||||
|
assert.dom(".more-topics__lists").hasText("hello there!");
|
||||||
|
|
||||||
|
await visit("/t/-/12");
|
||||||
|
assert.dom(".more-topics__container li").exists({ count: 2 });
|
||||||
|
assert.dom(".more-topics__container li:last-of-type").hasText("Other");
|
||||||
|
|
||||||
|
await click(`.more-topics__container button[title="Other"]`);
|
||||||
|
assert.dom(".more-topics__lists").hasText("hi!");
|
||||||
|
|
||||||
|
await visit("/t/-/54077");
|
||||||
|
assert.dom(".more-topics__container li").doesNotExist();
|
||||||
|
assert.dom(".more-topics__container #suggested-topics-title").exists();
|
||||||
|
});
|
||||||
|
});
|
|
@ -22,6 +22,7 @@ import {
|
||||||
import { clearToolbarCallbacks } from "discourse/components/d-editor";
|
import { clearToolbarCallbacks } from "discourse/components/d-editor";
|
||||||
import { clearExtraHeaderButtons as clearExtraGlimmerHeaderButtons } from "discourse/components/header";
|
import { clearExtraHeaderButtons as clearExtraGlimmerHeaderButtons } from "discourse/components/header";
|
||||||
import { clearExtraHeaderIcons as clearExtraGlimmerHeaderIcons } from "discourse/components/header/icons";
|
import { clearExtraHeaderIcons as clearExtraGlimmerHeaderIcons } from "discourse/components/header/icons";
|
||||||
|
import { clearRegisteredTabs } from "discourse/components/more-topics";
|
||||||
import { resetWidgetCleanCallbacks } from "discourse/components/mount-widget";
|
import { resetWidgetCleanCallbacks } from "discourse/components/mount-widget";
|
||||||
import { resetDecorators as resetPluginOutletDecorators } from "discourse/components/plugin-connector";
|
import { resetDecorators as resetPluginOutletDecorators } from "discourse/components/plugin-connector";
|
||||||
import { resetItemSelectCallbacks } from "discourse/components/search-menu/results/assistant-item";
|
import { resetItemSelectCallbacks } from "discourse/components/search-menu/results/assistant-item";
|
||||||
|
@ -253,6 +254,7 @@ export function testCleanup(container, app) {
|
||||||
clearAboutPageActivities();
|
clearAboutPageActivities();
|
||||||
resetWidgetCleanCallbacks();
|
resetWidgetCleanCallbacks();
|
||||||
clearPluginHeaderActionComponents();
|
clearPluginHeaderActionComponents();
|
||||||
|
clearRegisteredTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupCssGeneratorTags() {
|
function cleanupCssGeneratorTags() {
|
||||||
|
|
|
@ -7,11 +7,14 @@ in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.38.0] - 2024-10-30
|
||||||
|
|
||||||
|
- Added `registerMoreTopicsTab` and "more-topics-tabs" value transformer that allows to add or remove new tabs to the "more topics" (suggested/related) area.
|
||||||
|
|
||||||
## [1.37.3] - 2024-10-24
|
## [1.37.3] - 2024-10-24
|
||||||
|
|
||||||
- Added `disableDefaultKeyboardShortcuts` which allows plugins/TCs to disable default keyboard shortcuts.
|
- Added `disableDefaultKeyboardShortcuts` which allows plugins/TCs to disable default keyboard shortcuts.
|
||||||
|
|
||||||
|
|
||||||
## [1.37.2] - 2024-10-02
|
## [1.37.2] - 2024-10-02
|
||||||
|
|
||||||
- Fixed comments and text references to Font Awesome 5 in favor of the more generic Font Awesome due to core now having the latest version and no longer needing to specify version 5.
|
- Fixed comments and text references to Font Awesome 5 in favor of the more generic Font Awesome due to core now having the latest version and no longer needing to specify version 5.
|
||||||
|
@ -26,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [1.36.0] - 2024-08-06
|
## [1.36.0] - 2024-08-06
|
||||||
|
|
||||||
- Added `addLogSearchLinkClickedCallbacks` which allows plugins/TCs to register a callback when a search link is clicked and before a search log is created
|
- Added `addLogSearchLinkClickedCallbacks` which allows plugins/TCs to register a callback when a search link is clicked and before a search log is created
|
||||||
|
|
||||||
## [1.35.0] - 2024-07-30
|
## [1.35.0] - 2024-07-30
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user