Skip to content

Commit 69af463

Browse files
megothssdavidtaylorhq
authored andcommitted
DEV: Modernize the remaining of the post stream to Glimmer components (#32653)
Co-authored-by: David Taylor <david@taylorhq.com>
1 parent eb19018 commit 69af463

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2219
-820
lines changed

app/assets/javascripts/discourse/app/components/avatar-flair.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { observes } from "@ember-decorators/object";
22
import MountWidget from "discourse/components/mount-widget";
33

4+
// TODO (glimmer-post-stream): this component needs to be converted to Glimmer
45
export default class AvatarFlair extends MountWidget {
56
widget = "avatar-flair";
67

app/assets/javascripts/discourse/app/components/glimmer-site-header.gjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export default class GlimmerSiteHeader extends Component {
155155
parseInt(docStyle.getPropertyValue("--header-offset"), 10) || 0;
156156
const newHeaderOffset = Math.floor(headerWrapBottom);
157157
if (currentHeaderOffset !== newHeaderOffset) {
158+
this.header.headerOffset = newHeaderOffset;
158159
docStyle.setProperty("--header-offset", `${newHeaderOffset}px`);
159160
}
160161

@@ -164,6 +165,7 @@ export default class GlimmerSiteHeader extends Component {
164165
headerWrapBottom + mainOutletOffsetTop
165166
);
166167
if (currentMainOutletOffset !== newMainOutletOffset) {
168+
this.header.mainOutletOffset = newMainOutletOffset;
167169
docStyle.setProperty("--main-outlet-offset", `${newMainOutletOffset}px`);
168170
}
169171
}
@@ -199,6 +201,10 @@ export default class GlimmerSiteHeader extends Component {
199201
);
200202
this._resizeObserver.observe(document.querySelector(".discourse-root"));
201203
}
204+
205+
// the resize observer will not trigger on the first render, so we need to call it manually to get the initial value
206+
// set just after the header is inserted
207+
this.recalculateHeaderOffset();
202208
}
203209

