Locking a post prevents it from being edited. This is useful if the user has posted something which has been edited out, and the staff members don't want them to be able to edit it back in again.
546 lines
15 KiB
JavaScript
546 lines
15 KiB
JavaScript
import PostCooked from 'discourse/widgets/post-cooked';
|
|
import DecoratorHelper from 'discourse/widgets/decorator-helper';
|
|
import { createWidget, applyDecorators } from 'discourse/widgets/widget';
|
|
import { iconNode } from 'discourse-common/lib/icon-library';
|
|
import { transformBasicPost } from 'discourse/lib/transform-post';
|
|
import { h } from 'virtual-dom';
|
|
import DiscourseURL from 'discourse/lib/url';
|
|
import { dateNode } from 'discourse/helpers/node';
|
|
import { translateSize, avatarUrl, formatUsername } from 'discourse/lib/utilities';
|
|
import hbs from 'discourse/widgets/hbs-compiler';
|
|
|
|
export function avatarImg(wanted, attrs) {
|
|
const size = translateSize(wanted);
|
|
const url = avatarUrl(attrs.template, size);
|
|
|
|
// We won't render an invalid url
|
|
if (!url || url.length === 0) { return; }
|
|
const title = formatUsername(attrs.username);
|
|
|
|
const properties = {
|
|
attributes: { alt: '', width: size, height: size, src: Discourse.getURLWithCDN(url), title },
|
|
className: 'avatar'
|
|
};
|
|
|
|
return h('img', properties);
|
|
}
|
|
|
|
export function avatarFor(wanted, attrs) {
|
|
return h('a', {
|
|
className: `trigger-user-card ${attrs.className || ''}`,
|
|
attributes: { href: attrs.url, 'data-user-card': attrs.username }
|
|
}, avatarImg(wanted, attrs));
|
|
}
|
|
|
|
createWidget('select-post', {
|
|
tagName: 'div.select-posts',
|
|
|
|
html(attrs) {
|
|
const buttons = [];
|
|
|
|
if (!attrs.selected && attrs.post_number > 1) {
|
|
if (attrs.replyCount > 0) {
|
|
buttons.push(this.attach('button', {
|
|
label: 'topic.multi_select.select_replies.label',
|
|
title: 'topic.multi_select.select_replies.title',
|
|
action: 'selectReplies',
|
|
className: 'select-replies'
|
|
}));
|
|
}
|
|
buttons.push(this.attach('button', {
|
|
label: 'topic.multi_select.select_below.label',
|
|
title: 'topic.multi_select.select_below.title',
|
|
action: 'selectBelow',
|
|
className: 'select-below'
|
|
}));
|
|
}
|
|
|
|
const key = `topic.multi_select.${attrs.selected ? 'selected' : 'select' }_post`;
|
|
buttons.push(this.attach('button', {
|
|
label: key + ".label",
|
|
title: key + ".title",
|
|
action: 'togglePostSelection',
|
|
className: 'select-post'
|
|
}));
|
|
|
|
return buttons;
|
|
}
|
|
});
|
|
|
|
createWidget('reply-to-tab', {
|
|
tagName: 'a.reply-to-tab',
|
|
buildKey: attrs => `reply-to-tab-${attrs.id}`,
|
|
|
|
defaultState() {
|
|
return { loading: false };
|
|
},
|
|
|
|
html(attrs, state) {
|
|
if (state.loading) { return I18n.t('loading'); }
|
|
|
|
return [iconNode('mail-forward'),
|
|
' ',
|
|
avatarImg('small', {
|
|
template: attrs.replyToAvatarTemplate,
|
|
username: attrs.replyToUsername
|
|
}),
|
|
' ',
|
|
h('span', formatUsername(attrs.replyToUsername))];
|
|
},
|
|
|
|
click() {
|
|
this.state.loading = true;
|
|
this.sendWidgetAction('toggleReplyAbove').then(() => this.state.loading = false);
|
|
}
|
|
});
|
|
|
|
|
|
createWidget('post-avatar-user-info', {
|
|
tagName: 'div.post-avatar-user-info',
|
|
|
|
html(attrs) {
|
|
return this.attach('poster-name', attrs);
|
|
}
|
|
});
|
|
|
|
createWidget('post-avatar', {
|
|
tagName: 'div.topic-avatar',
|
|
|
|
settings: {
|
|
size: 'large',
|
|
displayPosterName: false
|
|
},
|
|
|
|
html(attrs) {
|
|
let body;
|
|
if (!attrs.user_id) {
|
|
body = iconNode('trash-o', { class: 'deleted-user-avatar' });
|
|
} else {
|
|
body = avatarFor.call(this, this.settings.size, {
|
|
template: attrs.avatar_template,
|
|
username: attrs.username,
|
|
url: attrs.usernameUrl,
|
|
className: 'main-avatar'
|
|
});
|
|
}
|
|
|
|
const result = [body];
|
|
|
|
if (attrs.primary_group_flair_url || attrs.primary_group_flair_bg_color) {
|
|
result.push(this.attach('avatar-flair', attrs));
|
|
}
|
|
|
|
result.push(h('div.poster-avatar-extra'));
|
|
|
|
if (this.settings.displayPosterName) {
|
|
result.push(this.attach('post-avatar-user-info', attrs));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
createWidget('post-locked-indicator', {
|
|
tagName: 'div.post-info.post-locked',
|
|
template: hbs`{{d-icon "lock"}}`,
|
|
title: () => I18n.t("post.locked")
|
|
});
|
|
|
|
createWidget('post-email-indicator', {
|
|
tagName: 'div.post-info.via-email',
|
|
|
|
title(attrs) {
|
|
return attrs.isAutoGenerated ?
|
|
I18n.t('post.via_auto_generated_email') :
|
|
I18n.t('post.via_email');
|
|
},
|
|
|
|
buildClasses(attrs) {
|
|
return attrs.canViewRawEmail ? 'raw-email' : null;
|
|
},
|
|
|
|
html(attrs) {
|
|
return attrs.isAutoGenerated ? iconNode('envelope') : iconNode('envelope-o');
|
|
},
|
|
|
|
click() {
|
|
if (this.attrs.canViewRawEmail) {
|
|
this.sendWidgetAction('showRawEmail');
|
|
}
|
|
}
|
|
});
|
|
|
|
function showReplyTab(attrs, siteSettings) {
|
|
return attrs.replyToUsername &&
|
|
(!attrs.replyDirectlyAbove || !siteSettings.suppress_reply_directly_above);
|
|
}
|
|
|
|
createWidget('post-meta-data', {
|
|
tagName: 'div.topic-meta-data',
|
|
|
|
settings: {
|
|
displayPosterName: true
|
|
},
|
|
|
|
html(attrs) {
|
|
let result = [];
|
|
if (this.settings.displayPosterName) {
|
|
result.push(this.attach('poster-name', attrs));
|
|
}
|
|
|
|
if (attrs.isWhisper) {
|
|
result.push(h('div.post-info.whisper', {
|
|
attributes: { title: I18n.t('post.whisper') },
|
|
}, iconNode('eye-slash')));
|
|
}
|
|
|
|
const lastWikiEdit = attrs.wiki && attrs.lastWikiEdit && new Date(attrs.lastWikiEdit);
|
|
const createdAt = new Date(attrs.created_at);
|
|
const date = lastWikiEdit ? dateNode(lastWikiEdit) : dateNode(createdAt);
|
|
const attributes = {
|
|
class: "post-date",
|
|
href: attrs.shareUrl,
|
|
'data-share-url': attrs.shareUrl,
|
|
'data-post-number': attrs.post_number
|
|
};
|
|
|
|
if (lastWikiEdit) {
|
|
attributes["class"] += " last-wiki-edit";
|
|
}
|
|
|
|
result.push(h('div.post-info.post-date', h('a', { attributes }, date)));
|
|
|
|
if (attrs.via_email) {
|
|
result.push(this.attach('post-email-indicator', attrs));
|
|
}
|
|
|
|
if (attrs.locked) {
|
|
result.push(this.attach('post-locked-indicator', attrs));
|
|
}
|
|
|
|
if (attrs.version > 1 || attrs.wiki) {
|
|
result.push(this.attach('post-edits-indicator', attrs));
|
|
}
|
|
|
|
if (attrs.multiSelect) {
|
|
result.push(this.attach('select-post', attrs));
|
|
}
|
|
|
|
if (showReplyTab(attrs, this.siteSettings)) {
|
|
result.push(this.attach('reply-to-tab', attrs));
|
|
}
|
|
|
|
result.push(h('div.read-state', {
|
|
className: attrs.read ? 'read' : null,
|
|
attributes: {
|
|
title: I18n.t('post.unread')
|
|
}
|
|
}, iconNode('circle')));
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
createWidget('expand-hidden', {
|
|
tagName: 'a.expand-hidden',
|
|
|
|
html() {
|
|
return I18n.t('post.show_hidden');
|
|
},
|
|
|
|
click() {
|
|
this.sendWidgetAction('expandHidden');
|
|
}
|
|
});
|
|
|
|
createWidget('expand-post-button', {
|
|
tagName: 'button.btn.expand-post',
|
|
buildKey: attrs => `expand-post-button-${attrs.id}`,
|
|
|
|
defaultState() {
|
|
return { loadingExpanded: false };
|
|
},
|
|
|
|
html(attrs, state) {
|
|
if (state.loadingExpanded) {
|
|
return I18n.t('loading');
|
|
} else {
|
|
return [I18n.t('post.show_full'), "..."];
|
|
}
|
|
},
|
|
|
|
click() {
|
|
this.state.loadingExpanded = true;
|
|
this.sendWidgetAction('expandFirstPost');
|
|
}
|
|
});
|
|
|
|
createWidget('post-contents', {
|
|
buildKey: attrs => `post-contents-${attrs.id}`,
|
|
|
|
defaultState() {
|
|
return { expandedFirstPost: false, repliesBelow: [] };
|
|
},
|
|
|
|
buildClasses(attrs) {
|
|
const classes = ['regular'];
|
|
if (!this.state.repliesShown) {
|
|
classes.push('contents');
|
|
}
|
|
if (showReplyTab(attrs, this.siteSettings)) {
|
|
classes.push('avoid-tab');
|
|
}
|
|
return classes;
|
|
},
|
|
|
|
html(attrs, state) {
|
|
let result = [new PostCooked(attrs, new DecoratorHelper(this))];
|
|
result = result.concat(applyDecorators(this, 'after-cooked', attrs, state));
|
|
|
|
if (attrs.cooked_hidden) {
|
|
result.push(this.attach('expand-hidden', attrs));
|
|
}
|
|
|
|
if (!state.expandedFirstPost && attrs.expandablePost) {
|
|
result.push(this.attach('expand-post-button', attrs));
|
|
}
|
|
|
|
const extraState = { state: { repliesShown: !!state.repliesBelow.length } };
|
|
result.push(this.attach('post-menu', attrs, extraState));
|
|
|
|
const repliesBelow = state.repliesBelow;
|
|
if (repliesBelow.length) {
|
|
result.push(h('section.embedded-posts.bottom', [
|
|
repliesBelow.map(p => {
|
|
return this.attach('embedded-post', p, { model: this.store.createRecord('post', p) });
|
|
}),
|
|
this.attach('button', {
|
|
title: 'post.collapse',
|
|
icon: 'chevron-up',
|
|
action: 'toggleRepliesBelow',
|
|
actionParam: 'true',
|
|
className: 'btn collapse-up'
|
|
})
|
|
]));
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
toggleRepliesBelow(goToPost = 'false') {
|
|
if (this.state.repliesBelow.length) {
|
|
this.state.repliesBelow = [];
|
|
if (goToPost === 'true') {
|
|
DiscourseURL.routeTo(`${this.attrs.topicUrl}/${this.attrs.post_number}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const post = this.findAncestorModel();
|
|
const topicUrl = post ? post.get('topic.url') : null;
|
|
return this.store.find('post-reply', { postId: this.attrs.id }).then(posts => {
|
|
this.state.repliesBelow = posts.map(p => {
|
|
p.shareUrl = `${topicUrl}/${p.post_number}`;
|
|
return transformBasicPost(p);
|
|
});
|
|
});
|
|
},
|
|
|
|
expandFirstPost() {
|
|
const post = this.findAncestorModel();
|
|
return post.expand().then(() => this.state.expandedFirstPost = true);
|
|
}
|
|
});
|
|
|
|
createWidget('post-body', {
|
|
tagName: 'div.topic-body.clearfix',
|
|
|
|
html(attrs, state) {
|
|
const postContents = this.attach('post-contents', attrs);
|
|
let result = [this.attach('post-meta-data', attrs)];
|
|
result = result.concat(applyDecorators(this, 'after-meta-data', attrs, state));
|
|
result.push(postContents);
|
|
result.push(this.attach('actions-summary', attrs));
|
|
result.push(this.attach('post-links', attrs));
|
|
if (attrs.showTopicMap) {
|
|
result.push(this.attach('topic-map', attrs));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
createWidget('post-article', {
|
|
tagName: 'article.boxed.onscreen-post',
|
|
buildKey: attrs => `post-article-${attrs.id}`,
|
|
|
|
defaultState() {
|
|
return { repliesAbove: [] };
|
|
},
|
|
|
|
buildId(attrs) {
|
|
return `post_${attrs.post_number}`;
|
|
},
|
|
|
|
buildClasses(attrs) {
|
|
let classNames = [];
|
|
if (attrs.via_email) { classNames.push('via-email'); }
|
|
if (attrs.isAutoGenerated) { classNames.push('is-auto-generated'); }
|
|
return classNames;
|
|
},
|
|
|
|
buildAttributes(attrs) {
|
|
return { 'data-post-id': attrs.id, 'data-user-id': attrs.user_id };
|
|
},
|
|
|
|
html(attrs, state) {
|
|
const rows = [h('a.tabLoc', { attributes: { href: ''} })];
|
|
if (state.repliesAbove.length) {
|
|
const replies = state.repliesAbove.map(p => {
|
|
return this.attach('embedded-post', p, { model: this.store.createRecord('post', p), state: { above: true } });
|
|
});
|
|
|
|
rows.push(h('div.row', h('section.embedded-posts.top.topic-body.offset2', [
|
|
this.attach('button', {
|
|
title: 'post.collapse',
|
|
icon: 'chevron-down',
|
|
action: 'toggleReplyAbove',
|
|
actionParam: 'true',
|
|
className: 'btn collapse-down'
|
|
}),
|
|
replies
|
|
])));
|
|
}
|
|
|
|
rows.push(h('div.row', [this.attach('post-avatar', attrs), this.attach('post-body', attrs)]));
|
|
return rows;
|
|
},
|
|
|
|
_getTopicUrl() {
|
|
const post = this.findAncestorModel();
|
|
return post ? post.get('topic.url') : null;
|
|
},
|
|
|
|
toggleReplyAbove(goToPost = 'false') {
|
|
const replyPostNumber = this.attrs.reply_to_post_number;
|
|
|
|
// jump directly on mobile
|
|
if (this.attrs.mobileView) {
|
|
const topicUrl = this._getTopicUrl();
|
|
if (topicUrl) {
|
|
DiscourseURL.routeTo(`${topicUrl}/${replyPostNumber}`);
|
|
}
|
|
return Ember.RSVP.Promise.resolve();
|
|
}
|
|
|
|
if (this.state.repliesAbove.length) {
|
|
this.state.repliesAbove = [];
|
|
if (goToPost === 'true') {
|
|
DiscourseURL.routeTo(`${this.attrs.topicUrl}/${this.attrs.post_number}`);
|
|
}
|
|
return Ember.RSVP.Promise.resolve();
|
|
} else {
|
|
const topicUrl = this._getTopicUrl();
|
|
return this.store.find('post-reply-history', { postId: this.attrs.id }).then(posts => {
|
|
this.state.repliesAbove = posts.map((p) => {
|
|
p.shareUrl = `${topicUrl}/${p.post_number}`;
|
|
return transformBasicPost(p);
|
|
});
|
|
});
|
|
}
|
|
},
|
|
|
|
});
|
|
|
|
let addPostClassesCallbacks = null;
|
|
export function addPostClassesCallback(callback) {
|
|
addPostClassesCallbacks = addPostClassesCallbacks || [];
|
|
addPostClassesCallbacks.push(callback);
|
|
}
|
|
|
|
export default createWidget('post', {
|
|
buildKey: attrs => `post-${attrs.id}`,
|
|
shadowTree: true,
|
|
|
|
buildAttributes(attrs) {
|
|
return attrs.height ? { style: `min-height: ${attrs.height}px` } : undefined;
|
|
},
|
|
|
|
buildId(attrs) {
|
|
return attrs.cloaked ? `post_${attrs.post_number}` : undefined;
|
|
},
|
|
|
|
buildClasses(attrs) {
|
|
if (attrs.cloaked) { return 'cloaked-post'; }
|
|
const classNames = ['topic-post', 'clearfix'];
|
|
|
|
if (attrs.id === -1 || attrs.isSaving) { classNames.push('staged'); }
|
|
if (attrs.selected) { classNames.push('selected'); }
|
|
if (attrs.topicOwner) { classNames.push('topic-owner'); }
|
|
if (attrs.hidden) { classNames.push('post-hidden'); }
|
|
if (attrs.deleted) { classNames.push('deleted'); }
|
|
if (attrs.primary_group_name) { classNames.push(`group-${attrs.primary_group_name}`); }
|
|
if (attrs.wiki) { classNames.push(`wiki`); }
|
|
if (attrs.isWhisper) { classNames.push('whisper'); }
|
|
if (attrs.isModeratorAction || (attrs.isWarning && attrs.firstPost)) {
|
|
classNames.push('moderator');
|
|
} else {
|
|
classNames.push('regular');
|
|
}
|
|
if (addPostClassesCallbacks) {
|
|
for(let i=0; i<addPostClassesCallbacks.length; i++) {
|
|
let pluginClasses = addPostClassesCallbacks[i].call(this, attrs);
|
|
if (pluginClasses) {
|
|
classNames.push.apply(classNames, pluginClasses);
|
|
}
|
|
}
|
|
}
|
|
return classNames;
|
|
},
|
|
|
|
html(attrs) {
|
|
if (attrs.cloaked) { return ''; }
|
|
|
|
return this.attach('post-article', attrs);
|
|
},
|
|
|
|
toggleLike() {
|
|
const post = this.model;
|
|
const likeAction = post.get('likeAction');
|
|
|
|
if (likeAction && likeAction.get('canToggle')) {
|
|
return likeAction.togglePromise(post).then(result => this._warnIfClose(result));
|
|
}
|
|
},
|
|
|
|
_warnIfClose(result) {
|
|
if (!result || !result.acted) { return; }
|
|
|
|
const kvs = this.keyValueStore;
|
|
const lastWarnedLikes = kvs.get('lastWarnedLikes');
|
|
|
|
// only warn once per day
|
|
const yesterday = new Date().getTime() - 1000 * 60 * 60 * 24;
|
|
if (lastWarnedLikes && parseInt(lastWarnedLikes) > yesterday) {
|
|
return;
|
|
}
|
|
|
|
const { remaining, max } = result;
|
|
const threshold = Math.ceil(max * 0.1);
|
|
if (remaining === threshold) {
|
|
bootbox.alert(I18n.t('post.few_likes_left'));
|
|
kvs.set({ key: 'lastWarnedLikes', value: new Date().getTime() });
|
|
}
|
|
},
|
|
|
|
undoPostAction(typeId) {
|
|
const post = this.model;
|
|
return post.get('actions_summary').findBy('id', typeId).undo(post);
|
|
},
|
|
|
|
deferPostActionFlags(typeId) {
|
|
const post = this.model;
|
|
return post.get('actions_summary').findBy('id', typeId).deferFlags(post);
|
|
}
|
|
});
|