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:
David Taylor 2024-02-13 10:41:22 +00:00 committed by GitHub
parent 06bbed69f9
commit 9883e6a0c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 84 additions and 18 deletions

View File

@ -42,19 +42,26 @@
/>
</div>
{{else}}
{{#if this.topicTrackingState.hasIncoming}}
{{#if (or this.topicTrackingState.hasIncoming @model.loadingBefore)}}
<div class="show-more {{if this.hasTopics 'has-topics'}}">
<a
tabindex="0"
href
{{on "click" this.showInserted}}
class="alert alert-info clickable"
class="alert alert-info clickable
{{if @model.loadingBefore 'loading'}}"
>
<CountI18n
@key="topic_count_"
@suffix={{this.topicTrackingState.filter}}
@count={{this.topicTrackingState.incomingCount}}
@count={{or
@model.loadingBefore
this.topicTrackingState.incomingCount
}}
/>
{{#if @model.loadingBefore}}
{{loading-spinner size="small"}}
{{/if}}
</a>
</div>
{{/if}}

View File

@ -1,7 +1,9 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import DismissNew from "discourse/components/modal/dismiss-new";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { filterTypeForMode } from "discourse/lib/filter-mode";
import { userPath } from "discourse/lib/url";
import Topic from "discourse/models/topic";
@ -15,6 +17,8 @@ export default class DiscoveryTopics extends Component {
@service topicTrackingState;
@service site;
@tracked loadingNew;
get redirectedReason() {
return this.currentUser?.user_option.redirected_to_top?.reason;
}
@ -56,7 +60,7 @@ export default class DiscoveryTopics extends Component {
dismissTopics = false,
untrack = false
) {
const tracked =
const isTracked =
(this.router.currentRoute.queryParams["f"] ||
this.router.currentRoute.queryParams["filter"]) === "tracked";
@ -65,7 +69,7 @@ export default class DiscoveryTopics extends Component {
this.args.category,
!this.args.noSubcategories,
{
tracked,
tracked: isTracked,
tag: this.args.tag,
topicIds,
dismissPosts,
@ -99,13 +103,22 @@ export default class DiscoveryTopics extends Component {
// Show newly inserted topics
@action
showInserted(event) {
async showInserted(event) {
event?.preventDefault();
const tracker = this.topicTrackingState;
// Move inserted into topics
this.args.model.loadBefore(tracker.get("newIncoming"), true);
tracker.resetTracking();
if (this.args.model.loadingBefore) {
return; // Already loading
}
const { topicTrackingState } = this;
try {
const topicIds = [...topicTrackingState.newIncoming];
await this.args.model.loadBefore(topicIds, true);
topicTrackingState.clearIncoming(topicIds);
} catch (e) {
popupAjaxError(e);
}
}
get showTopicsAndRepliesToggle() {

View File

@ -57,6 +57,16 @@ export default Component.extend({
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")
renderTopicListItem() {
const template = findRawTemplate("list/topic-list-item");

View File

@ -1,3 +1,4 @@
import { tracked } from "@glimmer/tracking";
import EmberObject from "@ember/object";
import { notEmpty } from "@ember/object/computed";
import { inject as service } from "@ember/service";
@ -121,6 +122,7 @@ export default class TopicList extends RestModel {
@service session;
@tracked loadingBefore = false;
@notEmpty("more_topics_url") canLoadMore;
forEachNew(topics, callback) {
@ -210,15 +212,19 @@ export default class TopicList extends RestModel {
}
// loads topics with these ids "before" the current topics
loadBefore(topic_ids, storeInSession) {
// refresh dupes
this.topics.removeObjects(
this.topics.filter((topic) => topic_ids.includes(topic.id))
);
async loadBefore(topic_ids, storeInSession) {
this.loadingBefore = topic_ids.length;
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;
this.forEachNew(TopicList.topicsFrom(this.store, result), (t) => {
// 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) {
this.session.set("topicList", this);
}
});
} finally {
this.loadingBefore = false;
}
}
}

View File

@ -335,6 +335,19 @@ export default class TopicTrackingState extends EmberObject {
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.
*

View File

@ -178,6 +178,13 @@
.alert {
margin: 0;
padding: 1.1em 2em 1.1em 0.65em;
gap: 0.65em;
align-items: center;
&.loading {
color: var(--primary-medium);
cursor: default;
}
}
}
}

View File

@ -574,3 +574,11 @@ td .main-link {
}
}
}
#list-area .show-more .alert {
align-items: center;
gap: 0.5em;
&.loading {
color: var(--primary-medium);
}
}