204210
_handleArrowKeysNav(event) {

app/assets/javascripts/discourse/app/components/invite-panel.gjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ export default class InvitePanel extends Component {
348348
.then(() => {
349349
model.setProperties({ saving: false, finished: true });
350350
this.inviteModel.reload().then(() => {
351+
// TODO (glimmer-post-stream) the Glimmer Post Stream does not listen to this event
351352
this.appEvents.trigger("post-stream:refresh");
352353
});
353354
})
@@ -361,6 +362,7 @@ export default class InvitePanel extends Component {
361362
this.get("inviteModel.details.allowed_users").pushObject(
362363
EmberObject.create(result.user)
363364
);
365+
// TODO (glimmer-post-stream) the Glimmer Post Stream does not listen to this event
364366
this.appEvents.trigger("post-stream:refresh", { force: true });
365367
} else if (
366368
this.invitingToTopic &&

app/assets/javascripts/discourse/app/components/modal/history.gjs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,10 @@ export default class History extends Component {
193193
return;
194194
}
195195

196-
postStream
197-
.triggerChangedPost(postId, this.args.model)
198-
.then(() =>
199-
this.appEvents.trigger("post-stream:refresh", { id: postId })
200-
);
196+
postStream.triggerChangedPost(postId, this.args.model).then(() =>
197+
// TODO (glimmer-post-stream) the Glimmer Post Stream does not listen to this event
198+
this.appEvents.trigger("post-stream:refresh", { id: postId })
199+
);
201200
} finally {
202201
this.loading = false;
203202
this.initialLoad = false;
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import Component from "@glimmer/component";
2+
import { cached, tracked } from "@glimmer/tracking";
3+
import { concat, fn, get, hash } from "@ember/helper";
4+
import { action } from "@ember/object";
5+
import { schedule } from "@ember/runloop";
6+
import { service } from "@ember/service";
7+
import { and, not } from "truth-helpers";
8+
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
9+
import LoadMore from "discourse/components/load-more";
10+
import PostFilteredNotice from "discourse/components/post/filtered-notice";
11+
import { bind } from "discourse/lib/decorators";
12+
import offsetCalculator from "discourse/lib/offset-calculator";
13+
import { Placeholder } from "discourse/lib/posts-with-placeholders";
14+
import PostStreamViewportTracker from "discourse/modifiers/post-stream-viewport-tracker";
15+
import Post from "./post";
16+
import PostGap from "./post/gap";
17+
import PostPlaceholder from "./post/placeholder";
18+
import PostSmallAction from "./post/small-action";
19+
import PostTimeGap from "./post/time-gap";
20+
import PostVisitedLine from "./post/visited-line";
21+
22+
const DAY_MS = 1000 * 60 * 60 * 24;
23+
24+
export default class PostStream extends Component {
25+
@service appEvents;
26+
@service capabilities;
27+
@service header;
28+
@service screenTrack;
29+
@service search;
30+
@service site;
31+
@service siteSettings;
32+
33+
@tracked cloakAbove;
34+
@tracked cloakBelow;
35+
36+
viewportTracker = new PostStreamViewportTracker();
37+
38+
willDestroy() {
39+
super.willDestroy(...arguments);
40+
41+
// clear pending references in the observer
42+
this.viewportTracker.destroy();
43+
}
44+
45+
get gapsBefore() {
46+
return this.args.gaps?.before || {};
47+
}
48+
49+
get gapsAfter() {
50+
return this.args.gaps?.after || {};
51+
}
52+
53+
@cached
54+
get posts() {
55+
const postsToRender = this.capabilities.isAndroid
56+
? this.args.postStream.posts
57+
: this.args.postStream.postsWithPlaceholders;
58+
59+
// TODO (glimmer-post-stream) ideally args.posts should be a TrackedArray
60+
return postsToRender.toArray();
61+
}
62+
63+
get firstAvailablePost() {
64+
return this.posts[0];
65+
}
66+
67+
get highlightTerm() {
68+
return this.search.highlightTerm;
69+
}
70+
71+
get lastAvailablePost() {
72+
return this.posts.at(-1);
73+
}
74+
75+
@cached
76+
get postTuples() {
77+
const posts = this.posts;
78+
79+
const length = posts.length;
80+
const result = [];
81+
82+
let i = 0;
83+
let previousPost = null;
84+
85+
while (i < length) {
86+
const post = posts[i];
87+
const nextPost = i < length - 1 ? posts[i + 1] : null;
88+
89+
result.push({ post, previousPost, nextPost });
90+
91+
previousPost = post;
92+
++i;
93+
}
94+
95+
return result;
96+
}
97+
98+
get shouldShowFilteredNotice() {
99+
return (
100+
this.args.streamFilters &&
101+
Object.keys(this.args.streamFilters).length &&
102+
(Object.keys(this.gapsBefore).length > 0 ||
103+
Object.keys(this.gapsAfter).length > 0)
104+
);
105+
}
106+
107+
isPlaceholder(post) {
108+
return post instanceof Placeholder;
109+
}
110+
111+
daysBetween(post1, post2) {
112+
const time1 = post1 ? new Date(post1.createdAt).getTime() : null;
113+
const time2 = post2 ? new Date(post2.createdAt).getTime() : null;
114+
115+
if (!time1 || !time2) {
116+
return null;
117+
}
118+
119+
return Math.floor((time2 - time1) / DAY_MS);
120+
}
121+
122+
@bind
123+
shouldShowTimeGap(daysSince) {
124+
return daysSince > this.siteSettings.show_time_gap_days;
125+
}
126+
127+
@bind
128+
shouldShowVisitedLine(post, arrayIndex) {
129+
const postsLength = this.posts.length;
130+
const maxPostNumber = postsLength > 0 ? this.posts.at(-1).post_number : 0;
131+
132+
return (
133+
arrayIndex !== postsLength - 1 && // do not show on the last post displayed
134+
maxPostNumber <= this.args.highestPostNumber && // do not show in the last existing post
135+
this.args.lastReadPostNumber === post.post_number
136+
);
137+
}
138+
139+
@action
140+
loadMoreAbove(post) {
141+
this.args.topVisibleChanged({
142+
post,
143+
refresh: () => {
144+
const refreshedElem =
145+
this.viewportTracker.postsOnScreen[post.post_number]?.element;
146+
147+
if (!refreshedElem) {
148+
return;
149+
}
150+
151+
// The getOffsetTop function calculates the total offset distance of an element from the top of the document.
152+
// Unlike `element.offsetTop` which only returns the offset relative to its nearest positioned ancestor, this
153+
// function recursively accumulates the offsetTop of an element and all of its offset parents(ancestors).
154+
// This ensures the total distance is measured from the very top of the document, accounting for any nested
155+
// elements and their respective offsets.
156+
const getOffsetTop = (element) => {
157+
if (!element) {
158+
return 0;
159+
}
160+
return element.offsetTop + getOffsetTop(element.offsetParent);
161+
};
162+
163+
window.scrollTo({
164+
top: getOffsetTop(refreshedElem) - offsetCalculator(),
165+
});
166+
167+
// This seems weird, but somewhat infrequently a rerender
168+
// will cause the browser to scroll to the top of the document
169+
// in Chrome. This makes sure the scroll works correctly if that
170+
// happens.
171+
schedule("afterRender", () => {
172+
window.scrollTo({
173+
top: getOffsetTop(refreshedElem) - offsetCalculator(),
174+
});
175+
});
176+
},
177+
});
178+
}
179+
180+
@action
181+
loadMoreBelow(post) {
182+
this.args.bottomVisibleChanged({ post });
183+
}
184+
185+
@action
186+
setCloakingBoundaries(above, below) {
187+
// requesting an animation frame to update the cloaking boundaries prevents Chrome from logging
188+
// [Violation] 'setTimeout' handler took <N>ms when scrolling fast
189+
requestAnimationFrame(() => {
190+
this.cloakAbove = above;
191+
this.cloakBelow = below;
192+
});
193+
}
194+
195+
<template>
196+
<ConditionalLoadingSpinner @condition={{@postStream.loadingAbove}} />
197+
<div
198+
class="post-stream"
199+
{{this.viewportTracker.setup
200+
currentPostChanged=@currentPostChanged
201+
currentPostScrolled=@currentPostScrolled
202+
headerOffset=this.header.headerOffset
203+
screenTrack=this.screenTrack
204+
setCloakingBoundaries=this.setCloakingBoundaries
205+
topicId=@topic.id
206+
}}
207+
>
208+
{{#if (and (not @postStream.loadingAbove) @postStream.canPrependMore)}}
209+
<LoadMore @action={{fn this.loadMoreAbove this.firstAvailablePost}} />
210+
{{/if}}
211+
212+
{{#each this.postTuples key="post.id" as |tuple index|}}
213+
{{#let
214+
tuple.post tuple.previousPost tuple.nextPost
215+
as |post previousPost nextPost|
216+
}}
217+
{{#if (this.isPlaceholder post)}}
218+
<PostPlaceholder />
219+
{{else}}
220+
{{#let (get this.gapsBefore post.id) as |gap|}}
221+
{{#if gap}}
222+
<PostGap
223+
@post={{post}}
224+
@gap={{gap}}
225+
@fillGap={{fn @fillGapBefore (hash post=post gap=gap)}}
226+
/>
227+
{{/if}}
228+
{{/let}}
229+
230+
{{#let (this.daysBetween previousPost post) as |daysSince|}}
231+
{{#if (this.shouldShowTimeGap daysSince)}}
232+
<PostTimeGap @daysSince={{daysSince}} />
233+
{{/if}}
234+
{{/let}}
235+
236+
{{#let
237+
(if post.isSmallAction PostSmallAction Post)
238+
(this.viewportTracker.getCloakingData
239+
post above=this.cloakAbove below=this.cloakBelow
240+
)
241+
as |PostComponent cloakingData|
242+
}}
243+
<PostComponent
244+
id={{concat "post_" post.post_number}}
245+
class={{if cloakingData.active "post-stream--cloaked"}}
246+
style={{cloakingData.style}}
247+
@cloaked={{cloakingData.active}}
248+
@post={{post}}
249+
@prevPost={{previousPost}}
250+
@nextPost={{nextPost}}
251+
@canCreatePost={{@canCreatePost}}
252+
@cancelFilter={{fn @cancelFilter post}}
253+
@changeNotice={{fn @changeNotice post}}
254+
@changePostOwner={{fn @changePostOwner post}}
255+
@deletePost={{fn @deletePost post}}
256+
@editPost={{fn @editPost post}}
257+
@expandHidden={{fn @expandHidden post}}
258+
@filteringRepliesToPostNumber={{@filteringRepliesToPostNumber}}
259+
@grantBadge={{fn @grantBadge post}}
260+
@highlightTerm={{this.highlightTerm}}
261+
@lockPost={{fn @lockPost post}}
262+
@multiSelect={{@multiSelect}}
263+
@permanentlyDeletePost={{fn @permanentlyDeletePost post}}
264+
@rebakePost={{fn @rebakePost post}}
265+
@recoverPost={{fn @recoverPost post}}
266+
@removeAllowedGroup={{fn @removeAllowedGroup post}}
267+
@removeAllowedUser={{fn @removeAllowedUser post}}
268+
@replyToPost={{fn @replyToPost post}}
269+
@selectBelow={{fn @selectBelow post}}
270+
@selectReplies={{fn @selectReplies post}}
271+
@selected={{if @multiSelect (@postSelected post)}}
272+
@showFlags={{fn @showFlags post}}
273+
@showHistory={{fn @showHistory post}}
274+
@showInvite={{fn @showInvite post}}
275+
@showLogin={{fn @showLogin post}}
276+
@showPagePublish={{fn @showPagePublish post}}
277+
@showRawEmail={{fn @showRawEmail post}}
278+
@showReadIndicator={{@showReadIndicator}}
279+
@togglePostSelection={{fn @togglePostSelection post}}
280+
@togglePostType={{fn @togglePostType post}}
281+
@toggleReplyAbove={{fn @toggleReplyAbove post}}
282+
@toggleWiki={{fn @toggleWiki post}}
283+
@topicPageQueryParams={{@topicPageQueryParams}}
284+
@unhidePost={{fn @unhidePost post}}
285+
@unlockPost={{fn @unlockPost post}}
286+
@updateTopicPageQueryParams={{@updateTopicPageQueryParams}}
287+
{{this.viewportTracker.registerPost post}}
288+
/>
289+
{{/let}}
290+
291+
{{#let (get this.gapsAfter post.id) as |gap|}}
292+
{{#if gap}}
293+
<PostGap
294+
@post={{post}}
295+
@gap={{gap}}
296+
@fillGap={{fn @fillGapAfter (hash post=post gap=gap)}}
297+
/>
298+
{{/if}}
299+
{{/let}}
300+
301+
{{#if (this.shouldShowVisitedLine post index)}}
302+
<PostVisitedLine @post={{post}} />
303+
{{/if}}
304+
{{/if}}
305+
{{/let}}
306+
{{/each}}
307+
308+
{{#unless @postStream.loadingBelow}}
309+
{{#if @postStream.canAppendMore}}
310+
<LoadMore @action={{fn this.loadMoreBelow this.lastAvailablePost}} />
311+
{{else}}
312+
<div
313+
class="post-stream__bottom-boundary"
314+
{{this.viewportTracker.registerBottomBoundary topicId=@topic.id}}
315+
></div>
316+
{{/if}}
317+
{{/unless}}
318+
319+
{{#if this.shouldShowFilteredNotice}}
320+
<PostFilteredNotice
321+
@posts={{this.posts}}
322+
@cancelFilter={{@cancelFilter}}
323+
@streamFilters={{@streamFilters}}
324+
@filteredPostsCount={{@filteredPostsCount}}
325+
/>
326+
{{/if}}
327+
</div>
328+
<ConditionalLoadingSpinner @condition={{@postStream.loadingBelow}} />
329+
</template>
330+
}

0 commit comments

Comments
 (0)