import userSearch from 'discourse/lib/user-search';
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags';
import { fetchUnseenTagHashtags, linkSeenTagHashtags } from 'discourse/lib/link-tag-hashtag';
import { load } from 'pretty-text/oneboxer';
import { ajax } from 'discourse/lib/ajax';
import InputValidation from 'discourse/models/input-validation';
import { tinyAvatar,
displayErrorForUpload,
getUploadMarkdown,
validateUploadedFiles } from 'discourse/lib/utilities';
export default Ember.Component.extend({
classNames: ['wmd-controls'],
classNameBindings: ['showToolbar:toolbar-visible', ':wmd-controls', 'showPreview', 'showPreview::hide-preview'],
uploadProgress: 0,
showPreview: true,
_xhr: null,
@computed
uploadPlaceholder() {
return `[${I18n.t('uploading')}]() `;
},
@on('init')
_setupPreview() {
const val = (this.site.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true'));
this.set('showPreview', val === 'true');
},
@computed('site.mobileView', 'showPreview')
forcePreview(mobileView, showPreview) {
return mobileView && showPreview;
},
@computed('showPreview')
toggleText: function(showPreview) {
return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview');
},
_renderUnseenTagHashtags($preview, unseen) {
fetchUnseenTagHashtags(unseen).then(() => {
linkSeenTagHashtags($preview);
});
},
@on('previewRefreshed')
paintTagHashtags($preview) {
if (!this.siteSettings.tagging_enabled) { return; }
const unseenTagHashtags = linkSeenTagHashtags($preview);
if (unseenTagHashtags.length) {
Ember.run.debounce(this, this._renderUnseenTagHashtags, $preview, unseenTagHashtags, 500);
}
},
@computed
markdownOptions() {
return {
lookupAvatarByPostNumber: (postNumber, topicId) => {
const topic = this.get('topic');
if (!topic) { return; }
const posts = topic.get('postStream.posts');
if (posts && topicId === topic.get('id')) {
const quotedPost = posts.findProperty("post_number", postNumber);
if (quotedPost) {
return tinyAvatar(quotedPost.get('avatar_template'));
}
}
}
};
},
@on('didInsertElement')
_composerEditorInit() {
const topicId = this.get('topic.id');
const template = this.container.lookup('template:user-selector-autocomplete.raw');
const $input = this.$('.d-editor-input');
$input.autocomplete({
template,
dataSource: term => userSearch({ term, topicId, includeGroups: true }),
key: "@",
transformComplete: v => v.username || v.name
});
$input.on('scroll', () => Ember.run.throttle(this, this._syncEditorAndPreviewScroll, 20));
// Focus on the body unless we have a title
if (!this.get('composer.canEditTitle') && !this.capabilities.isIOS) {
this.$('.d-editor-input').putCursorAtEnd();
}
this._bindUploadTarget();
this.appEvents.trigger('composer:will-open');
if (this.site.mobileView) {
$(window).on('resize.composer-popup-menu', () => {
if (this.get('optionsVisible')) {
this.appEvents.trigger('popup-menu:open', this._optionsLocation());
}
});
}
},
@computed('composer.reply', 'composer.replyLength', 'composer.missingReplyCharacters', 'composer.minimumPostLength', 'lastValidatedAt')
validation(reply, replyLength, missingReplyCharacters, minimumPostLength, lastValidatedAt) {
const postType = this.get('composer.post.post_type');
if (postType === this.site.get('post_types.small_action')) { return; }
let reason;
if (replyLength < 1) {
reason = I18n.t('composer.error.post_missing');
} else if (missingReplyCharacters > 0) {
reason = I18n.t('composer.error.post_length', {min: minimumPostLength});
const tl = Discourse.User.currentProp("trust_level");
if (tl === 0 || tl === 1) {
reason += "
" + I18n.t('composer.error.try_like');
}
}
if (reason) {
return InputValidation.create({ failed: true, reason, lastShownAt: lastValidatedAt });
}
},
_syncEditorAndPreviewScroll() {
const $input = this.$('.d-editor-input');
if (!$input) { return; }
const $preview = this.$('.d-editor-preview');
if ($input.scrollTop() === 0) {
$preview.scrollTop(0);
return;
}
const inputHeight = $input[0].scrollHeight;
const previewHeight = $preview[0].scrollHeight;
if (($input.height() + $input.scrollTop() + 100) > inputHeight) {
// cheat, special case for bottom
$preview.scrollTop(previewHeight);
return;
}
const scrollPosition = $input.scrollTop();
const factor = previewHeight / inputHeight;
const desired = scrollPosition * factor;
$preview.scrollTop(desired + 50);
},
_renderUnseenMentions: function($preview, unseen) {
fetchUnseenMentions($preview, unseen).then(() => {
linkSeenMentions($preview, this.siteSettings);
this._warnMentionedGroups($preview);
});
},
_renderUnseenCategoryHashtags: function($preview, unseen) {
fetchUnseenCategoryHashtags(unseen).then(() => {
linkSeenCategoryHashtags($preview);
});
},
_warnMentionedGroups($preview) {
Ember.run.scheduleOnce('afterRender', () => {
this._warnedMentions = this._warnedMentions || [];
var found = [];
$preview.find('.mention-group.notify').each((idx,e) => {
const $e = $(e);
var name = $e.data('name');
found.push(name);
if (this._warnedMentions.indexOf(name) === -1){
this._warnedMentions.push(name);
this.sendAction('groupsMentioned', [{name: name, user_count: $e.data('mentionable-user-count')}]);
}
});
this._warnedMentions = found;
});
},
_resetUpload(removePlaceholder) {
this._validUploads--;
if (this._validUploads === 0) {
this.setProperties({ uploadProgress: 0, isUploading: false, isCancellable: false });
}
if (removePlaceholder) {
this.set('composer.reply', this.get('composer.reply').replace(this.get('uploadPlaceholder'), ""));
}
},
_bindUploadTarget() {
this._unbindUploadTarget(); // in case it's still bound, let's clean it up first
const $element = this.$();
const csrf = this.session.get('csrfToken');
const uploadPlaceholder = this.get('uploadPlaceholder');
$element.fileupload({
url: Discourse.getURL(`/uploads.json?client_id=${this.messageBus.clientId}&authenticity_token=${encodeURIComponent(csrf)}`),
dataType: "json",
pasteZone: $element,
});
$element.on('fileuploadsubmit', (e, data) => {
const isUploading = validateUploadedFiles(data.files);
data.formData = { type: "composer" };
this.setProperties({ uploadProgress: 0, isUploading });
return isUploading;
});
$element.on("fileuploadprogressall", (e, data) => {
this.set("uploadProgress", parseInt(data.loaded / data.total * 100, 10));
});
$element.on("fileuploadsend", (e, data) => {
this._validUploads++;
// add upload placeholders
this.appEvents.trigger('composer:insert-text', uploadPlaceholder);
if (data.xhr && data.originalFiles.length === 1) {
this.set("isCancellable", true);
this._xhr = data.xhr();
}
});
$element.on("fileuploadfail", (e, data) => {
this._resetUpload(true);
const userCancelled = this._xhr && this._xhr._userCancelled;
this._xhr = null;
if (!userCancelled) {
displayErrorForUpload(data);
}
});
this.messageBus.subscribe("/uploads/composer", upload => {
// replace upload placeholder
if (upload && upload.url) {
if (!this._xhr || !this._xhr._userCancelled) {
const markdown = getUploadMarkdown(upload);
this.set('composer.reply', this.get('composer.reply').replace(uploadPlaceholder, markdown));
this._resetUpload(false);
} else {
this._resetUpload(true);
}
} else {
this._resetUpload(true);
displayErrorForUpload(upload);
}
});
if (this.site.mobileView) {
this.$(".mobile-file-upload").on("click.uploader", function () {
// redirect the click on the hidden file input
$("#mobile-uploader").click();
});
}
this._firefoxPastingHack();
},
_optionsLocation() {
// long term we want some smart positioning algorithm in popup-menu
// the problem is that positioning in a fixed panel is a nightmare
// cause offsetParent can end up returning a fixed element and then
// using offset() is not going to work, so you end up needing special logic
// especially since we allow for negative .top, provided there is room on screen
const myPos = this.$().position();
const buttonPos = this.$('.options').position();
const popupHeight = $('#reply-control .popup-menu').height();
const popupWidth = $('#reply-control .popup-menu').width();
var top = myPos.top + buttonPos.top - 15;
var left = myPos.left + buttonPos.left - (popupWidth/2);
const composerPos = $('#reply-control').position();
if (composerPos.top + top - popupHeight < 0) {
top = top + popupHeight + this.$('.options').height() + 50;
}
var replyWidth = $('#reply-control').width();
if (left + popupWidth > replyWidth) {
left = replyWidth - popupWidth - 40;
}
return { position: "absolute", left, top };
},
// Believe it or not pasting an image in Firefox doesn't work without this code
_firefoxPastingHack() {
const uaMatch = navigator.userAgent.match(/Firefox\/(\d+)\.\d/);
if (uaMatch && parseInt(uaMatch[1]) >= 24) {
this.$().append( Ember.$("