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}} />
|
||||
|
||||
{{#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}}
|
||||
<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")}} />
|
||||
|
|
|
@ -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 & {
|
||||
transition: none;
|
||||
}
|
||||
.dragging & {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-handle {
|
||||
|
|
|
@ -12,6 +12,10 @@ body.widget-dragging {
|
|||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
body.dragging {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
// Common classes
|
||||
.boxed {
|
||||
height: 100%;
|
||||
|
|
|
@ -83,7 +83,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||
:grouped_unread_notifications,
|
||||
:redesigned_user_menu_enabled,
|
||||
:redesigned_user_page_nav_enabled,
|
||||
:sidebar_list_destination
|
||||
:sidebar_list_destination,
|
||||
:redesigned_topic_timeline_enabled
|
||||
|
||||
delegate :user_stat, to: :object, private: true
|
||||
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
|
||||
|
@ -390,4 +391,12 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||
false
|
||||
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
|
||||
|
|
|
@ -2392,6 +2392,7 @@ en:
|
|||
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."
|
||||
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:
|
||||
invalid_css_color: "Invalid color. Enter a color name or hex value."
|
||||
|
|
|
@ -2050,6 +2050,13 @@ developer:
|
|||
include_associated_account_ids:
|
||||
default: false
|
||||
hidden: true
|
||||
enable_experimental_topic_timeline_groups:
|
||||
client: true
|
||||
type: group_list
|
||||
list_type: compact
|
||||
default: ""
|
||||
allow_any: false
|
||||
refresh: true
|
||||
|
||||
sidebar:
|
||||
enable_sidebar:
|
||||
|
|
Loading…
Reference in New Issue