DEV: Upgrade Topic Timeline to Glimmer (#17377)
In an effort to modernize our codebase to the latest Ember version we have selected the Topic Timeline as a candidate to be refactored. The topic timeline component was originally built with `Widgets` and this PR will upgrade it to `Glimmer Components`. The refactored timeline is hidden by default behind a group flag, `SiteSetting.enable_experimental_topic_timeline_groups`. Being part of a group included in this site setting will make the new timeline available for testing. ## Other points of interest This PR introduces a `Draggable Modifier` available to all components, which will take the place of the existing _drag functionality_ exclusive to widgets. It can be included like so: ``` {{draggable didStartDrag=@didStartDrag didEndDrag=@didEndDrag dragMove=@dragMove }} ```
This commit is contained in:
parent
3a4ac3a7c0
commit
6ccc0227f3
|
@ -0,0 +1,72 @@
|
||||||
|
<div class={{concat "timeline-container " this.class}}>
|
||||||
|
<div class="topic-timeline">
|
||||||
|
<TopicTimeline::Container
|
||||||
|
@model={{@model}}
|
||||||
|
@enteredIndex={{this.enteredIndex}}
|
||||||
|
@jumpTop={{@jumpTop}}
|
||||||
|
@jumpBottom={{@jumpBottom}}
|
||||||
|
@jumpEnd={{@jumpEnd}}
|
||||||
|
@jumpToIndex={{@jumpToIndex}}
|
||||||
|
@fullscreen={{@fullscreen}}
|
||||||
|
@mobileView={{@mobileView}}
|
||||||
|
@currentUser={{this.currentUser}}
|
||||||
|
@toggleMultiSelect={{@toggleMultiSelect}}
|
||||||
|
@showTopicSlowModeUpdate={{@showTopicSlowModeUpdate}}
|
||||||
|
@deleteTopic={{@deleteTopic}}
|
||||||
|
@recoverTopic={{@recoverTopic}}
|
||||||
|
@toggleClosed={{@toggleClosed}}
|
||||||
|
@toggleArchived={{@toggleArchived}}
|
||||||
|
@toggleVisibility={{@toggleVisibility}}
|
||||||
|
@showTopicTimerModal={{@showTopicTimerModal}}
|
||||||
|
@showFeatureTopic={{@showFeatureTopic}}
|
||||||
|
@showChangeTimestamp={{@showChangeTimestamp}}
|
||||||
|
@resetBumpDate={{@resetBumpDate}}
|
||||||
|
@convertToPublicTopic={{@convertToPublicTopic}}
|
||||||
|
@convertToPrivateMessage={{@convertToPrivateMessage}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="timeline-footer-controls">
|
||||||
|
{{#if this.displaySummary}}
|
||||||
|
<button type="button" class="show-summary btn-small" label={{i18n "summary.short_label"}} title={{i18n "summary.short_title"}} {{on "click" @showSummary}}>
|
||||||
|
{{d-icon "layer-group"}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if (and this.currentUser (not @fullscreen))}}
|
||||||
|
{{#if this.canCreatePost}}
|
||||||
|
<button type="button" class="btn btn-default create reply-to-post no-text btn-icon" title={{i18n "topic.reply.help"}} {{on "click" (fn @replyToPost null)}}>
|
||||||
|
{{d-icon "reply"}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if @fullscreen}}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
{{!-- we need to keep this a widget-button to not close the modal when opening form --}}
|
||||||
|
class="widget-button btn btn-text jump-to-post"
|
||||||
|
title={{i18n "topic.progress.jump_prompt_long"}}
|
||||||
|
{{on "click" @jumpToPostPrompt}}
|
||||||
|
>
|
||||||
|
<span class="d-button-label">
|
||||||
|
{{i18n "topic.progress.jump_prompt"}}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.currentUser}}
|
||||||
|
<TopicNotificationsButton
|
||||||
|
@notificationLevel={{@model.details.notification_level}}
|
||||||
|
@topic={{@model}}
|
||||||
|
@showFullTitle={{false}}
|
||||||
|
@appendReason={{false}}
|
||||||
|
@placement={{"bottom-end"}}
|
||||||
|
@showCaret={{false}}
|
||||||
|
/>
|
||||||
|
{{#if @mobileView}}
|
||||||
|
<TopicAdminMenuButton @topic={{@model}} @addKeyboardTargetClass={{true}} @openUpwards={{true}} />
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,96 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import optionalService from "discourse/lib/optional-service";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class GlimmerTopicTimeline extends Component {
|
||||||
|
@service site;
|
||||||
|
@service siteSettings;
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
@tracked dockAt = null;
|
||||||
|
@tracked dockBottom = null;
|
||||||
|
@tracked enteredIndex = this.args.enteredIndex;
|
||||||
|
|
||||||
|
adminTools = optionalService();
|
||||||
|
intersectionObserver = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
if (this.args.prevEvent) {
|
||||||
|
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() {
|
||||||
|
return (
|
||||||
|
this.siteSettings.summary_timeline_button &&
|
||||||
|
!this.args.fullScreen &&
|
||||||
|
this.args.model.has_summary &&
|
||||||
|
!this.args.model.postStream.summary
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get class() {
|
||||||
|
const classes = [];
|
||||||
|
if (this.args.fullscreen) {
|
||||||
|
if (this.addShowClass) {
|
||||||
|
classes.push("timeline-fullscreen show");
|
||||||
|
} else {
|
||||||
|
classes.push("timeline-fullscreen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dockAt) {
|
||||||
|
classes.push("timeline-docked");
|
||||||
|
if (this.dockBottom) {
|
||||||
|
classes.push("timeline-docked-bottom");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
get addShowClass() {
|
||||||
|
return this.args.fullscreen && !this.args.addShowClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canCreatePost() {
|
||||||
|
return this.args.model.details?.can_create_post;
|
||||||
|
}
|
||||||
|
|
||||||
|
get createdAt() {
|
||||||
|
return new Date(this.args.model.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
if (!this.site.mobileView) {
|
||||||
|
this.intersectionObserver?.disconnect();
|
||||||
|
this.intersectionObserver = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
<DButton
|
||||||
|
@type="button"
|
||||||
|
@class="btn-primary btn-small back-button"
|
||||||
|
@title={{i18n "topic.timeline.back_description"}}
|
||||||
|
@onClick={{@onClick}}
|
||||||
|
>
|
||||||
|
{{i18n "topic.timeline.back"}}
|
||||||
|
</DButton>
|
|
@ -0,0 +1,97 @@
|
||||||
|
{{#if @fullscreen}}
|
||||||
|
<div class="title">
|
||||||
|
<h2>
|
||||||
|
<a class="fancy-title" href {{on "click" @jumpTop}}>{{if @mobileView @model.fancyTitle ""}}</a>
|
||||||
|
</h2>
|
||||||
|
{{#if (or this.siteSettings.topic_featured_link_enabled this.showTags)}}
|
||||||
|
<div class="topic-header-extra">
|
||||||
|
{{#if this.showTags}}
|
||||||
|
<div class="list-tags">
|
||||||
|
{{discourse-tags @model mode="list" tags=@model.tags}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.siteSettings.topic_featured_link_enabled}}
|
||||||
|
{{topic-featured-link @model}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if (and (not @model.isPrivateMessage) @model.category)}}
|
||||||
|
<div class="topic-category">
|
||||||
|
{{#if @model.category.parentCategory}}
|
||||||
|
{{category-link @model.category.parentCategory}}
|
||||||
|
{{/if}}
|
||||||
|
{{category-link @model.category}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.excerpt}}
|
||||||
|
<div class="post-excerpt">{{html-safe this.excerpt}}</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if (and (not @fullscreen) @currentUser)}}
|
||||||
|
<div class="timeline-controls">
|
||||||
|
<TopicAdminMenuButton
|
||||||
|
@topic={{@model}}
|
||||||
|
@addKeyboardTargetClass={{true}}
|
||||||
|
@toggleMultiSelect={{@toggleMultiSelect}}
|
||||||
|
@showTopicSlowModeUpdate={{@showTopicSlowModeUpdate}}
|
||||||
|
@deleteTopic={{@deleteTopic}}
|
||||||
|
@recoverTopic={{@recoverTopic}}
|
||||||
|
@toggleClosed={{@toggleClosed}}
|
||||||
|
@toggleArchived={{@toggleArchived}}
|
||||||
|
@toggleVisibility={{@toggleVisibility}}
|
||||||
|
@showTopicTimerModal={{@showTopicTimerModal}}
|
||||||
|
@showFeatureTopic={{@showFeatureTopic}}
|
||||||
|
@showChangeTimestamp={{@showChangeTimestamp}}
|
||||||
|
@resetBumpDate={{@resetBumpDate}}
|
||||||
|
@convertToPublicTopic={{@convertToPublicTopic}}
|
||||||
|
@convertToPrivateMessage={{@convertToPrivateMessage}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.displayTimeLineScrollArea}}
|
||||||
|
<div class="timeline-scrollarea-wrapper">
|
||||||
|
<div class="timeline-date-wrapper">
|
||||||
|
<a class="start-date" onClick={{this.updatePercentage}} title={{this.startDate}}>
|
||||||
|
<span>
|
||||||
|
{{this.startDate}}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-scrollarea" style={{this.timelineScrollareaStyle}}>
|
||||||
|
<div class="timeline-padding" style={{this.beforePadding}} onClick={{this.updatePercentage}}></div>
|
||||||
|
<TopicTimeline::Scroller
|
||||||
|
@current={{this.current}}
|
||||||
|
@total={{this.total}}
|
||||||
|
@goBack={{this.goBack}}
|
||||||
|
@fullscreen={{@fullscreen}}
|
||||||
|
@showDockedButton={{this.showDockedButton}}
|
||||||
|
@date={{this.date}}
|
||||||
|
@didStartDrag={{this.didStartDrag}}
|
||||||
|
@dragMove={{this.dragMove}}
|
||||||
|
@didEndDrag={{this.didEndDrag}}
|
||||||
|
/>
|
||||||
|
<div class="timeline-padding" style={{this.afterPadding}} onClick={{this.updatePercentage}}></div>
|
||||||
|
|
||||||
|
{{#if this.hasBackPosition}}
|
||||||
|
<div class="timeline-last-read" style={{this.lastReadStyle}}>
|
||||||
|
{{d-icon "minus" class="progress"}}
|
||||||
|
{{#if this.showButton}}
|
||||||
|
<TopicTimeline::BackButton @onClick={{this.goBack}}/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline-date-wrapper">
|
||||||
|
<a class="now-date" onClick={{this.updatePercentage}} title={{this.nowDate}}>
|
||||||
|
<span>
|
||||||
|
{{this.nowDate}}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,335 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { relativeAge } from "discourse/lib/formatter";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { bind, debounce } from "discourse-common/utils/decorators";
|
||||||
|
import { actionDescriptionHtml } from "discourse/widgets/post-small-action";
|
||||||
|
import domUtils from "discourse-common/utils/dom-utils";
|
||||||
|
|
||||||
|
export const SCROLLER_HEIGHT = 50;
|
||||||
|
const MIN_SCROLLAREA_HEIGHT = 170;
|
||||||
|
const MAX_SCROLLAREA_HEIGHT = 300;
|
||||||
|
const LAST_READ_HEIGHT = 20;
|
||||||
|
|
||||||
|
export default class TopicTimelineScrollArea extends Component {
|
||||||
|
@service appEvents;
|
||||||
|
@service siteSettings;
|
||||||
|
|
||||||
|
@tracked showButton = false;
|
||||||
|
@tracked current;
|
||||||
|
@tracked percentage = this._percentFor(
|
||||||
|
this.args.model,
|
||||||
|
this.args.enteredIndex
|
||||||
|
);
|
||||||
|
@tracked total;
|
||||||
|
@tracked date;
|
||||||
|
@tracked lastReadPercentage = null;
|
||||||
|
@tracked displayTimeLineScrollArea = true;
|
||||||
|
@tracked before;
|
||||||
|
@tracked after;
|
||||||
|
@tracked timelineScrollareaStyle;
|
||||||
|
@tracked dragging = false;
|
||||||
|
@tracked excerpt = "";
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
if (!this.args.mobileView) {
|
||||||
|
const streamLength = this.args.model.postStream?.stream?.length;
|
||||||
|
|
||||||
|
if (streamLength === 1) {
|
||||||
|
const postsWrapper = document.querySelector(".posts-wrapper");
|
||||||
|
if (postsWrapper && postsWrapper.offsetHeight < 1000) {
|
||||||
|
this.displayTimeLineScrollArea = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen for scrolling event to update timeline
|
||||||
|
this.appEvents.on("topic:current-post-scrolled", this.postScrolled);
|
||||||
|
// listen for composer sizing changes to update timeline
|
||||||
|
this.appEvents.on("composer:opened", this.calculatePosition);
|
||||||
|
this.appEvents.on("composer:resized", this.calculatePosition);
|
||||||
|
this.appEvents.on("composer:closed", this.calculatePosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.calculatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
get showTags() {
|
||||||
|
return (
|
||||||
|
this.siteSettings.tagging_enabled && this.args.model.tags?.length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get style() {
|
||||||
|
return htmlSafe(`height: ${scrollareaHeight()}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get beforePadding() {
|
||||||
|
return htmlSafe(`height: ${this.before}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get afterPadding() {
|
||||||
|
return htmlSafe(`height: ${this.after}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get showDockedButton() {
|
||||||
|
return !this.args.mobileView && this.hasBackPosition && !this.showButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasBackPosition() {
|
||||||
|
return (
|
||||||
|
this.lastRead &&
|
||||||
|
this.lastRead > 3 &&
|
||||||
|
this.lastRead > this.current &&
|
||||||
|
Math.abs(this.lastRead - this.current) > 3 &&
|
||||||
|
Math.abs(this.lastRead - this.total) > 1 &&
|
||||||
|
this.lastRead !== this.total
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastReadStyle() {
|
||||||
|
return htmlSafe(
|
||||||
|
`height: ${LAST_READ_HEIGHT}px; top: ${this.topPosition}px`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get topPosition() {
|
||||||
|
const bottom = scrollareaHeight() - LAST_READ_HEIGHT / 2;
|
||||||
|
return this.lastReadTop > bottom ? bottom : this.lastReadTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
get bottomAge() {
|
||||||
|
return relativeAge(
|
||||||
|
new Date(this.args.model.last_posted_at || this.args.model.created_at),
|
||||||
|
{
|
||||||
|
addAgo: true,
|
||||||
|
defaultFormat: timelineDate,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get startDate() {
|
||||||
|
return timelineDate(this.args.model.createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
get nowDate() {
|
||||||
|
return this.bottomAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastReadHeight() {
|
||||||
|
return Math.round(this.lastReadPercentage * scrollareaHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
calculatePosition() {
|
||||||
|
this.timelineScrollareaStyle = htmlSafe(`height: ${scrollareaHeight()}px`);
|
||||||
|
|
||||||
|
const topic = this.args.model;
|
||||||
|
const postStream = topic.postStream;
|
||||||
|
this.total = postStream.filteredPostsCount;
|
||||||
|
|
||||||
|
this.scrollPosition =
|
||||||
|
this.clamp(Math.floor(this.total * this.percentage), 0, this.total) + 1;
|
||||||
|
|
||||||
|
this.current = this.clamp(this.scrollPosition, 1, this.total);
|
||||||
|
const daysAgo = postStream.closestDaysAgoFor(this.current);
|
||||||
|
|
||||||
|
let date;
|
||||||
|
if (daysAgo === undefined) {
|
||||||
|
const post = postStream.posts.findBy(
|
||||||
|
"id",
|
||||||
|
postStream.stream[this.current]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (post) {
|
||||||
|
date = new Date(post.created_at);
|
||||||
|
}
|
||||||
|
} else if (daysAgo !== null) {
|
||||||
|
date = new Date();
|
||||||
|
date.setDate(date.getDate() - daysAgo || 0);
|
||||||
|
} else {
|
||||||
|
date = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.date = date;
|
||||||
|
|
||||||
|
const lastReadId = topic.last_read_post_id;
|
||||||
|
const lastReadNumber = topic.last_read_post_number;
|
||||||
|
|
||||||
|
if (lastReadId && lastReadNumber) {
|
||||||
|
const idx = postStream.stream.indexOf(lastReadId) + 1;
|
||||||
|
this.lastRead = idx;
|
||||||
|
this.lastReadPercentage = this._percentFor(topic, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.position !== this.scrollPosition) {
|
||||||
|
this.position = this.scrollPosition;
|
||||||
|
this.updateScrollPosition(this.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.before = this.scrollareaRemaining() * this.percentage;
|
||||||
|
this.after = scrollareaHeight() - this.before - SCROLLER_HEIGHT;
|
||||||
|
|
||||||
|
if (this.percentage === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasBackPosition) {
|
||||||
|
this.lastReadTop = Math.round(
|
||||||
|
this.lastReadPercentage * scrollareaHeight()
|
||||||
|
);
|
||||||
|
this.showButton =
|
||||||
|
this.before + SCROLLER_HEIGHT - 5 < this.lastReadTop ||
|
||||||
|
this.before > this.lastReadTop + 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasBackPosition) {
|
||||||
|
this.lastReadTop = Math.round(
|
||||||
|
this.lastReadPercentage * scrollareaHeight()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@debounce(50)
|
||||||
|
updateScrollPosition(scrollPosition) {
|
||||||
|
// only ran on mobile
|
||||||
|
if (!this.args.fullscreen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = this.args.model.postStream;
|
||||||
|
|
||||||
|
if (!this.position === scrollPosition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have an off by one, stream is zero based,
|
||||||
|
stream.excerpt(scrollPosition - 1).then((info) => {
|
||||||
|
if (info && this.position === scrollPosition) {
|
||||||
|
let excerpt = "";
|
||||||
|
if (info.username) {
|
||||||
|
excerpt = "<span class='username'>" + info.username + ":</span> ";
|
||||||
|
}
|
||||||
|
if (info.excerpt) {
|
||||||
|
this.excerpt = excerpt + info.excerpt;
|
||||||
|
} else if (info.action_code) {
|
||||||
|
this.excerpt = `${excerpt} ${actionDescriptionHtml(
|
||||||
|
info.action_code,
|
||||||
|
info.created_at,
|
||||||
|
info.username
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
updatePercentage(e) {
|
||||||
|
// pageY for mouse and mobile
|
||||||
|
const y = e.pageY || e.touches[0].pageY;
|
||||||
|
const area = document.querySelector(".timeline-scrollarea");
|
||||||
|
const areaTop = domUtils.offset(area).top;
|
||||||
|
|
||||||
|
this.percentage = this.clamp(parseFloat(y - areaTop) / area.offsetHeight);
|
||||||
|
this.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
didStartDrag() {
|
||||||
|
this.dragging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
dragMove(e) {
|
||||||
|
this.updatePercentage(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
didEndDrag() {
|
||||||
|
this.dragging = false;
|
||||||
|
this.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
postScrolled(e) {
|
||||||
|
this.current = e.postIndex;
|
||||||
|
this.percentage = e.percent;
|
||||||
|
this.calculatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
goBack() {
|
||||||
|
this.args.jumpToIndex(this.lastRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
commit() {
|
||||||
|
this.calculatePosition();
|
||||||
|
|
||||||
|
if (!this.dragging) {
|
||||||
|
if (this.current === this.scrollPosition) {
|
||||||
|
this.args.jumpToIndex(this.current);
|
||||||
|
} else {
|
||||||
|
this.args.jumpEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clamp(p, min = 0.0, max = 1.0) {
|
||||||
|
return Math.max(Math.min(p, max), min);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollareaRemaining() {
|
||||||
|
return scrollareaHeight() - SCROLLER_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
if (!this.args.mobileView) {
|
||||||
|
this.appEvents.off("composer:opened", this.calculatePosition);
|
||||||
|
this.appEvents.off("composer:resized", this.calculatePosition);
|
||||||
|
this.appEvents.off("composer:closed", this.calculatePosition);
|
||||||
|
this.appEvents.off("topic:current-post-scrolled", this.postScrolled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_percentFor(topic, postIndex) {
|
||||||
|
const total = topic.postStream.filteredPostsCount;
|
||||||
|
switch (postIndex) {
|
||||||
|
// if first post, no top padding
|
||||||
|
case 0:
|
||||||
|
return 0;
|
||||||
|
// if last, no bottom padding
|
||||||
|
case total - 1:
|
||||||
|
return 1;
|
||||||
|
// otherwise, calculate
|
||||||
|
default:
|
||||||
|
return this.clamp(parseFloat(postIndex) / total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scrollareaHeight() {
|
||||||
|
const composerHeight =
|
||||||
|
document.getElementById("reply-control").offsetHeight || 0,
|
||||||
|
headerHeight = document.querySelector(".d-header")?.offsetHeight || 0;
|
||||||
|
|
||||||
|
// scrollarea takes up about half of the timeline's height
|
||||||
|
const availableHeight =
|
||||||
|
(window.innerHeight - composerHeight - headerHeight) / 2;
|
||||||
|
|
||||||
|
return Math.max(
|
||||||
|
MIN_SCROLLAREA_HEIGHT,
|
||||||
|
Math.min(availableHeight, MAX_SCROLLAREA_HEIGHT)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timelineDate(date) {
|
||||||
|
const fmt =
|
||||||
|
date.getFullYear() === new Date().getFullYear()
|
||||||
|
? "long_no_year_no_time"
|
||||||
|
: "timeline_date";
|
||||||
|
return moment(date).format(I18n.t(`dates.${fmt}`));
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
<div
|
||||||
|
style={{this.style}}
|
||||||
|
class="timeline-scroller"
|
||||||
|
{{draggable didStartDrag=@didStartDrag didEndDrag=@didEndDrag dragMove=@dragMove }}
|
||||||
|
>
|
||||||
|
{{#if @fullscreen}}
|
||||||
|
<div class="timeline-scroller-content">
|
||||||
|
<div class="timeline-replies">
|
||||||
|
{{this.repliesShort}}
|
||||||
|
</div>
|
||||||
|
{{#if @date}}
|
||||||
|
<div class="timeline-ago">
|
||||||
|
{{this.timelineAgo}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (and @showDockedButton (not @dragging)) }}
|
||||||
|
<TopicTimeline::BackButton @onClick={{@goBack}}/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="timeline-handle" />
|
||||||
|
{{else}}
|
||||||
|
<div class="timeline-handle" />
|
||||||
|
<div class="timeline-scroller-content">
|
||||||
|
<div class="timeline-replies">
|
||||||
|
{{this.repliesShort}}
|
||||||
|
</div>
|
||||||
|
{{#if @date}}
|
||||||
|
<div class="timeline-ago">
|
||||||
|
{{this.timelineAgo}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (and @showDockedButton (not @dragging)) }}
|
||||||
|
<TopicTimeline::BackButton @onClick={{@goBack}}/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import {
|
||||||
|
SCROLLER_HEIGHT,
|
||||||
|
timelineDate,
|
||||||
|
} from "discourse/components/topic-timeline/container";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
|
||||||
|
export default class TopicTimelineScroller extends Component {
|
||||||
|
style = htmlSafe(`height: ${SCROLLER_HEIGHT}px`);
|
||||||
|
|
||||||
|
get repliesShort() {
|
||||||
|
const current = this.args.current;
|
||||||
|
const total = this.args.total;
|
||||||
|
return I18n.t(`topic.timeline.replies_short`, { current, total });
|
||||||
|
}
|
||||||
|
|
||||||
|
get timelineAgo() {
|
||||||
|
return timelineDate(this.args.date);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import Modifier from "ember-modifier";
|
||||||
|
import { registerDestructor } from "@ember/destroyable";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
|
export default class DraggableModifier extends Modifier {
|
||||||
|
hasStarted = false;
|
||||||
|
element;
|
||||||
|
|
||||||
|
constructor(owner, args) {
|
||||||
|
super(owner, args);
|
||||||
|
registerDestructor(this, (instance) => instance.cleanup());
|
||||||
|
}
|
||||||
|
|
||||||
|
modify(el, _, { didStartDrag, didEndDrag, dragMove }) {
|
||||||
|
this.element = el;
|
||||||
|
this.didStartDragCallback = didStartDrag;
|
||||||
|
this.didEndDragCallback = didEndDrag;
|
||||||
|
this.dragMoveCallback = dragMove;
|
||||||
|
this.element.addEventListener("touchstart", this.dragMove, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
this.element.addEventListener("mousedown", this.dragMove, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
dragMove(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.hasStarted) {
|
||||||
|
this.hasStarted = true;
|
||||||
|
|
||||||
|
if (this.didStartDragCallback) {
|
||||||
|
this.didStartDragCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a global event to capture mouse moves when element 'clicked'.
|
||||||
|
document.addEventListener("touchmove", this.drag, { passive: false });
|
||||||
|
document.addEventListener("mousemove", this.drag, { passive: false });
|
||||||
|
document.body.classList.add("dragging");
|
||||||
|
|
||||||
|
// On leaving click, stop moving.
|
||||||
|
document.addEventListener("touchend", this.didEndDrag, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseup", this.didEndDrag, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
drag(e) {
|
||||||
|
if (this.hasStarted && this.dragMoveCallback) {
|
||||||
|
this.dragMoveCallback(e, this.element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
didEndDrag(e) {
|
||||||
|
if (this.hasStarted) {
|
||||||
|
this.didEndDragCallback(e, this.element);
|
||||||
|
|
||||||
|
document.removeEventListener("touchmove", this.drag);
|
||||||
|
document.removeEventListener("mousemove", this.drag);
|
||||||
|
|
||||||
|
document.body.classList.remove("dragging");
|
||||||
|
this.hasStarted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
document.removeEventListener("touchstart", this.dragMove);
|
||||||
|
document.removeEventListener("mousedown", this.dragMove);
|
||||||
|
document.removeEventListener("touchend", this.didEndDrag);
|
||||||
|
document.removeEventListener("mouseup", this.didEndDrag);
|
||||||
|
document.removeEventListener("mousemove", this.drag);
|
||||||
|
document.removeEventListener("touchmove", this.drag);
|
||||||
|
document.body.classList.remove("dragging");
|
||||||
|
}
|
||||||
|
}
|
|
@ -107,7 +107,38 @@
|
||||||
<PluginOutlet @name="topic-navigation" @connectorTagName="div" @args={{hash topic=this.model}} />
|
<PluginOutlet @name="topic-navigation" @connectorTagName="div" @args={{hash topic=this.model}} />
|
||||||
|
|
||||||
{{#if info.renderTimeline}}
|
{{#if info.renderTimeline}}
|
||||||
<TopicTimeline @topic={{this.model}} @notificationLevel={{this.model.details.notification_level}} @prevEvent={{info.prevEvent}} @fullscreen={{info.topicProgressExpanded}} @enteredIndex={{this.enteredIndex}} @loading={{this.model.postStream.loading}} @jumpToPost={{action "jumpToPost"}} @jumpTop={{action "jumpTop"}} @jumpBottom={{action "jumpBottom"}} @jumpEnd={{action "jumpEnd"}} @jumpToPostPrompt={{action "jumpToPostPrompt"}} @jumpToIndex={{action "jumpToIndex"}} @replyToPost={{action "replyToPost"}} @showSummary={{action "showSummary"}} @toggleMultiSelect={{action "toggleMultiSelect"}} @showTopicSlowModeUpdate={{route-action "showTopicSlowModeUpdate"}} @deleteTopic={{action "deleteTopic"}} @recoverTopic={{action "recoverTopic"}} @toggleClosed={{action "toggleClosed"}} @toggleArchived={{action "toggleArchived"}} @toggleVisibility={{action "toggleVisibility"}} @showTopicTimerModal={{route-action "showTopicTimerModal"}} @showFeatureTopic={{route-action "showFeatureTopic"}} @showChangeTimestamp={{route-action "showChangeTimestamp"}} @resetBumpDate={{action "resetBumpDate"}} @convertToPublicTopic={{action "convertToPublicTopic"}} @convertToPrivateMessage={{action "convertToPrivateMessage"}} />
|
{{#if this.currentUser.redesigned_topic_timeline_enabled}}
|
||||||
|
<GlimmerTopicTimeline
|
||||||
|
@info={{info}}
|
||||||
|
@model={{this.model}}
|
||||||
|
@replyToPost={{action "replyToPost"}}
|
||||||
|
@showSummary={{action "showSummary"}}
|
||||||
|
@jumpToPostPrompt={{action "jumpToPostPrompt"}}
|
||||||
|
@enteredIndex={{this.enteredIndex}}
|
||||||
|
@prevEvent={{info.prevEvent}}
|
||||||
|
@jumpTop={{action "jumpTop"}}
|
||||||
|
@jumpBottom={{action "jumpBottom"}}
|
||||||
|
@jumpEnd={{action "jumpEnd"}}
|
||||||
|
@jumpToIndex={{action "jumpToIndex"}}
|
||||||
|
@toggleMultiSelect={{action "toggleMultiSelect"}}
|
||||||
|
@showTopicSlowModeUpdate={{route-action "showTopicSlowModeUpdate"}}
|
||||||
|
@deleteTopic={{action "deleteTopic"}}
|
||||||
|
@recoverTopic={{action "recoverTopic"}}
|
||||||
|
@toggleClosed={{action "toggleClosed"}}
|
||||||
|
@toggleArchived={{action "toggleArchived"}}
|
||||||
|
@toggleVisibility={{action "toggleVisibility"}}
|
||||||
|
@showTopicTimerModal={{route-action "showTopicTimerModal"}}
|
||||||
|
@showFeatureTopic={{route-action "showFeatureTopic"}}
|
||||||
|
@showChangeTimestamp={{route-action "showChangeTimestamp"}}
|
||||||
|
@resetBumpDate={{action "resetBumpDate"}}
|
||||||
|
@convertToPublicTopic={{action "convertToPublicTopic"}}
|
||||||
|
@convertToPrivateMessage={{action "convertToPrivateMessage"}}
|
||||||
|
@fullscreen={{info.topicProgressExpanded}}
|
||||||
|
@mobileView={{this.site.mobileView}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<TopicTimeline @topic={{this.model}} @notificationLevel={{this.model.details.notification_level}} @prevEvent={{info.prevEvent}} @fullscreen={{info.topicProgressExpanded}} @enteredIndex={{this.enteredIndex}} @loading={{this.model.postStream.loading}} @jumpToPost={{action "jumpToPost"}} @jumpTop={{action "jumpTop"}} @jumpBottom={{action "jumpBottom"}} @jumpEnd={{action "jumpEnd"}} @jumpToPostPrompt={{action "jumpToPostPrompt"}} @jumpToIndex={{action "jumpToIndex"}} @replyToPost={{action "replyToPost"}} @showSummary={{action "showSummary"}} @toggleMultiSelect={{action "toggleMultiSelect"}} @showTopicSlowModeUpdate={{route-action "showTopicSlowModeUpdate"}} @deleteTopic={{action "deleteTopic"}} @recoverTopic={{action "recoverTopic"}} @toggleClosed={{action "toggleClosed"}} @toggleArchived={{action "toggleArchived"}} @toggleVisibility={{action "toggleVisibility"}} @showTopicTimerModal={{route-action "showTopicTimerModal"}} @showFeatureTopic={{route-action "showFeatureTopic"}} @showChangeTimestamp={{route-action "showChangeTimestamp"}} @resetBumpDate={{action "resetBumpDate"}} @convertToPublicTopic={{action "convertToPublicTopic"}} @convertToPrivateMessage={{action "convertToPrivateMessage"}} />
|
||||||
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<TopicProgress @prevEvent={{info.prevEvent}} @topic={{this.model}} @expanded={{info.topicProgressExpanded}} @jumpToPost={{action "jumpToPost"}}>
|
<TopicProgress @prevEvent={{info.prevEvent}} @topic={{this.model}} @expanded={{info.topicProgressExpanded}} @jumpToPost={{action "jumpToPost"}}>
|
||||||
<PluginOutlet @name="before-topic-progress" @tagName="span" @connectorTagName="div" @args={{hash model=this.model jumpToPost=(action "jumpToPost")}} />
|
<PluginOutlet @name="before-topic-progress" @tagName="span" @connectorTagName="div" @args={{hash model=this.model jumpToPost=(action "jumpToPost")}} />
|
||||||
|
|
|
@ -0,0 +1,401 @@
|
||||||
|
import { click, currentURL, visit } from "@ember/test-helpers";
|
||||||
|
import {
|
||||||
|
acceptance,
|
||||||
|
exists,
|
||||||
|
query,
|
||||||
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
import { test } from "qunit";
|
||||||
|
|
||||||
|
acceptance("Glimmer Topic Timeline", function (needs) {
|
||||||
|
needs.user({
|
||||||
|
admin: true,
|
||||||
|
redesigned_topic_timeline_enabled: true,
|
||||||
|
});
|
||||||
|
needs.pretender((server, helper) => {
|
||||||
|
server.get("/t/129.json", () => {
|
||||||
|
return helper.response({
|
||||||
|
post_stream: {
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
id: 132,
|
||||||
|
name: null,
|
||||||
|
username: "foo",
|
||||||
|
avatar_template:
|
||||||
|
"/letter_avatar_proxy/v4/letter/f/b19c9b/{size}.png",
|
||||||
|
created_at: "2020-07-08T15:03:53.166Z",
|
||||||
|
cooked: "<p>Deleted post</p>",
|
||||||
|
post_number: 1,
|
||||||
|
post_type: 1,
|
||||||
|
updated_at: "2020-07-08T15:04:33.425Z",
|
||||||
|
reply_count: 0,
|
||||||
|
reply_to_post_number: null,
|
||||||
|
quote_count: 0,
|
||||||
|
incoming_link_count: 0,
|
||||||
|
reads: 1,
|
||||||
|
readers_count: 0,
|
||||||
|
score: 0,
|
||||||
|
yours: true,
|
||||||
|
topic_id: 129,
|
||||||
|
topic_slug: "deleted-topic-with-whisper-post",
|
||||||
|
display_username: null,
|
||||||
|
primary_group_name: null,
|
||||||
|
flair_name: null,
|
||||||
|
flair_url: null,
|
||||||
|
flair_bg_color: null,
|
||||||
|
flair_color: null,
|
||||||
|
version: 1,
|
||||||
|
can_edit: true,
|
||||||
|
can_delete: false,
|
||||||
|
can_recover: true,
|
||||||
|
can_wiki: true,
|
||||||
|
read: true,
|
||||||
|
user_title: null,
|
||||||
|
bookmarked: false,
|
||||||
|
bookmarks: [],
|
||||||
|
actions_summary: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
can_act: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
can_act: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
can_act: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
can_act: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
moderator: false,
|
||||||
|
admin: true,
|
||||||
|
staff: true,
|
||||||
|
user_id: 7,
|
||||||
|
hidden: false,
|
||||||
|
trust_level: 4,
|
||||||
|
deleted_at: "2020-07-08T15:04:37.544Z",
|
||||||
|
deleted_by: {
|
||||||
|
id: 7,
|
||||||
|
username: "foo",
|
||||||
|
name: null,
|
||||||
|
avatar_template:
|
||||||
|
"/letter_avatar_proxy/v4/letter/f/b19c9b/{size}.png",
|
||||||
|
},
|
||||||
|
user_deleted: false,
|
||||||
|
edit_reason: null,
|
||||||
|
can_view_edit_history: true,
|
||||||
|
wiki: false,
|
||||||
|
reviewable_id: 0,
|
||||||
|
reviewable_score_count: 0,
|
||||||
|
reviewable_score_pending_count: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 133,
|
||||||
|
name: null,
|
||||||
|
username: "foo",
|
||||||
|
avatar_template:
|
||||||
|
"/letter_avatar_proxy/v4/letter/f/b19c9b/{size}.png",
|
||||||
|
created_at: "2020-07-08T15:04:23.190Z",
|
||||||
|
cooked: "<p>Whisper post</p>",
|
||||||
|
post_number: 2,
|
||||||
|
post_type: 4,
|
||||||
|
updated_at: "2020-07-08T15:04:23.190Z",
|
||||||
|
reply_count: 0,
|
||||||
|
reply_to_post_number: null,
|
||||||
|
quote_count: 0,
|
||||||
|
incoming_link_count: 0,
|
||||||
|
reads: 1,
|
||||||
|
readers_count: 0,
|
||||||
|
score: 0,
|
||||||
|
yours: true,
|
||||||
|
topic_id: 129,
|
||||||
|
topic_slug: "deleted-topic-with-whisper-post",
|
||||||
|
display_username: null,
|
||||||
|
primary_group_name: null,
|
||||||
|
flair_name: null,
|
||||||
|
flair_url: null,
|
||||||
|
flair_bg_color: null,
|
||||||
|
flair_color: null,
|
||||||
|
version: 1,
|
||||||
|
can_edit: true,
|
||||||
|
can_delete: true,
|
||||||
|
can_recover: false,
|
||||||
|
can_wiki: true,
|
||||||
|
read: true,
|
||||||
|
user_title: null,
|
||||||
|
bookmarked: false,
|
||||||
|
bookmarks: [],
|
||||||
|
actions_summary: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
can_act: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
can_act: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
can_act: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
can_act: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
moderator: false,
|
||||||
|
admin: true,
|
||||||
|
staff: true,
|
||||||
|
user_id: 7,
|
||||||
|
hidden: false,
|
||||||
|
trust_level: 4,
|
||||||
|
deleted_at: null,
|
||||||
|
user_deleted: false,
|
||||||
|
edit_reason: null,
|
||||||
|
can_view_edit_history: true,
|
||||||
|
wiki: false,
|
||||||
|
reviewable_id: 0,
|
||||||
|
reviewable_score_count: 0,
|
||||||
|
reviewable_score_pending_count: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stream: [132, 133],
|
||||||
|
},
|
||||||
|
timeline_lookup: [[1, 0]],
|
||||||
|
suggested_topics: [
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
title: "Welcome to Discourse",
|
||||||
|
fancy_title: "Welcome to Discourse",
|
||||||
|
slug: "welcome-to-discourse",
|
||||||
|
posts_count: 1,
|
||||||
|
reply_count: 0,
|
||||||
|
highest_post_number: 1,
|
||||||
|
image_url: null,
|
||||||
|
created_at: "2020-07-08T14:56:57.424Z",
|
||||||
|
last_posted_at: "2020-07-08T14:56:57.488Z",
|
||||||
|
bumped: true,
|
||||||
|
bumped_at: "2020-07-08T14:56:57.488Z",
|
||||||
|
archetype: "regular",
|
||||||
|
unseen: false,
|
||||||
|
pinned: true,
|
||||||
|
unpinned: null,
|
||||||
|
excerpt:
|
||||||
|
"The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It’s important! \nEdit this into a brief description of your community: \n\nWho is it for?\nWhat can they fi…",
|
||||||
|
visible: true,
|
||||||
|
closed: false,
|
||||||
|
archived: false,
|
||||||
|
bookmarked: null,
|
||||||
|
liked: null,
|
||||||
|
tags: [],
|
||||||
|
like_count: 0,
|
||||||
|
views: 0,
|
||||||
|
category_id: 1,
|
||||||
|
featured_link: null,
|
||||||
|
posters: [
|
||||||
|
{
|
||||||
|
extras: "latest single",
|
||||||
|
description: "Original Poster, Most Recent Poster",
|
||||||
|
user: {
|
||||||
|
id: -1,
|
||||||
|
username: "system",
|
||||||
|
name: "system",
|
||||||
|
avatar_template: "/images/discourse-logo-sketch-small.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tags: [],
|
||||||
|
id: 129,
|
||||||
|
title: "Deleted topic with whisper post",
|
||||||
|
fancy_title: "Deleted topic with whisper post",
|
||||||
|
posts_count: 0,
|
||||||
|
created_at: "2020-07-08T15:03:53.045Z",
|
||||||
|
views: 1,
|
||||||
|
reply_count: 0,
|
||||||
|
like_count: 0,
|
||||||
|
last_posted_at: null,
|
||||||
|
visible: true,
|
||||||
|
closed: false,
|
||||||
|
archived: false,
|
||||||
|
has_summary: false,
|
||||||
|
archetype: "regular",
|
||||||
|
slug: "deleted-topic-with-whisper-post",
|
||||||
|
category_id: 1,
|
||||||
|
word_count: 8,
|
||||||
|
deleted_at: "2020-07-08T15:04:37.580Z",
|
||||||
|
user_id: 7,
|
||||||
|
featured_link: null,
|
||||||
|
pinned_globally: false,
|
||||||
|
pinned_at: null,
|
||||||
|
pinned_until: null,
|
||||||
|
image_url: null,
|
||||||
|
slow_mode_seconds: 0,
|
||||||
|
draft: null,
|
||||||
|
draft_key: "topic_129",
|
||||||
|
draft_sequence: 5,
|
||||||
|
posted: true,
|
||||||
|
unpinned: null,
|
||||||
|
pinned: false,
|
||||||
|
current_post_number: 1,
|
||||||
|
highest_post_number: 2,
|
||||||
|
last_read_post_number: 0,
|
||||||
|
bookmarks: [],
|
||||||
|
last_read_post_id: null,
|
||||||
|
deleted_by: {
|
||||||
|
id: 7,
|
||||||
|
username: "foo",
|
||||||
|
name: null,
|
||||||
|
avatar_template: "/letter_avatar_proxy/v4/letter/f/b19c9b/{size}.png",
|
||||||
|
},
|
||||||
|
has_deleted: false,
|
||||||
|
actions_summary: [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
count: 0,
|
||||||
|
hidden: false,
|
||||||
|
can_act: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
count: 0,
|
||||||
|
hidden: false,
|
||||||
|
can_act: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
count: 0,
|
||||||
|
hidden: false,
|
||||||
|
can_act: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
chunk_size: 20,
|
||||||
|
bookmarked: false,
|
||||||
|
bookmarks: [],
|
||||||
|
topic_timer: null,
|
||||||
|
message_bus_last_id: 5,
|
||||||
|
participant_count: 1,
|
||||||
|
show_read_indicator: false,
|
||||||
|
thumbnails: null,
|
||||||
|
slow_mode_enabled_until: null,
|
||||||
|
details: {
|
||||||
|
can_edit: true,
|
||||||
|
notification_level: 3,
|
||||||
|
notifications_reason_id: 1,
|
||||||
|
can_move_posts: true,
|
||||||
|
can_recover: true,
|
||||||
|
can_remove_allowed_users: true,
|
||||||
|
can_invite_to: true,
|
||||||
|
can_invite_via_email: true,
|
||||||
|
can_reply_as_new_topic: true,
|
||||||
|
can_flag_topic: true,
|
||||||
|
can_review_topic: true,
|
||||||
|
can_close_topic: true,
|
||||||
|
can_archive_topic: true,
|
||||||
|
can_split_merge_topic: true,
|
||||||
|
can_edit_staff_notes: true,
|
||||||
|
can_toggle_topic_visibility: true,
|
||||||
|
can_pin_unpin_topic: true,
|
||||||
|
can_moderate_category: true,
|
||||||
|
can_remove_self_id: 7,
|
||||||
|
participants: [
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
username: "foo",
|
||||||
|
name: null,
|
||||||
|
avatar_template:
|
||||||
|
"/letter_avatar_proxy/v4/letter/f/b19c9b/{size}.png",
|
||||||
|
post_count: 1,
|
||||||
|
primary_group_name: null,
|
||||||
|
flair_name: null,
|
||||||
|
flair_url: null,
|
||||||
|
flair_color: null,
|
||||||
|
flair_bg_color: null,
|
||||||
|
admin: true,
|
||||||
|
trust_level: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
created_by: {
|
||||||
|
id: 7,
|
||||||
|
username: "foo",
|
||||||
|
name: null,
|
||||||
|
avatar_template:
|
||||||
|
"/letter_avatar_proxy/v4/letter/f/b19c9b/{size}.png",
|
||||||
|
},
|
||||||
|
last_poster: {
|
||||||
|
id: 7,
|
||||||
|
username: "foo",
|
||||||
|
name: null,
|
||||||
|
avatar_template:
|
||||||
|
"/letter_avatar_proxy/v4/letter/f/b19c9b/{size}.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it has a topic admin menu", async function (assert) {
|
||||||
|
await visit("/t/internationalization-localization");
|
||||||
|
assert.ok(
|
||||||
|
exists(".timeline-controls .topic-admin-menu-button"),
|
||||||
|
"admin menu is present"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it has a reply-to-post button", async function (assert) {
|
||||||
|
await visit("/t/internationalization-localization");
|
||||||
|
assert.ok(
|
||||||
|
exists(".timeline-footer-controls .reply-to-post"),
|
||||||
|
"reply to post button is present"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it has a topic notification button", async function (assert) {
|
||||||
|
await visit("/t/internationalization-localization");
|
||||||
|
assert.ok(
|
||||||
|
exists(".timeline-footer-controls .topic-notifications-button"),
|
||||||
|
"topic notifications button is present"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Shows dates of first and last posts", async function (assert) {
|
||||||
|
await visit("/t/deleted-topic-with-whisper-post/129");
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".timeline-date-wrapper .now-date").innerText,
|
||||||
|
"Jul 2020"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selecting start-date navigates you to the first post", async function (assert) {
|
||||||
|
await visit("/t/internationalization-localization/280/2");
|
||||||
|
await click(".timeline-date-wrapper .start-date");
|
||||||
|
assert.strictEqual(
|
||||||
|
currentURL(),
|
||||||
|
"/t/internationalization-localization/280/1",
|
||||||
|
"navigates to the first post"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selecting now-date navigates you to the last post", async function (assert) {
|
||||||
|
await visit("/t/internationalization-localization/280/1");
|
||||||
|
await click(".timeline-date-wrapper .now-date");
|
||||||
|
assert.strictEqual(
|
||||||
|
currentURL(),
|
||||||
|
"/t/internationalization-localization/280/11",
|
||||||
|
"navigates to the latest post"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clicking the timeline padding updates the position", async function (assert) {
|
||||||
|
await visit("/t/internationalization-localization/280/2");
|
||||||
|
await click(".timeline-scrollarea .timeline-padding");
|
||||||
|
assert.notOk(
|
||||||
|
currentURL().includes("/280/2"),
|
||||||
|
"The position of the currently viewed post has been updated from it's initial position"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -234,6 +234,9 @@
|
||||||
.widget-dragging & {
|
.widget-dragging & {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
.dragging & {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-handle {
|
.timeline-handle {
|
||||||
|
|
|
@ -12,6 +12,10 @@ body.widget-dragging {
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.dragging {
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
// Common classes
|
// Common classes
|
||||||
.boxed {
|
.boxed {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -83,7 +83,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
:grouped_unread_notifications,
|
:grouped_unread_notifications,
|
||||||
:redesigned_user_menu_enabled,
|
:redesigned_user_menu_enabled,
|
||||||
:redesigned_user_page_nav_enabled,
|
:redesigned_user_page_nav_enabled,
|
||||||
:sidebar_list_destination
|
:sidebar_list_destination,
|
||||||
|
:redesigned_topic_timeline_enabled
|
||||||
|
|
||||||
delegate :user_stat, to: :object, private: true
|
delegate :user_stat, to: :object, private: true
|
||||||
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
|
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
|
||||||
|
@ -390,4 +391,12 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def redesigned_topic_timeline_enabled
|
||||||
|
if SiteSetting.enable_experimental_topic_timeline_groups.present?
|
||||||
|
object.in_any_groups?(SiteSetting.enable_experimental_topic_timeline_groups.split("|").map(&:to_i))
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2392,6 +2392,7 @@ en:
|
||||||
default_sidebar_categories: "Selected categories will be displayed under Sidebar's Categories section by default."
|
default_sidebar_categories: "Selected categories will be displayed under Sidebar's Categories section by default."
|
||||||
default_sidebar_tags: "Selected tags will be displayed under Sidebar's Tags section by default."
|
default_sidebar_tags: "Selected tags will be displayed under Sidebar's Tags section by default."
|
||||||
enable_new_user_profile_nav_groups: "EXPERIMENTAL: Users of the selected groups will be shown the new user profile navigation menu"
|
enable_new_user_profile_nav_groups: "EXPERIMENTAL: Users of the selected groups will be shown the new user profile navigation menu"
|
||||||
|
enable_experimental_topic_timeline_groups: "EXPERIMENTAL: Users of the selected groups will be shown the refactored topic timeline"
|
||||||
|
|
||||||
errors:
|
errors:
|
||||||
invalid_css_color: "Invalid color. Enter a color name or hex value."
|
invalid_css_color: "Invalid color. Enter a color name or hex value."
|
||||||
|
|
|
@ -2050,6 +2050,13 @@ developer:
|
||||||
include_associated_account_ids:
|
include_associated_account_ids:
|
||||||
default: false
|
default: false
|
||||||
hidden: true
|
hidden: true
|
||||||
|
enable_experimental_topic_timeline_groups:
|
||||||
|
client: true
|
||||||
|
type: group_list
|
||||||
|
list_type: compact
|
||||||
|
default: ""
|
||||||
|
allow_any: false
|
||||||
|
refresh: true
|
||||||
|
|
||||||
sidebar:
|
sidebar:
|
||||||
enable_sidebar:
|
enable_sidebar:
|
||||||
|
|
Loading…
Reference in New Issue