mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
UX: Add loading indicator when loading 'new or updated topics' (#25649)
Also improves error handling so that the action can be retried if the network request fails
This commit is contained in:
parent
06bbed69f9
commit
9883e6a0c8
@ -42,19 +42,26 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if this.topicTrackingState.hasIncoming}}
|
{{#if (or this.topicTrackingState.hasIncoming @model.loadingBefore)}}
|
||||||
<div class="show-more {{if this.hasTopics 'has-topics'}}">
|
<div class="show-more {{if this.hasTopics 'has-topics'}}">
|
||||||
<a
|
<a
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
href
|
href
|
||||||
{{on "click" this.showInserted}}
|
{{on "click" this.showInserted}}
|
||||||
class="alert alert-info clickable"
|
class="alert alert-info clickable
|
||||||
|
{{if @model.loadingBefore 'loading'}}"
|
||||||
>
|
>
|
||||||
<CountI18n
|
<CountI18n
|
||||||
@key="topic_count_"
|
@key="topic_count_"
|
||||||
@suffix={{this.topicTrackingState.filter}}
|
@suffix={{this.topicTrackingState.filter}}
|
||||||
@count={{this.topicTrackingState.incomingCount}}
|
@count={{or
|
||||||
|
@model.loadingBefore
|
||||||
|
this.topicTrackingState.incomingCount
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{{#if @model.loadingBefore}}
|
||||||
|
{{loading-spinner size="small"}}
|
||||||
|
{{/if}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import DismissNew from "discourse/components/modal/dismiss-new";
|
import DismissNew from "discourse/components/modal/dismiss-new";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { filterTypeForMode } from "discourse/lib/filter-mode";
|
import { filterTypeForMode } from "discourse/lib/filter-mode";
|
||||||
import { userPath } from "discourse/lib/url";
|
import { userPath } from "discourse/lib/url";
|
||||||
import Topic from "discourse/models/topic";
|
import Topic from "discourse/models/topic";
|
||||||
@ -15,6 +17,8 @@ export default class DiscoveryTopics extends Component {
|
|||||||
@service topicTrackingState;
|
@service topicTrackingState;
|
||||||
@service site;
|
@service site;
|
||||||
|
|
||||||
|
@tracked loadingNew;
|
||||||
|
|
||||||
get redirectedReason() {
|
get redirectedReason() {
|
||||||
return this.currentUser?.user_option.redirected_to_top?.reason;
|
return this.currentUser?.user_option.redirected_to_top?.reason;
|
||||||
}
|
}
|
||||||
@ -56,7 +60,7 @@ export default class DiscoveryTopics extends Component {
|
|||||||
dismissTopics = false,
|
dismissTopics = false,
|
||||||
untrack = false
|
untrack = false
|
||||||
) {
|
) {
|
||||||
const tracked =
|
const isTracked =
|
||||||
(this.router.currentRoute.queryParams["f"] ||
|
(this.router.currentRoute.queryParams["f"] ||
|
||||||
this.router.currentRoute.queryParams["filter"]) === "tracked";
|
this.router.currentRoute.queryParams["filter"]) === "tracked";
|
||||||
|
|
||||||
@ -65,7 +69,7 @@ export default class DiscoveryTopics extends Component {
|
|||||||
this.args.category,
|
this.args.category,
|
||||||
!this.args.noSubcategories,
|
!this.args.noSubcategories,
|
||||||
{
|
{
|
||||||
tracked,
|
tracked: isTracked,
|
||||||
tag: this.args.tag,
|
tag: this.args.tag,
|
||||||
topicIds,
|
topicIds,
|
||||||
dismissPosts,
|
dismissPosts,
|
||||||
@ -99,13 +103,22 @@ export default class DiscoveryTopics extends Component {
|
|||||||
|
|
||||||
// Show newly inserted topics
|
// Show newly inserted topics
|
||||||
@action
|
@action
|
||||||
showInserted(event) {
|
async showInserted(event) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
const tracker = this.topicTrackingState;
|
|
||||||
|
|
||||||
// Move inserted into topics
|
if (this.args.model.loadingBefore) {
|
||||||
this.args.model.loadBefore(tracker.get("newIncoming"), true);
|
return; // Already loading
|
||||||
tracker.resetTracking();
|
}
|
||||||
|
|
||||||
|
const { topicTrackingState } = this;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const topicIds = [...topicTrackingState.newIncoming];
|
||||||
|
await this.args.model.loadBefore(topicIds, true);
|
||||||
|
topicTrackingState.clearIncoming(topicIds);
|
||||||
|
} catch (e) {
|
||||||
|
popupAjaxError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get showTopicsAndRepliesToggle() {
|
get showTopicsAndRepliesToggle() {
|
||||||
|
@ -57,6 +57,16 @@ export default Component.extend({
|
|||||||
this.renderTopicListItem();
|
this.renderTopicListItem();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Already-rendered topic is marked as highlighted
|
||||||
|
// Ideally this should be a modifier... but we can't do that
|
||||||
|
// until this component has its tagName removed.
|
||||||
|
@observes("topic.highlight")
|
||||||
|
topicHighlightChanged() {
|
||||||
|
if (this.topic.highlight) {
|
||||||
|
this._highlightIfNeeded();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
@observes("topic.pinned", "expandGloballyPinned", "expandAllPinned")
|
@observes("topic.pinned", "expandGloballyPinned", "expandAllPinned")
|
||||||
renderTopicListItem() {
|
renderTopicListItem() {
|
||||||
const template = findRawTemplate("list/topic-list-item");
|
const template = findRawTemplate("list/topic-list-item");
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import { notEmpty } from "@ember/object/computed";
|
import { notEmpty } from "@ember/object/computed";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
@ -121,6 +122,7 @@ export default class TopicList extends RestModel {
|
|||||||
|
|
||||||
@service session;
|
@service session;
|
||||||
|
|
||||||
|
@tracked loadingBefore = false;
|
||||||
@notEmpty("more_topics_url") canLoadMore;
|
@notEmpty("more_topics_url") canLoadMore;
|
||||||
|
|
||||||
forEachNew(topics, callback) {
|
forEachNew(topics, callback) {
|
||||||
@ -210,15 +212,19 @@ export default class TopicList extends RestModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loads topics with these ids "before" the current topics
|
// loads topics with these ids "before" the current topics
|
||||||
loadBefore(topic_ids, storeInSession) {
|
async loadBefore(topic_ids, storeInSession) {
|
||||||
// refresh dupes
|
this.loadingBefore = topic_ids.length;
|
||||||
this.topics.removeObjects(
|
|
||||||
this.topics.filter((topic) => topic_ids.includes(topic.id))
|
|
||||||
);
|
|
||||||
|
|
||||||
const url = `/${this.filter}.json?topic_ids=${topic_ids.join(",")}`;
|
try {
|
||||||
|
const url = `/${this.filter}.json?topic_ids=${topic_ids.join(",")}`;
|
||||||
|
|
||||||
|
const result = await ajax({ url, data: this.params });
|
||||||
|
|
||||||
|
// refresh dupes
|
||||||
|
this.topics.removeObjects(
|
||||||
|
this.topics.filter((topic) => topic_ids.includes(topic.id))
|
||||||
|
);
|
||||||
|
|
||||||
return ajax({ url, data: this.params }).then((result) => {
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
this.forEachNew(TopicList.topicsFrom(this.store, result), (t) => {
|
this.forEachNew(TopicList.topicsFrom(this.store, result), (t) => {
|
||||||
// highlight the first of the new topics so we can get a visual feedback
|
// highlight the first of the new topics so we can get a visual feedback
|
||||||
@ -230,6 +236,8 @@ export default class TopicList extends RestModel {
|
|||||||
if (storeInSession) {
|
if (storeInSession) {
|
||||||
this.session.set("topicList", this);
|
this.session.set("topicList", this);
|
||||||
}
|
}
|
||||||
});
|
} finally {
|
||||||
|
this.loadingBefore = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -335,6 +335,19 @@ export default class TopicTrackingState extends EmberObject {
|
|||||||
this.set("incomingCount", 0);
|
this.set("incomingCount", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given topic IDs from the list of incoming topics.
|
||||||
|
*
|
||||||
|
* @method clearIncoming
|
||||||
|
*/
|
||||||
|
clearIncoming(topicIds) {
|
||||||
|
const toRemove = new Set(topicIds);
|
||||||
|
this.newIncoming = this.newIncoming.filter(
|
||||||
|
(topicId) => !toRemove.has(topicId)
|
||||||
|
);
|
||||||
|
this.set("incomingCount", this.newIncoming.length);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track how many new topics came for the specified filter.
|
* Track how many new topics came for the specified filter.
|
||||||
*
|
*
|
||||||
|
@ -178,6 +178,13 @@
|
|||||||
.alert {
|
.alert {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1.1em 2em 1.1em 0.65em;
|
padding: 1.1em 2em 1.1em 0.65em;
|
||||||
|
gap: 0.65em;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
color: var(--primary-medium);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -574,3 +574,11 @@ td .main-link {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#list-area .show-more .alert {
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
&.loading {
|
||||||
|
color: var(--primary-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user