DEV: Added compatibility with the Glimmer Post Menu (#313)

This commit is contained in:
Sérgio Saquetim 2024-11-12 20:45:17 -03:00 committed by GitHub
parent c2f549fd4f
commit 28ae24ffb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 567 additions and 341 deletions

View File

@ -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);
}
<template>
<DButton
class="post-action-menu__solved-unaccepted unaccepted"
...attributes
@action={{this.acceptAnswer}}
@icon="far-check-square"
@label={{if this.showLabel "solved.solution"}}
@title="solved.accept_answer"
/>
</template>
}
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,
});
}
});
}

View File

@ -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);
}
<template>
<span class="extra-buttons">
{{#if @post.can_unaccept_answer}}
<DButton
class="post-action-menu__solved-accepted accepted fade-out"
...attributes
@action={{this.unacceptAnswer}}
@icon="check-square"
@label="solved.solution"
@title="solved.unaccept_answer"
/>
{{else}}
<span
class="accepted-text"
title={{i18n "solved.accepted_description"}}
>
<span>{{dIcon "check"}}</span>
<span class="accepted-label">
{{i18n "solved.solution"}}
</span>
</span>
{{/if}}
</span>
</template>
}
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);
}

View File

@ -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 = `
<aside class='quote accepted-answer' data-post="${
topic.get("accepted_answer").post_number
}" data-topic="${topic.id}">
<div class='title'>
${topic.acceptedAnswerHtml} <div class="quote-controls"><\/div>
</div>
<blockquote>
${topic.accepted_answer.excerpt}
</blockquote>
</aside>`;
const withoutExcerpt = `
<aside class='quote accepted-answer'>
<div class='title title-only'>
${topic.acceptedAnswerHtml}
</div>
</aside>`;
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 = `
<aside class='quote accepted-answer' data-post="${
topic.get("accepted_answer").post_number
}" data-topic="${topic.id}">
<div class='title'>
${topic.acceptedAnswerHtml} <div class="quote-controls"><\/div>
</div>
<blockquote>
${topic.accepted_answer.excerpt}
</blockquote>
</aside>`;
const withoutExcerpt = `
<aside class='quote accepted-answer'>
<div class='title title-only'>
${topic.acceptedAnswerHtml}
</div>
</aside>`;
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(

View File

@ -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 {

View File

@ -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");
});
}
);

View File

@ -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: "<p>How do I declare a variable in ruby?</p>",
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:
"<p>this is a long answer that potentially solves the question</p>",
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")

View File

@ -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");
});
}
);

View File

@ -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: "<p>How do I declare a variable in ruby?</p>",
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:
"<p>this is a long answer that potentially solves the question</p>",
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 },
};
};