diff --git a/app/controllers/discourse_solved/answer_controller.rb b/app/controllers/discourse_solved/answer_controller.rb index 0ca7654..67ac86d 100644 --- a/app/controllers/discourse_solved/answer_controller.rb +++ b/app/controllers/discourse_solved/answer_controller.rb @@ -13,9 +13,9 @@ class DiscourseSolved::AnswerController < ::ApplicationController guardian.ensure_can_accept_answer!(topic, post) - DiscourseSolved.accept_answer!(post, current_user, topic: topic) + accepted_answer = DiscourseSolved.accept_answer!(post, current_user, topic: topic) - render json: success_json + render_json_dump(accepted_answer) end def unaccept diff --git a/assets/javascripts/discourse/components/solved-accept-answer-button.gjs b/assets/javascripts/discourse/components/solved-accept-answer-button.gjs index b7147a2..c205e4c 100644 --- a/assets/javascripts/discourse/components/solved-accept-answer-button.gjs +++ b/assets/javascripts/discourse/components/solved-accept-answer-button.gjs @@ -1,4 +1,5 @@ import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; import { service } from "@ember/service"; import DButton from "discourse/components/d-button"; @@ -13,6 +14,8 @@ export default class SolvedAcceptAnswerButton extends Component { @service appEvents; @service currentUser; + @tracked saving = false; + get showLabel() { return this.currentUser?.id === this.args.post.topicCreatedById; } @@ -21,13 +24,17 @@ export default class SolvedAcceptAnswerButton extends Component { acceptAnswer() { const post = this.args.post; - acceptPost(post, this.currentUser); + this.saving = true; + try { + acceptPost(post, this.currentUser); + } finally { + this.saving = false; + } this.appEvents.trigger("discourse-solved:solution-toggled", post); + // TODO (glimmer-post-stream) the Glimmer Post Stream does not listen to this event post.get("topic.postStream.posts").forEach((p) => { - p.set("topic_accepted_answer", true); - // TODO (glimmer-post-stream) the Glimmer Post Stream does not listen to this event this.appEvents.trigger("post-stream:refresh", { id: p.id }); }); } @@ -37,6 +44,7 @@ export default class SolvedAcceptAnswerButton extends Component { class="post-action-menu__solved-unaccepted unaccepted" ...attributes @action={{this.acceptAnswer}} + @disabled={{this.saving}} @icon="far-square-check" @label={{if this.showLabel "solved.solution"}} @title="solved.accept_answer" @@ -44,42 +52,21 @@ export default class SolvedAcceptAnswerButton extends Component { } -function acceptPost(post, acceptingUser) { +async function acceptPost(post) { + if (!post.can_accept_answer || post.accepted_answer) { + return; + } + const topic = post.topic; - clearAccepted(topic); + try { + const acceptedAnswer = await ajax("/solution/accept", { + type: "POST", + data: { id: post.id }, + }); - post.setProperties({ - can_unaccept_answer: true, - can_accept_answer: false, - accepted_answer: true, - }); - - topic.set("accepted_answer", { - username: post.username, - name: post.name, - post_number: post.post_number, - excerpt: post.cooked, - accepter_username: acceptingUser.username, - accepter_name: acceptingUser.name, - }); - - ajax("/solution/accept", { - type: "POST", - data: { id: post.id }, - }).catch(popupAjaxError); -} - -function clearAccepted(topic) { - const posts = topic.get("postStream.posts"); - posts.forEach((post) => { - if (post.get("post_number") > 1) { - post.setProperties({ - accepted_answer: false, - can_accept_answer: true, - can_unaccept_answer: false, - topic_accepted_answer: false, - }); - } - }); + topic.setAcceptedSolution(acceptedAnswer); + } catch (e) { + popupAjaxError(e); + } } diff --git a/assets/javascripts/discourse/components/solved-unaccept-answer-button.gjs b/assets/javascripts/discourse/components/solved-unaccept-answer-button.gjs index f021426..e662728 100644 --- a/assets/javascripts/discourse/components/solved-unaccept-answer-button.gjs +++ b/assets/javascripts/discourse/components/solved-unaccept-answer-button.gjs @@ -1,7 +1,9 @@ import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; import { service } from "@ember/service"; import { htmlSafe } from "@ember/template"; +import { and, not } from "truth-helpers"; import DButton from "discourse/components/d-button"; import icon from "discourse/helpers/d-icon"; import { ajax } from "discourse/lib/ajax"; @@ -10,44 +12,11 @@ import { formatUsername } from "discourse/lib/utilities"; import { i18n } from "discourse-i18n"; import DTooltip from "float-kit/components/d-tooltip"; -function unacceptPost(post) { - if (!post.can_unaccept_answer) { - return; - } - const topic = post.topic; - - post.setProperties({ - can_accept_answer: true, - can_unaccept_answer: false, - accepted_answer: false, - }); - - topic.accepted_answer = undefined; - - ajax("/solution/unaccept", { - type: "POST", - data: { id: post.id }, - }).catch(popupAjaxError); -} - export default class SolvedUnacceptAnswerButton extends Component { @service appEvents; @service siteSettings; - @action - unacceptAnswer() { - const post = this.args.post; - - unacceptPost(post); - - this.appEvents.trigger("discourse-solved:solution-toggled", post); - - post.get("topic.postStream.posts").forEach((p) => { - p.set("topic_accepted_answer", false); - // TODO (glimmer-post-stream) the Glimmer Post Stream does not listen to this event - this.appEvents.trigger("post-stream:refresh", { id: p.id }); - }); - } + @tracked saving = false; get solvedBy() { if (!this.siteSettings.show_who_marked_solved) { @@ -68,9 +37,28 @@ export default class SolvedUnacceptAnswerButton extends Component { } } + @action + async unacceptAnswer() { + const post = this.args.post; + + this.saving = true; + try { + await unacceptPost(post); + } finally { + this.saving = false; + } + + this.appEvents.trigger("discourse-solved:solution-toggled", post); + + // TODO (glimmer-post-stream) the Glimmer Post Stream does not listen to this event + post.get("topic.postStream.posts").forEach((p) => { + this.appEvents.trigger("post-stream:refresh", { id: p.id }); + }); + } + } + +async function unacceptPost(post) { + if (!post.can_accept_answer || !post.accepted_answer) { + return; + } + + const topic = post.topic; + + try { + await ajax("/solution/unaccept", { + type: "POST", + data: { id: post.id }, + }); + + topic.setAcceptedSolution(undefined); + } catch (e) { + popupAjaxError(e); + } +} diff --git a/assets/javascripts/discourse/initializers/extend-for-solved-button.gjs b/assets/javascripts/discourse/initializers/extend-for-solved-button.gjs index ca02777..7241a7c 100644 --- a/assets/javascripts/discourse/initializers/extend-for-solved-button.gjs +++ b/assets/javascripts/discourse/initializers/extend-for-solved-button.gjs @@ -11,19 +11,11 @@ import SolvedUnacceptAnswerButton from "../components/solved-unaccept-answer-but function initializeWithApi(api) { customizePost(api); customizePostMenu(api); + handleMessages(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.modifyClass( "model:topic", @@ -31,8 +23,41 @@ function customizePost(api) { class extends Superclass { @tracked accepted_answer; @tracked has_accepted_answer; + + setAcceptedSolution(acceptedAnswer) { + this.postStream?.posts?.forEach((post) => { + if (!acceptedAnswer) { + post.setProperties({ + accepted_answer: false, + topic_accepted_answer: false, + }); + } else if (post.post_number > 1) { + post.setProperties( + acceptedAnswer.post_number === post.post_number + ? { + accepted_answer: true, + topic_accepted_answer: true, + } + : { + accepted_answer: false, + topic_accepted_answer: true, + } + ); + } + }); + + this.accepted_answer = acceptedAnswer; + } } ); +} + +function customizePost(api) { + api.addTrackedPostProperties( + "can_accept_answer", + "accepted_answer", + "topic_accepted_answer" + ); api.renderAfterWrapperOutlet( "post-content-cooked-html", @@ -84,10 +109,10 @@ function customizePostMenu(api) { }) => { let solvedButton; - if (post.can_accept_answer) { - solvedButton = SolvedAcceptAnswerButton; - } else if (post.accepted_answer) { + if (post.accepted_answer) { solvedButton = SolvedUnacceptAnswerButton; + } else if (post.can_accept_answer) { + solvedButton = SolvedAcceptAnswerButton; } solvedButton && @@ -110,19 +135,32 @@ function customizePostMenu(api) { ); } +function handleMessages(api) { + const handleMessages = async (controller, message) => { + const topic = controller.model; + + if (topic) { + topic.setAcceptedSolution(message.accepted_answer); + } + }; + + api.registerCustomPostMessageCallback("accepted_solution", handleMessages); + api.registerCustomPostMessageCallback("unaccepted_solution", handleMessages); +} + export default { name: "extend-for-solved-button", initialize() { - withPluginApi("1.34.0", initializeWithApi); + withPluginApi(initializeWithApi); - withPluginApi("0.8.10", (api) => { + withPluginApi((api) => { api.replaceIcon( "notification.solved.accepted_notification", "square-check" ); }); - withPluginApi("0.11.0", (api) => { + withPluginApi((api) => { api.addAdvancedSearchOptions({ statusOptions: [ { @@ -137,7 +175,7 @@ export default { }); }); - withPluginApi("0.11.7", (api) => { + withPluginApi((api) => { api.addSearchSuggestion("status:solved"); api.addSearchSuggestion("status:unsolved"); }); diff --git a/lib/discourse_solved/guardian_extensions.rb b/lib/discourse_solved/guardian_extensions.rb index 9f96b44..6ea7fec 100644 --- a/lib/discourse_solved/guardian_extensions.rb +++ b/lib/discourse_solved/guardian_extensions.rb @@ -21,7 +21,9 @@ module DiscourseSolved def can_accept_answer?(topic, post) return false if !authenticated? - return false if !topic || topic.private_message? || !post || post.whisper? + if !topic || topic.private_message? || !post || post.post_number <= 1 || post.whisper? + return false + end return false if !allow_accepted_answers?(topic.category_id, topic.tags.map(&:name)) return true if is_staff? diff --git a/lib/discourse_solved/topic_extension.rb b/lib/discourse_solved/topic_extension.rb index cfd6757..e9020a3 100644 --- a/lib/discourse_solved/topic_extension.rb +++ b/lib/discourse_solved/topic_extension.rb @@ -4,4 +4,43 @@ module DiscourseSolved::TopicExtension extend ActiveSupport::Concern prepended { has_one :solved, class_name: "DiscourseSolved::SolvedTopic", dependent: :destroy } + + def accepted_answer_post_info + return nil unless solved + + answer_post = solved.answer_post + + answer_post_user = answer_post.user + accepter = solved.accepter + + excerpt = + if SiteSetting.solved_quote_length > 0 + PrettyText.excerpt( + answer_post.cooked, + SiteSetting.solved_quote_length, + keep_emoji_images: true, + ) + else + nil + end + + accepted_answer = { + post_number: answer_post.post_number, + username: answer_post_user.username, + name: answer_post_user.name, + excerpt:, + } + + if SiteSetting.show_who_marked_solved + accepted_answer[:accepter_name] = accepter.name + accepted_answer[:accepter_username] = accepter.username + end + + if !SiteSetting.enable_names || !SiteSetting.display_name_on_posts + accepted_answer[:name] = nil + accepted_answer[:accepter_name] = nil + end + + accepted_answer + end end diff --git a/lib/discourse_solved/topic_view_serializer_extension.rb b/lib/discourse_solved/topic_view_serializer_extension.rb index cadfb49..d771af8 100644 --- a/lib/discourse_solved/topic_view_serializer_extension.rb +++ b/lib/discourse_solved/topic_view_serializer_extension.rb @@ -11,45 +11,6 @@ module DiscourseSolved::TopicViewSerializerExtension end def accepted_answer - accepted_answer_post_info - end - - private - - def accepted_answer_post_info - solved = object.topic.solved - answer_post = solved.answer_post - answer_post_user = answer_post.user - accepter = solved.accepter - - excerpt = - if SiteSetting.solved_quote_length > 0 - PrettyText.excerpt( - answer_post.cooked, - SiteSetting.solved_quote_length, - keep_emoji_images: true, - ) - else - nil - end - - accepted_answer = { - post_number: answer_post.post_number, - username: answer_post_user.username, - name: answer_post_user.name, - excerpt:, - } - - if SiteSetting.show_who_marked_solved - accepted_answer[:accepter_name] = accepter.name - accepted_answer[:accepter_username] = accepter.username - end - - if !SiteSetting.enable_names || !SiteSetting.display_name_on_posts - accepted_answer[:name] = nil - accepted_answer[:accepter_name] = nil - end - - accepted_answer + object.topic.accepted_answer_post_info end end diff --git a/plugin.rb b/plugin.rb index c37e984..1358fac 100644 --- a/plugin.rb +++ b/plugin.rb @@ -112,7 +112,14 @@ after_initialize do WebHook.enqueue_solved_hooks(:accepted_solution, post, payload) end + accepted_answer = topic.reload.accepted_answer_post_info + + message = { type: :accepted_solution, accepted_answer: } + DiscourseEvent.trigger(:accepted_solution, post) + MessageBus.publish("/topic/#{topic.id}", message) + + accepted_answer end end @@ -140,7 +147,9 @@ after_initialize do payload = WebHook.generate_payload(:post, post) WebHook.enqueue_solved_hooks(:unaccepted_solution, post, payload) end + DiscourseEvent.trigger(:unaccepted_solution, post) + MessageBus.publish("/topic/#{topic.id}", type: :unaccepted_solution) end end @@ -252,9 +261,7 @@ after_initialize do .count end add_to_serializer(:user_summary, :solved_count) { object.solved_count } - add_to_serializer(:post, :can_accept_answer) do - scope.can_accept_answer?(topic, object) && object.post_number > 1 && !accepted_answer - end + add_to_serializer(:post, :can_accept_answer) { scope.can_accept_answer?(topic, object) } add_to_serializer(:post, :can_unaccept_answer) do scope.can_accept_answer?(topic, object) && accepted_answer end