parent
2eb9c0f3dd
commit
adda53c462
|
@ -6,17 +6,11 @@ import DiscourseURL from "discourse/lib/url";
|
|||
import MobileScrollDirection from "discourse/mixins/mobile-scroll-direction";
|
||||
import Scrolling from "discourse/mixins/scrolling";
|
||||
import { alias } from "@ember/object/computed";
|
||||
import { highlightPost } from "discourse/lib/utilities";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
|
||||
const MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE = 300;
|
||||
|
||||
function highlight(postNumber) {
|
||||
const $contents = $(`#post_${postNumber} .topic-body`);
|
||||
|
||||
$contents.addClass("highlighted");
|
||||
$contents.on("animationend", () => $contents.removeClass("highlighted"));
|
||||
}
|
||||
|
||||
export default Component.extend(
|
||||
AddArchetypeClass,
|
||||
Scrolling,
|
||||
|
@ -58,7 +52,7 @@ export default Component.extend(
|
|||
},
|
||||
|
||||
_highlightPost(postNumber) {
|
||||
scheduleOnce("afterRender", null, highlight, postNumber);
|
||||
scheduleOnce("afterRender", null, highlightPost, postNumber);
|
||||
},
|
||||
|
||||
_hideTopicInHeader() {
|
||||
|
|
|
@ -45,7 +45,8 @@ export default MountWidget.extend({
|
|||
"selectedQuery",
|
||||
"selectedPostsCount",
|
||||
"searchService",
|
||||
"showReadIndicator"
|
||||
"showReadIndicator",
|
||||
"streamFilters"
|
||||
);
|
||||
},
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
|
|||
usernameClass: (username) => (username ? `user-card-${username}` : ""),
|
||||
|
||||
@discourseComputed("username", "topicPostCount")
|
||||
togglePostsLabel(username, count) {
|
||||
filterPostsLabel(username, count) {
|
||||
return I18n.t("topic.filter_to", { username, count });
|
||||
},
|
||||
|
||||
|
@ -210,8 +210,8 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
|
|||
this._close();
|
||||
},
|
||||
|
||||
togglePosts() {
|
||||
this.togglePosts(this.user);
|
||||
filterPosts() {
|
||||
this.filterPosts(this.user);
|
||||
this._close();
|
||||
},
|
||||
|
||||
|
|
|
@ -421,14 +421,31 @@ export default Controller.extend(bufferedProperty("model"), {
|
|||
}
|
||||
},
|
||||
|
||||
toggleSummary() {
|
||||
showSummary() {
|
||||
return this.get("model.postStream")
|
||||
.toggleSummary()
|
||||
.showSummary()
|
||||
.then(() => {
|
||||
this.updateQueryParams();
|
||||
});
|
||||
},
|
||||
|
||||
cancelFilter(previousFilters) {
|
||||
this.get("model.postStream").cancelFilter();
|
||||
this.get("model.postStream")
|
||||
.refresh()
|
||||
.then(() => {
|
||||
if (previousFilters) {
|
||||
if (previousFilters.replies_to_post_number) {
|
||||
this._jumpToPostNumber(previousFilters.replies_to_post_number);
|
||||
}
|
||||
if (previousFilters.filter_upwards_post_id) {
|
||||
this._jumpToPostId(previousFilters.filter_upwards_post_id);
|
||||
}
|
||||
}
|
||||
this.updateQueryParams();
|
||||
});
|
||||
},
|
||||
|
||||
removeAllowedUser(user) {
|
||||
return this.get("model.details")
|
||||
.removeAllowedUser(user)
|
||||
|
@ -867,9 +884,9 @@ export default Controller.extend(bufferedProperty("model"), {
|
|||
});
|
||||
},
|
||||
|
||||
toggleParticipant(user) {
|
||||
filterParticipant(user) {
|
||||
this.get("model.postStream")
|
||||
.toggleParticipant(user.get("username"))
|
||||
.filterParticipant(user.username)
|
||||
.then(() => this.updateQueryParams);
|
||||
},
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ export default Controller.extend({
|
|||
topic: controller(),
|
||||
|
||||
actions: {
|
||||
togglePosts(user) {
|
||||
filterPosts(user) {
|
||||
const topicController = this.topic;
|
||||
topicController.send("toggleParticipant", user);
|
||||
topicController.send("filterParticipant", user);
|
||||
},
|
||||
|
||||
showUser(user) {
|
||||
|
|
|
@ -41,7 +41,14 @@ export default class LockOn {
|
|||
}
|
||||
|
||||
const { top } = element.getBoundingClientRect();
|
||||
const offset = top + window.scrollY;
|
||||
let offset = top + window.scrollY;
|
||||
if (this.options.originalTopOffset) {
|
||||
// if element's original top offset is in the bottom half of the viewport
|
||||
// jump to it, otherwise respect the offset
|
||||
if (window.innerHeight / 2.25 > this.options.originalTopOffset) {
|
||||
return offset - this.options.originalTopOffset;
|
||||
}
|
||||
}
|
||||
|
||||
return offset - minimumOffset();
|
||||
}
|
||||
|
@ -118,6 +125,11 @@ export default class LockOn {
|
|||
this.previousTop = top;
|
||||
}
|
||||
|
||||
// Stop early when maintaining the original offset
|
||||
if (this.options.originalTopOffset) {
|
||||
return this.clearLock();
|
||||
}
|
||||
|
||||
// Stop after a little while
|
||||
if (Date.now() - this.startedAt > LOCK_DURATION_MS) {
|
||||
return this.clearLock();
|
||||
|
|
|
@ -101,6 +101,10 @@ export default function transformPost(
|
|||
const postTypes = site.post_types;
|
||||
const topic = post.topic;
|
||||
const details = topic.get("details");
|
||||
const filteredUpwardsPostID = topic.get("postStream.filterUpwardsPostID");
|
||||
const filteredRepliesPostNumber = topic.get(
|
||||
"postStream.filterRepliesToPostNumber"
|
||||
);
|
||||
|
||||
const postAtts = transformBasicPost(post);
|
||||
|
||||
|
@ -131,9 +135,13 @@ export default function transformPost(
|
|||
postAtts.isWarning = topic.is_warning;
|
||||
postAtts.links = post.get("internalLinks");
|
||||
postAtts.replyDirectlyBelow =
|
||||
nextPost && nextPost.reply_to_post_number === post.post_number;
|
||||
nextPost &&
|
||||
nextPost.reply_to_post_number === post.post_number &&
|
||||
post.post_number !== filteredRepliesPostNumber;
|
||||
postAtts.replyDirectlyAbove =
|
||||
prevPost && post.reply_to_post_number === prevPost.post_number;
|
||||
prevPost &&
|
||||
post.id !== filteredUpwardsPostID &&
|
||||
post.reply_to_post_number === prevPost.post_number;
|
||||
postAtts.linkCounts = post.link_counts;
|
||||
postAtts.actionCode = post.action_code;
|
||||
postAtts.actionCodeWho = post.action_code_who;
|
||||
|
|
|
@ -146,6 +146,7 @@ const DiscourseURL = EmberObject.extend({
|
|||
}
|
||||
|
||||
lockon = new LockOn(selector, {
|
||||
originalTopOffset: opts.originalTopOffset,
|
||||
finished() {
|
||||
_transitioning = false;
|
||||
lockon = null;
|
||||
|
|
|
@ -104,6 +104,25 @@ export function postUrl(slug, topicId, postNumber) {
|
|||
return url;
|
||||
}
|
||||
|
||||
export function highlightPost(postNumber) {
|
||||
const container = document.querySelector(`#post_${postNumber}`);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const element = container.querySelector(".topic-body");
|
||||
if (!element || element.classList.contains("highlighted")) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.classList.add("highlighted");
|
||||
|
||||
const removeHighlighted = function () {
|
||||
element.classList.remove("highlighted");
|
||||
element.removeEventListener("animationend", removeHighlighted);
|
||||
};
|
||||
element.addEventListener("animationend", removeHighlighted);
|
||||
}
|
||||
|
||||
export function emailValid(email) {
|
||||
// see: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
|
||||
const re = /^[a-zA-Z0-9!#$%&'*+\/=?\^_`{|}~\-]+(?:\.[a-zA-Z0-9!#$%&'\*+\/=?\^_`{|}~\-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/;
|
||||
|
|
|
@ -10,8 +10,10 @@ import { deepMerge } from "discourse-common/lib/object";
|
|||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { get } from "@ember/object";
|
||||
import { highlightPost } from "discourse/lib/utilities";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { loadTopicView } from "discourse/models/topic";
|
||||
import { schedule } from "@ember/runloop";
|
||||
|
||||
export default RestModel.extend({
|
||||
_identityMap: null,
|
||||
|
@ -27,6 +29,8 @@ export default RestModel.extend({
|
|||
stagingPost: null,
|
||||
postsWithPlaceholders: null,
|
||||
timelineLookup: null,
|
||||
filterRepliesToPostNumber: null,
|
||||
filterUpwardsPostID: null,
|
||||
|
||||
init() {
|
||||
this._identityMap = {};
|
||||
|
@ -42,6 +46,8 @@ export default RestModel.extend({
|
|||
stream: [],
|
||||
userFilters: [],
|
||||
summary: false,
|
||||
filterRepliesToPostNumber: false,
|
||||
filterUpwardsPostID: false,
|
||||
loaded: false,
|
||||
loadingAbove: false,
|
||||
loadingBelow: false,
|
||||
|
@ -117,10 +123,16 @@ export default RestModel.extend({
|
|||
Returns a JS Object of current stream filter options. It should match the query
|
||||
params for the stream.
|
||||
**/
|
||||
@discourseComputed("summary", "userFilters.[]")
|
||||
streamFilters(summary) {
|
||||
@discourseComputed(
|
||||
"summary",
|
||||
"userFilters.[]",
|
||||
"filterRepliesToPostNumber",
|
||||
"filterUpwardsPostID"
|
||||
)
|
||||
streamFilters() {
|
||||
const result = {};
|
||||
if (summary) {
|
||||
|
||||
if (this.summary) {
|
||||
result.filter = "summary";
|
||||
}
|
||||
|
||||
|
@ -129,6 +141,14 @@ export default RestModel.extend({
|
|||
result.username_filters = userFilters.join(",");
|
||||
}
|
||||
|
||||
if (this.filterRepliesToPostNumber) {
|
||||
result.replies_to_post_number = this.filterRepliesToPostNumber;
|
||||
}
|
||||
|
||||
if (this.filterUpwardsPostID) {
|
||||
result.filter_upwards_post_id = this.filterUpwardsPostID;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
|
@ -200,49 +220,75 @@ export default RestModel.extend({
|
|||
},
|
||||
|
||||
cancelFilter() {
|
||||
this.set("summary", false);
|
||||
this.userFilters.clear();
|
||||
this.setProperties({
|
||||
userFilters: [],
|
||||
summary: false,
|
||||
filterRepliesToPostNumber: false,
|
||||
filterUpwardsPostID: false,
|
||||
mixedHiddenPosts: false,
|
||||
});
|
||||
},
|
||||
|
||||
toggleSummary() {
|
||||
this.userFilters.clear();
|
||||
this.toggleProperty("summary");
|
||||
const opts = {};
|
||||
|
||||
if (!this.summary) {
|
||||
opts.filter = "none";
|
||||
}
|
||||
|
||||
return this.refresh(opts).then(() => {
|
||||
if (this.summary) {
|
||||
this.jumpToSecondVisible();
|
||||
refreshAndJumptoSecondVisible() {
|
||||
return this.refresh({}).then(() => {
|
||||
if (this.posts && this.posts.length > 1) {
|
||||
DiscourseURL.jumpToPost(this.posts[1].get("post_number"));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
jumpToSecondVisible() {
|
||||
const posts = this.posts;
|
||||
if (posts.length > 1) {
|
||||
const secondPostNum = posts[1].get("post_number");
|
||||
DiscourseURL.jumpToPost(secondPostNum);
|
||||
}
|
||||
showSummary() {
|
||||
this.cancelFilter();
|
||||
this.set("summary", true);
|
||||
return this.refreshAndJumptoSecondVisible();
|
||||
},
|
||||
|
||||
// Filter the stream to a particular user.
|
||||
toggleParticipant(username) {
|
||||
const userFilters = this.userFilters;
|
||||
this.set("summary", false);
|
||||
filterParticipant(username) {
|
||||
this.cancelFilter();
|
||||
this.userFilters.addObject(username);
|
||||
return this.refreshAndJumptoSecondVisible();
|
||||
},
|
||||
|
||||
let jump = false;
|
||||
if (userFilters.includes(username)) {
|
||||
userFilters.removeObject(username);
|
||||
} else {
|
||||
userFilters.addObject(username);
|
||||
jump = true;
|
||||
}
|
||||
return this.refresh().then(() => {
|
||||
if (jump) {
|
||||
this.jumpToSecondVisible();
|
||||
filterReplies(postNumber) {
|
||||
this.cancelFilter();
|
||||
this.set("filterRepliesToPostNumber", postNumber);
|
||||
return this.refresh({ refreshInPlace: true }).then(() => {
|
||||
const element = document.querySelector(`#post_${postNumber}`);
|
||||
|
||||
// order is important, we need to get the offset before triggering a refresh
|
||||
const originalTopOffset = element
|
||||
? element.getBoundingClientRect().top
|
||||
: null;
|
||||
|
||||
this.appEvents.trigger("post-stream:refresh");
|
||||
DiscourseURL.jumpToPost(postNumber, {
|
||||
originalTopOffset,
|
||||
});
|
||||
|
||||
const replyPostNumbers = this.posts.mapBy("post_number");
|
||||
replyPostNumbers.splice(0, 2);
|
||||
schedule("afterRender", () => {
|
||||
replyPostNumbers.forEach((postNum) => {
|
||||
highlightPost(postNum);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
filterUpwards(postID) {
|
||||
this.cancelFilter();
|
||||
this.set("filterUpwardsPostID", postID);
|
||||
return this.refresh({ refreshInPlace: true }).then(() => {
|
||||
this.appEvents.trigger("post-stream:refresh");
|
||||
|
||||
if (this.posts && this.posts.length > 1) {
|
||||
const postNumber = this.posts[1].get("post_number");
|
||||
DiscourseURL.jumpToPost(postNumber, { skipIfOnScreen: true });
|
||||
|
||||
schedule("afterRender", () => {
|
||||
highlightPost(postNumber);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -273,7 +319,9 @@ export default RestModel.extend({
|
|||
}
|
||||
|
||||
// TODO: if we have all the posts in the filter, don't go to the server for them.
|
||||
this.set("loadingFilter", true);
|
||||
if (!opts.refreshInPlace) {
|
||||
this.set("loadingFilter", true);
|
||||
}
|
||||
this.set("loadingNearPost", opts.nearPost);
|
||||
|
||||
opts = deepMerge(opts, this.streamFilters);
|
||||
|
@ -327,13 +375,13 @@ export default RestModel.extend({
|
|||
} else {
|
||||
delete this.get("gaps.before")[postId];
|
||||
}
|
||||
this.stream.arrayContentDidChange();
|
||||
this.postsWithPlaceholders.arrayContentDidChange(
|
||||
origIdx,
|
||||
0,
|
||||
posts.length
|
||||
);
|
||||
post.set("hasGap", false);
|
||||
this.gapExpanded();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -350,12 +398,22 @@ export default RestModel.extend({
|
|||
stream.pushObjects(gap);
|
||||
return this.appendMore().then(() => {
|
||||
delete this.get("gaps.after")[postId];
|
||||
this.stream.arrayContentDidChange();
|
||||
this.gapExpanded();
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
gapExpanded() {
|
||||
this.appEvents.trigger("post-stream:refresh");
|
||||
|
||||
// resets the reply count in posts-filtered-notice
|
||||
// because once a gap has been expanded that count is no longer exact
|
||||
if (this.streamFilters && this.streamFilters.replies_to_post_number) {
|
||||
this.set("streamFilters.mixedHiddenPosts", true);
|
||||
}
|
||||
},
|
||||
|
||||
// Appends the next window of posts to the stream. Call it when scrolling downwards.
|
||||
appendMore() {
|
||||
// Make sure we can append more posts
|
||||
|
|
|
@ -81,9 +81,9 @@
|
|||
<li>
|
||||
{{d-button
|
||||
class="btn-default"
|
||||
action=(action "togglePosts" this.user)
|
||||
action=(action "filterPosts" this.user)
|
||||
icon="filter"
|
||||
translatedLabel=this.togglePostsLabel}}
|
||||
translatedLabel=this.filterPostsLabel}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if this.hasUserFilters}}
|
||||
|
|
|
@ -203,6 +203,7 @@
|
|||
selectedQuery=selectedQuery
|
||||
gaps=model.postStream.gaps
|
||||
showReadIndicator=model.show_read_indicator
|
||||
streamFilters=model.postStream.streamFilters
|
||||
showFlags=(action "showPostFlags")
|
||||
editPost=(action "editPost")
|
||||
showHistory=(route-action "showHistory")
|
||||
|
@ -222,7 +223,8 @@
|
|||
unhidePost=(action "unhidePost")
|
||||
replyToPost=(action "replyToPost")
|
||||
toggleWiki=(action "toggleWiki")
|
||||
toggleSummary=(action "toggleSummary")
|
||||
showSummary=(action "showSummary")
|
||||
cancelFilter=(action "cancelFilter")
|
||||
removeAllowedUser=(action "removeAllowedUser")
|
||||
removeAllowedGroup=(action "removeAllowedGroup")
|
||||
topVisibleChanged=(action "topVisibleChanged")
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{{user-card-contents
|
||||
topic=topic.model
|
||||
showUser=(action "showUser")
|
||||
togglePosts=(action "togglePosts")
|
||||
filterPosts=(action "filterPosts")
|
||||
composePrivateMessage=(route-action "composePrivateMessage")
|
||||
createNewMessageViaParams=(route-action "createNewMessageViaParams")}}
|
||||
|
||||
|
|
|
@ -233,11 +233,17 @@ registerButton("wiki-edit", (attrs) => {
|
|||
|
||||
registerButton("replies", (attrs, state, siteSettings) => {
|
||||
const replyCount = attrs.replyCount;
|
||||
|
||||
if (!replyCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
let action = "toggleRepliesBelow",
|
||||
icon = state.repliesShown ? "chevron-up" : "chevron-down";
|
||||
|
||||
if (siteSettings.enable_filtered_replies_view) {
|
||||
action = "filterRepliesView";
|
||||
}
|
||||
|
||||
// Omit replies if the setting `suppress_reply_directly_below` is enabled
|
||||
if (
|
||||
replyCount === 1 &&
|
||||
|
@ -248,14 +254,16 @@ registerButton("replies", (attrs, state, siteSettings) => {
|
|||
}
|
||||
|
||||
return {
|
||||
action: "toggleRepliesBelow",
|
||||
action,
|
||||
icon,
|
||||
className: "show-replies",
|
||||
icon: state.repliesShown ? "chevron-up" : "chevron-down",
|
||||
titleOptions: { count: replyCount },
|
||||
title: "post.has_replies",
|
||||
title: siteSettings.enable_filtered_replies_view
|
||||
? "post.filtered_replies_hint"
|
||||
: "post.has_replies",
|
||||
labelOptions: { count: replyCount },
|
||||
label: "post.has_replies",
|
||||
iconRight: true,
|
||||
label: attrs.mobileView ? "post.has_replies_count" : "post.has_replies",
|
||||
iconRight: !siteSettings.enable_filtered_replies_view || attrs.mobileView,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -575,7 +583,10 @@ export default createWidget("post-menu", {
|
|||
const contents = [
|
||||
h(
|
||||
"nav.post-controls.clearfix" +
|
||||
(this.state.collapsed ? ".collapsed" : ".expanded"),
|
||||
(this.state.collapsed ? ".collapsed" : ".expanded") +
|
||||
(siteSettings.enable_filtered_replies_view
|
||||
? ".replies-button-visible"
|
||||
: ""),
|
||||
postControls
|
||||
),
|
||||
];
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import DiscourseURL from "discourse/lib/url";
|
||||
import I18n from "I18n";
|
||||
import { Placeholder } from "discourse/lib/posts-with-placeholders";
|
||||
import { addWidgetCleanCallback } from "discourse/components/mount-widget";
|
||||
import { avatarFor } from "discourse/widgets/post";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { h } from "virtual-dom";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import transformPost from "discourse/lib/transform-post";
|
||||
|
||||
|
@ -58,22 +63,133 @@ addWidgetCleanCallback("post-stream", () => {
|
|||
_heights = {};
|
||||
});
|
||||
|
||||
createWidget("posts-filtered-notice", {
|
||||
buildKey: (attrs) => `posts-filtered-notice-${attrs.id}`,
|
||||
|
||||
buildClasses() {
|
||||
return ["posts-filtered-notice"];
|
||||
},
|
||||
|
||||
html(attrs) {
|
||||
const filters = attrs.streamFilters;
|
||||
|
||||
if (filters.filter_upwards_post_id || filters.mixedHiddenPosts) {
|
||||
return [
|
||||
h(
|
||||
"span.filtered-replies-viewing",
|
||||
I18n.t("post.filtered_replies.viewing_subset")
|
||||
),
|
||||
this.attach("filter-show-all", attrs),
|
||||
];
|
||||
} else if (filters.replies_to_post_number) {
|
||||
const sourcePost = attrs.posts.findBy(
|
||||
"post_number",
|
||||
filters.replies_to_post_number
|
||||
);
|
||||
|
||||
return [
|
||||
h(
|
||||
"span.filtered-replies-viewing",
|
||||
I18n.t("post.filtered_replies.viewing", {
|
||||
reply_count: sourcePost.reply_count,
|
||||
})
|
||||
),
|
||||
h("span.filtered-user-row", [
|
||||
h(
|
||||
"span.filtered-avatar",
|
||||
avatarFor.call(this, "small", {
|
||||
template: sourcePost.avatar_template,
|
||||
username: sourcePost.username,
|
||||
url: sourcePost.usernameUrl,
|
||||
})
|
||||
),
|
||||
this.attach("filter-jump-to-post", {
|
||||
username: sourcePost.username,
|
||||
postNumber: filters.replies_to_post_number,
|
||||
}),
|
||||
]),
|
||||
this.attach("filter-show-all", attrs),
|
||||
];
|
||||
} else if (filters.filter && filters.filter === "summary") {
|
||||
return [
|
||||
h(
|
||||
"span.filtered-replies-viewing",
|
||||
I18n.t("post.filtered_replies.viewing_summary")
|
||||
),
|
||||
this.attach("filter-show-all", attrs),
|
||||
];
|
||||
} else if (filters.username_filters) {
|
||||
return [
|
||||
h(
|
||||
"span.filtered-replies-viewing",
|
||||
I18n.t("post.filtered_replies.viewing_posts_by", {
|
||||
post_count: attrs.posts.length,
|
||||
})
|
||||
),
|
||||
h(
|
||||
"span.filtered-avatar",
|
||||
avatarFor.call(this, "small", {
|
||||
template: attrs.posts[0].avatar_template,
|
||||
username: attrs.posts[0].username,
|
||||
url: attrs.posts[0].usernameUrl,
|
||||
})
|
||||
),
|
||||
this.attach("poster-name", attrs.posts[0]),
|
||||
this.attach("filter-show-all", attrs),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
createWidget("filter-jump-to-post", {
|
||||
tagName: "a.filtered-jump-to-post",
|
||||
buildKey: (attrs) => `jump-to-post-${attrs.id}`,
|
||||
|
||||
html(attrs) {
|
||||
return I18n.t("post.filtered_replies.post_number", {
|
||||
username: attrs.username,
|
||||
post_number: attrs.postNumber,
|
||||
});
|
||||
},
|
||||
|
||||
click() {
|
||||
DiscourseURL.jumpToPost(this.attrs.postNumber);
|
||||
},
|
||||
});
|
||||
|
||||
createWidget("filter-show-all", {
|
||||
tagName: "a.filtered-replies-show-all",
|
||||
buildKey: (attrs) => `filtered-show-all-${attrs.id}`,
|
||||
|
||||
buildClasses() {
|
||||
return ["btn", "btn-primary"];
|
||||
},
|
||||
|
||||
html() {
|
||||
return [iconNode("far-comments"), I18n.t("post.filtered_replies.show_all")];
|
||||
},
|
||||
|
||||
click() {
|
||||
this.sendWidgetAction("cancelFilter", this.attrs.streamFilters);
|
||||
},
|
||||
});
|
||||
|
||||
export default createWidget("post-stream", {
|
||||
tagName: "div.post-stream",
|
||||
|
||||
html(attrs) {
|
||||
const posts = attrs.posts || [];
|
||||
const postArray = posts.toArray();
|
||||
|
||||
const result = [];
|
||||
|
||||
const before = attrs.gaps && attrs.gaps.before ? attrs.gaps.before : {};
|
||||
const after = attrs.gaps && attrs.gaps.after ? attrs.gaps.after : {};
|
||||
const posts = attrs.posts || [],
|
||||
postArray = posts.toArray(),
|
||||
result = [],
|
||||
before = attrs.gaps && attrs.gaps.before ? attrs.gaps.before : {},
|
||||
after = attrs.gaps && attrs.gaps.after ? attrs.gaps.after : {},
|
||||
mobileView = this.site.mobileView;
|
||||
|
||||
let prevPost;
|
||||
let prevDate;
|
||||
|
||||
const mobileView = this.site.mobileView;
|
||||
for (let i = 0; i < postArray.length; i++) {
|
||||
const post = postArray[i];
|
||||
|
||||
|
@ -156,6 +272,20 @@ export default createWidget("post-stream", {
|
|||
|
||||
prevPost = post;
|
||||
}
|
||||
|
||||
if (
|
||||
attrs.streamFilters &&
|
||||
Object.keys(attrs.streamFilters).length &&
|
||||
(Object.keys(before).length > 0 || Object.keys(after).length > 0)
|
||||
) {
|
||||
result.push(
|
||||
this.attach("posts-filtered-notice", {
|
||||
posts: postArray,
|
||||
streamFilters: attrs.streamFilters,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -126,12 +126,10 @@ createWidget("reply-to-tab", {
|
|||
},
|
||||
|
||||
html(attrs, state) {
|
||||
if (state.loading) {
|
||||
return I18n.t("loading");
|
||||
}
|
||||
const icon = state.loading ? h("div.spinner.small") : iconNode("share");
|
||||
|
||||
return [
|
||||
iconNode("share"),
|
||||
icon,
|
||||
" ",
|
||||
avatarImg("small", {
|
||||
template: attrs.replyToAvatarTemplate,
|
||||
|
@ -436,6 +434,17 @@ createWidget("post-contents", {
|
|||
return lastWikiEdit ? lastWikiEdit : createdAt;
|
||||
},
|
||||
|
||||
filterRepliesView() {
|
||||
const post = this.findAncestorModel();
|
||||
const controller = this.register.lookup("controller:topic");
|
||||
post
|
||||
.get("topic.postStream")
|
||||
.filterReplies(this.attrs.post_number)
|
||||
.then(() => {
|
||||
controller.updateQueryParams();
|
||||
});
|
||||
},
|
||||
|
||||
toggleRepliesBelow(goToPost = "false") {
|
||||
if (this.state.repliesBelow.length) {
|
||||
this.state.repliesBelow = [];
|
||||
|
@ -617,6 +626,17 @@ createWidget("post-article", {
|
|||
toggleReplyAbove(goToPost = "false") {
|
||||
const replyPostNumber = this.attrs.reply_to_post_number;
|
||||
|
||||
if (this.siteSettings.enable_filtered_replies_view) {
|
||||
const post = this.findAncestorModel();
|
||||
const controller = this.register.lookup("controller:topic");
|
||||
return post
|
||||
.get("topic.postStream")
|
||||
.filterUpwards(this.attrs.id)
|
||||
.then(() => {
|
||||
controller.updateQueryParams();
|
||||
});
|
||||
}
|
||||
|
||||
// jump directly on mobile
|
||||
if (this.attrs.mobileView) {
|
||||
const topicUrl = this._getTopicUrl();
|
||||
|
|
|
@ -39,7 +39,7 @@ export default createWidget("toggle-topic-summary", {
|
|||
this.attach("button", {
|
||||
className: "btn btn-primary",
|
||||
label: attrs.topicSummaryEnabled ? "summary.disable" : "summary.enable",
|
||||
action: "toggleSummary",
|
||||
action: attrs.topicSummaryEnabled ? "cancelFilter" : "showSummary",
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
|
|
@ -937,10 +937,10 @@ discourseModule("Integration | Component | Widget | post", function (hooks) {
|
|||
|
||||
componentTest("topic map - has summary", {
|
||||
template:
|
||||
'{{mount-widget widget="post" args=args toggleSummary=(action "toggleSummary")}}',
|
||||
'{{mount-widget widget="post" args=args showSummary=(action "showSummary")}}',
|
||||
beforeEach() {
|
||||
this.set("args", { showTopicMap: true, hasTopicSummary: true });
|
||||
this.on("toggleSummary", () => (this.summaryToggled = true));
|
||||
this.on("showSummary", () => (this.summaryToggled = true));
|
||||
},
|
||||
async test(assert) {
|
||||
assert.equal(queryAll(".toggle-summary").length, 1);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { module, test } from "qunit";
|
||||
import AppEvents from "discourse/services/app-events";
|
||||
import ArrayProxy from "@ember/array/proxy";
|
||||
import Post from "discourse/models/post";
|
||||
import { Promise } from "rsvp";
|
||||
|
@ -14,6 +15,7 @@ function buildStream(id, stream) {
|
|||
if (stream) {
|
||||
ps.set("stream", stream);
|
||||
}
|
||||
ps.appEvents = AppEvents.create();
|
||||
return ps;
|
||||
}
|
||||
|
||||
|
@ -232,7 +234,7 @@ module("Unit | Model | post-stream", function () {
|
|||
postStream.cancelFilter();
|
||||
assert.ok(!postStream.get("summary"), "summary is cancelled");
|
||||
|
||||
postStream.toggleParticipant(participant);
|
||||
postStream.filterParticipant(participant);
|
||||
postStream.cancelFilter();
|
||||
assert.blank(
|
||||
postStream.get("userFilters"),
|
||||
|
@ -282,7 +284,7 @@ module("Unit | Model | post-stream", function () {
|
|||
);
|
||||
});
|
||||
|
||||
test("toggleParticipant", function (assert) {
|
||||
test("filterParticipant", function (assert) {
|
||||
const postStream = buildStream(1236);
|
||||
sinon.stub(postStream, "refresh").returns(Promise.resolve());
|
||||
|
||||
|
@ -292,16 +294,71 @@ module("Unit | Model | post-stream", function () {
|
|||
"by default no participants are toggled"
|
||||
);
|
||||
|
||||
postStream.toggleParticipant(participant.username);
|
||||
postStream.filterParticipant(participant.username);
|
||||
assert.ok(
|
||||
postStream.get("userFilters").includes("eviltrout"),
|
||||
"eviltrout is in the filters"
|
||||
);
|
||||
|
||||
postStream.toggleParticipant(participant.username);
|
||||
assert.blank(
|
||||
postStream.get("userFilters"),
|
||||
"toggling the participant again removes them"
|
||||
postStream.cancelFilter();
|
||||
assert.blank(postStream.get("userFilters"), "cancelFilter clears");
|
||||
});
|
||||
|
||||
test("filterReplies", function (assert) {
|
||||
const postStream = buildStream(1234),
|
||||
store = postStream.store;
|
||||
|
||||
postStream.appendPost(
|
||||
store.createRecord("post", { id: 2, post_number: 3 })
|
||||
);
|
||||
|
||||
sinon.stub(postStream, "refresh").returns(Promise.resolve());
|
||||
|
||||
assert.equal(
|
||||
postStream.get("filterRepliesToPostNumber"),
|
||||
false,
|
||||
"by default no replies are filtered"
|
||||
);
|
||||
|
||||
postStream.filterReplies(3);
|
||||
assert.equal(
|
||||
postStream.get("filterRepliesToPostNumber"),
|
||||
3,
|
||||
"postNumber is in the filters"
|
||||
);
|
||||
|
||||
postStream.cancelFilter();
|
||||
assert.equal(
|
||||
postStream.get("filterRepliesToPostNumber"),
|
||||
false,
|
||||
"cancelFilter clears"
|
||||
);
|
||||
});
|
||||
|
||||
test("filterUpwards", function (assert) {
|
||||
const postStream = buildStream(1234),
|
||||
store = postStream.store;
|
||||
|
||||
postStream.appendPost(
|
||||
store.createRecord("post", { id: 2, post_number: 3 })
|
||||
);
|
||||
|
||||
sinon.stub(postStream, "refresh").returns(Promise.resolve());
|
||||
|
||||
assert.equal(
|
||||
postStream.get("filterUpwardsPostID"),
|
||||
false,
|
||||
"by default filter is false"
|
||||
);
|
||||
|
||||
postStream.filterUpwards(2);
|
||||
assert.equal(postStream.get("filterUpwardsPostID"), 2, "filter is set");
|
||||
|
||||
postStream.cancelFilter();
|
||||
assert.equal(
|
||||
postStream.get("filterUpwardsPostID"),
|
||||
false,
|
||||
"filter cleared"
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -327,7 +384,7 @@ module("Unit | Model | post-stream", function () {
|
|||
);
|
||||
assert.ok(!postStream.get("hasNoFilters"), "now there are filters present");
|
||||
|
||||
postStream.toggleParticipant(participant.username);
|
||||
postStream.filterParticipant(participant.username);
|
||||
assert.deepEqual(
|
||||
postStream.get("streamFilters"),
|
||||
{
|
||||
|
@ -335,6 +392,24 @@ module("Unit | Model | post-stream", function () {
|
|||
},
|
||||
"streamFilters contains the username we filtered"
|
||||
);
|
||||
|
||||
postStream.filterUpwards(2);
|
||||
assert.deepEqual(
|
||||
postStream.get("streamFilters"),
|
||||
{
|
||||
filter_upwards_post_id: 2,
|
||||
},
|
||||
"streamFilters contains only the post ID"
|
||||
);
|
||||
|
||||
postStream.filterReplies(1);
|
||||
assert.deepEqual(
|
||||
postStream.get("streamFilters"),
|
||||
{
|
||||
replies_to_post_number: 1,
|
||||
},
|
||||
"streamFilters contains only the last filter"
|
||||
);
|
||||
});
|
||||
|
||||
test("loading", function (assert) {
|
||||
|
|
|
@ -1109,3 +1109,36 @@ a.mention-group {
|
|||
bottom: -2px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.posts-filtered-notice {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: var(--tertiary-low);
|
||||
bottom: 0;
|
||||
padding: 1em;
|
||||
margin-top: 0.5em;
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: calc(
|
||||
#{$topic-body-width} + (#{$topic-body-width-padding} * 2) + #{$topic-avatar-width} -
|
||||
(0.8em * 2)
|
||||
);
|
||||
|
||||
.filtered-avatar {
|
||||
margin: 0 0.5em;
|
||||
+ .names {
|
||||
flex: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.filtered-replies-show-all {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.filtered-user-row {
|
||||
@include ellipsis;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,9 +208,13 @@ nav.post-controls {
|
|||
background: var(--primary-low);
|
||||
}
|
||||
.d-icon {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
.d-button-label + .d-icon {
|
||||
margin-left: 5px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -102,6 +102,21 @@ span.badge-posts {
|
|||
}
|
||||
}
|
||||
}
|
||||
&.replies-button-visible {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.show-replies {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
font-size: $font-up-1;
|
||||
.d-icon {
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
.actions {
|
||||
flex-grow: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -268,20 +283,6 @@ span.post-count {
|
|||
padding: 15px 0;
|
||||
}
|
||||
|
||||
// mobile has no fixed width on topic-body so overflow: hidden causes problems
|
||||
.topic-body {
|
||||
overflow: visible;
|
||||
.cooked {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
// instead, for mobile we set overflow hidden on the post's #main-outlet
|
||||
// this prevents image overflow on deeply nested blockquotes, lists, etc
|
||||
[class*="archetype-"] #main-outlet {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quote-button.visible {
|
||||
z-index: z("tooltip");
|
||||
}
|
||||
|
@ -426,3 +427,30 @@ span.highlighted {
|
|||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.posts-filtered-notice {
|
||||
padding-right: 10em;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
padding-bottom: unquote("max(0.75em, env(safe-area-inset-bottom))");
|
||||
margin: 1em -9px;
|
||||
|
||||
z-index: 101;
|
||||
.filtered-replies-show-all {
|
||||
position: absolute;
|
||||
right: 2em;
|
||||
}
|
||||
|
||||
.filtered-replies-viewing {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filtered-avatar {
|
||||
margin-left: 0;
|
||||
img.avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ class TopicsController < ApplicationController
|
|||
# arrays are not supported
|
||||
params[:page] = params[:page].to_i rescue 1
|
||||
|
||||
opts = params.slice(:username_filters, :filter, :page, :post_number, :show_deleted)
|
||||
opts = params.slice(:username_filters, :filter, :page, :post_number, :show_deleted, :replies_to_post_number, :filter_upwards_post_id)
|
||||
username_filters = opts[:username_filters]
|
||||
|
||||
opts[:print] = true if params[:print].present?
|
||||
|
@ -1050,7 +1050,8 @@ class TopicsController < ApplicationController
|
|||
@topic_view,
|
||||
scope: guardian,
|
||||
root: false,
|
||||
include_raw: !!params[:include_raw]
|
||||
include_raw: !!params[:include_raw],
|
||||
exclude_suggested_and_related: !!params[:replies_to_post_number] || !!params[:filter_upwards_post_id]
|
||||
)
|
||||
|
||||
respond_to do |format|
|
||||
|
|
|
@ -7,10 +7,12 @@ module SuggestedTopicsMixin
|
|||
end
|
||||
|
||||
def include_related_messages?
|
||||
return false if @options[:exclude_suggested_and_related]
|
||||
object.next_page.nil? && object.related_messages&.topics
|
||||
end
|
||||
|
||||
def include_suggested_topics?
|
||||
return false if @options[:exclude_suggested_and_related]
|
||||
object.next_page.nil? && object.suggested_topics&.topics
|
||||
end
|
||||
|
||||
|
|
|
@ -2736,6 +2736,7 @@ en:
|
|||
has_replies:
|
||||
one: "%{count} Reply"
|
||||
other: "%{count} Replies"
|
||||
has_replies_count: "%{count}"
|
||||
|
||||
unknown_user: "(unknown/deleted user)"
|
||||
has_likes_title:
|
||||
|
@ -2747,6 +2748,10 @@ en:
|
|||
one: "you and %{count} other person liked this post"
|
||||
other: "you and %{count} other people liked this post"
|
||||
|
||||
filtered_replies_hint:
|
||||
one: "View this post and its reply"
|
||||
other: "View this post and its %{count} replies"
|
||||
|
||||
errors:
|
||||
create: "Sorry, there was an error creating your post. Please try again."
|
||||
edit: "Sorry, there was an error editing your post. Please try again."
|
||||
|
@ -2917,6 +2922,14 @@ en:
|
|||
name: "Edit bookmark"
|
||||
description: "Edit the bookmark name or change the reminder date and time"
|
||||
|
||||
filtered_replies:
|
||||
viewing: "Viewing %{reply_count} replies to"
|
||||
viewing_posts_by: "Viewing %{post_count} posts by"
|
||||
viewing_subset: "Some replies are collapsed"
|
||||
viewing_summary: "Viewing a summary of this topic"
|
||||
post_number: "%{username}, post #%{post_number}"
|
||||
show_all: "Show all"
|
||||
|
||||
category:
|
||||
can: "can… "
|
||||
none: "(no category)"
|
||||
|
|
|
@ -1863,6 +1863,7 @@ en:
|
|||
body_min_entropy: "The minimum entropy (unique characters, non-english count for more) required for a post body."
|
||||
allow_uppercase_posts: "Allow all caps in a topic title or a post body."
|
||||
max_consecutive_replies: "Number of posts a user has to make in a row in a topic before being prevented from adding another reply."
|
||||
enable_filtered_replies_view: "(n) replies button should collapse all other posts and only show the selected replies."
|
||||
|
||||
title_fancy_entities: "Convert common ASCII characters to fancy HTML entities in topic titles, ala SmartyPants <a href='https://daringfireball.net/projects/smartypants/' target='_blank'>https://daringfireball.net/projects/smartypants/</a>"
|
||||
|
||||
|
|
|
@ -725,6 +725,10 @@ posting:
|
|||
ja: true
|
||||
max_consecutive_replies:
|
||||
default: 3
|
||||
enable_filtered_replies_view:
|
||||
default: false
|
||||
client: true
|
||||
hidden: true
|
||||
title_prettify:
|
||||
default: true
|
||||
locale_default:
|
||||
|
|
|
@ -94,6 +94,7 @@ module SvgSprite
|
|||
"far-clipboard",
|
||||
"far-clock",
|
||||
"far-comment",
|
||||
"far-comments",
|
||||
"far-copyright",
|
||||
"far-dot-circle",
|
||||
"far-edit",
|
||||
|
|
|
@ -771,6 +771,44 @@ class TopicView
|
|||
@contains_gaps = true
|
||||
end
|
||||
|
||||
# Filter replies
|
||||
if @replies_to_post_number.present?
|
||||
@filtered_posts = @filtered_posts.where('
|
||||
posts.post_number = 1
|
||||
OR posts.post_number = :post_number
|
||||
OR posts.reply_to_post_number = :post_number', { post_number: @replies_to_post_number.to_i })
|
||||
|
||||
@contains_gaps = true
|
||||
end
|
||||
|
||||
# Filtering upwards
|
||||
if @filter_upwards_post_id.present?
|
||||
post = Post.find(@filter_upwards_post_id)
|
||||
post_ids = DB.query_single(<<~SQL, post_id: post.id, topic_id: post.topic_id)
|
||||
WITH RECURSIVE breadcrumb(id, reply_to_post_number) AS (
|
||||
SELECT p.id, p.reply_to_post_number FROM posts AS p
|
||||
WHERE p.id = :post_id
|
||||
UNION
|
||||
SELECT p.id, p.reply_to_post_number FROM posts AS p, breadcrumb
|
||||
WHERE breadcrumb.reply_to_post_number = p.post_number
|
||||
AND p.topic_id = :topic_id
|
||||
)
|
||||
SELECT id from breadcrumb
|
||||
WHERE id <> :post_id
|
||||
ORDER by id
|
||||
SQL
|
||||
|
||||
post_ids = (post_ids[(0 - SiteSetting.max_reply_history)..-1] || post_ids)
|
||||
post_ids.push(post.id)
|
||||
|
||||
@filtered_posts = @filtered_posts.where('
|
||||
posts.post_number = 1
|
||||
OR posts.id IN (:post_ids)
|
||||
OR posts.id > :max_post_id', { post_ids: post_ids, max_post_id: post_ids.max })
|
||||
|
||||
@contains_gaps = true
|
||||
end
|
||||
|
||||
# Deleted
|
||||
# This should be last - don't want to tell the admin about deleted posts that clicking the button won't show
|
||||
# copy the filter for has_deleted? method
|
||||
|
|
|
@ -2096,6 +2096,77 @@ RSpec.describe TopicsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#show filters' do
|
||||
let(:post) { Fabricate(:post) }
|
||||
let(:topic) { post.topic }
|
||||
|
||||
describe 'filter by replies to a post' do
|
||||
let!(:post2) { Fabricate(:post, topic: topic) }
|
||||
let!(:post3) { Fabricate(:post, topic: topic, reply_to_post_number: post2.post_number) }
|
||||
let!(:post4) { Fabricate(:post, topic: topic, reply_to_post_number: post2.post_number) }
|
||||
let!(:post5) { Fabricate(:post, topic: topic) }
|
||||
|
||||
it 'should return the right posts' do
|
||||
get "/t/#{topic.id}.json", params: {
|
||||
replies_to_post_number: post2.post_number
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
body = response.parsed_body
|
||||
|
||||
expect(body.has_key?("suggested_topics")).to eq(false)
|
||||
expect(body.has_key?("related_messages")).to eq(false)
|
||||
|
||||
ids = body["post_stream"]["posts"].map { |p| p["id"] }
|
||||
expect(ids).to eq([post.id, post2.id, post3.id, post4.id])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'filter upwards by post id' do
|
||||
let!(:post2) { Fabricate(:post, topic: topic) }
|
||||
let!(:post3) { Fabricate(:post, topic: topic) }
|
||||
let!(:post4) { Fabricate(:post, topic: topic, reply_to_post_number: post3.post_number) }
|
||||
let!(:post5) { Fabricate(:post, topic: topic, reply_to_post_number: post4.post_number) }
|
||||
let!(:post6) { Fabricate(:post, topic: topic) }
|
||||
|
||||
it 'should return the right posts' do
|
||||
get "/t/#{topic.id}.json", params: {
|
||||
filter_upwards_post_id: post5.id
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
body = response.parsed_body
|
||||
|
||||
expect(body.has_key?("suggested_topics")).to eq(false)
|
||||
expect(body.has_key?("related_messages")).to eq(false)
|
||||
|
||||
ids = body["post_stream"]["posts"].map { |p| p["id"] }
|
||||
# includes topic OP, current post and subsequent posts
|
||||
# but only one level of parents, respecting default max_reply_history = 1
|
||||
expect(ids).to eq([post.id, post4.id, post5.id, post6.id])
|
||||
end
|
||||
|
||||
it 'should respect max_reply_history site setting' do
|
||||
SiteSetting.max_reply_history = 2
|
||||
|
||||
get "/t/#{topic.id}.json", params: {
|
||||
filter_upwards_post_id: post5.id
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
body = response.parsed_body
|
||||
ids = body["post_stream"]["posts"].map { |p| p["id"] }
|
||||
|
||||
# includes 2 levels of replies (post3 and post4)
|
||||
expect(ids).to eq([post.id, post3.id, post4.id, post5.id, post6.id])
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "when 'login required' site setting has been enabled" do
|
||||
before { SiteSetting.login_required = true }
|
||||
|
||||
|
|
Loading…
Reference in New Issue