From 28ae24ffb8c80c0c6f1266bfa169c5b39c3b3d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Saquetim?= <1108771+megothss@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:45:17 -0300 Subject: [PATCH] DEV: Added compatibility with the Glimmer Post Menu (#313) --- .../solved-accept-answer-button.gjs | 85 +++++++ .../solved-unaccept-answer-button.gjs | 74 ++++++ .../initializers/extend-for-solved-button.js | 225 ++++++++---------- assets/stylesheets/solutions.scss | 9 - .../discourse-solved-post-menu-test.js | 52 ++++ .../acceptance/discourse-solved-test.js | 211 +--------------- .../discourse-solved-widget-post-menu-test.js | 48 ++++ .../helpers/discourse-solved-helpers.js | 204 ++++++++++++++++ 8 files changed, 567 insertions(+), 341 deletions(-) create mode 100644 assets/javascripts/discourse/components/solved-accept-answer-button.gjs create mode 100644 assets/javascripts/discourse/components/solved-unaccept-answer-button.gjs create mode 100644 test/javascripts/acceptance/discourse-solved-post-menu-test.js create mode 100644 test/javascripts/acceptance/discourse-solved-widget-post-menu-test.js create mode 100644 test/javascripts/helpers/discourse-solved-helpers.js diff --git a/assets/javascripts/discourse/components/solved-accept-answer-button.gjs b/assets/javascripts/discourse/components/solved-accept-answer-button.gjs new file mode 100644 index 0000000..5424a28 --- /dev/null +++ b/assets/javascripts/discourse/components/solved-accept-answer-button.gjs @@ -0,0 +1,85 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import DButton from "discourse/components/d-button"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +export default class SolvedAcceptAnswerButton extends Component { + static hidden(args) { + return args.post.topic_accepted_answer; + } + + @service appEvents; + @service currentUser; + + get showLabel() { + return this.currentUser?.id === this.args.post.topicCreatedById; + } + + @action + acceptAnswer() { + acceptAnswer(this.args.post, this.appEvents); + } + + +} + +export function acceptAnswer(post, appEvents) { + // TODO (glimmer-post-menu): Remove this exported function and move the code into the button action after the widget code is removed + acceptPost(post); + + appEvents.trigger("discourse-solved:solution-toggled", post); + + post.get("topic.postStream.posts").forEach((p) => { + p.set("topic_accepted_answer", true); + appEvents.trigger("post-stream:refresh", { id: p.id }); + }); +} + +function acceptPost(post) { + const topic = post.topic; + + clearAccepted(topic); + + 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, + }); + + 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, + }); + } + }); +} diff --git a/assets/javascripts/discourse/components/solved-unaccept-answer-button.gjs b/assets/javascripts/discourse/components/solved-unaccept-answer-button.gjs new file mode 100644 index 0000000..35c8eae --- /dev/null +++ b/assets/javascripts/discourse/components/solved-unaccept-answer-button.gjs @@ -0,0 +1,74 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import DButton from "discourse/components/d-button"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import dIcon from "discourse-common/helpers/d-icon"; +import i18n from "discourse-common/helpers/i18n"; + +export default class SolvedUnacceptAnswerButton extends Component { + @service appEvents; + + @action + unacceptAnswer() { + unacceptAnswer(this.args.post, this.appEvents); + } + + +} + +export function unacceptAnswer(post, appEvents) { + // TODO (glimmer-post-menu): Remove this exported function and move the code into the button action after the widget code is removed + unacceptPost(post); + + appEvents.trigger("discourse-solved:solution-toggled", post); + + post.get("topic.postStream.posts").forEach((p) => { + p.set("topic_accepted_answer", false); + appEvents.trigger("post-stream:refresh", { id: p.id }); + }); +} + +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.set("accepted_answer", undefined); + + ajax("/solution/unaccept", { + type: "POST", + data: { id: post.id }, + }).catch(popupAjaxError); +} diff --git a/assets/javascripts/discourse/initializers/extend-for-solved-button.js b/assets/javascripts/discourse/initializers/extend-for-solved-button.js index ad6afa1..0a711d3 100644 --- a/assets/javascripts/discourse/initializers/extend-for-solved-button.js +++ b/assets/javascripts/discourse/initializers/extend-for-solved-button.js @@ -1,75 +1,23 @@ import { computed } from "@ember/object"; import TopicStatusIcons from "discourse/helpers/topic-status-icons"; -import { ajax } from "discourse/lib/ajax"; -import { popupAjaxError } from "discourse/lib/ajax-error"; 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 TopicStatus from "discourse/raw-views/topic-status"; import PostCooked from "discourse/widgets/post-cooked"; +import { withSilencedDeprecations } from "discourse-common/lib/deprecated"; import { iconHTML, iconNode } from "discourse-common/lib/icon-library"; import I18n from "I18n"; - -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, - }); - } - }); -} - -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.set("accepted_answer", undefined); - - ajax("/solution/unaccept", { - type: "POST", - data: { id: post.id }, - }).catch(popupAjaxError); -} - -function acceptPost(post) { - const topic = post.topic; - - clearAccepted(topic); - - 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, - }); - - ajax("/solution/accept", { - type: "POST", - data: { id: post.id }, - }).catch(popupAjaxError); -} +import SolvedAcceptAnswerButton, { + acceptAnswer, +} from "../components/solved-accept-answer-button"; +import SolvedUnacceptAnswerButton, { + unacceptAnswer, +} from "../components/solved-unaccept-answer-button"; function initializeWithApi(api) { - const currentUser = api.getCurrentUser(); + customizePostMenu(api); TopicStatusIcons.addObject([ "has_accepted_answer", @@ -88,6 +36,100 @@ function initializeWithApi(api) { api.addDiscoveryQueryParam("solved", { replace: true, refreshModel: true }); } + api.decorateWidget("post-contents:after-cooked", (dec) => { + if (dec.attrs.post_number === 1) { + const postModel = dec.getModel(); + if (postModel) { + const topic = postModel.topic; + if (topic.accepted_answer) { + const hasExcerpt = !!topic.accepted_answer.excerpt; + + const withExcerpt = ` + `; + + const withoutExcerpt = ` + `; + + const cooked = new PostCooked( + { cooked: hasExcerpt ? withExcerpt : withoutExcerpt }, + dec + ); + return dec.rawHtml(cooked.init()); + } + } + } + }); + + api.attachWidgetAction("post", "acceptAnswer", function () { + acceptAnswer(this.model, this.appEvents); + }); + + api.attachWidgetAction("post", "unacceptAnswer", function () { + unacceptAnswer(this.model, this.appEvents); + }); +} + +function customizePostMenu(api) { + const transformerRegistered = 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, + ], + } + ); + } + ); + + const silencedKey = + transformerRegistered && "discourse.post-menu-widget-overrides"; + + withSilencedDeprecations(silencedKey, () => customizeWidgetPostMenu(api)); +} + +function customizeWidgetPostMenu(api) { + const currentUser = api.getCurrentUser(); + api.addPostMenuButton("solved", (attrs) => { if (attrs.can_accept_answer) { const isOp = currentUser?.id === attrs.topicCreatedById; @@ -131,67 +173,6 @@ function initializeWithApi(api) { } } }); - - api.decorateWidget("post-contents:after-cooked", (dec) => { - if (dec.attrs.post_number === 1) { - const postModel = dec.getModel(); - if (postModel) { - const topic = postModel.topic; - if (topic.accepted_answer) { - const hasExcerpt = !!topic.accepted_answer.excerpt; - - const withExcerpt = ` - `; - - const withoutExcerpt = ` - `; - - const cooked = new PostCooked( - { cooked: hasExcerpt ? withExcerpt : withoutExcerpt }, - dec - ); - return dec.rawHtml(cooked.init()); - } - } - } - }); - - api.attachWidgetAction("post", "acceptAnswer", function () { - const post = this.model; - acceptPost(post); - - this.appEvents.trigger("discourse-solved:solution-toggled", post); - - post.get("topic.postStream.posts").forEach((p) => { - p.set("topic_accepted_answer", true); - this.appEvents.trigger("post-stream:refresh", { id: p.id }); - }); - }); - - api.attachWidgetAction("post", "unacceptAnswer", function () { - const post = this.model; - unacceptPost(post); - - this.appEvents.trigger("discourse-solved:solution-toggled", post); - - post.get("topic.postStream.posts").forEach((p) => { - p.set("topic_accepted_answer", false); - this.appEvents.trigger("post-stream:refresh", { id: p.id }); - }); - }); } export default { @@ -252,7 +233,7 @@ export default { }), }); - withPluginApi("0.1", initializeWithApi); + withPluginApi("1.34.0", initializeWithApi); withPluginApi("0.8.10", (api) => { api.replaceIcon( diff --git a/assets/stylesheets/solutions.scss b/assets/stylesheets/solutions.scss index 11d183f..b4d0ef0 100644 --- a/assets/stylesheets/solutions.scss +++ b/assets/stylesheets/solutions.scss @@ -14,15 +14,6 @@ $solved-color: green; color: $solved-color; } -.post-controls { - .accepted, - .unaccepted { - .d-button-label { - margin-left: 7px; - } - } -} - .post-controls .extra-buttons { // anon text .accepted-text { diff --git a/test/javascripts/acceptance/discourse-solved-post-menu-test.js b/test/javascripts/acceptance/discourse-solved-post-menu-test.js new file mode 100644 index 0000000..60df5ba --- /dev/null +++ b/test/javascripts/acceptance/discourse-solved-post-menu-test.js @@ -0,0 +1,52 @@ +import { click, visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import { acceptance } from "discourse/tests/helpers/qunit-helpers"; +import I18n from "I18n"; +import { postStreamWithAcceptedAnswerExcerpt } from "../helpers/discourse-solved-helpers"; + +acceptance( + "Discourse Solved | Post Menu | Accept and Unaccept", + function (needs) { + needs.user({ + admin: true, + }); + + needs.settings({ + glimmer_post_menu_mode: "enabled", + solved_enabled: true, + allow_solved_on_all_topics: true, + }); + + needs.pretender((server, helper) => { + server.post("/solution/accept", () => helper.response({ success: "OK" })); + server.post("/solution/unaccept", () => + helper.response({ success: "OK" }) + ); + + server.get("/t/12.json", () => { + return helper.response(postStreamWithAcceptedAnswerExcerpt(null)); + }); + }); + + test("accepting and unaccepting a post works", async function (assert) { + await visit("/t/without-excerpt/12"); + + assert + .dom("#post_2 .post-action-menu__solved-accepted") + .exists("Unaccept button is visible") + .hasText(I18n.t("solved.solution"), "Unaccept button has correct text"); + + await click("#post_2 .post-action-menu__solved-accepted"); + + assert + .dom("#post_2 .post-action-menu__solved-unaccepted") + .exists("Accept button is visible"); + + await click("#post_2 .post-action-menu__solved-unaccepted"); + + assert + .dom("#post_2 .post-action-menu__solved-accepted") + .exists("Unccept button is visible again"); + }); + } +); diff --git a/test/javascripts/acceptance/discourse-solved-test.js b/test/javascripts/acceptance/discourse-solved-test.js index 0a9e5ea..8609580 100644 --- a/test/javascripts/acceptance/discourse-solved-test.js +++ b/test/javascripts/acceptance/discourse-solved-test.js @@ -3,221 +3,12 @@ import { test } from "qunit"; import { fixturesByUrl } from "discourse/tests/helpers/create-pretender"; import { acceptance, queryAll } from "discourse/tests/helpers/qunit-helpers"; import { cloneJSON } from "discourse-common/lib/object"; +import { postStreamWithAcceptedAnswerExcerpt } from "../helpers/discourse-solved-helpers"; acceptance("Discourse Solved Plugin", function (needs) { needs.user(); needs.pretender((server, helper) => { - const postStreamWithAcceptedAnswerExcerpt = (excerpt) => { - return { - post_stream: { - posts: [ - { - id: 21, - name: null, - username: "kzh", - avatar_template: - "/letter_avatar_proxy/v2/letter/k/ac91a4/{size}.png", - created_at: "2017-08-08T20:11:32.542Z", - cooked: "

How do I declare a variable in ruby?

", - post_number: 1, - post_type: 1, - updated_at: "2017-08-08T21:03:30.521Z", - reply_count: 0, - reply_to_post_number: null, - quote_count: 0, - avg_time: null, - incoming_link_count: 0, - reads: 1, - score: 0, - yours: true, - topic_id: 23, - topic_slug: "test-solved", - display_username: null, - primary_group_name: null, - primary_group_flair_url: null, - primary_group_flair_bg_color: null, - primary_group_flair_color: null, - version: 2, - can_edit: true, - can_delete: false, - can_recover: null, - can_wiki: true, - read: true, - user_title: null, - actions_summary: [ - { id: 3, can_act: true }, - { id: 4, can_act: true }, - { id: 5, hidden: true, can_act: true }, - { id: 7, can_act: true }, - { id: 8, can_act: true }, - ], - moderator: false, - admin: true, - staff: true, - user_id: 1, - hidden: false, - hidden_reason_id: null, - trust_level: 4, - deleted_at: null, - user_deleted: false, - edit_reason: null, - can_view_edit_history: true, - wiki: false, - can_accept_answer: false, - can_unaccept_answer: false, - accepted_answer: false, - }, - { - id: 22, - name: null, - username: "kzh", - avatar_template: - "/letter_avatar_proxy/v2/letter/k/ac91a4/{size}.png", - created_at: "2017-08-08T20:12:04.657Z", - cooked: - "

this is a long answer that potentially solves the question

", - post_number: 2, - post_type: 1, - updated_at: "2017-08-08T21:20:24.417Z", - reply_count: 0, - reply_to_post_number: null, - quote_count: 0, - avg_time: null, - incoming_link_count: 0, - reads: 1, - score: 0, - yours: true, - topic_id: 23, - topic_slug: "test-solved", - display_username: null, - primary_group_name: null, - primary_group_flair_url: null, - primary_group_flair_bg_color: null, - primary_group_flair_color: null, - version: 2, - can_edit: true, - can_delete: true, - can_recover: null, - can_wiki: true, - read: true, - user_title: null, - actions_summary: [ - { id: 3, can_act: true }, - { id: 4, can_act: true }, - { id: 5, hidden: true, can_act: true }, - { id: 7, can_act: true }, - { id: 8, can_act: true }, - ], - moderator: false, - admin: true, - staff: true, - user_id: 1, - hidden: false, - hidden_reason_id: null, - trust_level: 4, - deleted_at: null, - user_deleted: false, - edit_reason: null, - can_view_edit_history: true, - wiki: false, - can_accept_answer: false, - can_unaccept_answer: true, - accepted_answer: true, - }, - ], - stream: [21, 22], - }, - timeline_lookup: [[1, 0]], - id: 23, - title: "Test solved", - fancy_title: "Test solved", - posts_count: 2, - created_at: "2017-08-08T20:11:32.098Z", - views: 6, - reply_count: 0, - participant_count: 1, - like_count: 0, - last_posted_at: "2017-08-08T20:12:04.657Z", - visible: true, - closed: false, - archived: false, - has_summary: false, - archetype: "regular", - slug: "test-solved", - category_id: 1, - word_count: 18, - deleted_at: null, - pending_posts_count: 0, - user_id: 1, - pm_with_non_human_user: false, - draft: null, - draft_key: "topic_23", - draft_sequence: 6, - posted: true, - unpinned: null, - pinned_globally: false, - pinned: false, - pinned_at: null, - pinned_until: null, - details: { - created_by: { - id: 1, - username: "kzh", - avatar_template: - "/letter_avatar_proxy/v2/letter/k/ac91a4/{size}.png", - }, - last_poster: { - id: 1, - username: "kzh", - avatar_template: - "/letter_avatar_proxy/v2/letter/k/ac91a4/{size}.png", - }, - participants: [ - { - id: 1, - username: "kzh", - avatar_template: - "/letter_avatar_proxy/v2/letter/k/ac91a4/{size}.png", - post_count: 2, - primary_group_name: null, - primary_group_flair_url: null, - primary_group_flair_color: null, - primary_group_flair_bg_color: null, - }, - ], - notification_level: 3, - notifications_reason_id: 1, - can_move_posts: true, - can_edit: true, - can_delete: true, - can_remove_allowed_users: true, - can_invite_to: true, - can_invite_via_email: true, - can_create_post: true, - can_reply_as_new_topic: true, - can_flag_topic: true, - }, - highest_post_number: 2, - last_read_post_number: 2, - last_read_post_id: 22, - deleted_by: null, - has_deleted: false, - actions_summary: [ - { id: 4, count: 0, hidden: false, can_act: true }, - { id: 7, count: 0, hidden: false, can_act: true }, - { id: 8, count: 0, hidden: false, can_act: true }, - ], - chunk_size: 20, - bookmarked: false, - tags: [], - featured_link: null, - topic_timer: null, - message_bus_last_id: 0, - accepted_answer: { post_number: 2, username: "kzh", excerpt }, - }; - }; - server.get("/t/11.json", () => { return helper.response( postStreamWithAcceptedAnswerExcerpt("this is an excerpt") diff --git a/test/javascripts/acceptance/discourse-solved-widget-post-menu-test.js b/test/javascripts/acceptance/discourse-solved-widget-post-menu-test.js new file mode 100644 index 0000000..e390d2f --- /dev/null +++ b/test/javascripts/acceptance/discourse-solved-widget-post-menu-test.js @@ -0,0 +1,48 @@ +import { click, visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import { acceptance } from "discourse/tests/helpers/qunit-helpers"; +import I18n from "I18n"; +import { postStreamWithAcceptedAnswerExcerpt } from "../helpers/discourse-solved-helpers"; + +acceptance( + "Discourse Solved | Widget Post Menu |Accept and Unaccept", + function (needs) { + needs.user({ + admin: true, + }); + + needs.settings({ + glimmer_post_menu_mode: "disabled", + solved_enabled: true, + allow_solved_on_all_topics: true, + }); + + needs.pretender((server, helper) => { + server.post("/solution/accept", () => helper.response({ success: "OK" })); + server.post("/solution/unaccept", () => + helper.response({ success: "OK" }) + ); + + server.get("/t/12.json", () => { + return helper.response(postStreamWithAcceptedAnswerExcerpt(null)); + }); + }); + + test("accepting and unaccepting a post works", async function (assert) { + await visit("/t/without-excerpt/12"); + + assert + .dom("#post_2 .accepted") + .exists("Unaccept button is visible") + .hasText(I18n.t("solved.solution"), "Unaccept button has correct text"); + + await click("#post_2 .accepted"); + + assert.dom("#post_2 .unaccepted").exists("Accept button is visible"); + + await click("#post_2 .unaccepted"); + + assert.dom("#post_2 .accepted").exists("Unccept button is visible again"); + }); + } +); diff --git a/test/javascripts/helpers/discourse-solved-helpers.js b/test/javascripts/helpers/discourse-solved-helpers.js new file mode 100644 index 0000000..2ee351e --- /dev/null +++ b/test/javascripts/helpers/discourse-solved-helpers.js @@ -0,0 +1,204 @@ +export const postStreamWithAcceptedAnswerExcerpt = (excerpt) => { + return { + post_stream: { + posts: [ + { + id: 21, + name: null, + username: "kzh", + avatar_template: "/letter_avatar_proxy/v2/letter/k/ac91a4/{size}.png", + created_at: "2017-08-08T20:11:32.542Z", + cooked: "

How do I declare a variable in ruby?

", + post_number: 1, + post_type: 1, + updated_at: "2017-08-08T21:03:30.521Z", + reply_count: 0, + reply_to_post_number: null, + quote_count: 0, + avg_time: null, + incoming_link_count: 0, + reads: 1, + score: 0, + yours: true, + topic_id: 23, + topic_slug: "test-solved", + display_username: null, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_bg_color: null, + primary_group_flair_color: null, + version: 2, + can_edit: true, + can_delete: false, + can_recover: null, + can_wiki: true, + read: true, + user_title: null, + actions_summary: [ + { id: 3, can_act: true }, + { id: 4, can_act: true }, + { id: 5, hidden: true, can_act: true }, + { id: 7, can_act: true }, + { id: 8, can_act: true }, + ], + moderator: false, + admin: true, + staff: true, + user_id: 1, + hidden: false, + hidden_reason_id: null, + trust_level: 4, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false, + can_accept_answer: false, + can_unaccept_answer: false, + accepted_answer: false, + }, + { + id: 22, + name: null, + username: "kzh", + avatar_template: "/letter_avatar_proxy/v2/letter/k/ac91a4/{size}.png", + created_at: "2017-08-08T20:12:04.657Z", + cooked: + "

this is a long answer that potentially solves the question

", + post_number: 2, + post_type: 1, + updated_at: "2017-08-08T21:20:24.417Z", + reply_count: 0, + reply_to_post_number: null, + quote_count: 0, + avg_time: null, + incoming_link_count: 0, + reads: 1, + score: 0, + yours: true, + topic_id: 23, + topic_slug: "test-solved", + display_username: null, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_bg_color: null, + primary_group_flair_color: null, + version: 2, + can_edit: true, + can_delete: true, + can_recover: null, + can_wiki: true, + read: true, + user_title: null, + actions_summary: [ + { id: 3, can_act: true }, + { id: 4, can_act: true }, + { id: 5, hidden: true, can_act: true }, + { id: 7, can_act: true }, + { id: 8, can_act: true }, + ], + moderator: false, + admin: true, + staff: true, + user_id: 1, + hidden: false, + hidden_reason_id: null, + trust_level: 4, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false, + can_accept_answer: false, + can_unaccept_answer: true, + accepted_answer: true, + }, + ], + stream: [21, 22], + }, + timeline_lookup: [[1, 0]], + id: 23, + title: "Test solved", + fancy_title: "Test solved", + posts_count: 2, + created_at: "2017-08-08T20:11:32.098Z", + views: 6, + reply_count: 0, + participant_count: 1, + like_count: 0, + last_posted_at: "2017-08-08T20:12:04.657Z", + visible: true, + closed: false, + archived: false, + has_summary: false, + archetype: "regular", + slug: "test-solved", + category_id: 1, + word_count: 18, + deleted_at: null, + pending_posts_count: 0, + user_id: 1, + pm_with_non_human_user: false, + draft: null, + draft_key: "topic_23", + draft_sequence: 6, + posted: true, + unpinned: null, + pinned_globally: false, + pinned: false, + pinned_at: null, + pinned_until: null, + details: { + created_by: { + id: 1, + username: "kzh", + avatar_template: "/letter_avatar_proxy/v2/letter/k/ac91a4/{size}.png", + }, + last_poster: { + id: 1, + username: "kzh", + avatar_template: "/letter_avatar_proxy/v2/letter/k/ac91a4/{size}.png", + }, + participants: [ + { + id: 1, + username: "kzh", + avatar_template: "/letter_avatar_proxy/v2/letter/k/ac91a4/{size}.png", + post_count: 2, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_color: null, + primary_group_flair_bg_color: null, + }, + ], + notification_level: 3, + notifications_reason_id: 1, + can_move_posts: true, + can_edit: true, + can_delete: true, + can_remove_allowed_users: true, + can_invite_to: true, + can_invite_via_email: true, + can_create_post: true, + can_reply_as_new_topic: true, + can_flag_topic: true, + }, + highest_post_number: 2, + last_read_post_number: 2, + last_read_post_id: 22, + deleted_by: null, + has_deleted: false, + actions_summary: [ + { id: 4, count: 0, hidden: false, can_act: true }, + { id: 7, count: 0, hidden: false, can_act: true }, + { id: 8, count: 0, hidden: false, can_act: true }, + ], + chunk_size: 20, + bookmarked: false, + tags: [], + featured_link: null, + topic_timer: null, + message_bus_last_id: 0, + accepted_answer: { post_number: 2, username: "kzh", excerpt }, + }; +};