Skip to content
This repository was archived by the owner on Apr 21, 2026. It is now read-only.

Commit 22e27ca

Browse files
authored
DEV: General code cleanup (#29)
1 parent d9d2121 commit 22e27ca

4 files changed

Lines changed: 97 additions & 14 deletions

File tree

assets/javascripts/discourse/api-initializers/nested-view-redirect.js

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default apiInitializer((api) => {
3535
const topicTrackingState = api.container.lookup(
3636
"service:topic-tracking-state"
3737
);
38-
let composerSavedFromNested = false;
38+
let composerSaveInfo = null;
3939

4040
appEvents.on("composer:saved", () => {
4141
if (!siteSettings.nested_replies_enabled) {
@@ -44,7 +44,11 @@ export default apiInitializer((api) => {
4444

4545
const route = router.currentRouteName;
4646
if (route?.startsWith("nested")) {
47-
composerSavedFromNested = true;
47+
const nestedController = api.container.lookup("controller:nested");
48+
composerSaveInfo = {
49+
topicId: nestedController?.topic?.id,
50+
time: Date.now(),
51+
};
4852
}
4953
});
5054

@@ -100,10 +104,17 @@ export default apiInitializer((api) => {
100104
}
101105

102106
// After composer save on nested route, suppress the redirect to flat view.
103-
// Returning null tells routeTo to abort navigation.
104-
if (composerSavedFromNested && /^\/t\//.test(path)) {
105-
composerSavedFromNested = false;
106-
return null;
107+
// Returning null tells routeTo to abort navigation. Scoped to the same
108+
// topic and expires after 5 seconds to prevent stale flags from
109+
// suppressing unrelated navigations.
110+
if (composerSaveInfo && /^\/t\//.test(path)) {
111+
const match = TOPIC_URL_RE.exec(path);
112+
const elapsed = Date.now() - composerSaveInfo.time;
113+
const savedTopicId = composerSaveInfo.topicId;
114+
composerSaveInfo = null;
115+
if (match && parseInt(match[2], 10) === savedTopicId && elapsed < 5000) {
116+
return null;
117+
}
107118
}
108119

109120
// If already in flat view, don't redirect to nested (e.g. timeline navigation).
@@ -152,7 +163,8 @@ export default apiInitializer((api) => {
152163
// state yet), intercept before the topic route's model hook runs.
153164
// When the topic isn't in tracking state, abort and fetch topic info
154165
// to determine the category before deciding which view to load.
155-
const checkedTopicIds = new Set();
166+
const checkedTopicIds = new Map();
167+
const CHECKED_TOPIC_TTL_MS = 60_000;
156168

157169
router.on("routeWillChange", (transition) => {
158170
if (!siteSettings.nested_replies_enabled) {
@@ -205,15 +217,32 @@ export default apiInitializer((api) => {
205217

206218
// Topic not in tracking state (e.g. direct URL entry). Abort, look
207219
// up the category via a lightweight request, then redirect or resume.
208-
if (checkedTopicIds.has(topicId)) {
220+
const checkedAt = checkedTopicIds.get(topicId);
221+
if (checkedAt && Date.now() - checkedAt < CHECKED_TOPIC_TTL_MS) {
209222
return;
210223
}
211-
checkedTopicIds.add(topicId);
224+
checkedTopicIds.set(topicId, Date.now());
225+
226+
// Evict stale entries to prevent unbounded growth
227+
if (checkedTopicIds.size > 100) {
228+
const now = Date.now();
229+
for (const [id, time] of checkedTopicIds) {
230+
if (now - time > CHECKED_TOPIC_TTL_MS) {
231+
checkedTopicIds.delete(id);
232+
}
233+
}
234+
}
212235

236+
const fromRoute = router.currentRouteName;
213237
transition.abort();
214238

215239
ajax(`/t/${topicId}.json`, { data: { track_visit: false } })
216240
.then((data) => {
241+
// Bail if user navigated away during the async lookup
242+
if (router.currentRouteName !== fromRoute) {
243+
return;
244+
}
245+
217246
if (isNestedDefault(siteSettings, data.category_id)) {
218247
const queryParams = {};
219248
const nearPost = transition.to?.params?.nearPost;
@@ -226,7 +255,9 @@ export default apiInitializer((api) => {
226255
}
227256
})
228257
.catch(() => {
229-
// On error, let the normal topic route handle it
258+
if (router.currentRouteName !== fromRoute) {
259+
return;
260+
}
230261
transition.retry();
231262
});
232263
});

assets/javascripts/discourse/controllers/nested.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,9 @@ export default class NestedController extends Controller {
324324
}
325325

326326
if (this.topic?.id && this.messageBusLastId != null) {
327+
this._messageBusChannel = `/topic/${this.topic.id}`;
327328
this.messageBus.subscribe(
328-
`/topic/${this.topic.id}`,
329+
this._messageBusChannel,
329330
this._onMessage,
330331
this.messageBusLastId
331332
);
@@ -346,7 +347,10 @@ export default class NestedController extends Controller {
346347
);
347348
this._postEventsSubscribed = false;
348349
}
349-
this.messageBus.unsubscribe("/topic/*", this._onMessage);
350+
if (this._messageBusChannel) {
351+
this.messageBus.unsubscribe(this._messageBusChannel, this._onMessage);
352+
this._messageBusChannel = null;
353+
}
350354
this.postRegistry.clear();
351355
}
352356

@@ -406,14 +410,31 @@ export default class NestedController extends Controller {
406410
}
407411

408412
async _handlePostChanged(data) {
413+
if (data.type === "deleted") {
414+
this._markPostDeletedLocally(data.id);
415+
return;
416+
}
417+
409418
try {
410419
const postData = await ajax(`/posts/${data.id}.json`);
411-
this.store.createRecord("post", postData);
420+
const post = this.store.createRecord("post", postData);
421+
post.topic = this.topic;
412422
} catch {
413423
// Post may not be visible
414424
}
415425
}
416426

427+
_markPostDeletedLocally(postId) {
428+
for (const post of this.postRegistry.values()) {
429+
if (post.id === postId) {
430+
post.set("deleted", true);
431+
post.set("deleted_post_placeholder", true);
432+
post.set("cooked", "");
433+
break;
434+
}
435+
}
436+
}
437+
417438
@action
418439
async loadNewRoots() {
419440
const ids = [...this.newRootPostIds];
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import getURL from "discourse/lib/get-url";
2+
13
export default function nestedPostUrl(topic, postNumber) {
2-
return `/nested/${topic.slug}/${topic.id}?post_number=${postNumber}`;
4+
return getURL(`/nested/${topic.slug}/${topic.id}?post_number=${postNumber}`);
35
}

lib/discourse_nested_replies/post_preloader.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,35 @@ def pluck(*columns)
6262
map { |record| columns.map { |col| record.public_send(col) } }
6363
end
6464
end
65+
66+
def where(conditions = nil, *rest)
67+
return self if conditions.nil?
68+
result =
69+
select do |record|
70+
if conditions.is_a?(Hash)
71+
conditions.all? { |k, v| record.public_send(k) == v }
72+
else
73+
true
74+
end
75+
end
76+
PostsArray.new(result)
77+
end
78+
79+
def limit(_n)
80+
self
81+
end
82+
83+
def order(*_args)
84+
self
85+
end
86+
87+
def not(*_args)
88+
self
89+
end
90+
91+
def reorder(*_args)
92+
self
93+
end
6594
end
6695

6796
# Batch-preload associations that plugin serializer extensions access per-post.

0 commit comments

Comments
 (0)