mirror of
https://github.com/flarum/framework.git
synced 2025-02-02 02:13:01 +08:00
forum: add DiscussionList component with DiscussionListItem & TerminalPost
This commit is contained in:
parent
7485559cbf
commit
21d19df9bd
|
@ -20,7 +20,7 @@ export default class Discussion extends Model {
|
|||
lastPostNumber = Model.attribute('lastPostNumber') as () => number;
|
||||
|
||||
commentCount = Model.attribute('commentCount') as () => number;
|
||||
replyCount = computed('commentCount', commentCount => Math.max(0, commentCount - 1)) as () => string;
|
||||
replyCount = computed('commentCount', commentCount => Math.max(0, commentCount - 1)) as () => number;
|
||||
posts = Model.hasMany('posts') as () => Post[];
|
||||
mostRelevantPost = Model.hasOne('mostRelevantPost') as () => Post;
|
||||
|
||||
|
|
|
@ -6,10 +6,13 @@ import HeaderSecondary from './components/HeaderSecondary';
|
|||
|
||||
import Page from './components/Page';
|
||||
import IndexPage from './components/IndexPage';
|
||||
import DiscussionList from './components/DiscussionList';
|
||||
import DiscussionPage from './components/DiscussionPage';
|
||||
import PostsUserPage from './components/PostsUserPage';
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
|
||||
import CommentPost from './components/CommentPost';
|
||||
|
||||
import User from '../common/models/User';
|
||||
import Post from '../common/models/Post';
|
||||
import Discussion from '../common/models/Discussion';
|
||||
|
|
194
js/src/forum/components/DiscussionList.tsx
Normal file
194
js/src/forum/components/DiscussionList.tsx
Normal file
|
@ -0,0 +1,194 @@
|
|||
import Component, { ComponentProps } from '../../common/Component';
|
||||
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';
|
||||
|
||||
export interface DiscussionListProps extends ComponentProps {
|
||||
/**
|
||||
* A map of parameters used to construct a refined parameter object
|
||||
* to send along in the API request to get discussion results.
|
||||
*/
|
||||
params: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `DiscussionList` component displays a list of discussions.
|
||||
*/
|
||||
export default class DiscussionList<T extends DiscussionListProps = DiscussionListProps> extends Component<T> {
|
||||
/**
|
||||
* Whether or not discussion results are loading.
|
||||
*/
|
||||
loading = true;
|
||||
|
||||
/**
|
||||
* Whether or not there are more results that can be loaded.
|
||||
*/
|
||||
moreResults = false;
|
||||
|
||||
/**
|
||||
* The discussions in the discussion list.
|
||||
*/
|
||||
discussions: Discussion[] = [];
|
||||
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
view() {
|
||||
const params = this.props.params;
|
||||
let loading;
|
||||
|
||||
if (this.loading) {
|
||||
loading = LoadingIndicator.component();
|
||||
} else if (this.moreResults) {
|
||||
loading = Button.component({
|
||||
children: app.translator.trans('core.forum.discussion_list.load_more_button'),
|
||||
className: 'Button',
|
||||
onclick: this.loadMore.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.discussions.length === 0 && !this.loading) {
|
||||
const text = app.translator.trans('core.forum.discussion_list.empty_text');
|
||||
return <div className="DiscussionList">{Placeholder.component({ text })}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'DiscussionList' + (this.props.params.q ? ' DiscussionList--searchResults' : '')}>
|
||||
<ul className="DiscussionList-discussions">
|
||||
{this.discussions.map(discussion => {
|
||||
return (
|
||||
<li key={discussion.id()} data-id={discussion.id()}>
|
||||
{DiscussionListItem.component({ discussion, params })}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="DiscussionList-loadMore">{loading}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters that should be passed in the API request to get
|
||||
* discussion results.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
requestParams(): any {
|
||||
const params = { include: ['user', 'lastPostedUser'], filter: {} };
|
||||
|
||||
params.sort = this.sortMap()[this.props.params.sort];
|
||||
|
||||
if (this.props.params.q) {
|
||||
params.filter.q = this.props.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: any = {};
|
||||
|
||||
if (this.props.params.q) {
|
||||
map.relevance = '';
|
||||
}
|
||||
map.latest = '-lastPostedAt';
|
||||
map.top = '-commentCount';
|
||||
map.newest = '-createdAt';
|
||||
map.oldest = 'createdAt';
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and reload the discussion list.
|
||||
*/
|
||||
public refresh(clear = true) {
|
||||
if (clear) {
|
||||
this.loading = true;
|
||||
this.discussions = [];
|
||||
}
|
||||
|
||||
return this.loadResults().then(
|
||||
results => {
|
||||
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?: number): Promise<Discussion[]> {
|
||||
const preloadedDiscussions = app.preloadedApiDocument();
|
||||
|
||||
if (preloadedDiscussions) {
|
||||
return Promise.resolve(preloadedDiscussions);
|
||||
}
|
||||
|
||||
const params = this.requestParams();
|
||||
params.page = { offset };
|
||||
params.include = params.include.join(',');
|
||||
|
||||
return app.store.find('discussions', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next page of discussion results.
|
||||
*/
|
||||
public 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: Discussion[]): Discussion[] {
|
||||
[].push.apply(this.discussions, results);
|
||||
|
||||
this.loading = false;
|
||||
this.moreResults = !!results.payload.links.next;
|
||||
|
||||
m.redraw();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a discussion from the list if it is present.
|
||||
*/
|
||||
public removeDiscussion(discussion: Discussion) {
|
||||
const index = this.discussions.indexOf(discussion);
|
||||
|
||||
if (index !== -1) {
|
||||
this.discussions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a discussion to the top of the list.
|
||||
*/
|
||||
public addDiscussion(discussion: Discussion) {
|
||||
this.discussions.unshift(discussion);
|
||||
}
|
||||
}
|
207
js/src/forum/components/DiscussionListItem.tsx
Normal file
207
js/src/forum/components/DiscussionListItem.tsx
Normal file
|
@ -0,0 +1,207 @@
|
|||
import Component from '../../common/Component';
|
||||
import avatar from '../../common/helpers/avatar';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import highlight from '../../common/helpers/highlight';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import humanTime from '../../common/utils/humanTime';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import SubtreeRetainer from '../../common/utils/SubtreeRetainer';
|
||||
import LinkButton from '../../common/components/LinkButton';
|
||||
import abbreviateNumber from '../../common/utils/abbreviateNumber';
|
||||
import TerminalPost from './TerminalPost';
|
||||
import DiscussionControls from '../utils/DiscussionControls';
|
||||
import { DiscussionProp } from '../../common/concerns/ComponentProps';
|
||||
|
||||
export interface DiscussionListItemProps extends DiscussionProp {
|
||||
params: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `DiscussionListItem` component shows a single discussion in the
|
||||
* discussion list.
|
||||
*/
|
||||
export default class DiscussionListItem<T extends DiscussionListItemProps = DiscussionListItemProps> extends Component<T> {
|
||||
/**
|
||||
* Set up a subtree retainer so that the discussion will not be redrawn
|
||||
* unless new data comes in.
|
||||
*/
|
||||
subtree: SubtreeRetainer;
|
||||
|
||||
highlightRegExp?: RegExp;
|
||||
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.subtree = new SubtreeRetainer(
|
||||
() => this.props.discussion.freshness,
|
||||
() => {
|
||||
const time = app.session.user && app.session.user.markedAllAsReadAt();
|
||||
return time && time.getTime();
|
||||
},
|
||||
() => this.active()
|
||||
);
|
||||
}
|
||||
|
||||
attrs() {
|
||||
return {
|
||||
className: classNames('DiscussionListItem', this.active() && 'active', this.props.discussion.isHidden() && 'DiscussionListItem--hidden'),
|
||||
};
|
||||
}
|
||||
|
||||
view() {
|
||||
const discussion = this.props.discussion;
|
||||
const user = discussion.user();
|
||||
const isUnread = discussion.isUnread();
|
||||
const isRead = discussion.isRead();
|
||||
const showUnread = !this.showRepliesCount() && isUnread;
|
||||
let jumpTo = 0;
|
||||
const controls = DiscussionControls.controls(discussion, this).toArray();
|
||||
const attrs = this.attrs();
|
||||
|
||||
if (this.props.params.q) {
|
||||
const post = discussion.mostRelevantPost();
|
||||
if (post) {
|
||||
jumpTo = post.number();
|
||||
}
|
||||
|
||||
const phrase = this.props.params.q;
|
||||
this.highlightRegExp = new RegExp(phrase + '|' + phrase.trim().replace(/\s+/g, '|'), 'gi');
|
||||
} else {
|
||||
jumpTo = Math.min(discussion.lastPostNumber(), (discussion.lastReadPostNumber() || 0) + 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...attrs}>
|
||||
{controls.length
|
||||
? Dropdown.component({
|
||||
icon: 'fas fa-ellipsis-v',
|
||||
children: controls,
|
||||
className: 'DiscussionListItem-controls',
|
||||
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right',
|
||||
})
|
||||
: ''}
|
||||
|
||||
<a
|
||||
className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
|
||||
onclick={this.markAsRead.bind(this)}
|
||||
>
|
||||
{icon('fas fa-check')}
|
||||
</a>
|
||||
|
||||
<div className={classNames('DiscussionListItem-content', 'Slidable-content', isUnread && 'unread', isRead && 'read')}>
|
||||
<LinkButton
|
||||
href={user ? app.route.user(user) : '#'}
|
||||
className="DiscussionListItem-author"
|
||||
title={app.translator.transText('core.forum.discussion_list.started_text', {
|
||||
user: user,
|
||||
ago: humanTime(discussion.createdAt()),
|
||||
})}
|
||||
oncreate={vnode => $(vnode.dom).tooltip({ placement: 'right' })}
|
||||
>
|
||||
{avatar(user, { title: '' })}
|
||||
</LinkButton>
|
||||
|
||||
<ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>
|
||||
|
||||
<LinkButton href={app.route.discussion(discussion, jumpTo)} className="DiscussionListItem-main">
|
||||
<h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.highlightRegExp)}</h3>
|
||||
<ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
|
||||
</LinkButton>
|
||||
|
||||
<span
|
||||
className="DiscussionListItem-count"
|
||||
onclick={this.markAsRead.bind(this)}
|
||||
title={showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : ''}
|
||||
>
|
||||
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'replyCount']())}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
// If we're on a touch device, set up the discussion row to be slidable.
|
||||
// This allows the user to drag the row to either side of the screen to
|
||||
// reveal controls.
|
||||
if ('ontouchstart' in window) {
|
||||
const slidableInstance = slidable(this.$().addClass('Slidable'));
|
||||
|
||||
this.$('.DiscussionListItem-controls').on('hidden.bs.dropdown', () => slidableInstance.reset());
|
||||
}
|
||||
}
|
||||
|
||||
onbeforeupdate(vnode) {
|
||||
super.onbeforeupdate(vnode);
|
||||
|
||||
return this.subtree.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not the discussion is currently being viewed.
|
||||
*/
|
||||
active(): boolean {
|
||||
const idParam = m.route.param('id');
|
||||
|
||||
return idParam && idParam.split('-')[0] === this.props.discussion.id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not information about who started the discussion
|
||||
* should be displayed instead of information about the most recent reply to
|
||||
* the discussion.
|
||||
*/
|
||||
showFirstPost(): boolean {
|
||||
return ['newest', 'oldest'].indexOf(this.props.params.sort) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not the number of replies should be shown instead of
|
||||
* the number of unread posts.
|
||||
*/
|
||||
showRepliesCount(): boolean {
|
||||
return this.props.params.sort === 'replies';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the discussion as read.
|
||||
*/
|
||||
markAsRead() {
|
||||
const discussion = this.props.discussion;
|
||||
|
||||
if (discussion.isUnread()) {
|
||||
discussion.save({ lastReadPostNumber: discussion.lastPostNumber() });
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list of info for a discussion listing. By default this is
|
||||
* just the first/last post indicator.
|
||||
*/
|
||||
infoItems(): ItemList {
|
||||
const items = new ItemList();
|
||||
|
||||
if (this.props.params.q) {
|
||||
const post = this.props.discussion.mostRelevantPost() || this.props.discussion.firstPost();
|
||||
|
||||
if (post && post.contentType() === 'comment') {
|
||||
const excerpt = highlight(post.contentPlain(), this.highlightRegExp, 175);
|
||||
items.add('excerpt', excerpt, -100);
|
||||
}
|
||||
} else {
|
||||
items.add(
|
||||
'terminalPost',
|
||||
TerminalPost.component({
|
||||
discussion: this.props.discussion,
|
||||
lastPost: !this.showFirstPost(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
32
js/src/forum/components/TerminalPost.tsx
Normal file
32
js/src/forum/components/TerminalPost.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import Component from '../../common/Component';
|
||||
import humanTime from '../../common/helpers/humanTime';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import Post from '../../common/models/Post';
|
||||
import { DiscussionProp } from '../../common/concerns/ComponentProps';
|
||||
|
||||
export interface TerminalPostProps extends DiscussionProp {
|
||||
lastPost: Post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays information about a the first or last post in a discussion.
|
||||
*/
|
||||
export default class TerminalPost<T extends TerminalPostProps = TerminalPostProps> extends Component<T> {
|
||||
view() {
|
||||
const discussion = this.props.discussion;
|
||||
const lastPost = this.props.lastPost && discussion.replyCount();
|
||||
|
||||
const user = discussion[lastPost ? 'lastPostedUser' : 'user']();
|
||||
const time = discussion[lastPost ? 'lastPostedAt' : 'createdAt']();
|
||||
|
||||
return (
|
||||
<span>
|
||||
{lastPost ? icon('fas fa-reply') : ''}{' '}
|
||||
{app.translator.trans(`core.forum.discussion_list.${lastPost ? 'replied' : 'started'}_text`, {
|
||||
user,
|
||||
ago: humanTime(time),
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
244
js/src/forum/utils/DiscussionControls.tsx
Normal file
244
js/src/forum/utils/DiscussionControls.tsx
Normal file
|
@ -0,0 +1,244 @@
|
|||
import DiscussionPage from '../components/DiscussionPage';
|
||||
// import ReplyComposer from '../components/ReplyComposer';
|
||||
import LogInModal from '../components/LogInModal';
|
||||
import Button from '../../common/components/Button';
|
||||
import Separator from '../../common/components/Separator';
|
||||
// import RenameDiscussionModal from '../components/RenameDiscussionModal';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import Discussion from '../../common/models/Discussion';
|
||||
|
||||
/**
|
||||
* The `DiscussionControls` utility constructs a list of buttons for a
|
||||
* discussion which perform actions on it.
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Get a list of controls for a discussion.
|
||||
*
|
||||
* @param discussion
|
||||
* @param context The parent component under which the controls menu will
|
||||
* be displayed
|
||||
* @public
|
||||
*/
|
||||
controls(discussion: Discussion, context): ItemList {
|
||||
const items = new ItemList();
|
||||
|
||||
['user', 'moderation', 'destructive'].forEach(section => {
|
||||
const controls = this[section](discussion, 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 discussion pertaining to the current user (e.g. reply,
|
||||
* follow).
|
||||
*
|
||||
* @param discussion
|
||||
* @param context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @protected
|
||||
*/
|
||||
user(discussion: Discussion, context: any): ItemList {
|
||||
const items = new ItemList();
|
||||
|
||||
// Only add a reply control if this is the discussion's controls dropdown
|
||||
// for the discussion page itself. We don't want it to show up for
|
||||
// discussions in the discussion list, etc.
|
||||
if (context instanceof DiscussionPage) {
|
||||
items.add(
|
||||
'reply',
|
||||
!app.session.user || discussion.canReply()
|
||||
? Button.component({
|
||||
icon: 'fas fa-reply',
|
||||
children: app.translator.trans(
|
||||
app.session.user
|
||||
? 'core.forum.discussion_controls.reply_button'
|
||||
: 'core.forum.discussion_controls.log_in_to_reply_button'
|
||||
),
|
||||
onclick: this.replyAction.bind(discussion, true, false),
|
||||
})
|
||||
: Button.component({
|
||||
icon: 'fas fa-reply',
|
||||
children: app.translator.trans('core.forum.discussion_controls.cannot_reply_button'),
|
||||
className: 'disabled',
|
||||
title: app.translator.trans('core.forum.discussion_controls.cannot_reply_text'),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a discussion pertaining to moderation (e.g. rename, lock).
|
||||
*
|
||||
* @param discussion
|
||||
* @param context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @protected
|
||||
*/
|
||||
moderation(discussion): ItemList {
|
||||
const items = new ItemList();
|
||||
|
||||
if (discussion.canRename()) {
|
||||
items.add(
|
||||
'rename',
|
||||
Button.component({
|
||||
icon: 'fas fa-pencil-alt',
|
||||
children: app.translator.trans('core.forum.discussion_controls.rename_button'),
|
||||
onclick: this.renameAction.bind(discussion),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a discussion which are destructive (e.g. delete).
|
||||
*
|
||||
* @param discussion
|
||||
* @param context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @protected
|
||||
*/
|
||||
destructive(discussion: Discussion): ItemList {
|
||||
const items = new ItemList();
|
||||
|
||||
if (!discussion.isHidden()) {
|
||||
if (discussion.canHide()) {
|
||||
items.add(
|
||||
'hide',
|
||||
Button.component({
|
||||
icon: 'far fa-trash-alt',
|
||||
children: app.translator.trans('core.forum.discussion_controls.delete_button'),
|
||||
onclick: this.hideAction.bind(discussion),
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (discussion.canHide()) {
|
||||
items.add(
|
||||
'restore',
|
||||
Button.component({
|
||||
icon: 'fas fa-reply',
|
||||
children: app.translator.trans('core.forum.discussion_controls.restore_button'),
|
||||
onclick: this.restoreAction.bind(discussion),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (discussion.canDelete()) {
|
||||
items.add(
|
||||
'delete',
|
||||
Button.component({
|
||||
icon: 'fas fa-times',
|
||||
children: app.translator.trans('core.forum.discussion_controls.delete_forever_button'),
|
||||
onclick: this.deleteAction.bind(discussion),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the reply composer for the discussion. A promise will be returned,
|
||||
* which resolves when the composer opens successfully. If the user is not
|
||||
* logged in, they will be prompted. If they don't have permission to
|
||||
* reply, the promise will be rejected.
|
||||
*
|
||||
* @param goToLast Whether or not to scroll down to the last post if
|
||||
* the discussion is being viewed.
|
||||
* @param forceRefresh Whether or not to force a reload of the
|
||||
* composer component, even if it is already open for this discussion.
|
||||
*/
|
||||
replyAction(this: Discussion, goToLast: boolean, forceRefresh: boolean): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (app.session.user) {
|
||||
if (this.canReply()) {
|
||||
let component = app.composer.component;
|
||||
if (!app.composingReplyTo(this) || forceRefresh) {
|
||||
component = new ReplyComposer({
|
||||
user: app.session.user,
|
||||
discussion: this,
|
||||
});
|
||||
app.composer.load(component);
|
||||
}
|
||||
|
||||
app.composer.show();
|
||||
|
||||
if (goToLast && app.viewingDiscussion(this) && !app.composer.isFullScreen()) {
|
||||
app.current.stream.goToNumber('reply');
|
||||
}
|
||||
|
||||
return resolve(component);
|
||||
} else {
|
||||
return reject();
|
||||
}
|
||||
}
|
||||
|
||||
app.modal.show(new LogInModal());
|
||||
|
||||
reject();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide a discussion.
|
||||
*/
|
||||
hideAction(this: Discussion) {
|
||||
this.pushAttributes({ hiddenAt: new Date(), hiddenUser: app.session.user });
|
||||
|
||||
return this.save({ isHidden: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore a discussion.
|
||||
*/
|
||||
restoreAction(this: Discussion) {
|
||||
this.pushAttributes({ hiddenAt: null, hiddenUser: null });
|
||||
|
||||
return this.save({ isHidden: false });
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the discussion after confirming with the user.
|
||||
*/
|
||||
deleteAction(this: Discussion) {
|
||||
if (confirm(extractText(app.translator.trans('core.forum.discussion_controls.delete_confirmation')))) {
|
||||
// If we're currently viewing the discussion that was deleted, go back
|
||||
// to the previous page.
|
||||
if (app.viewingDiscussion(this)) {
|
||||
app.history.back();
|
||||
}
|
||||
|
||||
return this.delete().then(() => {
|
||||
// If there is a discussion list in the cache, remove this discussion.
|
||||
if (app.cache.discussionList) {
|
||||
app.cache.discussionList.removeDiscussion(this);
|
||||
m.redraw();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Rename the discussion.
|
||||
*/
|
||||
renameAction(this: Discussion) {
|
||||
return app.modal.show(
|
||||
new RenameDiscussionModal({
|
||||
currentTitle: this.title(),
|
||||
discussion: this,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user