- {{unbound view.tip}} + {{tip}}
- {{unbound view.tip}} + {{input value=imageUrl placeholder="http://example.com/image.png"}} + {{tip}}
+ {{input value=imageLink laceholder="http://example.com"}} {{i18n 'upload_selector.image_link'}}
{{unbound view.hint}}
++ {{#if capabilities.canPasteImages}} + {{i18n 'upload_selector.hint'}} + {{else}} + {{i18n 'upload_selector.hint_for_supported_browsers'}} + {{/if}} +
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}} {{/if}} diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index a46a9e1e8a..c9abb4c23d 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -1,103 +1,70 @@ -import userSearch from 'discourse/lib/user-search'; import afterTransition from 'discourse/lib/after-transition'; -import loadScript from 'discourse/lib/load-script'; import positioningWorkaround from 'discourse/lib/safari-hacks'; -import debounce from 'discourse/lib/debounce'; -import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions'; import { headerHeight } from 'discourse/views/header'; -import { showSelector } from 'discourse/lib/emoji/emoji-toolbar'; +import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; +import Composer from 'discourse/models/composer'; -const ComposerView = Ember.View.extend(Ember.Evented, { +const ComposerView = Ember.View.extend({ _lastKeyTimeout: null, - templateName: 'composer', elementId: 'reply-control', - classNameBindings: ['model.creatingPrivateMessage:private-message', + classNameBindings: ['composer.creatingPrivateMessage:private-message', 'composeState', - 'model.loading', - 'model.canEditTitle:edit-title', - 'postMade', - 'model.creatingTopic:topic', - 'model.showPreview', - 'model.hidePreview'], + 'composer.loading', + 'composer.canEditTitle:edit-title', + 'composer.createdPost:created-post', + 'composer.creatingTopic:topic'], - model: Em.computed.alias('controller.model'), + composer: Em.computed.alias('controller.model'), - // This is just in case something still references content. Can probably be removed - content: Em.computed.alias('model'), - - composeState: function() { - return this.get('model.composeState') || Discourse.Composer.CLOSED; - }.property('model.composeState'), - - // Disable fields when we're loading - loadingChanged: function() { - if (this.get('loading')) { - this.$('.wmd-input, #reply-title').prop('disabled', 'disabled'); - } else { - this.$('.wmd-input, #reply-title').prop('disabled', ''); - } - }.observes('loading'), - - postMade: function() { - return !Ember.isEmpty(this.get('model.createdPost')) ? 'created-post' : null; - }.property('model.createdPost'), - - refreshPreview: debounce(function() { - if (this.editor) { - this.editor.refreshPreview(); - } - }, 30), - - observeReplyChanges: function() { - if (this.get('model.hidePreview')) return; - Ember.run.scheduleOnce('afterRender', this, 'refreshPreview'); - }.observes('model.reply', 'model.hidePreview'), + @computed('composer.composeState') + composeState(composeState) { + return composeState || Composer.CLOSED; + }, movePanels(sizePx) { + $('#main-outlet').css('padding-bottom', sizePx); $('.composer-popup').css('bottom', sizePx); + // signal the progress bar it should move! this.appEvents.trigger("composer:resized"); }, - resize: function() { + @observes('composeState', 'composer.action') + resize() { Ember.run.scheduleOnce('afterRender', () => { - let h = $('#reply-control').height() || 0; + const h = $('#reply-control').height() || 0; this.movePanels(h + "px"); // Figure out the size of the fields const $fields = this.$('.composer-fields'); - let pos = $fields.position(); - - if (pos) { - this.$('.wmd-controls').css('top', $fields.height() + pos.top + 5); + const fieldPos = $fields.position(); + if (fieldPos) { + this.$('.wmd-controls').css('top', $fields.height() + fieldPos.top + 5); } // get the submit panel height - pos = this.$('.submit-panel').position(); - if (pos) { - this.$('.wmd-controls').css('bottom', h - pos.top + 7); + const submitPos = this.$('.submit-panel').position(); + if (submitPos) { + this.$('.wmd-controls').css('bottom', h - submitPos.top + 7); } - }); - }.observes('model.composeState', 'model.action'), + }, keyUp() { const controller = this.get('controller'); controller.checkReplyLength(); - this.get('controller.model').typing(); + this.get('composer').typing(); const lastKeyUp = new Date(); - this.set('lastKeyUp', lastKeyUp); + this._lastKeyUp = lastKeyUp; // One second from now, check to see if the last key was hit when // we recorded it. If it was, the user paused typing. - const self = this; - Ember.run.cancel(this._lastKeyTimeout); - this._lastKeyTimeout = Ember.run.later(function() { - if (lastKeyUp !== self.get('lastKeyUp')) return; + this._lastKeyTimeout = Ember.run.later(() => { + if (lastKeyUp !== this._lastKeyUp) { return; } // Search for similar topics if the user pauses typing controller.findSimilarTopics(); @@ -106,7 +73,6 @@ const ComposerView = Ember.View.extend(Ember.Evented, { keyDown(e) { if (e.which === 27) { - // ESC this.get('controller').send('hitEsc'); return false; } else if (e.which === 13 && (e.ctrlKey || e.metaKey)) { @@ -116,557 +82,25 @@ const ComposerView = Ember.View.extend(Ember.Evented, { } }, - _enableResizing: function() { + @on('didInsertElement') + _enableResizing() { const $replyControl = $('#reply-control'); - - const runResize = () => { - Ember.run(() => this.resize()); - }; + const resize = () => Ember.run(() => this.resize()); $replyControl.DivResizer({ - maxHeight(winHeight) { - return winHeight - headerHeight(); - }, - resize: runResize, - onDrag: (sizePx) => this.movePanels(sizePx) + resize, + maxHeight: winHeight => winHeight - headerHeight(), + onDrag: sizePx => this.movePanels(sizePx) }); - afterTransition($replyControl, runResize); - this.set('controller.view', this); - + afterTransition($replyControl, resize); positioningWorkaround(this.$()); - }.on('didInsertElement'), - - _unlinkView: function() { - this.set('controller.view', null); - }.on('willDestroyElement'), + }, click() { this.get('controller').send('openIfDraft'); - }, - - // Called after the preview renders. Debounced for performance - afterRender() { - if (this._state !== "inDOM") { return; } - - const $wmdPreview = this.$('.wmd-preview'); - if ($wmdPreview.length === 0) return; - - const post = this.get('model.post'); - let refresh = false; - - // If we are editing a post, we'll refresh its contents once. This is a feature that - // allows a user to refresh its contents once. - if (post && !post.get('refreshedPost')) { - refresh = true; - post.set('refreshedPost', true); - } - - // Load the post processing effects - $('a.onebox', $wmdPreview).each(function(i, e) { - Discourse.Onebox.load(e, refresh); - }); - - const unseen = linkSeenMentions($wmdPreview, this.siteSettings); - if (unseen.length) { - Ember.run.debounce(this, this._renderUnseen, $wmdPreview, unseen, 500); - } - - this.trigger('previewRefreshed', $wmdPreview); - }, - - _renderUnseen: function($wmdPreview, unseen) { - fetchUnseenMentions($wmdPreview, unseen, this.siteSettings).then(() => { - linkSeenMentions($wmdPreview, this.siteSettings); - this.trigger('previewRefreshed', $wmdPreview); - }); - }, - - _applyEmojiAutocomplete() { - if (!this.siteSettings.enable_emoji) { return; } - - const container = this.container; - const template = container.lookup('template:emoji-selector-autocomplete.raw'); - const controller = this.get('controller'); - - this.$('.wmd-input').autocomplete({ - template: template, - key: ":", - - transformComplete(v) { - if (v.code) { - return `${v.code}:`; - } else { - showSelector({ - container, - onSelect(title) { - controller.appendTextAtCursor(title + ':', {space: false}); - } - }); - return ""; - } - }, - - dataSource(term) { - return new Ember.RSVP.Promise(resolve => { - const full = `:${term}`; - term = term.toLowerCase(); - - if (term === "") { - return resolve(["smile", "smiley", "wink", "sunny", "blush"]); - } - - if (Discourse.Emoji.translations[full]) { - return resolve([Discourse.Emoji.translations[full]]); - } - - const options = Discourse.Emoji.search(term, {maxResults: 5}); - - return resolve(options); - }).then(list => list.map(code => { - return {code, src: Discourse.Emoji.urlFor(code)}; - })).then(list => { - if (list.length) { - list.push({ label: I18n.t("composer.more_emoji") }); - } - return list; - }); - } - }); - }, - - initEditor() { - // not quite right, need a callback to pass in, meaning this gets called once, - // but if you start replying to another topic it will get the avatars wrong - let $wmdInput; - const self = this; - const controller = this.get('controller'); - - this.wmdInput = $wmdInput = this.$('.wmd-input'); - if ($wmdInput.length === 0 || $wmdInput.data('init') === true) return; - - loadScript('defer/html-sanitizer-bundle'); - ComposerView.trigger("initWmdEditor"); - this._applyEmojiAutocomplete(); - - const template = this.container.lookup('template:user-selector-autocomplete.raw'); - $wmdInput.data('init', true); - $wmdInput.autocomplete({ - template: template, - dataSource(term) { - return userSearch({ - term: term, - topicId: controller.get('controllers.topic.model.id'), - includeGroups: true - }); - }, - key: "@", - transformComplete(v) { - return v.username ? v.username : v.usernames.join(", @"); - } - }); - - - const options = { - containerElement: this.element, - lookupAvatarByPostNumber(postNumber, topicId) { - const posts = controller.get('controllers.topic.model.postStream.posts'); - if (posts && topicId === controller.get('controllers.topic.model.id')) { - const quotedPost = posts.findProperty("post_number", postNumber); - if (quotedPost) { - return Discourse.Utilities.tinyAvatar(quotedPost.get('avatar_template')); - } - } - } - }; - - const showOptions = controller.get('canWhisper'); - if (showOptions) { - options.appendButtons = [{ - id: 'wmd-composer-options', - description: I18n.t("composer.options"), - execute() { - const toolbarPos = self.$('.wmd-controls').position(); - const pos = self.$('.wmd-composer-options').position(); - - const location = { - position: "absolute", - left: toolbarPos.left + pos.left, - top: toolbarPos.top + pos.top, - }; - controller.send('showOptions', location); - } - }]; - } - - this.editor = Discourse.Markdown.createEditor(options); - - // HACK to change the upload icon of the composer's toolbar - if (!Discourse.Utilities.allowsAttachments()) { - Em.run.scheduleOnce("afterRender", function() { - $("#wmd-image-button").addClass("image-only"); - }); - } - - this.editor.hooks.insertImageDialog = function(callback) { - callback(null); - controller.send('showUploadSelector', self); - return true; - }; - - this.editor.hooks.onPreviewRefresh = function() { - return self.afterRender(); - }; - - this.editor.run(); - this.set('editor', this.editor); - this.loadingChanged(); - - const saveDraft = debounce((function() { - return controller.saveDraft(); - }), 2000); - - $wmdInput.keyup(function() { - saveDraft(); - return true; - }); - - const $replyTitle = $('#reply-title'); - - $replyTitle.keyup(function() { - saveDraft(); - // removes the red background once the requirements are met - if (self.get('model.missingTitleCharacters') <= 0) { - $replyTitle.removeClass("requirements-not-met"); - } - return true; - }); - - // when the title field loses the focus... - $replyTitle.blur(function(){ - // ...and the requirements are not met (ie. the minimum number of characters) - if (self.get('model.missingTitleCharacters') > 0) { - // then, "redify" the background - $replyTitle.toggleClass("requirements-not-met", true); - } - }); - - // in case it's still bound somehow - this._unbindUploadTarget(); - - const $uploadTarget = $("#reply-control"), - csrf = Discourse.Session.currentProp("csrfToken"), - reset = () => this.setProperties({ uploadProgress: 0, isUploading: false }); - - var cancelledByTheUser; - - this.messageBus.subscribe("/uploads/composer", upload => { - // reset upload state - reset(); - // replace upload placeholder - if (upload && upload.url) { - if (!cancelledByTheUser) { - const uploadPlaceholder = Discourse.Utilities.getUploadPlaceholder(), - markdown = Discourse.Utilities.getUploadMarkdown(upload); - this.replaceMarkdown(uploadPlaceholder, markdown); - } - } else { - Discourse.Utilities.displayErrorForUpload(upload); - } - }); - - $uploadTarget.fileupload({ - url: Discourse.getURL("/uploads.json?client_id=" + this.messageBus.clientId + "&authenticity_token=" + encodeURIComponent(csrf)), - dataType: "json", - pasteZone: $uploadTarget, - }); - - $uploadTarget.on("fileuploadsubmit", (e, data) => { - const isValid = Discourse.Utilities.validateUploadedFiles(data.files); - data.formData = { type: "composer" }; - this.setProperties({ uploadProgress: 0, isUploading: isValid }); - return isValid; - }); - - $uploadTarget.on("fileuploadsend", (e, data) => { - // hide the "file selector" modal - controller.send("closeModal"); - // deal with cancellation - cancelledByTheUser = false; - // add upload placeholder - const uploadPlaceholder = Discourse.Utilities.getUploadPlaceholder(); - this.addMarkdown(uploadPlaceholder); - - if (data["xhr"]) { - const jqHXR = data.xhr(); - if (jqHXR) { - // need to wait for the link to show up in the DOM - Em.run.schedule("afterRender", () => { - const $cancel = $("#cancel-file-upload"); - $cancel.on("click", () => { - if (jqHXR) { - // signal the upload was cancelled by the user - cancelledByTheUser = true; - // immediately remove upload placeholder - this.replaceMarkdown(uploadPlaceholder, ""); - // might trigger a "fileuploadfail" event with status = 0 - jqHXR.abort(); - // make sure we always reset the uploading status - reset(); - } - // unbind - $cancel.off("click"); - }); - }); - } - } - }); - - $uploadTarget.on("fileuploadprogressall", (e, data) => { - const progress = parseInt(data.loaded / data.total * 100, 10); - this.set("uploadProgress", progress); - }); - - $uploadTarget.on("fileuploadfail", (e, data) => { - // reset upload state - reset(); - - if (!cancelledByTheUser) { - // remove upload placeholder when there's a failure - const uploadPlaceholder = Discourse.Utilities.getUploadPlaceholder(); - this.replaceMarkdown(uploadPlaceholder, ""); - // display the error - Discourse.Utilities.displayErrorForUpload(data); - } - }); - - // contenteditable div hack for getting image paste to upload working in - // Firefox. This is pretty dangerous because it can potentially break - // Ctrl+v to paste so we should be conservative about what browsers this runs - // in. - const uaMatch = navigator.userAgent.match(/Firefox\/(\d+)\.\d/); - if (uaMatch && parseInt(uaMatch[1]) >= 24) { - self.$().append( Ember.$("") ); - self.$("textarea").off('keydown.contenteditable'); - self.$("textarea").on('keydown.contenteditable', function(event) { - // Catch Ctrl+v / Cmd+v and hijack focus to a contenteditable div. We can't - // use the onpaste event because for some reason the paste isn't resumed - // after we switch focus, probably because it is being executed too late. - if ((event.ctrlKey || event.metaKey) && (event.keyCode === 86)) { - // Save the current textarea selection. - const textarea = self.$("textarea")[0], - selectionStart = textarea.selectionStart, - selectionEnd = textarea.selectionEnd; - - // Focus the contenteditable div. - const contentEditableDiv = self.$('#contenteditable'); - contentEditableDiv.focus(); - - // The paste doesn't finish immediately and we don't have any onpaste - // event, so wait for 100ms which _should_ be enough time. - setTimeout(function() { - const pastedImg = contentEditableDiv.find('img'); - - if ( pastedImg.length === 1 ) { - pastedImg.remove(); - } - - // For restoring the selection. - textarea.focus(); - const textareaContent = $(textarea).val(), - startContent = textareaContent.substring(0, selectionStart), - endContent = textareaContent.substring(selectionEnd); - - const restoreSelection = function(pastedText) { - $(textarea).val( startContent + pastedText + endContent ); - textarea.selectionStart = selectionStart + pastedText.length; - textarea.selectionEnd = textarea.selectionStart; - }; - - if (contentEditableDiv.html().length > 0) { - // If the image wasn't the only pasted content we just give up and - // fall back to the original pasted text. - contentEditableDiv.find("br").replaceWith("\n"); - restoreSelection(contentEditableDiv.text()); - } else { - // Depending on how the image is pasted in, we may get either a - // normal URL or a data URI. If we get a data URI we can convert it - // to a Blob and upload that, but if it is a regular URL that - // operation is prevented for security purposes. When we get a regular - // URL let's just create an
" + I18n.t('composer.error.try_like'); - } - } - - if (reason) { - return Discourse.InputValidation.create({ failed: true, reason }); - } - }.property('model.reply', 'model.replyLength', 'model.missingReplyCharacters', 'model.minimumPostLength'), + } }); RSVP.EventTarget.mixin(ComposerView); - export default ComposerView; diff --git a/app/assets/javascripts/discourse/views/discovery-categories.js.es6 b/app/assets/javascripts/discourse/views/discovery-categories.js.es6 index 59721af089..ee4b13d7dc 100644 --- a/app/assets/javascripts/discourse/views/discovery-categories.js.es6 +++ b/app/assets/javascripts/discourse/views/discovery-categories.js.es6 @@ -1,11 +1,16 @@ import UrlRefresh from 'discourse/mixins/url-refresh'; +import { on } from 'ember-addons/ember-computed-decorators'; + +const CATEGORIES_LIST_BODY_CLASS = "categories-list"; export default Ember.View.extend(UrlRefresh, { - _addBodyClass: function() { - $('body').addClass('categories-list'); - }.on('didInsertElement'), + @on("didInsertElement") + addBodyClass() { + $('body').addClass(CATEGORIES_LIST_BODY_CLASS); + }, - _removeBodyClass: function() { - $('body').removeClass('categories-list'); - }.on('willDestroyElement') + @on("willDestroyElement") + removeBodyClass() { + $('body').removeClass(CATEGORIES_LIST_BODY_CLASS); + }, }); diff --git a/app/assets/javascripts/discourse/views/navigation-categories.js.es6 b/app/assets/javascripts/discourse/views/navigation-categories.js.es6 new file mode 100644 index 0000000000..dcd408de52 --- /dev/null +++ b/app/assets/javascripts/discourse/views/navigation-categories.js.es6 @@ -0,0 +1,15 @@ +import { on } from 'ember-addons/ember-computed-decorators'; + +const CATEGORIES_BODY_CLASS = "navigation-categories"; + +export default Ember.View.extend({ + @on("didInsertElement") + addBodyClass() { + $('body').addClass(CATEGORIES_BODY_CLASS); + }, + + @on("willDestroyElement") + removeBodyClass() { + $('body').removeClass(CATEGORIES_BODY_CLASS); + }, +}); diff --git a/app/assets/javascripts/discourse/views/navigation-category.js.es6 b/app/assets/javascripts/discourse/views/navigation-category.js.es6 index f5e5c3d970..6a871ecae3 100644 --- a/app/assets/javascripts/discourse/views/navigation-category.js.es6 +++ b/app/assets/javascripts/discourse/views/navigation-category.js.es6 @@ -1,5 +1,5 @@ import AddCategoryClass from 'discourse/mixins/add-category-class'; -export default Em.View.extend(AddCategoryClass, { - categoryFullSlug: Em.computed.alias('controller.category.fullSlug') +export default Ember.View.extend(AddCategoryClass, { + categoryFullSlug: Ember.computed.alias('controller.category.fullSlug') }); diff --git a/app/assets/javascripts/discourse/views/quote-button.js.es6 b/app/assets/javascripts/discourse/views/quote-button.js.es6 index 8f4c5ab66d..da9ba9040c 100644 --- a/app/assets/javascripts/discourse/views/quote-button.js.es6 +++ b/app/assets/javascripts/discourse/views/quote-button.js.es6 @@ -36,9 +36,8 @@ export default Ember.View.extend({ // the quote reply widget // // Same hack applied to Android cause it has unreliable touchend - const caps = this.capabilities; - const android = caps.get('android'); - if (caps.get('winphone') || android) { + const isAndroid = this.capabilities.isAndroid; + if (this.capabilities.isWinphone || isAndroid) { onSelectionChanged = _.debounce(onSelectionChanged, 500); } @@ -72,7 +71,7 @@ export default Ember.View.extend({ // Android is dodgy, touchend often will not fire // https://code.google.com/p/android/issues/detail?id=19827 - if (!android) { + if (!isAndroid) { $(document) .on('touchstart.quote-button', function(){ view.set('isTouchInProgress', true); diff --git a/app/assets/javascripts/discourse/views/topic-map-container.js.es6 b/app/assets/javascripts/discourse/views/topic-map-container.js.es6 index e13a1ae410..5bd42eb367 100644 --- a/app/assets/javascripts/discourse/views/topic-map-container.js.es6 +++ b/app/assets/javascripts/discourse/views/topic-map-container.js.es6 @@ -1,5 +1,5 @@ import ContainerView from 'discourse/views/container'; -import { default as property, observes, on } from 'ember-addons/ember-computed-decorators'; +import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators'; export default ContainerView.extend({ classNameBindings: ['hidden', ':topic-map'], @@ -9,7 +9,7 @@ export default ContainerView.extend({ Ember.run.once(this, 'rerender'); }, - @property + @computed hidden() { if (!this.get('post.firstPost')) return true; diff --git a/app/assets/javascripts/discourse/views/topic-progress.js.es6 b/app/assets/javascripts/discourse/views/topic-progress.js.es6 index b539b618b4..7fc2047204 100644 --- a/app/assets/javascripts/discourse/views/topic-progress.js.es6 +++ b/app/assets/javascripts/discourse/views/topic-progress.js.es6 @@ -76,7 +76,7 @@ export default Ember.View.extend({ _focusWhenOpened: function() { // Don't focus on mobile or touch - if (Discourse.Mobile.mobileView || this.capabilities.get('touch')) { + if (Discourse.Mobile.mobileView || this.capabilities.touch) { return; } @@ -98,6 +98,7 @@ export default Ember.View.extend({ const controller = this.get('controller'); if (controller.get('expanded')) { if (e.keyCode === 13) { + this.$('input').blur(); controller.send('jumpPost'); } else if (e.keyCode === 27) { controller.send('toggleExpansion'); diff --git a/app/assets/javascripts/discourse/views/topic.js.es6 b/app/assets/javascripts/discourse/views/topic.js.es6 index 993f756dff..186dc0f9ec 100644 --- a/app/assets/javascripts/discourse/views/topic.js.es6 +++ b/app/assets/javascripts/discourse/views/topic.js.es6 @@ -4,7 +4,6 @@ import ClickTrack from 'discourse/lib/click-track'; import { listenForViewEvent } from 'discourse/lib/app-events'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import Scrolling from 'discourse/mixins/scrolling'; -import isElementInViewport from "discourse/lib/is-element-in-viewport"; const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolling, { templateName: 'topic', @@ -95,7 +94,6 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli // The user has scrolled the window, or it is finished rendering and ready for processing. scrolled() { - if (this.isDestroyed || this.isDestroying || this._state !== 'inDOM') { return; } @@ -118,14 +116,6 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli headerController.set('showExtraInfo', topic.get('postStream.firstPostNotLoaded')); } - // automatically unpin topics when the user reaches the bottom - if (topic.get("pinned")) { - const $topicFooterButtons = $("#topic-footer-buttons"); - if ($topicFooterButtons.length > 0 && isElementInViewport($topicFooterButtons)) { - Em.run.next(() => topic.clearPin()); - } - } - // Trigger a scrolled event this.appEvents.trigger('topic:scrolled', offset); }, diff --git a/app/assets/javascripts/discourse/views/upload-selector.js.es6 b/app/assets/javascripts/discourse/views/upload-selector.js.es6 index 4bf80f2aad..7f6af6752b 100644 --- a/app/assets/javascripts/discourse/views/upload-selector.js.es6 +++ b/app/assets/javascripts/discourse/views/upload-selector.js.es6 @@ -1,74 +1,33 @@ import ModalBodyView from "discourse/views/modal-body"; +import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; +import { uploadTranslate } from 'discourse/controllers/upload-selector'; -function uploadTranslate(key, options) { - const opts = options || {}; - if (Discourse.Utilities.allowsAttachments()) { key += "_with_attachments"; } - return I18n.t("upload_selector." + key, opts); -} export default ModalBodyView.extend({ - templateName: 'modal/upload_selector', + templateName: 'modal/upload-selector', classNames: ['upload-selector'], - // cf. http://stackoverflow.com/a/9851769/11983 - isOpera: !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0, - isFirefox: typeof InstallTrigger !== 'undefined', - isSafari: Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0, - isChrome: !!window.chrome && !this.isOpera, + @computed() + title() { + return uploadTranslate("title"); + }, - title: function() { return uploadTranslate("title"); }.property(), - uploadIcon: function() { return Discourse.Utilities.allowsAttachments() ? "fa-upload" : "fa-picture-o"; }.property(), - - touchStart: function(evt) { + touchStart(evt) { // HACK: workaround Safari iOS being really weird and not shipping click events - if (this.isSafari && evt.target.id === "filename-input") { + if (this.capabilities.isSafari && evt.target.id === "filename-input") { this.$('#filename-input').click(); } }, - tip: function() { - const source = this.get("controller.local") ? "local" : "remote"; - const authorized_extensions = Discourse.Utilities.authorizesAllExtensions() ? "" : `(${Discourse.Utilities.authorizedExtensions()})`; - return uploadTranslate(source + "_tip", { authorized_extensions }); - }.property("controller.local"), - - hint: function() { - const isSupported = this.isChrome || this.isFirefox; - // chrome is the only browser that support copy & paste of images. - return I18n.t("upload_selector.hint" + (isSupported ? "_for_supported_browsers" : "")); - }.property(), - - _selectOnInsert: function() { - this.selectedChanged(); - }.on('didInsertElement'), - - selectedChanged: function() { - const self = this; - Em.run.next(function() { + @on('didInsertElement') + @observes('controller.local') + selectedChanged() { + Ember.run.next(() => { // *HACK* to select the proper radio button - var value = self.get('controller.local') ? 'local' : 'remote'; + const value = this.get('controller.local') ? 'local' : 'remote'; $('input:radio[name="upload"]').val([value]); - // focus the input $('.inputs input:first').focus(); }); - }.observes('controller.local'), - - actions: { - upload: function() { - if (this.get("controller.local")) { - $('#reply-control').fileupload('add', { fileInput: $('#filename-input') }); - } else { - const imageUrl = $('#fileurl-input').val(), - imageLink = $('#link-input').val(), - composerView = this.get('controller.composerView'); - if (this.get("controller.showMore") && imageLink.length > 3) { - composerView.addMarkdown("[](" + imageLink + ")"); - } else { - composerView.addMarkdown(imageUrl); - } - this.get('controller').send('closeModal'); - } - } } }); diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index fbb72ad3f3..853d0d5e24 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -1,9 +1,6 @@ //= require ./discourse/mixins/ajax //= require ./discourse -// Pagedown customizations -//= require ./pagedown_custom.js - // Stuff we need to load first //= require_tree ./ember-addons/utils //= require ./ember-addons/decorator-alias @@ -77,6 +74,7 @@ //= require ./discourse/lib/emoji/emoji //= require ./discourse/lib/emoji/emoji-groups //= require ./discourse/lib/emoji/emoji-toolbar +//= require ./discourse/components/d-editor //= require ./discourse/views/composer //= require ./discourse/lib/show-modal //= require ./discourse/lib/screen-track diff --git a/app/assets/javascripts/pagedown_custom.js b/app/assets/javascripts/pagedown_custom.js deleted file mode 100644 index 8dec516811..0000000000 --- a/app/assets/javascripts/pagedown_custom.js +++ /dev/null @@ -1,37 +0,0 @@ -window.PagedownCustom = { - insertButtons: [ - { - id: 'wmd-quote-post', - description: I18n.t("composer.quote_post_title"), - execute: function() { - return Discourse.__container__.lookup('controller:composer').send('importQuote'); - } - } - ], - - appendButtons: [], - - customActions: { - "doBlockquote": function(chunk, postProcessing, oldDoBlockquote) { - - // When traditional linebreaks are set, use the default Pagedown implementation - if (Discourse.SiteSettings.traditional_markdown_linebreaks) { - return oldDoBlockquote.call(this, chunk, postProcessing); - } - - // Our custom blockquote for non-traditional markdown linebreaks - var result = []; - chunk.selection.split(/\n/).forEach(function (line) { - var newLine = ""; - if (line.indexOf("> ") === 0) { - newLine += line.substr(2); - } else { - if (/\S/.test(line)) { newLine += "> " + line; } - } - result.push(newLine); - }); - chunk.selection = result.join("\n"); - - } - } -}; diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 9046e2505a..e361da9834 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1188,7 +1188,7 @@ table.api-keys { height: 200px; } - .wmd-input { + .d-editor-input { width: 98%; height: 200px; } diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 56bfa28076..4d2c8ad658 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -38,11 +38,28 @@ } } -.textarea-wrapper .spinner { - z-index: 1000; - margin-top: 5em; +#reply-control { + .d-editor-textarea-wrapper .spinner { + z-index: 1000; + margin-top: 5em; + } + + .d-editor-button-bar { + -moz-box-sizing: border-box; + box-sizing: border-box; + + margin: 0px; + padding: 5px; + border-bottom: 2px solid dark-light-diff($primary, $secondary, 90%, -60%); + height: 33px; + } + + textarea { + box-shadow: none; + } } + .saving-text .spinner { display: inline-block; left: 5px; @@ -58,6 +75,23 @@ div.ac-wrap.disabled { } } +div.ac-wrap div.item a.remove, .remove-link { + margin-left: 4px; + font-size: 11px; + line-height: 10px; + padding: 1.5px 1.5px 1.5px 2.5px; + border-radius: 12px; + width: 10px; + display: inline-block; + border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + &:hover { + background-color: scale-color($danger, $lightness: 75%); + border: 1px solid scale-color($danger, $lightness: 30%); + text-decoration: none; + color: $danger; + } +} + div.ac-wrap { background-color: $secondary; border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); @@ -71,22 +105,6 @@ div.ac-wrap { display: inline-block; line-height: 20px; } - a.remove { - margin-left: 4px; - font-size: 11px; - line-height: 10px; - padding: 1.5px 1.5px 1.5px 2.5px; - border-radius: 12px; - width: 10px; - display: inline-block; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - &:hover { - background-color: scale-color($danger, $lightness: 75%); - border: 1px solid scale-color($danger, $lightness: 30%); - text-decoration: none; - color: $danger; - } - } } input[type="text"] { float: left; @@ -99,10 +117,6 @@ div.ac-wrap { } } -#reply-control.topic #wmd-quote-post { - display: none; -} - .auto-close-fields { div:not(:first-child) { margin-top: 10px; @@ -175,7 +189,7 @@ div.ac-wrap { // this removes the topmost margin from the first element in the topic post // if we don't do this, all posts would have extra space at the top -.wmd-preview > *:first-child { +.d-editor-preview > *:first-child { margin-top: 0; } .cooked > *:first-child { diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 2580ff1eff..95ccddffbc 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -149,7 +149,7 @@ body { background-color: dark-light-choose(scale-color($danger, $lightness: 80%), scale-color($danger, $lightness: -60%)); } - .wmd-input { + .d-editor-input { resize: none; } diff --git a/app/assets/stylesheets/common/base/emoji.scss b/app/assets/stylesheets/common/base/emoji.scss index 107fbd11af..c11d968702 100644 --- a/app/assets/stylesheets/common/base/emoji.scss +++ b/app/assets/stylesheets/common/base/emoji.scss @@ -14,7 +14,7 @@ body img.emoji { left: 50%; top: 50%; width: 445px; - height: 264px; + min-height: 264px; margin-top: -132px; margin-left: -222px; background-color: dark-light-choose(#dadada, blend-primary-secondary(5%)); diff --git a/app/assets/stylesheets/common/base/pagedown.scss b/app/assets/stylesheets/common/base/pagedown.scss deleted file mode 100644 index 1ee211aa50..0000000000 --- a/app/assets/stylesheets/common/base/pagedown.scss +++ /dev/null @@ -1,145 +0,0 @@ -// styles that apply to the PageDown editor -// http://code.google.com/p/pagedown/ - -.wmd-panel { - margin-left: 25%; - margin-right: 25%; - width: 50%; - min-width: 500px; -} - -.wmd-button-bar { - width: 100%; -} - -.wmd-button-row { - margin: 5px; - padding: 0; - height: 20px; - overflow: hidden; -} - -.wmd-spacer { - width: 1px; - height: 20px; - margin-right: 8px; - margin-left: 5px; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); - display: inline-block; - float: left; -} - -.wmd-button { - margin-right: 5px; - border: 0; - position: relative; - float: left; - font-family: FontAwesome; - font-weight: normal; - font-style: normal; - text-decoration: inherit; - display: inline; - width: auto; - height: auto; - line-height: normal; - vertical-align: baseline; - background-image: none !important; - background-position: 0 0; - background-repeat: repeat; - background: transparent; - padding: 4px; -} - -.wmd-button:hover { - background-color: dark-light-diff($primary, $secondary, 90%, -60%); -} - - -.wmd-bold-button:before { - content: "\f032"; -} - -.wmd-italic-button:before { - content: "\f033"; -} - -.wmd-link-button:before { - content: "\f0c1"; -} - -.wmd-quote-button:before { - content: "\f10e"; -} - -.wmd-code-button:before { - content: "\f121"; -} - -.wmd-image-button:before { - content: "\f093"; -} - -.wmd-image-button.image-only:before { - content: "\f03e"; -} - -.wmd-olist-button:before { - content: "\f0cb"; -} - -.wmd-ulist-button:before { - content: "\f0ca"; -} - -.wmd-heading-button:before { - content: "\f031"; -} - -.wmd-hr-button:before { - content: "\f068"; -} - -.wmd-undo-button:before { - content: "\f0e2"; -} - -.wmd-redo-button:before { - content: "\f01e"; -} - -.wmd-quote-post:before { - content: "\f0e5"; -} - -.wmd-composer-options:before { - content: "\f013"; -} - -.wmd-prompt-background { - background-color: #111; - box-shadow: 0 3px 7px rgba(0,0,0, .8); -} - -.wmd-prompt-dialog { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - background-color: dark-light-diff($primary, $secondary, 90%, -60%); -} - -.wmd-prompt-dialog > div { - font-size: 0.8em; - font-family: arial, helvetica, sans-serif; -} - -.wmd-prompt-dialog > form > input[type="text"] { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - color: $primary; -} - -.wmd-prompt-dialog > form > input[type="button"] { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - background: dark-light-choose(initial, blend-primary-secondary(50%)); - color: dark-light-choose(inherit, $secondary); - font-family: trebuchet MS, helvetica, sans-serif; - font-size: 0.8em; - font-weight: bold; -} diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index dab4f08f13..02cacad328 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -26,7 +26,7 @@ } // global styles for the cooked HTML content in posts (and preview) -.cooked, .wmd-preview { +.cooked, .d-editor-preview { word-wrap: break-word; h1, h2, h3, h4, h5, h6 { margin: 30px 0 10px; } h1 { line-height: 1em; } /* normalize.css sets h1 font size but not line height */ @@ -36,7 +36,7 @@ } -.cooked, .wmd-preview { +.cooked, .d-editor-preview { video { max-width: 100%; } diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss index 9f812fa899..a7d25e8771 100644 --- a/app/assets/stylesheets/common/base/user-badges.scss +++ b/app/assets/stylesheets/common/base/user-badges.scss @@ -65,57 +65,80 @@ } /* Badge listing in /badges. */ -table.badges-listing { +.badges-listing { margin: 20px 0; + tr { + border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + td { + padding: 10px 0; + } + } + border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); width: 90%; + padding: 10px; + display: table; + + .row { + display: table-row; + > div { + display: table-cell; + vertical-align: middle; + } + } .user-badge { font-size: $base-font-size; } - tr.title td { - padding-top: 30px; - padding-bottom: 15px; - } - - tr.title { - border-top: 0px solid; - } - - td { - padding: 10px 0px; - } - - td.granted{ - color: $success; - font-size: 120%; - } - - td.grant-count { + .grant-count { text-align: center; color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); font-size: 120%; } - td.badge, td.grant-count { + .badge, .grant-count { white-space: nowrap; - padding-right: 10px; } - td.info { + .info { font-size: 0.9em; text-align: right; } - td.description { + .description { } - tr { - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); +} + +@media all and (max-width: 750px) { + .show-badge .user-badge-with-posts .badge-user a.post-link { + width: auto; + } + .show-badge div.badge-user { + padding: 0; + } + .badges-listing { + display: block; + + .info, .grant-count { + text-align: left; + } + + .row > div.info { display: none; } + + .row { + display: block; + > div { + display: block; + margin-top: 10px; + } + } } } + /* /badges/:id/:slug page styling. */ .show-badge { .badge-user { @@ -124,6 +147,7 @@ table.badges-listing { padding: 5px 10px; margin-bottom: 10px; display: inline-block; + vertical-align: top; .details { margin: 0 10px; @@ -131,6 +155,10 @@ table.badges-listing { color: $primary; } + .username { + word-wrap: break-word; + } + .date { display: block; color: lighten($primary, 40%); diff --git a/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss b/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss index aeff2b7774..098bfa420c 100644 --- a/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss +++ b/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss @@ -3,8 +3,7 @@ } .topic-list tr.selected td:first-child, .topic-list-item.selected td:first-child, .topic-post.selected { - box-shadow: -2px 0 0 $danger; - border-left: 1px solid $danger; + box-shadow: -3px 0 0 $danger; } .topic-list-item.selected { diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index d9bdbca353..aa03a1edc4 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -10,10 +10,12 @@ position: absolute; background-color: black; opacity: 0.8; + z-index: 1000; } .d-editor-modals { position: absolute; + z-index: 1001; } .d-editor .d-editor-modal { @@ -71,7 +73,6 @@ color: $primary; border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%); overflow: auto; - visibility: visible; cursor: default; margin-top: 8px; padding: 8px 8px 0 8px; diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index 100b443757..9a7d4a8f74 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -65,7 +65,7 @@ } .similar-topics { - background-color: dark-light-choose(scale-color($tertiary, $lightness: 60%), scale-color($tertiary, $lightness: -60%)); + background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); a[href] { color: dark-light-diff($primary, $secondary, -10%, 10%); @@ -278,14 +278,14 @@ background-color: dark-light-diff($primary, $secondary, 90%, -60%); } } - .wmd-input:disabled { + .d-editor-input:disabled { background-color: dark-light-diff($primary, $secondary, 90%, -60%); } - .wmd-input, .wmd-preview { + .d-editor-input, .d-editor-preview { color: $primary; } - .wmd-preview { + .d-editor-preview { border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%); overflow: auto; visibility: visible; @@ -303,7 +303,7 @@ visibility: hidden; } } - .wmd-input { + .d-editor-input { bottom: 35px; } @@ -351,19 +351,18 @@ } #reply-control { - &.hide-preview { - .wmd-controls { - .wmd-input { - width: 100%; - } - .preview-wrapper { - display: none; - } - .textarea-wrapper { - width: 100%; - } + .wmd-controls.hide-preview { + .d-editor-input { + width: 100%; + } + .d-editor-preview-wrapper { + display: none; + } + .d-editor-textarea-wrapper { + width: 100%; } } + .wmd-controls { left: 30px; right: 30px; @@ -372,7 +371,7 @@ top: 50px; - .wmd-input, .wmd-preview-scroller, .wmd-preview { + .d-editor-input, .d-editor-preview { -moz-box-sizing: border-box; box-sizing: border-box; width: 100%; @@ -383,7 +382,7 @@ background-color: $secondary; word-wrap: break-word; } - .wmd-input, .wmd-preview-scroller { + .d-editor-input, .d-editor-preview-header { position: absolute; left: 0; top: 0; @@ -391,18 +390,17 @@ border-top: 30px solid transparent; @include border-radius-all(0); } - .wmd-preview-scroller { + .d-editor-preview-header { font-size: 0.929em; line-height: 18px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; overflow: scroll; - visibility: hidden; .marker, .caret { display: inline-block; vertical-align: top; } } - .textarea-wrapper, .preview-wrapper { + .d-editor, .d-editor-container, .d-editor-textarea-wrapper, .d-editor-preview-wrapper { position: relative; -moz-box-sizing: border-box; box-sizing: border-box; @@ -410,9 +408,9 @@ min-height: 100%; margin: 0; padding: 0; - width: 50%; } - .textarea-wrapper { + .d-editor-textarea-wrapper { + width: 50%; padding-right: 5px; float: left; .popup-tip { @@ -420,18 +418,27 @@ right: 4px; } } - .preview-wrapper { + .d-editor-preview-wrapper { + width: 50%; padding-left: 5px; float: right; } } - .wmd-button-bar { + .d-editor-button-bar { top: 0; position: absolute; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); background-color: $secondary; z-index: 100; + overflow: hidden; + width: 50%; + + -moz-box-sizing: border-box; + box-sizing: border-box; + + button { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + } } } diff --git a/app/assets/stylesheets/desktop/queued-posts.scss b/app/assets/stylesheets/desktop/queued-posts.scss index 06e24ab1e1..6d306bae6e 100644 --- a/app/assets/stylesheets/desktop/queued-posts.scss +++ b/app/assets/stylesheets/desktop/queued-posts.scss @@ -18,7 +18,7 @@ width: $topic-body-width; float: left; - .wmd-input { + .d-editor-input { width: 98%; height: 15em; } diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index cd1f837e67..71d1481196 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -39,10 +39,6 @@ } } - .bio-composer #wmd-quote-post { - display: none; - } - .static { color: $primary; display: inline-block; @@ -105,6 +101,21 @@ .user-main { margin-bottom: 50px; + // name hacky so lastpass does not freak out + // -search- means it is bypassed + #add-user-to-group { + button, .ac-wrap { + float: left; + } + button { + margin-top: 3px; + margin-left: 10px; + } + #user-search-selector { + width: 400px; + } + } + table.group-members { width: 100%; p { @@ -120,6 +131,16 @@ } td.avatar { width: 60px; + position: relative; + .is-owner { + position: absolute; + right: 0; + top: 20px; + color: dark-light-diff($primary, $secondary, 50%, -50%); + } + } + td.remove-user { + text-align: right; } td { padding: 0.5em; diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index d63afa772d..aac01b2e9a 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -163,13 +163,13 @@ input { background-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); } } - .wmd-input:disabled { + .d-editor-input:disabled { background-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); } - .wmd-input { + .d-editor-input { color: dark-light-choose(darken($primary, 40%), blend-primary-secondary(90%)); } - .wmd-input { + .d-editor-input { bottom: 35px; } .submit-panel { @@ -196,7 +196,7 @@ input { width: 240px; right: 5px; } - .textarea-wrapper .popup-tip { + .d-editor-textarea-wrapper .popup-tip { top: 28px; } button.btn.no-text { @@ -214,6 +214,14 @@ input { #reply-control { + .d-editor { + height: 100%; + } + + .d-editor-container { + height: 100%; + } + .wmd-controls { left: 10px; right: 10px; @@ -221,23 +229,22 @@ input { top: 40px; bottom: 50px; display: block; - - .wmd-input { + .d-editor-container { + padding: 0; + } + .d-editor-preview-wrapper { + display: none; + } + .d-editor-input { width: 100%; height: 100%; - min-height: 100%; padding: 7px; margin: 0; background-color: $secondary; word-wrap: break-word; box-sizing: border-box; } - .wmd-input { - position: absolute; - left: 0; - top: 0; - } - .textarea-wrapper { + .d-editor-textarea-wrapper { position: relative; box-sizing: border-box; height: 100%; @@ -250,7 +257,7 @@ input { } } } - .wmd-button-bar { + .d-editor-button-bar { display: none; } } diff --git a/app/assets/stylesheets/mobile/emoji.scss b/app/assets/stylesheets/mobile/emoji.scss index 101f5e95b8..6d94493756 100644 --- a/app/assets/stylesheets/mobile/emoji.scss +++ b/app/assets/stylesheets/mobile/emoji.scss @@ -1,3 +1,10 @@ .emoji-table-wrapper { min-width: 320px; } + +.emoji-modal { + width: 340px; + margin-top: -132px; + margin-left: -170px; + background-color: dark-light-choose(#dadada, blend-primary-secondary(5%)); +} diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index c7340dcdc7..7f70b260fa 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -92,8 +92,8 @@ margin-right: 0; } &.new-topic { - padding-right: 0; - top: -3px; + padding: 0; + top: -2px; } } diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index fe1ff813f9..c58ab353a7 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -344,10 +344,8 @@ span.post-count { padding: 15px 0; } -.topic-post { - &.moderator { - background-color: dark-light-diff($highlight, $secondary, 70%, -80%); - } +.moderator .topic-body { + background-color: dark-light-diff($highlight, $secondary, 70%, -80%); } .quote-button.visible { @@ -528,4 +526,3 @@ span.highlighted { margin-left: -10px; margin-right: -10px; } - diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index bd0b706504..b32607a7e2 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -56,7 +56,7 @@ width: 0; right: 0; bottom: 0; - z-index: 500; + z-index: 950; margin-right: 148px; } diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 58e9ed8f0b..7398fc6b5f 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -63,10 +63,6 @@ padding: 5px 8px; } - .bio-composer #wmd-quote-post { - display: none; - } - textarea {width: 100%;} } @@ -99,10 +95,6 @@ } } - .bio-composer #wmd-quote-post { - display: none; - } - .static { color: $primary; display: inline-block; diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 67e8d86966..f614a15b5f 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -107,58 +107,30 @@ class Admin::GroupsController < Admin::AdminController render json: success_json end - def add_members + def add_owners group = Group.find(params.require(:id)) - return can_not_modify_automatic if group.automatic - if params[:usernames].present? - users = User.where(username: params[:usernames].split(",")) - elsif params[:user_ids].present? - users = User.find(params[:user_ids].split(",")) - elsif params[:user_emails].present? - users = User.where(email: params[:user_emails].split(",")) - else - raise Discourse::InvalidParameters.new('user_ids or usernames or user_emails must be present') - end + users = User.where(username: params[:usernames].split(",")) users.each do |user| if !group.users.include?(user) group.add(user) - else - return render_json_error I18n.t('groups.errors.member_already_exist', username: user.username) end + group.group_users.where(user_id: user.id).update_all(owner: true) end - if group.save - render json: success_json - else - render_json_error(group) - end + render json: success_json end - def remove_member + def remove_owner group = Group.find(params.require(:id)) - return can_not_modify_automatic if group.automatic - if params[:user_id].present? - user = User.find(params[:user_id]) - elsif params[:username].present? - user = User.find_by_username(params[:username]) - else - raise Discourse::InvalidParameters.new('user_id or username must be present') - end + user = User.find(params[:user_id].to_i) + group.group_users.where(user_id: user.id).update_all(owner: false) - user.primary_group_id = nil if user.primary_group_id == group.id - - group.users.delete(user.id) - - if group.save && user.save - render json: success_json - else - render_json_error(group) - end + render json: success_json end protected diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index db2812b528..d162e27f0a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -138,13 +138,8 @@ class ApplicationController < ActionController::Base end def set_locale - I18n.locale = if current_user - current_user.effective_locale - else - SiteSetting.default_locale - end - - I18n.fallbacks.ensure_loaded! + I18n.locale = current_user.try(:effective_locale) || SiteSetting.default_locale + I18n.ensure_all_loaded! end def store_preloaded(key, json) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 9a51833a62..605d04b7d2 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -23,10 +23,12 @@ class GroupsController < ApplicationController offset = params[:offset].to_i total = group.users.count - members = group.users.order(:username_lower).limit(limit).offset(offset) + members = group.users.order('NOT group_users.owner').order(:username_lower).limit(limit).offset(offset) + owners = group.users.order(:username_lower).where('group_users.owner') render json: { members: serialize_data(members, GroupUserSerializer), + owners: serialize_data(owners, GroupUserSerializer), meta: { total: total, limit: limit, @@ -36,35 +38,56 @@ class GroupsController < ApplicationController end def add_members - guardian.ensure_can_edit!(the_group) + group = Group.find(params[:id]) + guardian.ensure_can_edit!(group) - added_users = [] - usernames = params.require(:usernames) - usernames.split(",").each do |username| - if user = User.find_by_username(username) - unless the_group.users.include?(user) - the_group.add(user) - added_users << user - end + if params[:usernames].present? + users = User.where(username: params[:usernames].split(",")) + elsif params[:user_ids].present? + users = User.find(params[:user_ids].split(",")) + elsif params[:user_emails].present? + users = User.where(email: params[:user_emails].split(",")) + else + raise Discourse::InvalidParameters.new('user_ids or usernames or user_emails must be present') + end + + users.each do |user| + if !group.users.include?(user) + group.add(user) + else + return render_json_error I18n.t('groups.errors.member_already_exist', username: user.username) end end - # always succeeds, even if bogus usernames were provided - render_serialized(added_users, GroupUserSerializer) + if group.save + render json: success_json + else + render_json_error(group) + end end def remove_member - guardian.ensure_can_edit!(the_group) + group = Group.find(params[:id]) + guardian.ensure_can_edit!(group) - removed_users = [] - username = params.require(:username) - if user = User.find_by_username(username) - the_group.remove(user) - removed_users << user + if params[:user_id].present? + user = User.find(params[:user_id]) + elsif params[:username].present? + user = User.find_by_username(params[:username]) + else + raise Discourse::InvalidParameters.new('user_id or username must be present') + end + + user.primary_group_id = nil if user.primary_group_id == group.id + + group.users.delete(user.id) + + if group.save && user.save + render json: success_json + else + render_json_error(group) end - # always succeeds, even if user was not a member - render_serialized(removed_users, GroupUserSerializer) end private diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index f3f1eadc51..f700c81d8a 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -235,7 +235,7 @@ class ListController < ApplicationController redirect_or_not_found and return if !@category @description_meta = @category.description_text - guardian.ensure_can_see!(@category) + raise Discourse::NotFound unless guardian.can_see?(@category) end def build_topic_list_options diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 6bc22d2c74..0c20e433a2 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -130,6 +130,9 @@ class PostsController < ApplicationController post = Post.where(id: params[:id]) post = post.with_deleted if guardian.is_staff? post = post.first + + raise Discourse::NotFound if post.blank? + post.image_sizes = params[:image_sizes] if params[:image_sizes].present? if too_late_to(:edit, post) @@ -155,15 +158,18 @@ class PostsController < ApplicationController opts[:skip_validations] = true end - revisor = PostRevisor.new(post) + topic = post.topic + topic = Topic.with_deleted.find(post.topic_id) if guardian.is_staff? + + revisor = PostRevisor.new(post, topic) revisor.revise!(current_user, changes, opts) return render_json_error(post) if post.errors.present? - return render_json_error(post.topic) if post.topic.errors.present? + return render_json_error(topic) if topic.errors.present? post_serializer = PostSerializer.new(post, scope: guardian, root: false) - post_serializer.draft_sequence = DraftSequence.current(current_user, post.topic.draft_key) - link_counts = TopicLink.counts_for(guardian,post.topic, [post]) + post_serializer.draft_sequence = DraftSequence.current(current_user, topic.draft_key) + link_counts = TopicLink.counts_for(guardian, topic, [post]) post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present? result = { post: post_serializer.as_json } diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 60765e0cd8..bcf9db0ee4 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -192,7 +192,7 @@ class SessionController < ApplicationController RateLimiter.new(nil, "forgot-password-min-#{request.remote_ip}", 3, 1.minute).performed! user = User.find_by_username_or_email(params[:login]) - user_presence = user.present? && user.id != Discourse::SYSTEM_USER_ID + user_presence = user.present? && user.id != Discourse::SYSTEM_USER_ID && !user.staged if user_presence email_token = user.email_tokens.create(email: user.email) Jobs.enqueue(:user_email, type: :forgot_password, user_id: user.id, email_token: email_token.token) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index d1eacdc891..7848cbb34b 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -25,8 +25,7 @@ class TopicsController < ApplicationController :reset_new, :change_post_owners, :change_timestamps, - :bookmark, - :unsubscribe] + :bookmark] before_filter :consider_user_for_promotion, only: :show @@ -103,6 +102,11 @@ class TopicsController < ApplicationController end def unsubscribe + if current_user.blank? + cookies[:destination_url] = request.fullpath + return redirect_to "/login-preferences" + end + @topic_view = TopicView.new(params[:topic_id], current_user) if slugs_do_not_match || (!request.format.json? && params[:slug].blank?) diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index d1251784ff..770e1763e2 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -12,6 +12,12 @@ class UploadsController < ApplicationController # HACK FOR IE9 to prevent the "download dialog" response.headers["Content-Type"] = "text/plain" if request.user_agent =~ /MSIE 9/ + if type == "avatar" + if SiteSetting.sso_overrides_avatar || !SiteSetting.allow_uploaded_avatars + return render json: failed_json, status: 422 + end + end + if synchronous data = create_upload(type, file, url) render json: data.as_json @@ -67,10 +73,14 @@ class UploadsController < ApplicationController # allow users to upload large images that will be automatically reduced to allowed size if SiteSetting.max_image_size_kb > 0 && FileHelper.is_image?(filename) && File.size(tempfile.path) > 0 + attempt = 2 allow_animation = type == "avatar" ? SiteSetting.allow_animated_avatars : SiteSetting.allow_animated_thumbnails - attempt = 5 while attempt > 0 && File.size(tempfile.path) > SiteSetting.max_image_size_kb.kilobytes - OptimizedImage.downsize(tempfile.path, tempfile.path, "80%", filename: filename, allow_animation: allow_animation) + image_info = FastImage.new(tempfile.path) rescue nil + w, h = *(image_info.try(:size) || [0, 0]) + break if w == 0 || h == 0 + dimensions = "#{(w * 0.8).floor}x#{(h * 0.8).floor}" + OptimizedImage.downsize(tempfile.path, tempfile.path, dimensions, filename: filename, allow_animation: allow_animation) attempt -= 1 end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 1c2be6d1ba..de7722fe58 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -259,7 +259,12 @@ class UsersController < ApplicationController return fail_with("login.reserved_username") end - user = User.new(user_params) + if user = User.where(staged: true).find_by(email: params[:email].strip.downcase) + user_params.each { |k, v| user.send("#{k}=", v) } + user.staged = false + else + user = User.new(user_params) + end # Handle custom fields user_fields = UserField.all @@ -547,6 +552,16 @@ class UsersController < ApplicationController type = params[:type] upload_id = params[:upload_id] + if SiteSetting.sso_overrides_avatar + return render json: failed_json, status: 422 + end + + if !SiteSetting.allow_uploaded_avatars + if type == "uploaded" || type == "custom" + return render json: failed_json, status: 422 + end + end + user.uploaded_avatar_id = upload_id if AVATAR_TYPES_WITH_UPLOAD.include?(type) diff --git a/app/jobs/base.rb b/app/jobs/base.rb index a187dfb682..b14cf59589 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -149,7 +149,7 @@ module Jobs begin RailsMultisite::ConnectionManagement.establish_connection(db: db) I18n.locale = SiteSetting.default_locale - I18n.fallbacks.ensure_loaded! + I18n.ensure_all_loaded! begin execute(opts) rescue => e diff --git a/app/jobs/regular/create_backup.rb b/app/jobs/regular/create_backup.rb index 5f1cf57602..1f5313b071 100644 --- a/app/jobs/regular/create_backup.rb +++ b/app/jobs/regular/create_backup.rb @@ -5,7 +5,7 @@ module Jobs sidekiq_options retry: false def execute(args) - BackupRestore.backup!(Discourse.system_user.id, publish_to_message_bus: false) + BackupRestore.backup!(Discourse.system_user.id, publish_to_message_bus: false, with_uploads: SiteSetting.backup_with_uploads) end end end diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index 8c5ece783d..9da1f35393 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -61,9 +61,9 @@ module Jobs # Markdown inline -  raw.gsub!(/!\[([^\]]*)\]\(#{escaped_src}\)/) { "" } # Markdown reference - [x]: http:// - raw.gsub!(/\[(\d+)\]: #{escaped_src}/) { "[#{$1}]: #{url}" } + raw.gsub!(/\[([^\]]+)\]:\s?#{escaped_src}/) { "[#{$1}]: #{url}" } # Direct link - raw.gsub!(src, "