mirror of
https://github.com/flarum/framework.git
synced 2025-02-21 10:23:41 +08:00
Create abstract PaginatedListState for DiscussionList and others (#2781)
This commit is contained in:
parent
9a26b2bef4
commit
4e0fdb4c77
@ -78,6 +78,7 @@ import userOnline from './helpers/userOnline';
|
||||
import listItems from './helpers/listItems';
|
||||
import Fragment from './Fragment';
|
||||
import DefaultResolver from './resolvers/DefaultResolver';
|
||||
import PaginatedListState from './states/PaginatedListState';
|
||||
|
||||
export default {
|
||||
extend: extend,
|
||||
@ -160,4 +161,5 @@ export default {
|
||||
'helpers/userOnline': userOnline,
|
||||
'helpers/listItems': listItems,
|
||||
'resolvers/DefaultResolver': DefaultResolver,
|
||||
'states/PaginatedListState': PaginatedListState,
|
||||
};
|
||||
|
230
js/src/common/states/PaginatedListState.ts
Normal file
230
js/src/common/states/PaginatedListState.ts
Normal file
@ -0,0 +1,230 @@
|
||||
import Model from '../Model';
|
||||
|
||||
export interface Page<TModel> {
|
||||
number: number;
|
||||
items: TModel[];
|
||||
|
||||
hasPrev?: boolean;
|
||||
hasNext?: boolean;
|
||||
}
|
||||
|
||||
export interface PaginationLocation {
|
||||
page: number;
|
||||
startIndex?: number;
|
||||
endIndex?: number;
|
||||
}
|
||||
|
||||
export default abstract class PaginatedListState<T extends Model> {
|
||||
protected location!: PaginationLocation;
|
||||
protected pageSize: number;
|
||||
|
||||
protected pages: Page<T>[] = [];
|
||||
protected params: any = {};
|
||||
|
||||
protected initialLoading: boolean = false;
|
||||
protected loadingPrev: boolean = false;
|
||||
protected loadingNext: boolean = false;
|
||||
|
||||
protected constructor(params: any = {}, page: number = 1, pageSize: number = 20) {
|
||||
this.params = params;
|
||||
|
||||
this.location = { page };
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
abstract get type(): string;
|
||||
|
||||
public clear() {
|
||||
this.pages = [];
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
public loadPrev(): Promise<void> {
|
||||
if (this.loadingPrev || this.getLocation().page === 1) return Promise.resolve();
|
||||
|
||||
this.loadingPrev = true;
|
||||
|
||||
const page: number = this.getPrevPageNumber();
|
||||
|
||||
return this.loadPage(page)
|
||||
.then(this.parseResults.bind(this, page))
|
||||
.finally(() => (this.loadingPrev = false));
|
||||
}
|
||||
|
||||
public loadNext(): Promise<void> {
|
||||
if (this.loadingNext) return Promise.resolve();
|
||||
|
||||
this.loadingNext = true;
|
||||
|
||||
const page: number = this.getNextPageNumber();
|
||||
|
||||
return this.loadPage(page)
|
||||
.then(this.parseResults.bind(this, page))
|
||||
.finally(() => (this.loadingNext = false));
|
||||
}
|
||||
|
||||
protected parseResults(pg: number, results: T[]) {
|
||||
const pageNum = Number(pg);
|
||||
|
||||
const links = results.payload?.links || {};
|
||||
const page = {
|
||||
number: pageNum,
|
||||
items: results,
|
||||
hasNext: !!links.next,
|
||||
hasPrev: !!links.prev,
|
||||
};
|
||||
|
||||
if (this.isEmpty() || pageNum > this.getNextPageNumber() - 1) {
|
||||
this.pages.push(page);
|
||||
} else {
|
||||
this.pages.unshift(page);
|
||||
}
|
||||
|
||||
this.location = { page: pageNum };
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a new page of results.
|
||||
*/
|
||||
protected loadPage(page = 1): Promise<T[]> {
|
||||
const params = this.requestParams();
|
||||
params.page = { offset: this.pageSize * (page - 1) };
|
||||
|
||||
if (Array.isArray(params.include)) {
|
||||
params.include = params.include.join(',');
|
||||
}
|
||||
|
||||
return app.store.find(this.type, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters that should be passed in the API request.
|
||||
* Do not include page offset unless subclass overrides loadPage.
|
||||
*
|
||||
* @abstract
|
||||
* @see loadPage
|
||||
*/
|
||||
protected requestParams(): any {
|
||||
return this.params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the `this.params` object, calling `refresh` if they have changed.
|
||||
* Use `requestParams` for converting `this.params` into API parameters
|
||||
*
|
||||
* @param newParams
|
||||
* @param page
|
||||
* @see requestParams
|
||||
*/
|
||||
public refreshParams(newParams, page: number) {
|
||||
if (this.isEmpty() || this.paramsChanged(newParams)) {
|
||||
this.params = newParams;
|
||||
|
||||
return this.refresh(page);
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(page: number = 1) {
|
||||
this.initialLoading = true;
|
||||
this.loadingPrev = false;
|
||||
this.loadingNext = false;
|
||||
|
||||
this.clear();
|
||||
|
||||
this.location = { page };
|
||||
|
||||
return this.loadPage()
|
||||
.then((results: T[]) => {
|
||||
this.pages = [];
|
||||
this.parseResults(this.location.page, results);
|
||||
})
|
||||
.finally(() => (this.initialLoading = false));
|
||||
}
|
||||
|
||||
public getPages() {
|
||||
return this.pages;
|
||||
}
|
||||
public getLocation(): PaginationLocation {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
public isLoading(): boolean {
|
||||
return this.initialLoading || this.loadingNext || this.loadingPrev;
|
||||
}
|
||||
public isInitialLoading(): boolean {
|
||||
return this.initialLoading;
|
||||
}
|
||||
public isLoadingPrev(): boolean {
|
||||
return this.loadingPrev;
|
||||
}
|
||||
public isLoadingNext(): boolean {
|
||||
return this.loadingNext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the number of items across all loaded pages is not 0.
|
||||
*
|
||||
* @see isEmpty
|
||||
*/
|
||||
public hasItems(): boolean {
|
||||
return !!this.getAllItems().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when there aren't any items *and* the state has already done its initial loading.
|
||||
* If you want to know whether there are items regardless of load state, use `hasItems()` instead
|
||||
*
|
||||
* @see hasItems
|
||||
*/
|
||||
public isEmpty(): boolean {
|
||||
return !this.isInitialLoading() && !this.hasItems();
|
||||
}
|
||||
|
||||
public hasPrev(): boolean {
|
||||
return !!this.pages[0]?.hasPrev;
|
||||
}
|
||||
public hasNext(): boolean {
|
||||
return !!this.pages[this.pages.length - 1]?.hasNext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stored state parameters.
|
||||
*/
|
||||
public getParams(): any {
|
||||
return this.params;
|
||||
}
|
||||
|
||||
protected getNextPageNumber(): number {
|
||||
const pg = this.pages[this.pages.length - 1]?.number;
|
||||
|
||||
if (pg && !isNaN(pg)) {
|
||||
return pg + 1;
|
||||
} else {
|
||||
return this.location.page;
|
||||
}
|
||||
}
|
||||
protected getPrevPageNumber(): number {
|
||||
const pg = this.pages[0]?.number;
|
||||
|
||||
if (pg && !isNaN(pg)) {
|
||||
// If the calculated page number is less than 1,
|
||||
// return 1 as the prev page (first possible page number)
|
||||
return Math.max(pg - 1, 1);
|
||||
} else {
|
||||
return this.location.page;
|
||||
}
|
||||
}
|
||||
|
||||
protected paramsChanged(newParams): boolean {
|
||||
return Object.keys(newParams).some((key) => this.getParams()[key] !== newParams[key]);
|
||||
}
|
||||
|
||||
protected getAllItems(): T[] {
|
||||
return this.getPages()
|
||||
.map((pg) => pg.items)
|
||||
.flat();
|
||||
}
|
||||
}
|
@ -90,7 +90,7 @@ export default class ForumApplication extends Application {
|
||||
*
|
||||
* @type {DiscussionListState}
|
||||
*/
|
||||
this.discussions = new DiscussionListState({}, this);
|
||||
this.discussions = new DiscussionListState({});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,6 +3,7 @@ import DiscussionListItem from './DiscussionListItem';
|
||||
import Button from '../../common/components/Button';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import Placeholder from '../../common/components/Placeholder';
|
||||
import Discussion from '../../common/models/Discussion';
|
||||
|
||||
/**
|
||||
* The `DiscussionList` component displays a list of discussions.
|
||||
@ -13,24 +14,27 @@ import Placeholder from '../../common/components/Placeholder';
|
||||
*/
|
||||
export default class DiscussionList extends Component {
|
||||
view() {
|
||||
/**
|
||||
* @type DiscussionListState
|
||||
*/
|
||||
const state = this.attrs.state;
|
||||
|
||||
const params = state.getParams();
|
||||
let loading;
|
||||
|
||||
if (state.isLoading()) {
|
||||
if (state.isInitialLoading() || state.isLoadingNext()) {
|
||||
loading = <LoadingIndicator />;
|
||||
} else if (state.moreResults) {
|
||||
} else if (state.hasNext()) {
|
||||
loading = Button.component(
|
||||
{
|
||||
className: 'Button',
|
||||
onclick: state.loadMore.bind(state),
|
||||
onclick: state.loadNext.bind(state),
|
||||
},
|
||||
app.translator.trans('core.forum.discussion_list.load_more_button')
|
||||
);
|
||||
}
|
||||
|
||||
if (state.empty()) {
|
||||
if (state.isEmpty()) {
|
||||
const text = app.translator.trans('core.forum.discussion_list.empty_text');
|
||||
return <div className="DiscussionList">{Placeholder.component({ text })}</div>;
|
||||
}
|
||||
@ -38,12 +42,12 @@ export default class DiscussionList extends Component {
|
||||
return (
|
||||
<div className={'DiscussionList' + (state.isSearchResults() ? ' DiscussionList--searchResults' : '')}>
|
||||
<ul className="DiscussionList-discussions">
|
||||
{state.discussions.map((discussion) => {
|
||||
return (
|
||||
{state.getPages().map((pg) => {
|
||||
return pg.items.map((discussion) => (
|
||||
<li key={discussion.id()} data-id={discussion.id()}>
|
||||
{DiscussionListItem.component({ discussion, params })}
|
||||
</li>
|
||||
);
|
||||
));
|
||||
})}
|
||||
</ul>
|
||||
<div className="DiscussionList-loadMore">{loading}</div>
|
||||
|
@ -37,7 +37,7 @@ export default class IndexPage extends Page {
|
||||
app.discussions.clear();
|
||||
}
|
||||
|
||||
app.discussions.refreshParams(app.search.params());
|
||||
app.discussions.refreshParams(app.search.params(), m.route.param('page'));
|
||||
|
||||
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
|
||||
|
||||
|
@ -12,7 +12,7 @@ import Discussion from '../../common/models/Discussion';
|
||||
export default class NotificationList extends Component {
|
||||
view() {
|
||||
const state = this.attrs.state;
|
||||
const pages = state.getNotificationPages();
|
||||
const pages = state.getPages();
|
||||
|
||||
return (
|
||||
<div className="NotificationList">
|
||||
@ -30,12 +30,12 @@ export default class NotificationList extends Component {
|
||||
</div>
|
||||
|
||||
<div className="NotificationList-content">
|
||||
{pages.length
|
||||
? pages.map((notifications) => {
|
||||
{state.hasItems()
|
||||
? pages.map((page) => {
|
||||
const groups = [];
|
||||
const discussions = {};
|
||||
|
||||
notifications.forEach((notification) => {
|
||||
page.items.forEach((notification) => {
|
||||
const subject = notification.subject();
|
||||
|
||||
if (typeof subject === 'undefined') return;
|
||||
@ -84,7 +84,7 @@ export default class NotificationList extends Component {
|
||||
})
|
||||
: ''}
|
||||
{state.isLoading() ? (
|
||||
<LoadingIndicator />
|
||||
<LoadingIndicator className="LoadingIndicator--block" />
|
||||
) : pages.length ? (
|
||||
''
|
||||
) : (
|
||||
@ -124,8 +124,8 @@ export default class NotificationList extends Component {
|
||||
// by a fraction of a pixel, so we compensate for that.
|
||||
const atBottom = Math.abs(scrollParent.scrollHeight - scrollParent.scrollTop - scrollParent.clientHeight) <= 1;
|
||||
|
||||
if (state.hasMoreResults() && !state.isLoading() && atBottom) {
|
||||
state.loadMore();
|
||||
if (state.hasNext() && !state.isLoadingNext() && atBottom) {
|
||||
state.loadNext();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,196 +0,0 @@
|
||||
export default class DiscussionListState {
|
||||
constructor(params = {}, app = window.app) {
|
||||
this.params = params;
|
||||
|
||||
this.app = app;
|
||||
|
||||
this.discussions = [];
|
||||
|
||||
this.moreResults = false;
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters that should be passed in the API request to get
|
||||
* discussion results.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
requestParams() {
|
||||
const params = { include: ['user', 'lastPostedUser'], filter: {} };
|
||||
|
||||
params.sort = this.sortMap()[this.params.sort];
|
||||
|
||||
if (this.params.q) {
|
||||
params.filter.q = this.params.q;
|
||||
|
||||
params.include.push('mostRelevantPost', 'mostRelevantPost.user');
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map of sort keys (which appear in the URL, and are used for
|
||||
* translation) to the API sort value that they represent.
|
||||
*/
|
||||
sortMap() {
|
||||
const map = {};
|
||||
|
||||
if (this.params.q) {
|
||||
map.relevance = '';
|
||||
}
|
||||
map.latest = '-lastPostedAt';
|
||||
map.top = '-commentCount';
|
||||
map.newest = '-createdAt';
|
||||
map.oldest = 'createdAt';
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the search parameters.
|
||||
*/
|
||||
getParams() {
|
||||
return this.params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached discussions.
|
||||
*/
|
||||
clear() {
|
||||
this.discussions = [];
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are no cached discussions or the new params differ from the
|
||||
* old ones, update params and refresh the discussion list from the database.
|
||||
*/
|
||||
refreshParams(newParams) {
|
||||
if (!this.hasDiscussions() || Object.keys(newParams).some((key) => this.getParams()[key] !== newParams[key])) {
|
||||
this.params = newParams;
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and reload the discussion list. Passing the option `deferClear: true`
|
||||
* will clear discussions only after new data has been received.
|
||||
* This can be used to refresh discussions without loading animations.
|
||||
*/
|
||||
refresh({ deferClear = false } = {}) {
|
||||
this.loading = true;
|
||||
|
||||
if (!deferClear) {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
return this.loadResults().then(
|
||||
(results) => {
|
||||
// This ensures that any changes made while waiting on this request
|
||||
// are ignored. Otherwise, we could get duplicate discussions.
|
||||
// We don't use `this.clear()` to avoid an unnecessary redraw.
|
||||
this.discussions = [];
|
||||
this.parseResults(results);
|
||||
},
|
||||
() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a new page of discussion results.
|
||||
*
|
||||
* @param offset The index to start the page at.
|
||||
*/
|
||||
loadResults(offset) {
|
||||
const preloadedDiscussions = this.app.preloadedApiDocument();
|
||||
|
||||
if (preloadedDiscussions) {
|
||||
return Promise.resolve(preloadedDiscussions);
|
||||
}
|
||||
|
||||
const params = this.requestParams();
|
||||
params.page = { offset };
|
||||
params.include = params.include.join(',');
|
||||
|
||||
return this.app.store.find('discussions', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next page of discussion results.
|
||||
*/
|
||||
loadMore() {
|
||||
this.loading = true;
|
||||
|
||||
this.loadResults(this.discussions.length).then(this.parseResults.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse results and append them to the discussion list.
|
||||
*/
|
||||
parseResults(results) {
|
||||
this.discussions.push(...results);
|
||||
|
||||
this.loading = false;
|
||||
this.moreResults = !!results.payload.links && !!results.payload.links.next;
|
||||
|
||||
m.redraw();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a discussion from the list if it is present.
|
||||
*/
|
||||
removeDiscussion(discussion) {
|
||||
const index = this.discussions.indexOf(discussion);
|
||||
|
||||
if (index !== -1) {
|
||||
this.discussions.splice(index, 1);
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a discussion to the top of the list.
|
||||
*/
|
||||
addDiscussion(discussion) {
|
||||
this.discussions.unshift(discussion);
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Are there discussions stored in the discussion list state?
|
||||
*/
|
||||
hasDiscussions() {
|
||||
return this.discussions.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are discussions currently being loaded?
|
||||
*/
|
||||
isLoading() {
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
/**
|
||||
* In the last request, has the user searched for a discussion?
|
||||
*/
|
||||
isSearchResults() {
|
||||
return !!this.params.q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Have the search results come up empty?
|
||||
*/
|
||||
empty() {
|
||||
return !this.hasDiscussions() && !this.isLoading();
|
||||
}
|
||||
}
|
119
js/src/forum/states/DiscussionListState.ts
Normal file
119
js/src/forum/states/DiscussionListState.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import PaginatedListState, { Page } from '../../common/states/PaginatedListState';
|
||||
import Discussion from '../../common/models/Discussion';
|
||||
|
||||
export default class DiscussionListState extends PaginatedListState<Discussion> {
|
||||
protected extraDiscussions: Discussion[] = [];
|
||||
|
||||
constructor(params: any, page: number) {
|
||||
super(params, page, 20);
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return 'discussions';
|
||||
}
|
||||
|
||||
requestParams() {
|
||||
const params: any = { include: ['user', 'lastPostedUser'], filter: {} };
|
||||
|
||||
params.sort = this.sortMap()[this.params.sort];
|
||||
|
||||
if (this.params.q) {
|
||||
params.filter.q = this.params.q;
|
||||
|
||||
params.include.push('mostRelevantPost', 'mostRelevantPost.user');
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
protected loadPage(page: number = 1): any {
|
||||
const preloadedDiscussions = app.preloadedApiDocument();
|
||||
|
||||
if (preloadedDiscussions) {
|
||||
this.initialLoading = false;
|
||||
|
||||
return Promise.resolve(preloadedDiscussions);
|
||||
}
|
||||
|
||||
return super.loadPage(page);
|
||||
}
|
||||
|
||||
clear() {
|
||||
super.clear();
|
||||
|
||||
this.extraDiscussions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map of sort keys (which appear in the URL, and are used for
|
||||
* translation) to the API sort value that they represent.
|
||||
*/
|
||||
sortMap() {
|
||||
const map: any = {};
|
||||
|
||||
if (this.params.q) {
|
||||
map.relevance = '';
|
||||
}
|
||||
map.latest = '-lastPostedAt';
|
||||
map.top = '-commentCount';
|
||||
map.newest = '-createdAt';
|
||||
map.oldest = 'createdAt';
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* In the last request, has the user searched for a discussion?
|
||||
*/
|
||||
isSearchResults() {
|
||||
return !!this.params.q;
|
||||
}
|
||||
|
||||
removeDiscussion(discussion: Discussion) {
|
||||
for (const page of this.pages) {
|
||||
const index = page.items.indexOf(discussion);
|
||||
|
||||
if (index !== -1) {
|
||||
page.items.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const index = this.extraDiscussions.indexOf(discussion);
|
||||
|
||||
if (index !== -1) {
|
||||
this.extraDiscussions.splice(index);
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a discussion to the top of the list.
|
||||
*/
|
||||
addDiscussion(discussion: Discussion) {
|
||||
this.removeDiscussion(discussion);
|
||||
this.extraDiscussions.unshift(discussion);
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
protected getAllItems(): Discussion[] {
|
||||
return this.extraDiscussions.concat(super.getAllItems());
|
||||
}
|
||||
|
||||
public getPages(): Page<Discussion>[] {
|
||||
const pages = super.getPages();
|
||||
|
||||
if (this.extraDiscussions.length) {
|
||||
return [
|
||||
{
|
||||
number: -1,
|
||||
items: this.extraDiscussions,
|
||||
},
|
||||
...pages,
|
||||
];
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
export default class NotificationListState {
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
|
||||
this.notificationPages = [];
|
||||
|
||||
this.loading = false;
|
||||
|
||||
this.moreResults = false;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.notificationPages = [];
|
||||
}
|
||||
|
||||
getNotificationPages() {
|
||||
return this.notificationPages;
|
||||
}
|
||||
|
||||
isLoading() {
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
hasMoreResults() {
|
||||
return this.moreResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load notifications into the application's cache if they haven't already
|
||||
* been loaded.
|
||||
*/
|
||||
load() {
|
||||
if (this.app.session.user.newNotificationCount()) {
|
||||
this.notificationPages = [];
|
||||
}
|
||||
|
||||
if (this.notificationPages.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.app.session.user.pushAttributes({ newNotificationCount: 0 });
|
||||
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next page of notification results.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
loadMore() {
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
const params = this.notificationPages.length > 0 ? { page: { offset: this.notificationPages.length * 10 } } : null;
|
||||
|
||||
return this.app.store
|
||||
.find('notifications', params)
|
||||
.then(this.parseResults.bind(this))
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse results and append them to the notification list.
|
||||
*
|
||||
* @param {Notification[]} results
|
||||
* @return {Notification[]}
|
||||
*/
|
||||
parseResults(results) {
|
||||
if (results.length) this.notificationPages.push(results);
|
||||
|
||||
this.moreResults = !!results.payload.links.next;
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all of the notifications as read.
|
||||
*/
|
||||
markAllAsRead() {
|
||||
if (this.notificationPages.length === 0) return;
|
||||
|
||||
this.app.session.user.pushAttributes({ unreadNotificationCount: 0 });
|
||||
|
||||
this.notificationPages.forEach((notifications) => {
|
||||
notifications.forEach((notification) => notification.pushAttributes({ isRead: true }));
|
||||
});
|
||||
|
||||
this.app.request({
|
||||
url: this.app.forum.attribute('apiUrl') + '/notifications/read',
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
}
|
48
js/src/forum/states/NotificationListState.ts
Normal file
48
js/src/forum/states/NotificationListState.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import PaginatedListState from '../../common/states/PaginatedListState';
|
||||
import Notification from '../../common/models/Notification';
|
||||
|
||||
export default class NotificationListState extends PaginatedListState<Notification> {
|
||||
constructor() {
|
||||
super({}, 1, 10);
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return 'notifications';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next page of notification results.
|
||||
*/
|
||||
load(): Promise<void> {
|
||||
if (app.session.user.newNotificationCount()) {
|
||||
this.pages = [];
|
||||
this.location = { page: 1 };
|
||||
}
|
||||
|
||||
if (this.pages.length > 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
app.session.user.pushAttributes({ newNotificationCount: 0 });
|
||||
|
||||
return super.loadNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all of the notifications as read.
|
||||
*/
|
||||
markAllAsRead() {
|
||||
if (this.pages.length === 0) return;
|
||||
|
||||
app.session.user.pushAttributes({ unreadNotificationCount: 0 });
|
||||
|
||||
this.pages.forEach((page) => {
|
||||
page.items.forEach((notification) => notification.pushAttributes({ isRead: true }));
|
||||
});
|
||||
|
||||
return app.request({
|
||||
url: app.forum.attribute('apiUrl') + '/notifications/read',
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"jsx": "preserve",
|
||||
"lib": ["es2015", "es2017", "dom"],
|
||||
"lib": ["es2015", "es2017", "es2018.promise", "dom"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user