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 } from 'discourse/lib/utilities'; 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 = 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.replyCount > 0 && !attrs.selected) { buttons.push(this.attach('button', { label: 'topic.multi_select.select_replies', action: 'selectReplies' })); } const selectPostKey = attrs.selected ? 'topic.multi_select.selected' : 'topic.multi_select.select'; buttons.push(this.attach('button', { className: 'select-post', label: selectPostKey, labelOptions: { count: attrs.selectedPostsCount }, action: 'selectPost' })); 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', attrs.replyToUsername)]; }, click() { this.state.loading = true; this.sendWidgetAction('toggleReplyAbove').then(() => this.state.loading = false); } }); createWidget('post-avatar', { tagName: 'div.topic-avatar', settings: { size: 'large' }, 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')); return result; } }); 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', html(attrs) { const result = [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 createdAt = new Date(attrs.created_at); if (createdAt) { result.push(h('div.post-info', h('a.post-date', { attributes: { href: attrs.shareUrl, 'data-share-url': attrs.shareUrl, 'data-post-number': attrs.post_number, } }, dateNode(createdAt)) )); } if (attrs.via_email) { result.push(this.attach('post-email-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) }); }))); } return result; }, toggleRepliesBelow() { if (this.state.repliesBelow.length) { this.state.repliesBelow = []; 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) { const postContents = this.attach('post-contents', attrs); const result = [this.attach('post-meta-data', attrs), 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', 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() { 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 = []; 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 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); } });