mirror of
https://github.com/discourse/discourse.git
synced 2025-06-07 11:54:44 +08:00
DEV: Dock glimmer-topic-timeline with animation (#20166)
- Move docking logic (intersection / dockAt / etc) from `glimmer-topic-timeline` -> `topic-timeline/container` to live alongside the `postScrolled` hook. - Toggle `timeline-docked` and `timeline-docked-bottom` when we are at the bottom of a topic. This returns the missing animation to the glimmer-topic-timeline (pictured below). https://user-images.githubusercontent.com/50783505/216655735-906ccd2a-b77e-45af-9a7b-c22680eca2dc.mov
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
<div
|
<div
|
||||||
class={{concat "timeline-container " this.classes}}
|
class={{concat-class "timeline-container" this.classes}}
|
||||||
{{did-insert this.addShowClass}}
|
{{did-insert this.addShowClass}}
|
||||||
>
|
>
|
||||||
<div class="topic-timeline" {{did-insert this.addUserTip}}>
|
<div class="topic-timeline" {{did-insert this.addUserTip}}>
|
||||||
@ -13,7 +13,6 @@
|
|||||||
@jumpToPostPrompt={{@jumpToPostPrompt}}
|
@jumpToPostPrompt={{@jumpToPostPrompt}}
|
||||||
@fullscreen={{@fullscreen}}
|
@fullscreen={{@fullscreen}}
|
||||||
@mobileView={{@mobileView}}
|
@mobileView={{@mobileView}}
|
||||||
@currentUser={{this.currentUser}}
|
|
||||||
@toggleMultiSelect={{@toggleMultiSelect}}
|
@toggleMultiSelect={{@toggleMultiSelect}}
|
||||||
@showTopicSlowModeUpdate={{@showTopicSlowModeUpdate}}
|
@showTopicSlowModeUpdate={{@showTopicSlowModeUpdate}}
|
||||||
@deleteTopic={{@deleteTopic}}
|
@deleteTopic={{@deleteTopic}}
|
||||||
@ -28,6 +27,8 @@
|
|||||||
@convertToPublicTopic={{@convertToPublicTopic}}
|
@convertToPublicTopic={{@convertToPublicTopic}}
|
||||||
@convertToPrivateMessage={{@convertToPrivateMessage}}
|
@convertToPrivateMessage={{@convertToPrivateMessage}}
|
||||||
@replyToPost={{@replyToPost}}
|
@replyToPost={{@replyToPost}}
|
||||||
|
@setDocked={{this.setDocked}}
|
||||||
|
@setDockedBottom={{this.setDockedBottom}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -4,18 +4,17 @@ import optionalService from "discourse/lib/optional-service";
|
|||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
|
||||||
export default class GlimmerTopicTimeline extends Component {
|
export default class GlimmerTopicTimeline extends Component {
|
||||||
@service site;
|
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
|
|
||||||
@tracked dockAt = null;
|
|
||||||
@tracked dockBottom = null;
|
|
||||||
@tracked enteredIndex = this.args.enteredIndex;
|
@tracked enteredIndex = this.args.enteredIndex;
|
||||||
|
@tracked docked = false;
|
||||||
|
@tracked dockedBottom = false;
|
||||||
|
|
||||||
adminTools = optionalService();
|
adminTools = optionalService();
|
||||||
intersectionObserver = null;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
@ -23,29 +22,6 @@ export default class GlimmerTopicTimeline extends Component {
|
|||||||
if (this.args.prevEvent) {
|
if (this.args.prevEvent) {
|
||||||
this.enteredIndex = this.args.prevEvent.postIndex - 1;
|
this.enteredIndex = this.args.prevEvent.postIndex - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.site.mobileView) {
|
|
||||||
this.intersectionObserver = new IntersectionObserver((entries) => {
|
|
||||||
for (const entry of entries) {
|
|
||||||
const bounds = entry.boundingClientRect;
|
|
||||||
|
|
||||||
if (entry.target.id === "topic-bottom") {
|
|
||||||
this.topicBottom = bounds.y + window.scrollY;
|
|
||||||
} else {
|
|
||||||
this.topicTop = bounds.y + window.scrollY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const elements = [
|
|
||||||
document.querySelector(".container.posts"),
|
|
||||||
document.querySelector("#topic-bottom"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < elements.length; i++) {
|
|
||||||
this.intersectionObserver.observe(elements[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get displaySummary() {
|
get displaySummary() {
|
||||||
@ -57,15 +33,19 @@ export default class GlimmerTopicTimeline extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get createdAt() {
|
||||||
|
return new Date(this.args.model.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
get classes() {
|
get classes() {
|
||||||
const classes = [];
|
const classes = [];
|
||||||
if (this.args.fullscreen) {
|
if (this.args.fullscreen) {
|
||||||
classes.push("timeline-fullscreen");
|
classes.push("timeline-fullscreen");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.dockAt) {
|
if (this.docked) {
|
||||||
classes.push("timeline-docked");
|
classes.push("timeline-docked");
|
||||||
if (this.dockBottom) {
|
if (this.dockedBottom) {
|
||||||
classes.push("timeline-docked-bottom");
|
classes.push("timeline-docked-bottom");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,10 +53,6 @@ export default class GlimmerTopicTimeline extends Component {
|
|||||||
return classes.join(" ");
|
return classes.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
get createdAt() {
|
|
||||||
return new Date(this.args.model.created_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
addShowClass(element) {
|
addShowClass(element) {
|
||||||
if (this.args.fullscreen && !this.args.addShowClass) {
|
if (this.args.fullscreen && !this.args.addShowClass) {
|
||||||
@ -96,10 +72,17 @@ export default class GlimmerTopicTimeline extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
willDestroy() {
|
@action
|
||||||
if (!this.site.mobileView) {
|
setDocked(value) {
|
||||||
this.intersectionObserver?.disconnect();
|
if (this.docked !== value) {
|
||||||
this.intersectionObserver = null;
|
this.docked = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setDockedBottom(value) {
|
||||||
|
if (this.dockedBottom !== value) {
|
||||||
|
this.dockedBottom = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if (and (not @fullscreen) @currentUser)}}
|
{{#if (and (not @fullscreen) this.currentUser)}}
|
||||||
<div class="timeline-controls">
|
<div class="timeline-controls">
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
@name="timeline-controls-before"
|
@name="timeline-controls-before"
|
||||||
@ -130,7 +130,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if (and @currentUser (not @fullscreen))}}
|
{{#if (and this.currentUser (not @fullscreen))}}
|
||||||
{{#if this.canCreatePost}}
|
{{#if this.canCreatePost}}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -156,7 +156,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if @currentUser}}
|
{{#if this.currentUser}}
|
||||||
<TopicNotificationsButton
|
<TopicNotificationsButton
|
||||||
@notificationLevel={{@model.details.notification_level}}
|
@notificationLevel={{@model.details.notification_level}}
|
||||||
@topic={{@model}}
|
@topic={{@model}}
|
||||||
|
@ -8,6 +8,7 @@ import { inject as service } from "@ember/service";
|
|||||||
import { bind, debounce } from "discourse-common/utils/decorators";
|
import { bind, debounce } from "discourse-common/utils/decorators";
|
||||||
import { actionDescriptionHtml } from "discourse/widgets/post-small-action";
|
import { actionDescriptionHtml } from "discourse/widgets/post-small-action";
|
||||||
import domUtils from "discourse-common/utils/dom-utils";
|
import domUtils from "discourse-common/utils/dom-utils";
|
||||||
|
import { headerOffset } from "discourse/lib/offset-calculator";
|
||||||
|
|
||||||
export const SCROLLER_HEIGHT = 50;
|
export const SCROLLER_HEIGHT = 50;
|
||||||
const MIN_SCROLLAREA_HEIGHT = 170;
|
const MIN_SCROLLAREA_HEIGHT = 170;
|
||||||
@ -17,6 +18,7 @@ const LAST_READ_HEIGHT = 20;
|
|||||||
export default class TopicTimelineScrollArea extends Component {
|
export default class TopicTimelineScrollArea extends Component {
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
@tracked showButton = false;
|
@tracked showButton = false;
|
||||||
@tracked current;
|
@tracked current;
|
||||||
@ -35,6 +37,8 @@ export default class TopicTimelineScrollArea extends Component {
|
|||||||
@tracked dragging = false;
|
@tracked dragging = false;
|
||||||
@tracked excerpt = "";
|
@tracked excerpt = "";
|
||||||
|
|
||||||
|
intersectionObserver = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
|
||||||
@ -48,7 +52,29 @@ export default class TopicTimelineScrollArea extends Component {
|
|||||||
this.appEvents.on("post-stream:posted", this.calculatePosition);
|
this.appEvents.on("post-stream:posted", this.calculatePosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.intersectionObserver = new IntersectionObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const bounds = entry.boundingClientRect;
|
||||||
|
|
||||||
|
if (entry.target.id === "topic-bottom") {
|
||||||
|
this.topicBottom = bounds.y + window.scrollY;
|
||||||
|
} else {
|
||||||
|
this.topicTop = bounds.y + window.scrollY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const elements = [
|
||||||
|
document.querySelector(".container.posts"),
|
||||||
|
document.querySelector("#topic-bottom"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < elements.length; i++) {
|
||||||
|
this.intersectionObserver.observe(elements[i]);
|
||||||
|
}
|
||||||
|
|
||||||
this.calculatePosition();
|
this.calculatePosition();
|
||||||
|
this.dockCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
get displayTimeLineScrollArea() {
|
get displayTimeLineScrollArea() {
|
||||||
@ -269,6 +295,7 @@ export default class TopicTimelineScrollArea extends Component {
|
|||||||
this.current = e.postIndex;
|
this.current = e.postIndex;
|
||||||
this.percentage = e.percent;
|
this.percentage = e.percent;
|
||||||
this.calculatePosition();
|
this.calculatePosition();
|
||||||
|
this.dockCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -276,6 +303,40 @@ export default class TopicTimelineScrollArea extends Component {
|
|||||||
this.args.jumpToIndex(this.lastRead);
|
this.args.jumpToIndex(this.lastRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dockCheck() {
|
||||||
|
const timeline = document.querySelector(".timeline-container");
|
||||||
|
const timelineHeight = (timeline && timeline.offsetHeight) || 400;
|
||||||
|
|
||||||
|
const prevDockAt = this.dockAt;
|
||||||
|
const positionTop = headerOffset() + window.pageYOffset;
|
||||||
|
const currentPosition = positionTop + timelineHeight;
|
||||||
|
|
||||||
|
this.dockBottom = false;
|
||||||
|
if (positionTop < this.topicTop) {
|
||||||
|
this.dockAt = parseInt(this.topicTop, 10);
|
||||||
|
} else if (currentPosition > this.topicBottom) {
|
||||||
|
this.dockAt = parseInt(this.topicBottom - timelineHeight, 10);
|
||||||
|
this.dockBottom = true;
|
||||||
|
if (this.dockAt < 0) {
|
||||||
|
this.dockAt = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.dockAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dockAt !== prevDockAt) {
|
||||||
|
if (this.dockAt) {
|
||||||
|
this.args.setDocked(true);
|
||||||
|
if (this.dockBottom) {
|
||||||
|
this.args.setDockedBottom(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.args.setDocked(false);
|
||||||
|
this.args.setDockedBottom(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
commit() {
|
commit() {
|
||||||
this.calculatePosition();
|
this.calculatePosition();
|
||||||
|
|
||||||
@ -298,6 +359,9 @@ export default class TopicTimelineScrollArea extends Component {
|
|||||||
|
|
||||||
willDestroy() {
|
willDestroy() {
|
||||||
if (!this.args.mobileView) {
|
if (!this.args.mobileView) {
|
||||||
|
this.intersectionObserver?.disconnect();
|
||||||
|
this.intersectionObserver = null;
|
||||||
|
|
||||||
this.appEvents.off("composer:opened", this.calculatePosition);
|
this.appEvents.off("composer:opened", this.calculatePosition);
|
||||||
this.appEvents.off("composer:resized", this.calculatePosition);
|
this.appEvents.off("composer:resized", this.calculatePosition);
|
||||||
this.appEvents.off("composer:closed", this.calculatePosition);
|
this.appEvents.off("composer:closed", this.calculatePosition);
|
||||||
|
Reference in New Issue
Block a user