Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions assets/javascripts/discourse/components/solved-accepted-answer.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import concatClass from "discourse/helpers/concat-class";
import { iconHTML } from "discourse/lib/icon-library";
import { formatUsername } from "discourse/lib/utilities";
import { i18n } from "discourse-i18n";

export default class SolvedAcceptedAnswer extends Component {
@service siteSettings;
@service store;

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

get hasExcerpt() {
return !!this.topic.accepted_answer.excerpt;
}

get htmlAccepter() {
const username = this.topic.accepted_answer.accepter_username;
const name = this.topic.accepted_answer.accepter_name;

if (!this.siteSettings.show_who_marked_solved) {
return;
}

const formattedUsername =
this.siteSettings.display_name_on_posts && name
? name
: formatUsername(username);

return htmlSafe(
i18n("solved.marked_solved_by", {
username: formattedUsername,
username_lower: username.toLowerCase(),
})
);
}

get htmlExcerpt() {
return htmlSafe(this.topic.accepted_answer.excerpt);
}

get htmlSolvedBy() {
const username = this.topic.accepted_answer.username;
const name = this.topic.accepted_answer.name;
const postNumber = this.topic.accepted_answer.post_number;

if (!username || !postNumber) {
return;
}

const displayedUser =
this.siteSettings.display_name_on_posts && name
? name
: formatUsername(username);

const data = {
icon: iconHTML("square-check", { class: "accepted" }),
username_lower: username.toLowerCase(),
username: displayedUser,
post_path: `${this.topic.url}/${postNumber}`,
post_number: postNumber,
user_path: this.store.createRecord("user", { username }).path,
};

return htmlSafe(i18n("solved.accepted_html", data));
}

<template>
<aside
class="quote accepted-answer"
data-post={{this.topic.accepted_answer.post_number}}
data-topic={{this.topic.id}}
>
<div class={{concatClass "title" (unless this.hasExcerpt "title-only")}}>
<div class="accepted-answer--solver-accepter">
<div class="accepted-answer--solver">
{{this.htmlSolvedBy}}
</div>
<div class="accepted-answer--accepter">
{{this.htmlAccepter}}
</div>
</div>
<div class="quote-controls"></div>
</div>
{{#if this.hasExcerpt}}
<blockquote>
{{this.htmlExcerpt}}
</blockquote>
{{/if}}
</aside>
</template>
}
136 changes: 136 additions & 0 deletions assets/javascripts/discourse/initializers/extend-for-solved-button.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import Component from "@glimmer/component";
import { computed } from "@ember/object";
import { withSilencedDeprecations } from "discourse/lib/deprecated";
import { iconHTML } from "discourse/lib/icon-library";
import { withPluginApi } from "discourse/lib/plugin-api";
import { formatUsername } from "discourse/lib/utilities";
import Topic from "discourse/models/topic";
import User from "discourse/models/user";
import PostCooked from "discourse/widgets/post-cooked";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import { i18n } from "discourse-i18n";
import SolvedAcceptAnswerButton from "../components/solved-accept-answer-button";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
import SolvedUnacceptAnswerButton from "../components/solved-unaccept-answer-button";

function initializeWithApi(api) {
customizePost(api);
customizePostMenu(api);

if (api.addDiscoveryQueryParam) {
api.addDiscoveryQueryParam("solved", { replace: true, refreshModel: true });
}
}

function customizePost(api) {
api.addTrackedPostProperties(
"can_accept_answer",
"can_unaccept_answer",
"accepted_answer",
"topic_accepted_answer"
);

api.renderAfterWrapperOutlet(
"post-content-cooked-html",
class extends Component {
static shouldRender(args) {
return args.post.post_number === 1 && args.post.topic.accepted_answer;
}

<template><SolvedAcceptedAnswer @post={{@outletArgs.post}} /></template>
}
);

withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
customizeWidgetPost(api)
);
}

function customizeWidgetPost(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
let post = helper.getModel();

if (helper.attrs.post_number === 1 && post?.topic?.accepted_answer) {
return new RenderGlimmer(
helper.widget,
"div",
<template><SolvedAcceptedAnswer @post={{@data.post}} /></template>,
{ post }
);
}
});
}

function customizePostMenu(api) {
api.registerValueTransformer(
"post-menu-buttons",
({
value: dag,
context: {
post,
firstButtonKey,
secondLastHiddenButtonKey,
lastHiddenButtonKey,
},
}) => {
let solvedButton;

if (post.can_accept_answer) {
solvedButton = SolvedAcceptAnswerButton;
} else if (post.accepted_answer) {
solvedButton = SolvedUnacceptAnswerButton;
}

solvedButton &&
dag.add(
"solved",
solvedButton,
post.topic_accepted_answer && !post.accepted_answer
? {
before: lastHiddenButtonKey,
after: secondLastHiddenButtonKey,
}
: {
before: [
"assign", // button added by the assign plugin
firstButtonKey,
],
}
);
}
);
}

export default {
name: "extend-for-solved-button",
initialize() {
withPluginApi("1.34.0", initializeWithApi);

withPluginApi("0.8.10", (api) => {
api.replaceIcon(
"notification.solved.accepted_notification",
"square-check"
);
});

withPluginApi("0.11.0", (api) => {
api.addAdvancedSearchOptions({
statusOptions: [
{
name: i18n("search.advanced.statuses.solved"),
value: "solved",
},
{
name: i18n("search.advanced.statuses.unsolved"),
value: "unsolved",
},
],
});
});

withPluginApi("0.11.7", (api) => {
api.addSearchSuggestion("status:solved");
api.addSearchSuggestion("status:unsolved");
});
},
};
Loading