diff --git a/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.hbs b/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.hbs new file mode 100644 index 00000000000..d7243af8a54 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.hbs @@ -0,0 +1,72 @@ +
+
+ + + +
+
diff --git a/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.js b/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.js new file mode 100644 index 00000000000..814fb951cc1 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/glimmer-topic-timeline.js @@ -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; + } + } +} diff --git a/app/assets/javascripts/discourse/app/components/topic-timeline/back-button.hbs b/app/assets/javascripts/discourse/app/components/topic-timeline/back-button.hbs new file mode 100644 index 00000000000..ad1cd7410e2 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/topic-timeline/back-button.hbs @@ -0,0 +1,8 @@ + + {{i18n "topic.timeline.back"}} + diff --git a/app/assets/javascripts/discourse/app/components/topic-timeline/container.hbs b/app/assets/javascripts/discourse/app/components/topic-timeline/container.hbs new file mode 100644 index 00000000000..93fce8a0916 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/topic-timeline/container.hbs @@ -0,0 +1,97 @@ +{{#if @fullscreen}} +
+

+ {{if @mobileView @model.fancyTitle ""}} +

+ {{#if (or this.siteSettings.topic_featured_link_enabled this.showTags)}} +
+ {{#if this.showTags}} +
+ {{discourse-tags @model mode="list" tags=@model.tags}} +
+ {{/if}} + {{#if this.siteSettings.topic_featured_link_enabled}} + {{topic-featured-link @model}} + {{/if}} +
+ {{/if}} + + {{#if (and (not @model.isPrivateMessage) @model.category)}} +
+ {{#if @model.category.parentCategory}} + {{category-link @model.category.parentCategory}} + {{/if}} + {{category-link @model.category}} +
+ {{/if}} + {{#if this.excerpt}} +
{{html-safe this.excerpt}}
+ {{/if}} +
+{{/if}} + +{{#if (and (not @fullscreen) @currentUser)}} +
+ +
+{{/if}} + +{{#if this.displayTimeLineScrollArea}} +
+
+ + + {{this.startDate}} + + +
+
+
+ +
+ + {{#if this.hasBackPosition}} +
+ {{d-icon "minus" class="progress"}} + {{#if this.showButton}} + + {{/if}} +
+ {{/if}} +
+ +
+ + + {{this.nowDate}} + + +
+
+{{/if}} diff --git a/app/assets/javascripts/discourse/app/components/topic-timeline/container.js b/app/assets/javascripts/discourse/app/components/topic-timeline/container.js new file mode 100644 index 00000000000..9442219d485 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/topic-timeline/container.js @@ -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 = "" + info.username + ": "; + } + 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}`)); +} diff --git a/app/assets/javascripts/discourse/app/components/topic-timeline/scroller.hbs b/app/assets/javascripts/discourse/app/components/topic-timeline/scroller.hbs new file mode 100644 index 00000000000..0b0463184dd --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/topic-timeline/scroller.hbs @@ -0,0 +1,37 @@ +
+ {{#if @fullscreen}} +
+
+ {{this.repliesShort}} +
+ {{#if @date}} +
+ {{this.timelineAgo}} +
+ {{/if}} + {{#if (and @showDockedButton (not @dragging)) }} + + {{/if}} +
+
+ {{else}} +
+
+
+ {{this.repliesShort}} +
+ {{#if @date}} +
+ {{this.timelineAgo}} +
+ {{/if}} + {{#if (and @showDockedButton (not @dragging)) }} + + {{/if}} +
+ {{/if}} +
diff --git a/app/assets/javascripts/discourse/app/components/topic-timeline/scroller.js b/app/assets/javascripts/discourse/app/components/topic-timeline/scroller.js new file mode 100644 index 00000000000..5dba236ac7d --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/topic-timeline/scroller.js @@ -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); + } +} diff --git a/app/assets/javascripts/discourse/app/modifiers/draggable.js b/app/assets/javascripts/discourse/app/modifiers/draggable.js new file mode 100644 index 00000000000..d2d6a8035b1 --- /dev/null +++ b/app/assets/javascripts/discourse/app/modifiers/draggable.js @@ -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"); + } +} diff --git a/app/assets/javascripts/discourse/app/templates/topic.hbs b/app/assets/javascripts/discourse/app/templates/topic.hbs index 85186edfe29..b7099d84c13 100644 --- a/app/assets/javascripts/discourse/app/templates/topic.hbs +++ b/app/assets/javascripts/discourse/app/templates/topic.hbs @@ -107,7 +107,38 @@ {{#if info.renderTimeline}} - + {{#if this.currentUser.redesigned_topic_timeline_enabled}} + + {{else}} + + {{/if}} {{else}} diff --git a/app/assets/javascripts/discourse/tests/acceptance/glimmer-topic-timeline-test.js b/app/assets/javascripts/discourse/tests/acceptance/glimmer-topic-timeline-test.js new file mode 100644 index 00000000000..fc19b1ab83a --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/glimmer-topic-timeline-test.js @@ -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: "

Deleted post

", + 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: "

Whisper post

", + 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" + ); + }); +}); diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index 888330e495b..2e2c8e45fdb 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -234,6 +234,9 @@ .widget-dragging & { transition: none; } + .dragging & { + transition: none; + } } .timeline-handle { diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index 7984aca95fe..32e1fd72a05 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -12,6 +12,10 @@ body.widget-dragging { cursor: ns-resize; } +body.dragging { + cursor: ns-resize; +} + // Common classes .boxed { height: 100%; diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 0cd66b1d657..278f432965c 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -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 diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 6591d85b24a..bed0fe448b3 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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." diff --git a/config/site_settings.yml b/config/site_settings.yml index 96edaa9d6df..c9de05c5e89 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -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: