mirror of
https://github.com/flarum/core.git
synced 2025-08-06 16:36:47 +02:00
Start work on user page, fix routes not working
This commit is contained in:
@@ -45,7 +45,7 @@ export default class Model {
|
||||
* Get the model's ID.
|
||||
* @final
|
||||
*/
|
||||
id(): number {
|
||||
id(): string|number {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
|
@@ -21,13 +21,13 @@ interface LinkButtonProps extends ButtonProps {
|
||||
export default class LinkButton extends Button<LinkButtonProps> {
|
||||
static initProps(props: LinkButtonProps) {
|
||||
props.active = this.isActive(props);
|
||||
props.oncreate = props.oncreate || m.route;
|
||||
props.oncreate = props.oncreate;
|
||||
}
|
||||
|
||||
view(vnode) {
|
||||
const vdom = super.view(vnode);
|
||||
|
||||
vdom.tag = 'a';
|
||||
vdom.tag = m.route.Link;
|
||||
|
||||
return vdom;
|
||||
}
|
||||
|
33
js/src/common/utils/humanTime.ts
Normal file
33
js/src/common/utils/humanTime.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* The `humanTime` utility converts a date to a localized, human-readable time-
|
||||
* ago string.
|
||||
*/
|
||||
export default function humanTime(time: Date): string {
|
||||
let m = dayjs(time);
|
||||
const now = dayjs();
|
||||
|
||||
// To prevent showing things like "in a few seconds" due to small offsets
|
||||
// between client and server time, we always reset future dates to the
|
||||
// current time. This will result in "just now" being shown instead.
|
||||
if (m.isAfter(now)) {
|
||||
m = now;
|
||||
}
|
||||
|
||||
const day = 864e5;
|
||||
const diff = m.diff(dayjs());
|
||||
let ago = null;
|
||||
|
||||
// If this date was more than a month ago, we'll show the name of the month
|
||||
// in the string. If it wasn't this year, we'll show the year as well.
|
||||
if (diff < -30 * day) {
|
||||
if (m.year() === dayjs().year()) {
|
||||
ago = m.format('D MMM');
|
||||
} else {
|
||||
ago = m.format('MMM \'YY');
|
||||
}
|
||||
} else {
|
||||
ago = m.fromNow();
|
||||
}
|
||||
|
||||
return ago;
|
||||
};
|
@@ -1,14 +1,21 @@
|
||||
import Application from '../common/Application';
|
||||
import History from './utils/History';
|
||||
|
||||
import IndexPage from './components/IndexPage';
|
||||
import HeaderPrimary from './components/HeaderPrimary';
|
||||
import HeaderSecondary from './components/HeaderSecondary';
|
||||
|
||||
import IndexPage from './components/IndexPage';
|
||||
import PostsUserPage from './components/PostsUserPage';
|
||||
|
||||
export default class Forum extends Application {
|
||||
routes = {
|
||||
'index': { path: '/', component: IndexPage.component() },
|
||||
'index.filter': {path: '/:filter', component: IndexPage.component() },
|
||||
'index': { path: '/', component: new IndexPage() },
|
||||
'index.filter': { path: '/:filter', component: new IndexPage() },
|
||||
|
||||
'user': { path: '/u/:username', component: new PostsUserPage() },
|
||||
'user.posts': { path: '/u/:username', component: new PostsUserPage() },
|
||||
'user.discussions': { path: '/u/:username', component: new PostsUserPage() },
|
||||
'settings': { path: '/u/:username', component: new PostsUserPage() },
|
||||
};
|
||||
|
||||
/**
|
||||
|
221
js/src/forum/components/AvatarEditor.tsx
Normal file
221
js/src/forum/components/AvatarEditor.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import Component, {ComponentProps} from '../../common/Component';
|
||||
import avatar from '../../common/helpers/avatar';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import Button from '../../common/components/Button';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import User from '../../common/models/User';
|
||||
|
||||
export interface AvatarEditorProps extends ComponentProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `AvatarEditor` component displays a user's avatar along with a dropdown
|
||||
* menu which allows the user to upload/remove the avatar.
|
||||
*/
|
||||
export default class AvatarEditor extends Component<AvatarEditorProps> {
|
||||
/**
|
||||
* Whether or not an avatar upload is in progress.
|
||||
*/
|
||||
loading = false;
|
||||
|
||||
/**
|
||||
* Whether or not an image has been dragged over the dropzone.
|
||||
*/
|
||||
isDraggedOver = false;
|
||||
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className = props.className || '';
|
||||
}
|
||||
|
||||
view() {
|
||||
const user = this.props.user;
|
||||
|
||||
return (
|
||||
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '') + (this.isDraggedOver ? ' dragover' : '')}>
|
||||
{avatar(user)}
|
||||
<a className={ user.avatarUrl() ? "Dropdown-toggle" : "Dropdown-toggle AvatarEditor--noAvatar" }
|
||||
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
|
||||
data-toggle="dropdown"
|
||||
onclick={this.quickUpload.bind(this)}
|
||||
ondragover={this.enableDragover.bind(this)}
|
||||
ondragenter={this.enableDragover.bind(this)}
|
||||
ondragleave={this.disableDragover.bind(this)}
|
||||
ondragend={this.disableDragover.bind(this)}
|
||||
ondrop={this.dropUpload.bind(this)}>
|
||||
{this.loading ? LoadingIndicator.component() : (user.avatarUrl() ? icon('fas fa-pencil-alt') : icon('fas fa-plus-circle'))}
|
||||
</a>
|
||||
<ul className="Dropdown-menu Menu">
|
||||
{listItems(this.controlItems().toArray())}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the items in the edit avatar dropdown menu.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
controlItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('upload',
|
||||
Button.component({
|
||||
icon: 'fas fa-upload',
|
||||
children: app.translator.trans('core.forum.user.avatar_upload_button'),
|
||||
onclick: this.openPicker.bind(this)
|
||||
})
|
||||
);
|
||||
|
||||
items.add('remove',
|
||||
Button.component({
|
||||
icon: 'fas fa-times',
|
||||
children: app.translator.trans('core.forum.user.avatar_remove_button'),
|
||||
onclick: this.remove.bind(this)
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable dragover style
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
enableDragover(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.isDraggedOver = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable dragover style
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
disableDragover(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.isDraggedOver = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload avatar when file is dropped into dropzone.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
dropUpload(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.isDraggedOver = false;
|
||||
this.upload(e.dataTransfer.files[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user doesn't have an avatar, there's no point in showing the
|
||||
* controls dropdown, because only one option would be viable: uploading.
|
||||
* Thus, when the avatar editor's dropdown toggle button is clicked, we prompt
|
||||
* the user to upload an avatar immediately.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
quickUpload(e) {
|
||||
if (!this.props.user.avatarUrl()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.openPicker();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload avatar using file picker
|
||||
*/
|
||||
openPicker() {
|
||||
if (this.loading) return;
|
||||
|
||||
// Create a hidden HTML input element and click on it so the user can select
|
||||
// an avatar file. Once they have, we will upload it via the API.
|
||||
const user = this.props.user;
|
||||
const $input = $('<input type="file">');
|
||||
|
||||
$input.appendTo('body').hide().click().on('change', e => {
|
||||
this.upload($(e.target)[0].files[0]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload avatar
|
||||
*
|
||||
* @param {File} file
|
||||
*/
|
||||
upload(file) {
|
||||
if (this.loading) return;
|
||||
|
||||
const user = this.props.user;
|
||||
const data = new FormData();
|
||||
data.append('avatar', file);
|
||||
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
|
||||
serialize: raw => raw,
|
||||
data
|
||||
}).then(
|
||||
this.success.bind(this),
|
||||
this.failure.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the user's avatar.
|
||||
*/
|
||||
remove() {
|
||||
const user = this.props.user;
|
||||
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'DELETE',
|
||||
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar'
|
||||
}).then(
|
||||
this.success.bind(this),
|
||||
this.failure.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* After a successful upload/removal, push the updated user data into the
|
||||
* store, and force a recomputation of the user's avatar color.
|
||||
*
|
||||
* @param {Object} response
|
||||
* @protected
|
||||
*/
|
||||
success(response) {
|
||||
app.store.pushPayload(response);
|
||||
delete this.props.user.avatarColor;
|
||||
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* If avatar upload/removal fails, stop loading.
|
||||
*
|
||||
* @param {Object} response
|
||||
* @protected
|
||||
*/
|
||||
failure(response) {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
}
|
35
js/src/forum/components/Page.tsx
Normal file
35
js/src/forum/components/Page.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Component from '../../common/Component';
|
||||
|
||||
/**
|
||||
* The `Page` component
|
||||
*/
|
||||
export default abstract class Page extends Component {
|
||||
/**
|
||||
* A class name to apply to the body while the route is active.
|
||||
*/
|
||||
bodyClass: string = '';
|
||||
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
if (this.bodyClass) {
|
||||
$('#app').addClass(this.bodyClass);
|
||||
}
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
app.previous = app.current;
|
||||
app.current = this;
|
||||
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
}
|
||||
|
||||
onremove(vnode) {
|
||||
super.onremove(vnode);
|
||||
|
||||
$('#app').removeClass(this.bodyClass);
|
||||
}
|
||||
}
|
144
js/src/forum/components/PostsUserPage.tsx
Normal file
144
js/src/forum/components/PostsUserPage.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import UserPage from './UserPage';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import Button from '../../common/components/Button';
|
||||
// import Placeholder from '../../common/components/Placeholder';
|
||||
// import CommentPost from './CommentPost';
|
||||
import Post from "../../common/models/Post";
|
||||
|
||||
/**
|
||||
* The `PostsUserPage` component shows a user's activity feed inside of their
|
||||
* profile.
|
||||
*/
|
||||
export default class PostsUserPage extends UserPage {
|
||||
/**
|
||||
* Whether or not the activity feed is currently loading.
|
||||
*/
|
||||
loading = true;
|
||||
|
||||
/**
|
||||
* Whether or not there are any more activity items that can be loaded.
|
||||
*/
|
||||
moreResults = false;
|
||||
|
||||
/**
|
||||
* The Post models in the feed.
|
||||
*/
|
||||
posts: Post[] = [];
|
||||
|
||||
/**
|
||||
* The number of activity items to load per request.
|
||||
*/
|
||||
loadLimit = 20;
|
||||
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.loadUser(m.route.param('username'));
|
||||
}
|
||||
|
||||
content() {
|
||||
return <p>test</p>;
|
||||
|
||||
if (this.posts.length === 0 && ! this.loading) {
|
||||
return (
|
||||
<div className="PostsUserPage">
|
||||
<Placeholder text={app.translator.trans('core.forum.user.posts_empty_text')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let footer;
|
||||
|
||||
if (this.loading) {
|
||||
footer = LoadingIndicator.component();
|
||||
} else if (this.moreResults) {
|
||||
footer = (
|
||||
<div className="PostsUserPage-loadMore">
|
||||
{Button.component({
|
||||
children: app.translator.trans('core.forum.user.posts_load_more_button'),
|
||||
className: 'Button',
|
||||
onclick: this.loadMore.bind(this)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="PostsUserPage">
|
||||
<ul className="PostsUserPage-list">
|
||||
{this.posts.map(post => (
|
||||
<li>
|
||||
<div className="PostsUserPage-discussion">
|
||||
{app.translator.trans('core.forum.user.in_discussion_text', {discussion: <a href={app.route.post(post)} oncreate={m.route}>{post.discussion().title()}</a>})}
|
||||
</div>
|
||||
{CommentPost.component({post})}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="PostsUserPage-loadMore">
|
||||
{footer}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component with a user, and trigger the loading of their
|
||||
* activity feed.
|
||||
*/
|
||||
show(user) {
|
||||
super.show(user);
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and reload the user's activity feed.
|
||||
*/
|
||||
refresh() {
|
||||
this.loading = true;
|
||||
this.posts = [];
|
||||
|
||||
m.redraw();
|
||||
|
||||
this.loadResults().then(this.parseResults.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a new page of the user's activity feed.
|
||||
*
|
||||
* @param offset The position to start getting results from.
|
||||
*/
|
||||
protected loadResults(offset?: number): Promise<Post[]> {
|
||||
return app.store.find<Post>('posts', {
|
||||
filter: {
|
||||
user: this.user.id(),
|
||||
type: 'comment'
|
||||
},
|
||||
page: {offset, limit: this.loadLimit},
|
||||
sort: '-createdAt'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next page of results.
|
||||
*/
|
||||
loadMore() {
|
||||
this.loading = true;
|
||||
this.loadResults(this.posts.length).then(this.parseResults.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse results and append them to the activity feed.
|
||||
*/
|
||||
parseResults(results: Post[]): Post[] {
|
||||
this.loading = false;
|
||||
|
||||
[].push.apply(this.posts, results);
|
||||
|
||||
this.moreResults = results.length >= this.loadLimit;
|
||||
m.redraw();
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
@@ -42,14 +42,14 @@ export default class SessionDropdown extends Dropdown {
|
||||
const items = new ItemList();
|
||||
const user = app.session.user;
|
||||
|
||||
// items.add('profile',
|
||||
// LinkButton.component({
|
||||
// icon: 'fas fa-user',
|
||||
// children: app.translator.trans('core.forum.header.profile_button'),
|
||||
// href: app.route.user(user)
|
||||
// }),
|
||||
// 100
|
||||
// );
|
||||
items.add('profile',
|
||||
LinkButton.component({
|
||||
icon: 'fas fa-user',
|
||||
children: app.translator.trans('core.forum.header.profile_button'),
|
||||
href: app.route.user(user)
|
||||
}),
|
||||
100
|
||||
);
|
||||
|
||||
// items.add('settings',
|
||||
// LinkButton.component({
|
||||
|
100
js/src/forum/components/UserCard.tsx
Normal file
100
js/src/forum/components/UserCard.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import Component, {ComponentProps} from '../../common/Component';
|
||||
import humanTime from '../../common/utils/humanTime';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import UserControls from '../utils/UserControls';
|
||||
import avatar from '../../common/helpers/avatar';
|
||||
import username from '../../common/helpers/username';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import AvatarEditor from './AvatarEditor';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import User from "../../common/models/User";
|
||||
|
||||
export interface UserCardProps extends ComponentProps {
|
||||
user: User;
|
||||
editable: boolean;
|
||||
controlsButtonClassName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `UserCard` component displays a user's profile card. This is used both on
|
||||
* the `UserPage` (in the hero) and in discussions, shown when hovering over a
|
||||
* post author.
|
||||
*/
|
||||
export default class UserCard extends Component<UserCardProps> {
|
||||
view() {
|
||||
const user = this.props.user;
|
||||
const controls = UserControls.controls(user, this).toArray();
|
||||
const color = user.color();
|
||||
const badges = user.badges().toArray();
|
||||
|
||||
return (
|
||||
<div className={'UserCard ' + (this.props.className || '')}
|
||||
style={color ? {backgroundColor: color} : ''}>
|
||||
<div className="darkenBackground">
|
||||
|
||||
<div className="container">
|
||||
{controls.length ? Dropdown.component({
|
||||
children: controls,
|
||||
className: 'UserCard-controls App-primaryControl',
|
||||
menuClassName: 'Dropdown-menu--right',
|
||||
buttonClassName: this.props.controlsButtonClassName,
|
||||
label: app.translator.trans('core.forum.user_controls.button'),
|
||||
icon: 'fas fa-ellipsis-v'
|
||||
}) : ''}
|
||||
|
||||
<div className="UserCard-profile">
|
||||
<h2 className="UserCard-identity">
|
||||
{this.props.editable
|
||||
? [AvatarEditor.component({user, className: 'UserCard-avatar'}), username(user)]
|
||||
: (
|
||||
<a href={app.route.user(user)} oncreate={m.route}>
|
||||
<div className="UserCard-avatar">{avatar(user)}</div>
|
||||
{username(user)}
|
||||
</a>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
{badges.length ? (
|
||||
<ul className="UserCard-badges badges">
|
||||
{listItems(badges)}
|
||||
</ul>
|
||||
) : ''}
|
||||
|
||||
<ul className="UserCard-info">
|
||||
{listItems(this.infoItems().toArray())}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list of tidbits of info to show on this user's profile.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
infoItems() {
|
||||
const items = new ItemList();
|
||||
const user = this.props.user;
|
||||
const lastSeenAt = user.lastSeenAt();
|
||||
|
||||
if (lastSeenAt) {
|
||||
const online = user.isOnline();
|
||||
|
||||
items.add('lastSeen', (
|
||||
<span className={'UserCard-lastSeen' + (online ? ' online' : '')}>
|
||||
{online
|
||||
? [icon('fas fa-circle'), ' ', app.translator.trans('core.forum.user.online_text')]
|
||||
: [icon('far fa-clock'), ' ', humanTime(lastSeenAt)]}
|
||||
</span>
|
||||
));
|
||||
}
|
||||
|
||||
items.add('joined', app.translator.trans('core.forum.user.joined_date_text', {ago: humanTime(user.joinTime())}));
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
145
js/src/forum/components/UserPage.tsx
Normal file
145
js/src/forum/components/UserPage.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import Page from './Page';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import affixSidebar from '../utils/affixSidebar';
|
||||
import UserCard from './UserCard';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||
import LinkButton from '../../common/components/LinkButton';
|
||||
import Separator from '../../common/components/Separator';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
|
||||
import User from '../../common/models/User';
|
||||
|
||||
/**
|
||||
* The `UserPage` component shows a user's profile. It can be extended to show
|
||||
* content inside of the content area. See `ActivityPage` and `SettingsPage` for
|
||||
* examples.
|
||||
*/
|
||||
export default abstract class UserPage extends Page {
|
||||
/**
|
||||
* The user this page is for.
|
||||
*/
|
||||
user: User;
|
||||
bodyClass: string = 'App--user';
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div className="UserPage">
|
||||
{this.user ? [
|
||||
UserCard.component({
|
||||
user: this.user,
|
||||
className: 'Hero UserHero',
|
||||
editable: this.user.canEdit() || this.user === app.session.user,
|
||||
controlsButtonClassName: 'Button'
|
||||
}),
|
||||
<div className="container">
|
||||
<div className="sideNavContainer">
|
||||
<nav className="sideNav UserPage-nav" config={affixSidebar}>
|
||||
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
<div className="sideNavOffset UserPage-content">
|
||||
{this.content()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
] : [
|
||||
LoadingIndicator.component({lassName: 'LoadingIndicator--block'})
|
||||
]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content to display in the user page.
|
||||
*/
|
||||
abstract content();
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the component with a user, and trigger the loading of their
|
||||
* activity feed.
|
||||
*/
|
||||
protected show(user: User) {
|
||||
this.user = user;
|
||||
|
||||
app.setTitle(user.displayName());
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
loadUser(username: string) {
|
||||
const lowercaseUsername = username.toLowerCase();
|
||||
|
||||
app.store.all('users').some(user => {
|
||||
if (user.username().toLowerCase() === lowercaseUsername && user.joinTime()) {
|
||||
this.show(user);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.user) {
|
||||
app.store.find('users', username).then(this.show.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the content of the sidebar.
|
||||
*/
|
||||
sidebarItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('nav',
|
||||
SelectDropdown.component({
|
||||
children: this.navItems().toArray(),
|
||||
className: 'App-titleControl',
|
||||
buttonClassName: 'Button'
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the navigation in the sidebar.
|
||||
*/
|
||||
navItems() {
|
||||
const items = new ItemList();
|
||||
const user = this.user;
|
||||
|
||||
items.add('posts',
|
||||
LinkButton.component({
|
||||
href: app.route('user.posts', {username: user.username()}),
|
||||
children: [app.translator.trans('core.forum.user.posts_link'), <span className="Button-badge">{user.commentCount()}</span>],
|
||||
icon: 'far fa-comment'
|
||||
}),
|
||||
100
|
||||
);
|
||||
|
||||
items.add('discussions',
|
||||
LinkButton.component({
|
||||
href: app.route('user.discussions', {username: user.username()}),
|
||||
children: [app.translator.trans('core.forum.user.discussions_link'), <span className="Button-badge">{user.discussionCount()}</span>],
|
||||
icon: 'fas fa-bars'
|
||||
}),
|
||||
90
|
||||
);
|
||||
|
||||
if (app.session.user === user) {
|
||||
items.add('separator', Separator.component(), -90);
|
||||
items.add('settings',
|
||||
LinkButton.component({
|
||||
href: app.route('settings'),
|
||||
children: app.translator.trans('core.forum.user.settings_link'),
|
||||
icon: 'fas fa-cog'
|
||||
}),
|
||||
-100
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
@@ -41,7 +41,7 @@ export default class UsersSearchSource extends SearchSource {
|
||||
|
||||
return (
|
||||
<li className="UserSearchResult" data-index={'users' + user.id()}>
|
||||
<a href={app.route.user(user)} config={m.route}>
|
||||
<a href={app.route.user(user)} oncreate={m.route}>
|
||||
{avatar(user)}
|
||||
{name}
|
||||
</a>
|
||||
|
96
js/src/forum/utils/UserControls.ts
Normal file
96
js/src/forum/utils/UserControls.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import Button from '../../common/components/Button';
|
||||
import Separator from '../../common/components/Separator';
|
||||
// import EditUserModal from '../components/EditUserModal';
|
||||
import UserPage from '../components/UserPage';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import User from "../../common/models/User";
|
||||
|
||||
/**
|
||||
* The `UserControls` utility constructs a list of buttons for a user which
|
||||
* perform actions on it.
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Get a list of controls for a user.
|
||||
*
|
||||
* @param user
|
||||
* @param context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
*/
|
||||
controls(user: User, context: any): ItemList {
|
||||
const items = new ItemList();
|
||||
|
||||
['user', 'moderation', 'destructive'].forEach(section => {
|
||||
const controls = this[section + 'Controls'](user, context).toArray();
|
||||
if (controls.length) {
|
||||
controls.forEach(item => items.add(item.itemName, item));
|
||||
items.add(section + 'Separator', Separator.component());
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a user pertaining to the current user (e.g. poke, follow).
|
||||
*/
|
||||
userControls(): ItemList {
|
||||
return new ItemList();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a user pertaining to moderation (e.g. suspend, edit).
|
||||
*/
|
||||
moderationControls(user: User): ItemList {
|
||||
const items = new ItemList();
|
||||
|
||||
if (user.canEdit()) {
|
||||
items.add('edit', Button.component({
|
||||
icon: 'fas fa-pencil-alt',
|
||||
children: app.translator.trans('core.forum.user_controls.edit_button'),
|
||||
onclick: this.editAction.bind(user)
|
||||
}));
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a user which are destructive (e.g. delete).
|
||||
*/
|
||||
destructiveControls(user: User): ItemList {
|
||||
const items = new ItemList();
|
||||
|
||||
if (user.id() !== '1' && user.canDelete()) {
|
||||
items.add('delete', Button.component({
|
||||
icon: 'fas fa-times',
|
||||
children: app.translator.trans('core.forum.user_controls.delete_button'),
|
||||
onclick: this.deleteAction.bind(user)
|
||||
}));
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the user.
|
||||
*/
|
||||
deleteAction() {
|
||||
if (confirm(app.translator.trans('core.forum.user_controls.delete_confirmation'))) {
|
||||
this.delete().then(() => {
|
||||
if (app.current instanceof UserPage && app.current.user === this) {
|
||||
app.history.back();
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit the user.
|
||||
*/
|
||||
editAction() {
|
||||
app.modal.show(new EditUserModal({user: this}));
|
||||
}
|
||||
};
|
17
js/src/forum/utils/affixSidebar.ts
Normal file
17
js/src/forum/utils/affixSidebar.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Setup the sidebar DOM element to be affixed to the top of the viewport
|
||||
* using hcSticky.
|
||||
*/
|
||||
export default function affixSidebar(vnode) {
|
||||
const element = vnode.dom;
|
||||
const $sidebar = $(element);
|
||||
const $header = $('#header');
|
||||
const $affixElement = $sidebar.find('> ul')[0];
|
||||
|
||||
$(window).off('.affix');
|
||||
|
||||
new hcSticky($affixElement, {
|
||||
stickTo: element,
|
||||
top: $header.outerHeight(true) + parseInt($sidebar.css('margin-top'), 10),
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user