mirror of
https://github.com/flarum/framework.git
synced 2024-11-29 12:43:52 +08:00
refactor: convert AlertManager
IndexPage
and UserPage
components to TS (#3536)
* chore: convert `AlertManager` component to TypeScript * chore: `compat.js` to `compat.ts` * chore: convert `IndexPage` component to TypeScript * chore: convert `UserPage` component and inheritors to TypeScript * chore: `yarn format` * chore: import types instead
This commit is contained in:
parent
5721a2f487
commit
0c017c2aa0
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-expect-error We need to explicitly use the prefix to distinguish between the extend folder.
|
||||||
import * as extend from './extend.ts';
|
import * as extend from './extend.ts';
|
||||||
import Session from './Session';
|
import Session from './Session';
|
||||||
import Store from './Store';
|
import Store from './Store';
|
|
@ -1,31 +0,0 @@
|
||||||
import Component from '../Component';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `AlertManager` component provides an area in which `Alert` components can
|
|
||||||
* be shown and dismissed.
|
|
||||||
*/
|
|
||||||
export default class AlertManager extends Component {
|
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
this.state = this.attrs.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
return (
|
|
||||||
<div class="AlertManager">
|
|
||||||
{Object.entries(this.state.getActiveAlerts()).map(([key, alert]) => {
|
|
||||||
const urgent = alert.attrs.type === 'error';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="AlertManager-alert" role="alert" aria-live={urgent ? 'assertive' : 'polite'}>
|
|
||||||
<alert.componentClass {...alert.attrs} ondismiss={this.state.dismiss.bind(this.state, key)}>
|
|
||||||
{alert.children}
|
|
||||||
</alert.componentClass>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
42
framework/core/js/src/common/components/AlertManager.tsx
Normal file
42
framework/core/js/src/common/components/AlertManager.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import Component, { ComponentAttrs } from '../Component';
|
||||||
|
import AlertManagerState from '../states/AlertManagerState';
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
|
||||||
|
export interface IAlertManagerAttrs extends ComponentAttrs {
|
||||||
|
state: AlertManagerState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `AlertManager` component provides an area in which `Alert` components can
|
||||||
|
* be shown and dismissed.
|
||||||
|
*/
|
||||||
|
export default class AlertManager<CustomAttrs extends IAlertManagerAttrs = IAlertManagerAttrs> extends Component<CustomAttrs, AlertManagerState> {
|
||||||
|
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
this.state = this.attrs.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
const activeAlerts = this.state.getActiveAlerts();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="AlertManager">
|
||||||
|
{Object.keys(activeAlerts)
|
||||||
|
.map(Number)
|
||||||
|
.map((key) => {
|
||||||
|
const alert = activeAlerts[key];
|
||||||
|
const urgent = alert.attrs.type === 'error';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="AlertManager-alert" role="alert" aria-live={urgent ? 'assertive' : 'polite'}>
|
||||||
|
<alert.componentClass {...alert.attrs} ondismiss={this.state.dismiss.bind(this.state, key)}>
|
||||||
|
{alert.children}
|
||||||
|
</alert.componentClass>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ export interface IPageAttrs {
|
||||||
*
|
*
|
||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
export default abstract class Page<CustomAttrs extends IPageAttrs = IPageAttrs> extends Component<CustomAttrs> {
|
export default abstract class Page<CustomAttrs extends IPageAttrs = IPageAttrs, CustomState = undefined> extends Component<CustomAttrs, CustomState> {
|
||||||
/**
|
/**
|
||||||
* A class name to apply to the body while the route is active.
|
* A class name to apply to the body while the route is active.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -6,6 +6,8 @@ import Alert, { AlertAttrs } from '../components/Alert';
|
||||||
*/
|
*/
|
||||||
export type AlertIdentifier = number;
|
export type AlertIdentifier = number;
|
||||||
|
|
||||||
|
export type AlertArray = { [id: AlertIdentifier]: AlertState };
|
||||||
|
|
||||||
export interface AlertState {
|
export interface AlertState {
|
||||||
componentClass: typeof Alert;
|
componentClass: typeof Alert;
|
||||||
attrs: AlertAttrs;
|
attrs: AlertAttrs;
|
||||||
|
@ -13,8 +15,8 @@ export interface AlertState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AlertManagerState {
|
export default class AlertManagerState {
|
||||||
protected activeAlerts: { [id: number]: AlertState } = {};
|
protected activeAlerts: AlertArray = {};
|
||||||
protected alertId = 0;
|
protected alertId: AlertIdentifier = 0;
|
||||||
|
|
||||||
getActiveAlerts() {
|
getActiveAlerts() {
|
||||||
return this.activeAlerts;
|
return this.activeAlerts;
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import UserPage from './UserPage';
|
import UserPage, { IUserPageAttrs } from './UserPage';
|
||||||
import DiscussionList from './DiscussionList';
|
import DiscussionList from './DiscussionList';
|
||||||
import DiscussionListState from '../states/DiscussionListState';
|
import DiscussionListState from '../states/DiscussionListState';
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
import type User from '../../common/models/User';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `DiscussionsUserPage` component shows a discussion list inside of a user
|
* The `DiscussionsUserPage` component shows a discussion list inside of a user
|
||||||
* page.
|
* page.
|
||||||
*/
|
*/
|
||||||
export default class DiscussionsUserPage extends UserPage {
|
export default class DiscussionsUserPage extends UserPage<IUserPageAttrs, DiscussionListState> {
|
||||||
oninit(vnode) {
|
oninit(vnode: Mithril.Vnode<IUserPageAttrs, this>) {
|
||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
this.loadUser(m.route.param('username'));
|
this.loadUser(m.route.param('username'));
|
||||||
}
|
}
|
||||||
|
|
||||||
show(user) {
|
show(user: User): void {
|
||||||
super.show(user);
|
super.show(user);
|
||||||
|
|
||||||
this.state = new DiscussionListState({
|
this.state = new DiscussionListState({
|
|
@ -1,5 +1,5 @@
|
||||||
import app from '../../forum/app';
|
import app from '../../forum/app';
|
||||||
import Page from '../../common/components/Page';
|
import Page, { IPageAttrs } from '../../common/components/Page';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import listItems from '../../common/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
import DiscussionList from './DiscussionList';
|
import DiscussionList from './DiscussionList';
|
||||||
|
@ -11,15 +11,21 @@ import Dropdown from '../../common/components/Dropdown';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import LinkButton from '../../common/components/LinkButton';
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||||
|
import extractText from '../../common/utils/extractText';
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
import type Discussion from '../../common/models/Discussion';
|
||||||
|
|
||||||
|
export interface IIndexPageAttrs extends IPageAttrs {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `IndexPage` component displays the index page, including the welcome
|
* The `IndexPage` component displays the index page, including the welcome
|
||||||
* hero, the sidebar, and the discussion list.
|
* hero, the sidebar, and the discussion list.
|
||||||
*/
|
*/
|
||||||
export default class IndexPage extends Page {
|
export default class IndexPage<CustomAttrs extends IIndexPageAttrs = IIndexPageAttrs, CustomState = {}> extends Page<CustomAttrs, CustomState> {
|
||||||
static providesInitialSearch = true;
|
static providesInitialSearch = true;
|
||||||
|
lastDiscussion?: Discussion;
|
||||||
|
|
||||||
oninit(vnode) {
|
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
// If the user is returning from a discussion page, then take note of which
|
// If the user is returning from a discussion page, then take note of which
|
||||||
|
@ -37,9 +43,9 @@ export default class IndexPage extends Page {
|
||||||
app.discussions.clear();
|
app.discussions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.discussions.refreshParams(app.search.params(), m.route.param('page'));
|
app.discussions.refreshParams(app.search.params(), Number(m.route.param('page')));
|
||||||
|
|
||||||
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
|
app.history.push('index', extractText(app.translator.trans('core.forum.header.back_to_index_tooltip')));
|
||||||
|
|
||||||
this.bodyClass = 'App--index';
|
this.bodyClass = 'App--index';
|
||||||
this.scrollTopOnCreate = false;
|
this.scrollTopOnCreate = false;
|
||||||
|
@ -68,11 +74,11 @@ export default class IndexPage extends Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
setTitle() {
|
setTitle() {
|
||||||
app.setTitle(app.translator.trans('core.forum.index.meta_title_text'));
|
app.setTitle(extractText(app.translator.trans('core.forum.index.meta_title_text')));
|
||||||
app.setTitleCount(0);
|
app.setTitleCount(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||||
super.oncreate(vnode);
|
super.oncreate(vnode);
|
||||||
|
|
||||||
this.setTitle();
|
this.setTitle();
|
||||||
|
@ -80,11 +86,11 @@ export default class IndexPage extends Page {
|
||||||
// Work out the difference between the height of this hero and that of the
|
// Work out the difference between the height of this hero and that of the
|
||||||
// previous hero. Maintain the same scroll position relative to the bottom
|
// previous hero. Maintain the same scroll position relative to the bottom
|
||||||
// of the hero so that the sidebar doesn't jump around.
|
// of the hero so that the sidebar doesn't jump around.
|
||||||
const oldHeroHeight = app.cache.heroHeight;
|
const oldHeroHeight = app.cache.heroHeight as number;
|
||||||
const heroHeight = (app.cache.heroHeight = this.$('.Hero').outerHeight() || 0);
|
const heroHeight = (app.cache.heroHeight = this.$('.Hero').outerHeight() || 0);
|
||||||
const scrollTop = app.cache.scrollTop;
|
const scrollTop = app.cache.scrollTop as number;
|
||||||
|
|
||||||
$('#app').css('min-height', $(window).height() + heroHeight);
|
$('#app').css('min-height', ($(window).height() || 0) + heroHeight);
|
||||||
|
|
||||||
// Let browser handle scrolling on page reload.
|
// Let browser handle scrolling on page reload.
|
||||||
if (app.previous.type == null) return;
|
if (app.previous.type == null) return;
|
||||||
|
@ -104,10 +110,11 @@ export default class IndexPage extends Page {
|
||||||
const $discussion = this.$(`li[data-id="${this.lastDiscussion.id()}"] .DiscussionListItem`);
|
const $discussion = this.$(`li[data-id="${this.lastDiscussion.id()}"] .DiscussionListItem`);
|
||||||
|
|
||||||
if ($discussion.length) {
|
if ($discussion.length) {
|
||||||
const indexTop = $('#header').outerHeight();
|
const indexTop = $('#header').outerHeight() || 0;
|
||||||
const indexBottom = $(window).height();
|
const indexBottom = $(window).height() || 0;
|
||||||
const discussionTop = $discussion.offset().top;
|
const discussionOffset = $discussion.offset();
|
||||||
const discussionBottom = discussionTop + $discussion.outerHeight();
|
const discussionTop = (discussionOffset && discussionOffset.top) || 0;
|
||||||
|
const discussionBottom = discussionTop + ($discussion.outerHeight() || 0);
|
||||||
|
|
||||||
if (discussionTop < scrollTop + indexTop || discussionBottom > scrollTop + indexBottom) {
|
if (discussionTop < scrollTop + indexTop || discussionBottom > scrollTop + indexBottom) {
|
||||||
$(window).scrollTop(discussionTop - indexTop);
|
$(window).scrollTop(discussionTop - indexTop);
|
||||||
|
@ -116,7 +123,7 @@ export default class IndexPage extends Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onbeforeremove(vnode) {
|
onbeforeremove(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||||
super.onbeforeremove(vnode);
|
super.onbeforeremove(vnode);
|
||||||
|
|
||||||
// Save the scroll position so we can restore it when we return to the
|
// Save the scroll position so we can restore it when we return to the
|
||||||
|
@ -124,7 +131,7 @@ export default class IndexPage extends Page {
|
||||||
app.cache.scrollTop = $(window).scrollTop();
|
app.cache.scrollTop = $(window).scrollTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
onremove(vnode) {
|
onremove(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||||
super.onremove(vnode);
|
super.onremove(vnode);
|
||||||
|
|
||||||
$('#app').css('min-height', '');
|
$('#app').css('min-height', '');
|
||||||
|
@ -132,8 +139,6 @@ export default class IndexPage extends Page {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the component to display as the hero.
|
* Get the component to display as the hero.
|
||||||
*
|
|
||||||
* @return {import('mithril').Children}
|
|
||||||
*/
|
*/
|
||||||
hero() {
|
hero() {
|
||||||
return WelcomeHero.component();
|
return WelcomeHero.component();
|
||||||
|
@ -143,11 +148,9 @@ export default class IndexPage extends Page {
|
||||||
* Build an item list for the sidebar of the index page. By default this is a
|
* Build an item list for the sidebar of the index page. By default this is a
|
||||||
* "New Discussion" button, and then a DropdownSelect component containing a
|
* "New Discussion" button, and then a DropdownSelect component containing a
|
||||||
* list of navigation items.
|
* list of navigation items.
|
||||||
*
|
|
||||||
* @return {ItemList<import('mithril').Children>}
|
|
||||||
*/
|
*/
|
||||||
sidebarItems() {
|
sidebarItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList<Mithril.Children>();
|
||||||
const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user;
|
const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user;
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
|
@ -176,7 +179,7 @@ export default class IndexPage extends Page {
|
||||||
className: 'App-titleControl',
|
className: 'App-titleControl',
|
||||||
accessibleToggleLabel: app.translator.trans('core.forum.index.toggle_sidenav_dropdown_accessible_label'),
|
accessibleToggleLabel: app.translator.trans('core.forum.index.toggle_sidenav_dropdown_accessible_label'),
|
||||||
},
|
},
|
||||||
this.navItems(this).toArray()
|
this.navItems().toArray()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -186,11 +189,9 @@ export default class IndexPage extends Page {
|
||||||
/**
|
/**
|
||||||
* Build an item list for the navigation in the sidebar of the index page. By
|
* Build an item list for the navigation in the sidebar of the index page. By
|
||||||
* default this is just the 'All Discussions' link.
|
* default this is just the 'All Discussions' link.
|
||||||
*
|
|
||||||
* @return {ItemList<import('mithril').Children>}
|
|
||||||
*/
|
*/
|
||||||
navItems() {
|
navItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList<Mithril.Children>();
|
||||||
const params = app.search.stickyParams();
|
const params = app.search.stickyParams();
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
|
@ -212,14 +213,12 @@ export default class IndexPage extends Page {
|
||||||
* Build an item list for the part of the toolbar which is concerned with how
|
* Build an item list for the part of the toolbar which is concerned with how
|
||||||
* the results are displayed. By default this is just a select box to change
|
* the results are displayed. By default this is just a select box to change
|
||||||
* the way discussions are sorted.
|
* the way discussions are sorted.
|
||||||
*
|
|
||||||
* @return {ItemList<import('mithril').Children>}
|
|
||||||
*/
|
*/
|
||||||
viewItems() {
|
viewItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList<Mithril.Children>();
|
||||||
const sortMap = app.discussions.sortMap();
|
const sortMap = app.discussions.sortMap();
|
||||||
|
|
||||||
const sortOptions = Object.keys(sortMap).reduce((acc, sortId) => {
|
const sortOptions = Object.keys(sortMap).reduce((acc: any, sortId) => {
|
||||||
acc[sortId] = app.translator.trans(`core.forum.index_sort.${sortId}_button`);
|
acc[sortId] = app.translator.trans(`core.forum.index_sort.${sortId}_button`);
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
@ -254,11 +253,9 @@ export default class IndexPage extends Page {
|
||||||
/**
|
/**
|
||||||
* Build an item list for the part of the toolbar which is about taking action
|
* Build an item list for the part of the toolbar which is about taking action
|
||||||
* on the results. By default this is just a "mark all as read" button.
|
* on the results. By default this is just a "mark all as read" button.
|
||||||
*
|
|
||||||
* @return {ItemList<import('mithril').Children>}
|
|
||||||
*/
|
*/
|
||||||
actionItems() {
|
actionItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList<Mithril.Children>();
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'refresh',
|
'refresh',
|
||||||
|
@ -269,7 +266,7 @@ export default class IndexPage extends Page {
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
app.discussions.refresh();
|
app.discussions.refresh();
|
||||||
if (app.session.user) {
|
if (app.session.user) {
|
||||||
app.store.find('users', app.session.user.id());
|
app.store.find('users', app.session.user.id()!);
|
||||||
m.redraw();
|
m.redraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -293,10 +290,8 @@ export default class IndexPage extends Page {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the composer for a new discussion or prompt the user to login.
|
* Open the composer for a new discussion or prompt the user to login.
|
||||||
*
|
|
||||||
* @return {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
newDiscussionAction() {
|
newDiscussionAction(): Promise<unknown> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (app.session.user) {
|
if (app.session.user) {
|
||||||
app.composer.load(DiscussionComposer, { user: app.session.user });
|
app.composer.load(DiscussionComposer, { user: app.session.user });
|
||||||
|
@ -315,10 +310,10 @@ export default class IndexPage extends Page {
|
||||||
* Mark all discussions as read.
|
* Mark all discussions as read.
|
||||||
*/
|
*/
|
||||||
markAllAsRead() {
|
markAllAsRead() {
|
||||||
const confirmation = confirm(app.translator.trans('core.forum.index.mark_all_as_read_confirmation'));
|
const confirmation = confirm(extractText(app.translator.trans('core.forum.index.mark_all_as_read_confirmation')));
|
||||||
|
|
||||||
if (confirmation) {
|
if (confirmation) {
|
||||||
app.session.user.save({ markedAllAsReadAt: new Date() });
|
app.session.user?.save({ markedAllAsReadAt: new Date() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,46 +1,41 @@
|
||||||
import app from '../../forum/app';
|
import app from '../../forum/app';
|
||||||
import UserPage from './UserPage';
|
import UserPage, { IUserPageAttrs } from './UserPage';
|
||||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Link from '../../common/components/Link';
|
import Link from '../../common/components/Link';
|
||||||
import Placeholder from '../../common/components/Placeholder';
|
import Placeholder from '../../common/components/Placeholder';
|
||||||
import CommentPost from './CommentPost';
|
import CommentPost from './CommentPost';
|
||||||
|
import type Post from '../../common/models/Post';
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
import type User from '../../common/models/User';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `PostsUserPage` component shows a user's activity feed inside of their
|
* The `PostsUserPage` component shows a user's activity feed inside of their
|
||||||
* profile.
|
* profile.
|
||||||
*/
|
*/
|
||||||
export default class PostsUserPage extends UserPage {
|
export default class PostsUserPage extends UserPage {
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the activity feed is currently loading.
|
* Whether or not the activity feed is currently loading.
|
||||||
*
|
|
||||||
* @type {Boolean}
|
|
||||||
*/
|
*/
|
||||||
this.loading = true;
|
loading: boolean = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not there are any more activity items that can be loaded.
|
* Whether or not there are any more activity items that can be loaded.
|
||||||
*
|
|
||||||
* @type {Boolean}
|
|
||||||
*/
|
*/
|
||||||
this.moreResults = false;
|
moreResults: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Post models in the feed.
|
* The Post models in the feed.
|
||||||
*
|
|
||||||
* @type {Post[]}
|
|
||||||
*/
|
*/
|
||||||
this.posts = [];
|
posts: Post[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of activity items to load per request.
|
* The number of activity items to load per request.
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
*/
|
||||||
this.loadLimit = 20;
|
loadLimit: number = 20;
|
||||||
|
|
||||||
|
oninit(vnode: Mithril.Vnode<IUserPageAttrs, this>) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
this.loadUser(m.route.param('username'));
|
this.loadUser(m.route.param('username'));
|
||||||
}
|
}
|
||||||
|
@ -92,7 +87,7 @@ export default class PostsUserPage extends UserPage {
|
||||||
* Initialize the component with a user, and trigger the loading of their
|
* Initialize the component with a user, and trigger the loading of their
|
||||||
* activity feed.
|
* activity feed.
|
||||||
*/
|
*/
|
||||||
show(user) {
|
show(user: User): void {
|
||||||
super.show(user);
|
super.show(user);
|
||||||
|
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
@ -113,14 +108,12 @@ export default class PostsUserPage extends UserPage {
|
||||||
/**
|
/**
|
||||||
* Load a new page of the user's activity feed.
|
* Load a new page of the user's activity feed.
|
||||||
*
|
*
|
||||||
* @param {number} [offset] The position to start getting results from.
|
|
||||||
* @return {Promise<import('../../common/models/Post').default[]>}
|
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
loadResults(offset) {
|
loadResults(offset = 0) {
|
||||||
return app.store.find('posts', {
|
return app.store.find<Post[]>('posts', {
|
||||||
filter: {
|
filter: {
|
||||||
author: this.user.username(),
|
author: this.user!.username(),
|
||||||
type: 'comment',
|
type: 'comment',
|
||||||
},
|
},
|
||||||
page: { offset, limit: this.loadLimit },
|
page: { offset, limit: this.loadLimit },
|
||||||
|
@ -138,11 +131,8 @@ export default class PostsUserPage extends UserPage {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse results and append them to the activity feed.
|
* Parse results and append them to the activity feed.
|
||||||
*
|
|
||||||
* @param {import('../../common/models/Post').default[]} results
|
|
||||||
* @return {import('../../common/models/Post').default[]}
|
|
||||||
*/
|
*/
|
||||||
parseResults(results) {
|
parseResults(results: Post[]): Post[] {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
this.posts.push(...results);
|
this.posts.push(...results);
|
|
@ -1,5 +1,5 @@
|
||||||
import app from '../../forum/app';
|
import app from '../../forum/app';
|
||||||
import Page from '../../common/components/Page';
|
import Page, { IPageAttrs } from '../../common/components/Page';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import UserCard from './UserCard';
|
import UserCard from './UserCard';
|
||||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||||
|
@ -8,6 +8,10 @@ import LinkButton from '../../common/components/LinkButton';
|
||||||
import Separator from '../../common/components/Separator';
|
import Separator from '../../common/components/Separator';
|
||||||
import listItems from '../../common/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
import AffixedSidebar from './AffixedSidebar';
|
import AffixedSidebar from './AffixedSidebar';
|
||||||
|
import type User from '../../common/models/User';
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
|
||||||
|
export interface IUserPageAttrs extends IPageAttrs {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `UserPage` component shows a user's profile. It can be extended to show
|
* The `UserPage` component shows a user's profile. It can be extended to show
|
||||||
|
@ -16,24 +20,20 @@ import AffixedSidebar from './AffixedSidebar';
|
||||||
*
|
*
|
||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
export default class UserPage extends Page {
|
export default class UserPage<CustomAttrs extends IUserPageAttrs = IUserPageAttrs, CustomState = undefined> extends Page<CustomAttrs, CustomState> {
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user this page is for.
|
* The user this page is for.
|
||||||
*
|
|
||||||
* @type {User}
|
|
||||||
*/
|
*/
|
||||||
this.user = null;
|
user: User | null = null;
|
||||||
|
|
||||||
|
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
this.bodyClass = 'App--user';
|
this.bodyClass = 'App--user';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base view template for the user page.
|
* Base view template for the user page.
|
||||||
*
|
|
||||||
* @return {import('mithril').Children}
|
|
||||||
*/
|
*/
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
|
@ -64,19 +64,16 @@ export default class UserPage extends Page {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the content to display in the user page.
|
* Get the content to display in the user page.
|
||||||
*
|
|
||||||
* @return {import('mithril').Children}
|
|
||||||
*/
|
*/
|
||||||
content() {}
|
content(): Mithril.Children | void {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the component with a user, and trigger the loading of their
|
* Initialize the component with a user, and trigger the loading of their
|
||||||
* activity feed.
|
* activity feed.
|
||||||
*
|
*
|
||||||
* @param {import('../../common/models/User').default} user
|
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
show(user) {
|
show(user: User): void {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
|
||||||
app.current.set('user', user);
|
app.current.set('user', user);
|
||||||
|
@ -89,10 +86,8 @@ export default class UserPage extends Page {
|
||||||
/**
|
/**
|
||||||
* Given a username, load the user's profile from the store, or make a request
|
* Given a username, load the user's profile from the store, or make a request
|
||||||
* if we don't have it yet. Then initialize the profile page with that user.
|
* if we don't have it yet. Then initialize the profile page with that user.
|
||||||
*
|
|
||||||
* @param {string} username
|
|
||||||
*/
|
*/
|
||||||
loadUser(username) {
|
loadUser(username: string) {
|
||||||
const lowercaseUsername = username.toLowerCase();
|
const lowercaseUsername = username.toLowerCase();
|
||||||
|
|
||||||
// Load the preloaded user object, if any, into the global app store
|
// Load the preloaded user object, if any, into the global app store
|
||||||
|
@ -100,25 +95,25 @@ export default class UserPage extends Page {
|
||||||
// instead of the parsed models
|
// instead of the parsed models
|
||||||
app.preloadedApiDocument();
|
app.preloadedApiDocument();
|
||||||
|
|
||||||
app.store.all('users').some((user) => {
|
app.store.all<User>('users').some((user) => {
|
||||||
if ((user.username().toLowerCase() === lowercaseUsername || user.id() === username) && user.joinTime()) {
|
if ((user.username().toLowerCase() === lowercaseUsername || user.id() === username) && user.joinTime()) {
|
||||||
this.show(user);
|
this.show(user);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.user) {
|
if (!this.user) {
|
||||||
app.store.find('users', username, { bySlug: true }).then(this.show.bind(this));
|
app.store.find<User>('users', username, { bySlug: true }).then(this.show.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an item list for the content of the sidebar.
|
* Build an item list for the content of the sidebar.
|
||||||
*
|
|
||||||
* @return {ItemList<import('mithril').Children>}
|
|
||||||
*/
|
*/
|
||||||
sidebarItems() {
|
sidebarItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList<Mithril.Children>();
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'nav',
|
'nav',
|
||||||
|
@ -132,12 +127,10 @@ export default class UserPage extends Page {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an item list for the navigation in the sidebar.
|
* Build an item list for the navigation in the sidebar.
|
||||||
*
|
|
||||||
* @return {ItemList<import('mithril').Children>}
|
|
||||||
*/
|
*/
|
||||||
navItems() {
|
navItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList<Mithril.Children>();
|
||||||
const user = this.user;
|
const user = this.user!;
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'posts',
|
'posts',
|
Loading…
Reference in New Issue
Block a user