Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ export default class NestedContextView extends Component {
@showFlags={{@showFlags}}
@showHistory={{@showHistory}}
@postScreenTracker={{@postScreenTracker}}
@expansionState={{@expansionState}}
@fetchedChildrenCache={{@fetchedChildrenCache}}
@scrollAnchor={{@scrollAnchor}}
/>
{{/each}}
</div>
Expand Down
32 changes: 32 additions & 0 deletions assets/javascripts/discourse/components/nested-post-children.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ export default class NestedPostChildren extends Component {
this,
this._onChildCreated
);

const cached = this.args.fetchedChildrenCache?.get(
this.args.parentPostNumber
);
if (cached) {
this.childNodes = cached.childNodes;
this.page = cached.page;
this.hasMore = cached.hasMore;
this.loaded = true;
this._fetchedFromServer = cached.fetchedFromServer;
return;
}

if (this.args.preloadedChildren?.length > 0) {
this.childNodes = this.args.preloadedChildren;
this.loaded = true;
Expand All @@ -56,6 +69,19 @@ export default class NestedPostChildren extends Component {
this,
this._onChildCreated
);
this._reportToCache();
}

_reportToCache() {
if (!this.loaded || !this.args.fetchedChildrenCache) {
return;
}
this.args.fetchedChildrenCache.set(this.args.parentPostNumber, {
childNodes: this.childNodes,
page: this.page,
hasMore: this.hasMore,
fetchedFromServer: this._fetchedFromServer,
});
}

_onChildCreated({ post, parentPostNumber }) {
Expand All @@ -72,6 +98,7 @@ export default class NestedPostChildren extends Component {

this.childNodes = [{ post, children: [] }, ...this.childNodes];
this.loaded = true;
this._reportToCache();
}

get childDepth() {
Expand Down Expand Up @@ -109,6 +136,7 @@ export default class NestedPostChildren extends Component {
this.hasMore = data.has_more || false;
this.loaded = true;
this._fetchedFromServer = true;
this._reportToCache();
} catch (e) {
popupAjaxError(e);
} finally {
Expand Down Expand Up @@ -152,6 +180,7 @@ export default class NestedPostChildren extends Component {

this.page = data.page;
this.hasMore = data.has_more || false;
this._reportToCache();
} catch (e) {
popupAjaxError(e);
} finally {
Expand Down Expand Up @@ -185,6 +214,9 @@ export default class NestedPostChildren extends Component {
@unhighlightParentLine={{@unhighlightParentLine}}
@parentLineHighlighted={{@parentLineHighlighted}}
@postScreenTracker={{@postScreenTracker}}
@expansionState={{@expansionState}}
@fetchedChildrenCache={{@fetchedChildrenCache}}
@scrollAnchor={{@scrollAnchor}}
/>
{{/each}}

Expand Down
42 changes: 36 additions & 6 deletions assets/javascripts/discourse/components/nested-post.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,41 @@ export default class NestedPost extends Component {
@service site;
@service siteSettings;

@tracked
expanded =
((this.args.children?.length ?? 0) > 0 ||
this.args.post.deleted_post_placeholder === true) &&
!this.args.defaultCollapsed;
@tracked expanded;
@tracked lineHighlighted = false;
@tracked collapsed = false;
@tracked collapsed;

trackPost = modifier((element) => {
this.args.postScreenTracker?.observe(element, this.args.post);
return () => this.args.postScreenTracker?.unobserve(element);
});

restoreScroll = modifier((element) => {
const anchor = this.args.scrollAnchor;
if (anchor?.postNumber !== this.args.post.post_number) {
return;
}
const rect = element.getBoundingClientRect();
window.scrollTo(0, window.scrollY + rect.top - anchor.offsetFromTop);
});

@tracked _childWasCreated = false;

constructor() {
super(...arguments);

const cached = this.args.expansionState?.get(this.args.post.post_number);
if (cached !== undefined) {
this.expanded = cached.expanded;
this.collapsed = cached.collapsed;
} else {
this.expanded =
((this.args.children?.length ?? 0) > 0 ||
this.args.post.deleted_post_placeholder === true) &&
!this.args.defaultCollapsed;
this.collapsed = false;
}

this.appEvents.on(
"nested-replies:child-created",
this,
Expand Down Expand Up @@ -79,6 +97,10 @@ export default class NestedPost extends Component {
if (isOwnPost && !this.expanded) {
this.expanded = true;
this.collapsed = false;
this.args.expansionState?.set(this.args.post.post_number, {
expanded: true,
collapsed: false,
});
}
}

Expand Down Expand Up @@ -161,6 +183,10 @@ export default class NestedPost extends Component {
this.expanded = true;
this.collapsed = false;
}
this.args.expansionState?.set(this.args.post.post_number, {
expanded: this.expanded,
collapsed: this.collapsed,
});
}

@action
Expand Down Expand Up @@ -248,6 +274,7 @@ export default class NestedPost extends Component {
(if @post.isWhisper "nested-post--whisper")
(if @post.deleted "nested-post--deleted")
}}
{{this.restoreScroll}}
>
{{#if @collapseParent}}
<button
Expand Down Expand Up @@ -404,6 +431,9 @@ export default class NestedPost extends Component {
@unhighlightParentLine={{this.unhighlightLine}}
@parentLineHighlighted={{this.lineHighlighted}}
@postScreenTracker={{@postScreenTracker}}
@expansionState={{@expansionState}}
@fetchedChildrenCache={{@fetchedChildrenCache}}
@scrollAnchor={{@scrollAnchor}}
/>
{{/if}}
</div>
Expand Down
3 changes: 3 additions & 0 deletions assets/javascripts/discourse/components/nested-view.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ export default class NestedView extends Component {
@showFlags={{@showFlags}}
@showHistory={{@showHistory}}
@postScreenTracker={{@postScreenTracker}}
@expansionState={{@expansionState}}
@fetchedChildrenCache={{@fetchedChildrenCache}}
@scrollAnchor={{@scrollAnchor}}
/>
{{else}}
<div class="nested-view__empty">
Expand Down
14 changes: 14 additions & 0 deletions assets/javascripts/discourse/controllers/nested.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default class NestedController extends Controller {
@service dialog;
@service currentUser;
@service messageBus;
@service nestedViewCache;
@service router;

@tracked topic;
Expand All @@ -41,6 +42,18 @@ export default class NestedController extends Controller {
@tracked pinnedPostNumber = null;
queryParams = ["sort", "post_number", "context"];

// Externalized expansion state: postNumber → { expanded, collapsed }
// Components read on construction, write on toggle.
// Persisted across back/forward navigations via NestedViewCache.
expansionState = new Map();

// Cache of dynamically loaded children: postNumber → { childNodes, page, hasMore, fetchedFromServer }
// Populated by NestedPostChildren on every mutation, read on restoration.
fetchedChildrenCache = new Map();

// Scroll anchor for cache restoration: { postNumber, offsetFromTop }
scrollAnchor = null;

quoteState = new QuoteState();

// Flat registry of all rendered posts by post_number.
Expand Down Expand Up @@ -110,6 +123,7 @@ export default class NestedController extends Controller {

@action
viewFullThread() {
this.nestedViewCache.useNextTransition();
this.router.transitionTo("nested", this.topic.slug, this.topic.id, {
queryParams: { sort: this.sort, post_number: null, context: null },
});
Expand Down
92 changes: 89 additions & 3 deletions assets/javascripts/discourse/routes/nested.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import processNode from "../lib/process-node";

export default class NestedRoute extends Route {
@service header;
@service nestedViewCache;
@service screenTrack;
@service siteSettings;
@service store;
Expand All @@ -26,6 +27,19 @@ export default class NestedRoute extends Route {
const sort =
params.sort || this.siteSettings.nested_replies_default_sort || "top";

const cacheKey = this.nestedViewCache.buildKey(topic_id, {
...params,
sort,
});
if (this.nestedViewCache.consumeTraversal()) {
const cached = this.nestedViewCache.get(cacheKey);
if (cached) {
this._restoringFromCache = cached;
return cached.modelData;
}
}
this._restoringFromCache = null;

if (post_number) {
const queryParts = [`sort=${sort}`];
if (params.context !== undefined && params.context !== null) {
Expand All @@ -45,6 +59,18 @@ export default class NestedRoute extends Route {
}

setupController(controller, model) {
if (this._restoringFromCache) {
controller.expansionState = this._restoringFromCache.expansionState;
controller.fetchedChildrenCache =
this._restoringFromCache.fetchedChildrenCache;
controller.scrollAnchor = this._restoringFromCache.scrollAnchor;
this._restoringFromCache = null;
} else {
controller.expansionState = new Map();
controller.fetchedChildrenCache = new Map();
controller.scrollAnchor = null;
}

controller.setProperties(model);
controller.subscribe();

Expand Down Expand Up @@ -77,10 +103,70 @@ export default class NestedRoute extends Route {

deactivate() {
super.deactivate(...arguments);
this.controller.unsubscribe();

const controller = this.controller;
this._saveToCache(controller);

controller.unsubscribe();
this.screenTrack.stop();
this.controller.postScreenTracker?.destroy();
this.controller.postScreenTracker = null;
controller.postScreenTracker?.destroy();
controller.postScreenTracker = null;
}

_saveToCache(controller) {
if (!controller.topic) {
return;
}

const cacheKey = this.nestedViewCache.buildKey(controller.topic.id, {
sort: controller.sort,
post_number: controller.postNumber,
context: controller.contextNoAncestors ? 0 : undefined,
});

this.nestedViewCache.save(cacheKey, {
modelData: {
topic: controller.topic,
opPost: controller.opPost,
rootNodes: controller.rootNodes,
page: controller.page,
hasMoreRoots: controller.hasMoreRoots,
sort: controller.sort,
messageBusLastId: controller.messageBusLastId,
pinnedPostNumber: controller.pinnedPostNumber,
postNumber: controller.postNumber,
contextMode: controller.contextMode,
contextChain: controller.contextChain,
targetPostNumber: controller.targetPostNumber,
contextNoAncestors: controller.contextNoAncestors,
ancestorsTruncated: controller.ancestorsTruncated,
topAncestorPostNumber: controller.topAncestorPostNumber,
},
expansionState: new Map(controller.expansionState),
fetchedChildrenCache: new Map(controller.fetchedChildrenCache),
scrollAnchor: this._findScrollAnchor(),
});
}

_findScrollAnchor() {
const articles = document.querySelectorAll(
".nested-post [data-post-number]"
);
let best = null;
let bestDistance = Infinity;

for (const el of articles) {
const rect = el.getBoundingClientRect();
const distance = Math.abs(rect.top);
if (distance < bestDistance) {
bestDistance = distance;
best = {
postNumber: Number(el.dataset.postNumber),
offsetFromTop: rect.top,
};
}
}
return best;
}

_processResponse(data, params) {
Expand Down
Loading