Skip to content

Commit 8e72136

Browse files
authored
DEV: Add compatibility with the Glimmer Post Stream (#363)
1 parent ae01ad3 commit 8e72136

File tree

6 files changed

+368
-234
lines changed

6 files changed

+368
-234
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { on } from "@ember/modifier";
4+
import { action } from "@ember/object";
5+
import { service } from "@ember/service";
6+
import { htmlSafe } from "@ember/template";
7+
import AsyncContent from "discourse/components/async-content";
8+
import PostCookedHtml from "discourse/components/post/cooked-html";
9+
import concatClass from "discourse/helpers/concat-class";
10+
import icon from "discourse/helpers/d-icon";
11+
import { ajax } from "discourse/lib/ajax";
12+
import { iconHTML } from "discourse/lib/icon-library";
13+
import { formatUsername } from "discourse/lib/utilities";
14+
import { i18n } from "discourse-i18n";
15+
16+
export default class SolvedAcceptedAnswer extends Component {
17+
@service siteSettings;
18+
@service store;
19+
20+
@tracked expanded = false;
21+
22+
get acceptedAnswer() {
23+
return this.topic.accepted_answer;
24+
}
25+
26+
get quoteId() {
27+
return `accepted-answer-${this.topic.id}-${this.acceptedAnswer.post_number}`;
28+
}
29+
30+
get topic() {
31+
return this.args.post.topic;
32+
}
33+
34+
get hasExcerpt() {
35+
return !!this.acceptedAnswer.excerpt;
36+
}
37+
38+
get htmlAccepter() {
39+
const username = this.acceptedAnswer.accepter_username;
40+
const name = this.acceptedAnswer.accepter_name;
41+
42+
if (!this.siteSettings.show_who_marked_solved) {
43+
return;
44+
}
45+
46+
const formattedUsername =
47+
this.siteSettings.display_name_on_posts && name
48+
? name
49+
: formatUsername(username);
50+
51+
return htmlSafe(
52+
i18n("solved.marked_solved_by", {
53+
username: formattedUsername,
54+
username_lower: username.toLowerCase(),
55+
})
56+
);
57+
}
58+
59+
get htmlSolvedBy() {
60+
const username = this.acceptedAnswer.username;
61+
const name = this.acceptedAnswer.name;
62+
const postNumber = this.acceptedAnswer.post_number;
63+
64+
if (!username || !postNumber) {
65+
return;
66+
}
67+
68+
const displayedUser =
69+
this.siteSettings.display_name_on_posts && name
70+
? name
71+
: formatUsername(username);
72+
73+
const data = {
74+
icon: iconHTML("square-check", { class: "accepted" }),
75+
username_lower: username.toLowerCase(),
76+
username: displayedUser,
77+
post_path: `${this.topic.url}/${postNumber}`,
78+
post_number: postNumber,
79+
user_path: this.store.createRecord("user", { username }).path,
80+
};
81+
82+
return htmlSafe(i18n("solved.accepted_html", data));
83+
}
84+
85+
@action
86+
toggleExpandedPost() {
87+
if (!this.hasExcerpt) {
88+
return;
89+
}
90+
91+
this.expanded = !this.expanded;
92+
}
93+
94+
@action
95+
async loadExpandedAcceptedAnswer(postNumber) {
96+
const acceptedAnswer = await ajax(
97+
`/posts/by_number/${this.topic.id}/${postNumber}`
98+
);
99+
100+
return this.store.createRecord("post", acceptedAnswer);
101+
}
102+
103+
<template>
104+
<aside
105+
class="quote accepted-answer"
106+
data-post={{this.acceptedAnswer.post_number}}
107+
data-topic={{this.topic.id}}
108+
data-expanded={{this.expanded}}
109+
>
110+
{{! template-lint-disable no-invalid-interactive }}
111+
<div
112+
class={{concatClass
113+
"title"
114+
(unless this.hasExcerpt "title-only")
115+
(if this.hasExcerpt "quote__title--can-toggle-content")
116+
}}
117+
{{on "click" this.toggleExpandedPost}}
118+
>
119+
<div class="accepted-answer--solver-accepter">
120+
<div class="accepted-answer--solver">
121+
{{this.htmlSolvedBy}}
122+
</div>
123+
<div class="accepted-answer--accepter">
124+
{{this.htmlAccepter}}
125+
</div>
126+
</div>
127+
{{#if this.hasExcerpt}}
128+
<div class="quote-controls">
129+
<button
130+
aria-controls={{this.quoteId}}
131+
aria-expanded={{this.expanded}}
132+
class="quote-toggle btn-flat"
133+
type="button"
134+
>
135+
{{icon
136+
(if this.expanded "chevron-up" "chevron-down")
137+
title="post.expand_collapse"
138+
}}
139+
</button>
140+
</div>
141+
{{/if}}
142+
</div>
143+
{{#if this.hasExcerpt}}
144+
<blockquote id={{this.quoteId}}>
145+
{{#if this.expanded}}
146+
<AsyncContent
147+
@asyncData={{this.loadExpandedAcceptedAnswer}}
148+
@context={{this.acceptedAnswer.post_number}}
149+
>
150+
<:content as |expandedAnswer|>
151+
<div class="expanded-quote" data-post-id={{expandedAnswer.id}}>
152+
<PostCookedHtml
153+
@post={{expandedAnswer}}
154+
@streamElement={{false}}
155+
/>
156+
</div>
157+
</:content>
158+
</AsyncContent>
159+
{{else}}
160+
{{htmlSafe this.acceptedAnswer.excerpt}}
161+
{{/if}}
162+
</blockquote>
163+
{{/if}}
164+
</aside>
165+
</template>
166+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import Component from "@glimmer/component";
2+
import { computed } from "@ember/object";
3+
import { withSilencedDeprecations } from "discourse/lib/deprecated";
4+
import { iconHTML } from "discourse/lib/icon-library";
5+
import { withPluginApi } from "discourse/lib/plugin-api";
6+
import { formatUsername } from "discourse/lib/utilities";
7+
import Topic from "discourse/models/topic";
8+
import User from "discourse/models/user";
9+
import PostCooked from "discourse/widgets/post-cooked";
10+
import RenderGlimmer from "discourse/widgets/render-glimmer";
11+
import { i18n } from "discourse-i18n";
12+
import SolvedAcceptAnswerButton from "../components/solved-accept-answer-button";
13+
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
14+
import SolvedUnacceptAnswerButton from "../components/solved-unaccept-answer-button";
15+
16+
function initializeWithApi(api) {
17+
customizePost(api);
18+
customizePostMenu(api);
19+
20+
if (api.addDiscoveryQueryParam) {
21+
api.addDiscoveryQueryParam("solved", { replace: true, refreshModel: true });
22+
}
23+
}
24+
25+
function customizePost(api) {
26+
api.addTrackedPostProperties(
27+
"can_accept_answer",
28+
"can_unaccept_answer",
29+
"accepted_answer",
30+
"topic_accepted_answer"
31+
);
32+
33+
api.renderAfterWrapperOutlet(
34+
"post-content-cooked-html",
35+
class extends Component {
36+
static shouldRender(args) {
37+
return args.post.post_number === 1 && args.post.topic.accepted_answer;
38+
}
39+
40+
<template><SolvedAcceptedAnswer @post={{@outletArgs.post}} /></template>
41+
}
42+
);
43+
44+
withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
45+
customizeWidgetPost(api)
46+
);
47+
}
48+
49+
function customizeWidgetPost(api) {
50+
api.decorateWidget("post-contents:after-cooked", (helper) => {
51+
let post = helper.getModel();
52+
53+
if (helper.attrs.post_number === 1 && post?.topic?.accepted_answer) {
54+
return new RenderGlimmer(
55+
helper.widget,
56+
"div",
57+
<template><SolvedAcceptedAnswer @post={{@data.post}} /></template>,
58+
{ post }
59+
);
60+
}
61+
});
62+
}
63+
64+
function customizePostMenu(api) {
65+
api.registerValueTransformer(
66+
"post-menu-buttons",
67+
({
68+
value: dag,
69+
context: {
70+
post,
71+
firstButtonKey,
72+
secondLastHiddenButtonKey,
73+
lastHiddenButtonKey,
74+
},
75+
}) => {
76+
let solvedButton;
77+
78+
if (post.can_accept_answer) {
79+
solvedButton = SolvedAcceptAnswerButton;
80+
} else if (post.accepted_answer) {
81+
solvedButton = SolvedUnacceptAnswerButton;
82+
}
83+
84+
solvedButton &&
85+
dag.add(
86+
"solved",
87+
solvedButton,
88+
post.topic_accepted_answer && !post.accepted_answer
89+
? {
90+
before: lastHiddenButtonKey,
91+
after: secondLastHiddenButtonKey,
92+
}
93+
: {
94+
before: [
95+
"assign", // button added by the assign plugin
96+
firstButtonKey,
97+
],
98+
}
99+
);
100+
}
101+
);
102+
}
103+
104+
export default {
105+
name: "extend-for-solved-button",
106+
initialize() {
107+
withPluginApi("1.34.0", initializeWithApi);
108+
109+
withPluginApi("0.8.10", (api) => {
110+
api.replaceIcon(
111+
"notification.solved.accepted_notification",
112+
"square-check"
113+
);
114+
});
115+
116+
withPluginApi("0.11.0", (api) => {
117+
api.addAdvancedSearchOptions({
118+
statusOptions: [
119+
{
120+
name: i18n("search.advanced.statuses.solved"),
121+
value: "solved",
122+
},
123+
{
124+
name: i18n("search.advanced.statuses.unsolved"),
125+
value: "unsolved",
126+
},
127+
],
128+
});
129+
});
130+
131+
withPluginApi("0.11.7", (api) => {
132+
api.addSearchSuggestion("status:solved");
133+
api.addSearchSuggestion("status:unsolved");
134+
});
135+
},
136+
};

0 commit comments

Comments
 (0)