mirror of
https://github.com/flarum/framework.git
synced 2024-11-29 12:43:52 +08:00
Fix listItems isSeparator function, add m() children to attrs, work on posts, subtree retainer
This commit is contained in:
parent
49d2539aef
commit
0de0c83353
12
js/dist/admin.js
vendored
12
js/dist/admin.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/admin.js.map
vendored
2
js/dist/admin.js.map
vendored
File diff suppressed because one or more lines are too long
12
js/dist/forum.js
vendored
12
js/dist/forum.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
vendored
2
js/dist/forum.js.map
vendored
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
||||||
import Mithril from "mithril";
|
import Mithril from 'mithril';
|
||||||
|
|
||||||
import Bus from './Bus';
|
import Bus from './Bus';
|
||||||
import Translator from './Translator';
|
import Translator from './Translator';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import MicroModal from 'micromodal';
|
import MicroModal from 'micromodal';
|
||||||
|
|
||||||
import Component from "../Component";
|
import Component from '../Component';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -106,7 +106,7 @@ export default class ModalManager extends Component {
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
onready() {
|
onready() {
|
||||||
if (this.component && this.component.onready) {
|
if (this.component?.onready) {
|
||||||
this.component.onready();
|
this.component.onready();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
js/src/common/components/Placeholder.tsx
Normal file
19
js/src/common/components/Placeholder.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import Component, {ComponentProps} from '../Component';
|
||||||
|
|
||||||
|
export interface PlaceholderProps extends ComponentProps {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `Placeholder` component displays a muted text with some call to action,
|
||||||
|
* usually used as an empty state.
|
||||||
|
*/
|
||||||
|
export default class Placeholder extends Component<PlaceholderProps> {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className="Placeholder">
|
||||||
|
<p>{this.props.text}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import Separator from '../components/Separator';
|
import Separator from '../components/Separator';
|
||||||
|
|
||||||
export function isSeparator(item) {
|
export function isSeparator(item) {
|
||||||
return item && item.component === Separator;
|
return item?.tag === Separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withoutUnnecessarySeparators(items) {
|
export function withoutUnnecessarySeparators(items) {
|
||||||
|
|
14
js/src/common/helpers/userOnline.tsx
Normal file
14
js/src/common/helpers/userOnline.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import icon from './icon';
|
||||||
|
import User from '../models/User';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `useronline` helper displays a green circle if the user is online
|
||||||
|
*
|
||||||
|
* @param {User} user
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
export default function userOnline(user: User) {
|
||||||
|
if (user.lastSeenAt() && user.isOnline()) {
|
||||||
|
return <span className="UserOnline">{icon('fas fa-circle')}</span>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import stringToColor from '../utils/stringToColor';
|
||||||
import ItemList from '../utils/ItemList';
|
import ItemList from '../utils/ItemList';
|
||||||
import computed from '../utils/computed';
|
import computed from '../utils/computed';
|
||||||
import GroupBadge from '../components/GroupBadge';
|
import GroupBadge from '../components/GroupBadge';
|
||||||
import Group from "./Group";
|
import Group from './Group';
|
||||||
|
|
||||||
export default class User extends Model {
|
export default class User extends Model {
|
||||||
username = Model.attribute('username') as () => string;
|
username = Model.attribute('username') as () => string;
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default class ItemList<T = any> {
|
||||||
* Get the content of an item.
|
* Get the content of an item.
|
||||||
*/
|
*/
|
||||||
get(key: any): T {
|
get(key: any): T {
|
||||||
return this.items[key].content;
|
return this.items[key]?.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,6 +66,7 @@ export default class ItemList<T = any> {
|
||||||
if (this.items[i] !== null && this.items[i] instanceof Item) {
|
if (this.items[i] !== null && this.items[i] instanceof Item) {
|
||||||
this.items[i].content = Object(this.items[i].content);
|
this.items[i].content = Object(this.items[i].content);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
this.items[i].content.itemName = i;
|
this.items[i].content.itemName = i;
|
||||||
items.push(this.items[i]);
|
items.push(this.items[i]);
|
||||||
this.items[i].key = items.length;
|
this.items[i].key = items.length;
|
||||||
|
|
30
js/src/common/utils/SubtreeRetainer.ts
Normal file
30
js/src/common/utils/SubtreeRetainer.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
export default class SubtreeRetainer {
|
||||||
|
callbacks: Function[];
|
||||||
|
data = {};
|
||||||
|
|
||||||
|
constructor(...callbacks: Function[]) {
|
||||||
|
this.callbacks = callbacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
check(...callbacks: Function[]) {
|
||||||
|
this.callbacks.concat(...callbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether the component should redraw.
|
||||||
|
*/
|
||||||
|
update(): boolean {
|
||||||
|
let update = false;
|
||||||
|
|
||||||
|
this.callbacks.forEach((callback, i) => {
|
||||||
|
const result = callback();
|
||||||
|
|
||||||
|
if (result !== this.data[i]) {
|
||||||
|
this.data[i] = result;
|
||||||
|
update = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,29 @@
|
||||||
import m from 'mithril';
|
import m from 'mithril';
|
||||||
import prop from 'mithril/stream';
|
import prop from 'mithril/stream';
|
||||||
|
|
||||||
|
import Component from '../Component';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const mo = window['m'];
|
const mo = window['m'];
|
||||||
|
|
||||||
const _m = function (comp, ...args) {
|
const _m = function (comp, ...args) {
|
||||||
|
if (!arguments[1]) arguments[1] = {};
|
||||||
|
|
||||||
|
if (comp.prototype && comp.prototype instanceof Component) {
|
||||||
|
let children = args.slice(1);
|
||||||
|
if (children.length === 1 && Array.isArray(children[0])) {
|
||||||
|
children = children[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children) {
|
||||||
|
Object.defineProperty(arguments[1], 'children', {
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
arguments[1].children = (arguments[1].children || []).concat(children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const node = mo.apply(this, arguments);
|
const node = mo.apply(this, arguments);
|
||||||
|
|
||||||
if (!node.attrs) node.attrs = {};
|
if (!node.attrs) node.attrs = {};
|
||||||
|
|
151
js/src/forum/components/CommentPost.tsx
Normal file
151
js/src/forum/components/CommentPost.tsx
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import Post from './Post';
|
||||||
|
import PostUser from './PostUser';
|
||||||
|
// import PostMeta from './PostMeta';
|
||||||
|
// import PostEdited from './PostEdited';
|
||||||
|
// import EditPostComposer from './EditPostComposer';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `CommentPost` component displays a standard `comment`-typed post. This
|
||||||
|
* includes a number of item lists (controls, header, and footer) surrounding
|
||||||
|
* the post's HTML content.
|
||||||
|
*/
|
||||||
|
export default class CommentPost extends Post {
|
||||||
|
/**
|
||||||
|
* If the post has been hidden, then this flag determines whether or not its
|
||||||
|
* content has been expanded.
|
||||||
|
*/
|
||||||
|
revealContent: boolean = false;
|
||||||
|
|
||||||
|
postUser: PostUser;
|
||||||
|
|
||||||
|
oninit(vnode) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
// Create an instance of the component that displays the post's author so
|
||||||
|
// that we can force the post to rerender when the user card is shown.
|
||||||
|
this.postUser = PostUser.component({post: this.props.post});
|
||||||
|
|
||||||
|
this.subtree.check(
|
||||||
|
() => this.postUser.cardVisible,
|
||||||
|
() => this.isEditing()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
// Note: we avoid using JSX for the <ul> below because it results in some
|
||||||
|
// weirdness in Mithril.js 0.1.x (see flarum/core#975). This workaround can
|
||||||
|
// be reverted when we upgrade to Mithril 1.0.
|
||||||
|
return super.content().concat([
|
||||||
|
<header className="Post-header">{m('ul', listItems(this.headerItems().toArray()))}</header>,
|
||||||
|
<div className="Post-body">
|
||||||
|
{this.isEditing()
|
||||||
|
? <div className="Post-preview" config={this.configPreview.bind(this)}/>
|
||||||
|
: m.trust(this.props.post.contentHtml())}
|
||||||
|
</div>
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
onupdate(vnode) {
|
||||||
|
super.onupdate(vnode);
|
||||||
|
|
||||||
|
const contentHtml = this.isEditing() ? '' : this.props.post.contentHtml();
|
||||||
|
|
||||||
|
// TODO: idk what this is supposed to be
|
||||||
|
|
||||||
|
// If the post content has changed since the last render, we'll run through
|
||||||
|
// all of the <script> tags in the content and evaluate them. This is
|
||||||
|
// necessary because TextFormatter outputs them for e.g. syntax highlighting.
|
||||||
|
if (vnode.contentHtml !== contentHtml) {
|
||||||
|
this.$('.Post-body script').each(function() {
|
||||||
|
eval.call(window, $(this).text());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
vnode.contentHtml = contentHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditing() {
|
||||||
|
return false; // TODO
|
||||||
|
// return app.composer?.component instanceof EditPostComposer &&
|
||||||
|
// app.composer.component.props.post === this.props.post;
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs() {
|
||||||
|
const post = this.props.post;
|
||||||
|
const attrs = super.attrs();
|
||||||
|
|
||||||
|
attrs.className = (attrs.className || '') + ' ' + classNames({
|
||||||
|
'CommentPost': true,
|
||||||
|
'Post--hidden': post.isHidden(),
|
||||||
|
'Post--edited': post.isEdited(),
|
||||||
|
'revealContent': this.revealContent,
|
||||||
|
'editing': this.isEditing()
|
||||||
|
});
|
||||||
|
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO change so it works
|
||||||
|
configPreview(element, isInitialized, context) {
|
||||||
|
if (isInitialized) return;
|
||||||
|
|
||||||
|
// Every 50ms, if the composer content has changed, then update the post's
|
||||||
|
// body with a preview.
|
||||||
|
let preview;
|
||||||
|
const updatePreview = () => {
|
||||||
|
const content = app.composer.component.content();
|
||||||
|
|
||||||
|
if (preview === content) return;
|
||||||
|
|
||||||
|
preview = content;
|
||||||
|
|
||||||
|
s9e.TextFormatter.preview(preview || '', element);
|
||||||
|
};
|
||||||
|
updatePreview();
|
||||||
|
|
||||||
|
const updateInterval = setInterval(updatePreview, 50);
|
||||||
|
context.onunload = () => clearInterval(updateInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the visibility of a hidden post's content.
|
||||||
|
*/
|
||||||
|
toggleContent() {
|
||||||
|
this.revealContent = !this.revealContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list for the post's header.
|
||||||
|
*
|
||||||
|
* @return {ItemList}
|
||||||
|
*/
|
||||||
|
headerItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
const post = this.props.post;
|
||||||
|
const props = {post};
|
||||||
|
|
||||||
|
items.add('user', this.postUser, 100);
|
||||||
|
// items.add('meta', PostMeta.component(props));
|
||||||
|
|
||||||
|
if (post.isEdited() && !post.isHidden()) {
|
||||||
|
items.add('edited', PostEdited.component(props));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the post is hidden, add a button that allows toggling the visibility
|
||||||
|
// of the post's content.
|
||||||
|
if (post.isHidden()) {
|
||||||
|
items.add('toggle', (
|
||||||
|
Button.component({
|
||||||
|
className: 'Button Button--default Button--more',
|
||||||
|
icon: 'fas fa-ellipsis-h',
|
||||||
|
onclick: this.toggleContent.bind(this)
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
118
js/src/forum/components/Post.tsx
Normal file
118
js/src/forum/components/Post.tsx
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import Component, {ComponentProps} from '../../common/Component';
|
||||||
|
import Dropdown from '../../common/components/Dropdown';
|
||||||
|
import PostControls from '../utils/PostControls';
|
||||||
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import SubtreeRetainer from "../../common/utils/SubtreeRetainer";
|
||||||
|
import PostModel from '../../common/models/Post';
|
||||||
|
|
||||||
|
export interface PostProps extends ComponentProps {
|
||||||
|
post: PostModel
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `Post` component displays a single post. The basic post template just
|
||||||
|
* includes a controls dropdown; subclasses must implement `content` and `attrs`
|
||||||
|
* methods.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
export default class Post<T extends PostProps = PostProps> extends Component<PostProps> {
|
||||||
|
loading = false;
|
||||||
|
controlsOpen = false;
|
||||||
|
|
||||||
|
subtree: SubtreeRetainer;
|
||||||
|
|
||||||
|
oninit(vnode) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up a subtree retainer so that the post will not be redrawn
|
||||||
|
* unless new data comes in.
|
||||||
|
*/
|
||||||
|
this.subtree = new SubtreeRetainer(
|
||||||
|
() => this.props.post.freshness,
|
||||||
|
() => {
|
||||||
|
const user = this.props.post.user();
|
||||||
|
return user?.freshness;
|
||||||
|
},
|
||||||
|
() => this.controlsOpen
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
const controls = PostControls.controls(this.props.post, this).toArray();
|
||||||
|
const attrs = this.attrs();
|
||||||
|
|
||||||
|
attrs.className = classNames('Post', this.loading && 'Post--loading', attrs.className);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article {...attrs}>
|
||||||
|
<div>
|
||||||
|
{this.content()}
|
||||||
|
<aside className="Post-actions">
|
||||||
|
<ul>
|
||||||
|
{listItems(this.actionItems().toArray())}
|
||||||
|
{controls.length ? <li>
|
||||||
|
<Dropdown
|
||||||
|
className="Post-controls"
|
||||||
|
buttonClassName="Button Button--icon Button--flat"
|
||||||
|
menuClassName="Dropdown-menu--right"
|
||||||
|
icon="fas fa-ellipsis-h"
|
||||||
|
onshow={() => this.$('.Post-actions').addClass('open')}
|
||||||
|
onhide={() => this.$('.Post-actions').removeClass('open')}>
|
||||||
|
{controls}
|
||||||
|
</Dropdown>
|
||||||
|
</li> : ''}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
<footer className="Post-footer"><ul>{listItems(this.footerItems().toArray())}</ul></footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onbeforeupdate(vnode) {
|
||||||
|
super.onbeforeupdate(vnode);
|
||||||
|
|
||||||
|
return this.subtree.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
|
||||||
|
const $actions = this.$('.Post-actions');
|
||||||
|
const $controls = this.$('.Post-controls');
|
||||||
|
|
||||||
|
$actions.toggleClass('open', $controls.hasClass('open'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get attributes for the post element.
|
||||||
|
*/
|
||||||
|
attrs(): ComponentProps {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the post's content.
|
||||||
|
*/
|
||||||
|
content() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list for the post's actions.
|
||||||
|
*/
|
||||||
|
actionItems() {
|
||||||
|
return new ItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list for the post's footer.
|
||||||
|
*/
|
||||||
|
footerItems() {
|
||||||
|
return new ItemList();
|
||||||
|
}
|
||||||
|
}
|
94
js/src/forum/components/PostUser.tsx
Normal file
94
js/src/forum/components/PostUser.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import Component from '../../common/Component';
|
||||||
|
import UserCard from './UserCard';
|
||||||
|
import avatar from '../../common/helpers/avatar';
|
||||||
|
import username from '../../common/helpers/username';
|
||||||
|
import userOnline from '../../common/helpers/userOnline';
|
||||||
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
import {PostProps} from "./Post";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `PostUser` component shows the avatar and username of a post's author.
|
||||||
|
*/
|
||||||
|
export default class PostUser extends Component<PostProps> {
|
||||||
|
/**
|
||||||
|
* Whether or not the user hover card is visible.
|
||||||
|
*/
|
||||||
|
cardVisible = false;
|
||||||
|
|
||||||
|
view() {
|
||||||
|
const post = this.props.post;
|
||||||
|
const user = post.user();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<div className="PostUser">
|
||||||
|
<h3>{avatar(user, {className: 'PostUser-avatar'})} {username(user)}</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let card = '';
|
||||||
|
|
||||||
|
if (!post.isHidden() && this.cardVisible) {
|
||||||
|
card = UserCard.component({
|
||||||
|
user,
|
||||||
|
className: 'UserCard--popover',
|
||||||
|
controlsButtonClassName: 'Button Button--icon Button--flat'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="PostUser">
|
||||||
|
<h3>
|
||||||
|
<m.route.Link href={app.route.user(user)}>
|
||||||
|
{avatar(user, {className: 'PostUser-avatar'})}
|
||||||
|
{userOnline(user)}
|
||||||
|
{username(user)}
|
||||||
|
</m.route.Link>
|
||||||
|
</h3>
|
||||||
|
<ul className="PostUser-badges badges">
|
||||||
|
{listItems(user.badges().toArray())}
|
||||||
|
</ul>
|
||||||
|
{card}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
|
||||||
|
let timeout;
|
||||||
|
|
||||||
|
this.$()
|
||||||
|
.on('mouseover', 'h3 a, .UserCard', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(this.showCard.bind(this), 500);
|
||||||
|
})
|
||||||
|
.on('mouseout', 'h3 a, .UserCard', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(this.hideCard.bind(this), 250);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the user card.
|
||||||
|
*/
|
||||||
|
showCard() {
|
||||||
|
this.cardVisible = true;
|
||||||
|
|
||||||
|
m.redraw();
|
||||||
|
|
||||||
|
setTimeout(() => this.$('.UserCard').addClass('in'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the user card.
|
||||||
|
*/
|
||||||
|
hideCard() {
|
||||||
|
this.$('.UserCard').removeClass('in')
|
||||||
|
.one('transitionend webkitTransitionEnd oTransitionEnd', () => {
|
||||||
|
this.cardVisible = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import UserPage from './UserPage';
|
import UserPage 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 Placeholder from '../../common/components/Placeholder';
|
import Placeholder from '../../common/components/Placeholder';
|
||||||
// import CommentPost from './CommentPost';
|
import CommentPost from './CommentPost';
|
||||||
import Post from "../../common/models/Post";
|
import Post from '../../common/models/Post';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `PostsUserPage` component shows a user's activity feed inside of their
|
* The `PostsUserPage` component shows a user's activity feed inside of their
|
||||||
|
@ -43,8 +43,6 @@ export default class PostsUserPage extends UserPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
return <p>test</p>;
|
|
||||||
|
|
||||||
if (this.posts.length === 0 && ! this.loading) {
|
if (this.posts.length === 0 && ! this.loading) {
|
||||||
return (
|
return (
|
||||||
<div className="PostsUserPage">
|
<div className="PostsUserPage">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import highlight from '../../common/helpers/highlight';
|
import highlight from '../../common/helpers/highlight';
|
||||||
import avatar from '../../common/helpers/avatar';
|
import avatar from '../../common/helpers/avatar';
|
||||||
import username from '../../common/helpers/username';
|
import username from '../../common/helpers/username';
|
||||||
import SearchSource from "./SearchSource";
|
import SearchSource from './SearchSource';
|
||||||
import User from '../../common/models/User';
|
import User from '../../common/models/User';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
175
js/src/forum/utils/PostControls.tsx
Normal file
175
js/src/forum/utils/PostControls.tsx
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
import {Vnode} from "mithril";
|
||||||
|
|
||||||
|
// import EditPostComposer from '../components/EditPostComposer';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import Separator from '../../common/components/Separator';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import Post from "../../common/models/Post";
|
||||||
|
import PostComponent from "../../forum/components/Post";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `PostControls` utility constructs a list of buttons for a post which
|
||||||
|
* perform actions on it.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Get a list of controls for a post.
|
||||||
|
*
|
||||||
|
* @param {Post} post
|
||||||
|
* @param {*} context The parent component under which the controls menu will
|
||||||
|
* be displayed.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
controls(post: Post, context) {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
['user', 'moderation', 'destructive'].forEach(section => {
|
||||||
|
const controls = this[section + 'Controls'](post, 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 post pertaining to the current user (e.g. report).
|
||||||
|
*
|
||||||
|
* @param {Post} post
|
||||||
|
* @param {*} context The parent component under which the controls menu will
|
||||||
|
* be displayed.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
userControls(post: Post, context) {
|
||||||
|
return new ItemList();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get controls for a post pertaining to moderation (e.g. edit).
|
||||||
|
*
|
||||||
|
* @param {Post} post
|
||||||
|
* @param {*} context The parent component under which the controls menu will
|
||||||
|
* be displayed.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
moderationControls(post: Post, context) {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
if (post.contentType() === 'comment' && post.canEdit()) {
|
||||||
|
if (!post.isHidden()) {
|
||||||
|
items.add('edit', Button.component({
|
||||||
|
icon: 'fas fa-pencil-alt',
|
||||||
|
onclick: this.editAction.bind(post)
|
||||||
|
}, app.translator.trans('core.forum.post_controls.edit_button')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get controls for a post that are destructive (e.g. delete).
|
||||||
|
*
|
||||||
|
* @param {Post} post
|
||||||
|
* @param {*} context The parent component under which the controls menu will
|
||||||
|
* be displayed.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
destructiveControls(post: Post, context) {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
if (post.contentType() === 'comment' && !post.isHidden()) {
|
||||||
|
if (post.canHide()) {
|
||||||
|
items.add('hide', Button.component({
|
||||||
|
icon: 'far fa-trash-alt',
|
||||||
|
children: app.translator.trans('core.forum.post_controls.delete_button'),
|
||||||
|
onclick: this.hideAction.bind(post)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (post.contentType() === 'comment' && post.canHide()) {
|
||||||
|
items.add('restore', Button.component({
|
||||||
|
icon: 'fas fa-reply',
|
||||||
|
children: app.translator.trans('core.forum.post_controls.restore_button'),
|
||||||
|
onclick: this.restoreAction.bind(post)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (post.canDelete()) {
|
||||||
|
items.add('delete', Button.component({
|
||||||
|
icon: 'fas fa-times',
|
||||||
|
children: app.translator.trans('core.forum.post_controls.delete_forever_button'),
|
||||||
|
onclick: this.deleteAction.bind(post, context)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the composer to edit a post.
|
||||||
|
*/
|
||||||
|
editAction(this: Post) {
|
||||||
|
return new Promise<EditPostComposer>(resolve => {
|
||||||
|
const component = new EditPostComposer({ post: this });
|
||||||
|
|
||||||
|
app.composer.load(component);
|
||||||
|
app.composer.show();
|
||||||
|
|
||||||
|
resolve(component);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide a post.
|
||||||
|
*/
|
||||||
|
hideAction(this: Post) {
|
||||||
|
this.pushAttributes({ hiddenAt: new Date(), hiddenUser: app.session.user });
|
||||||
|
|
||||||
|
return this.save({ isHidden: true }).then(() => m.redraw());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore a post.
|
||||||
|
*/
|
||||||
|
restoreAction(this: Post) {
|
||||||
|
this.pushAttributes({ hiddenAt: null, hiddenUser: null });
|
||||||
|
|
||||||
|
return this.save({ isHidden: false }).then(() => m.redraw());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a post.
|
||||||
|
*/
|
||||||
|
deleteAction(this: Post, context: PostComponent) {
|
||||||
|
if (context) context.loading = true;
|
||||||
|
|
||||||
|
return this.delete()
|
||||||
|
.then(() => {
|
||||||
|
const discussion = this.discussion();
|
||||||
|
|
||||||
|
discussion.removePost(this.id());
|
||||||
|
|
||||||
|
// If this was the last post in the discussion, then we will assume that
|
||||||
|
// the whole discussion was deleted too.
|
||||||
|
if (!discussion.postIds().length) {
|
||||||
|
// If there is a discussion list in the cache, remove this discussion.
|
||||||
|
if (app.cache.discussionList) {
|
||||||
|
app.cache.discussionList.removeDiscussion(discussion);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.viewingDiscussion(discussion)) {
|
||||||
|
app.history.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.then(() => {
|
||||||
|
if (context) context.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user