diff --git a/.gitignore b/.gitignore index 95d44c86f8..fcdaca12ca 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,6 @@ # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile ~/.gitignore_global -tags - .DS_Store ._.DS_Store dump.rdb diff --git a/.mention-bot b/.mention-bot new file mode 100644 index 0000000000..16c45ceb88 --- /dev/null +++ b/.mention-bot @@ -0,0 +1,5 @@ +{ + "maxReviewers": 2, + "message": "Thanks @pullRequester for your pull request :+1:. By analyzing the blame information on this pull request, I identified @reviewers to be potential reviewers.", + "requiredOrgs": ["discourse"] +} diff --git a/Gemfile b/Gemfile index fbe22c895f..d5d1c06179 100644 --- a/Gemfile +++ b/Gemfile @@ -52,7 +52,7 @@ gem 'ember-source', '1.12.2' gem 'barber' gem 'babel-transpiler' -gem 'message_bus', '2.0.0.beta.5' +gem 'message_bus', '2.0.0.beta.8' gem 'rails_multisite' @@ -61,7 +61,7 @@ gem 'fast_xs' gem 'fast_xor' # while we sort out https://github.com/sdsykes/fastimage/pull/46 -gem 'fastimage_discourse', require: 'fastimage' +gem 'discourse_fastimage', require: 'fastimage' gem 'aws-sdk', require: false gem 'excon', require: false gem 'unf', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 19ac560327..be5294173a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,6 +73,7 @@ GEM diff-lcs (1.2.5) discourse-qunit-rails (0.0.9) railties + discourse_fastimage (2.0.0) docile (1.1.5) domain_name (0.5.25) unf (>= 0.0.5, < 1.0.0) @@ -107,7 +108,6 @@ GEM rake rake-compiler fast_xs (0.8.0) - fastimage_discourse (1.6.6) ffi (1.9.10) flamegraph (0.1.0) fast_stack @@ -156,7 +156,7 @@ GEM mail (2.6.4) mime-types (>= 1.16, < 4) memory_profiler (0.9.6) - message_bus (2.0.0.beta.5) + message_bus (2.0.0.beta.8) rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) @@ -213,7 +213,7 @@ GEM omniauth-twitter (1.2.1) json (~> 1.3) omniauth-oauth (~> 1.1) - onebox (1.5.38) + onebox (1.5.39) htmlentities (~> 4.3.4) moneta (~> 0.8) multi_json (~> 1.11) @@ -413,6 +413,7 @@ DEPENDENCIES byebug certified discourse-qunit-rails + discourse_fastimage email_reply_trimmer (= 0.1.3) ember-rails (= 0.18.5) ember-source (= 1.12.2) @@ -422,7 +423,6 @@ DEPENDENCIES fast_blank fast_xor fast_xs - fastimage_discourse flamegraph foreman gctools @@ -437,7 +437,7 @@ DEPENDENCIES lru_redux mail memory_profiler - message_bus (= 2.0.0.beta.5) + message_bus (= 2.0.0.beta.8) mime-types minitest mocha diff --git a/app/assets/javascripts/admin/controllers/admin-api.js.es6 b/app/assets/javascripts/admin/controllers/admin-api.js.es6 index 1bb0a7dc2f..82366d3bd7 100644 --- a/app/assets/javascripts/admin/controllers/admin-api.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-api.js.es6 @@ -63,6 +63,6 @@ export default Ember.ArrayController.extend({ **/ hasMasterKey: function() { return !!this.get('model').findBy('user', null); - }.property('model.@each') + }.property('model.[]') }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 new file mode 100644 index 0000000000..ae75d18715 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 @@ -0,0 +1,9 @@ +import AdminEmailLogsController from 'admin/controllers/admin-email-logs'; +import debounce from 'discourse/lib/debounce'; +import EmailLog from 'admin/models/email-log'; + +export default AdminEmailLogsController.extend({ + filterEmailLogs: debounce(function() { + EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs)); + }, 250).observes("filter.{user,address,type,skipped_reason}") +}); diff --git a/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 b/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 index 619d08c36f..5277f2feb2 100644 --- a/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 @@ -3,7 +3,8 @@ export default Ember.ArrayController.extend({ actions: { emojiUploaded(emoji) { - this.pushObject(Em.Object.create(emoji)); + emoji.url += "?t=" + new Date().getTime(); + this.pushObject(Ember.Object.create(emoji)); }, destroy(emoji) { diff --git a/app/assets/javascripts/admin/controllers/admin-reports.js.es6 b/app/assets/javascripts/admin/controllers/admin-reports.js.es6 index 17d0c1243f..61b46740ac 100644 --- a/app/assets/javascripts/admin/controllers/admin-reports.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-reports.js.es6 @@ -26,6 +26,11 @@ export default Ember.Controller.extend({ return arr.concat(this.site.groups.map((i) => {return {name: i['name'], value: i['id']};})); }, + @computed('model.type') + showCategoryOptions(modelType) { + return !modelType.match(/_private_messages$/); + }, + @computed('model.type') showGroupOptions(modelType) { return modelType === "visits" || modelType === "signups" || modelType === "profile_views"; diff --git a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 index 8d919e5f26..c8c0ba6219 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 @@ -40,7 +40,7 @@ export default Ember.ArrayController.extend({ return _(expanded).sortBy(group => group.granted_at).reverse().value(); - }.property('model', 'model.@each', 'model.expandedBadges.@each'), + }.property('model', 'model.[]', 'model.expandedBadges.[]'), /** Array of badges that have not been granted to this user. @@ -62,7 +62,7 @@ export default Ember.ArrayController.extend({ }); return _.sortBy(badges, badge => badge.get('name')); - }.property('badges.@each', 'model.@each'), + }.property('badges.[]', 'model.[]'), /** Whether there are any badges that can be granted. diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 index 454aa96f84..a5e34fc257 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -27,7 +27,7 @@ export default Ember.Controller.extend(CanCheckEmails, { }); } return []; - }.property('model.user_fields.@each'), + }.property('model.user_fields.[]'), actions: { toggleTitleEdit() { diff --git a/app/assets/javascripts/admin/routes/admin-email-bounced.js.es6 b/app/assets/javascripts/admin/routes/admin-email-bounced.js.es6 new file mode 100644 index 0000000000..027a6c0f30 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-email-bounced.js.es6 @@ -0,0 +1,2 @@ +import AdminEmailLogs from 'admin/routes/admin-email-logs'; +export default AdminEmailLogs.extend({ status: "bounced" }); diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index 64a8e393a0..8c1f988af3 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -10,6 +10,7 @@ export default { this.resource('adminEmail', { path: '/email'}, function() { this.route('sent'); this.route('skipped'); + this.route('bounced'); this.route('received'); this.route('rejected'); this.route('previewDigest', { path: '/preview-digest' }); diff --git a/app/assets/javascripts/admin/templates/email-bounced.hbs b/app/assets/javascripts/admin/templates/email-bounced.hbs new file mode 100644 index 0000000000..9c21c428cd --- /dev/null +++ b/app/assets/javascripts/admin/templates/email-bounced.hbs @@ -0,0 +1,49 @@ +{{#load-more selector=".email-list tr" action="loadMore"}} + + + + + + + + + + + + + + + + + + + + {{#each l in model}} + + + + + + + + {{else}} + + {{/each}} + +
{{i18n 'admin.email.time'}}{{i18n 'admin.email.user'}}{{i18n 'admin.email.to_address'}}{{i18n 'admin.email.email_type'}}{{i18n 'admin.email.skipped_reason'}}
{{i18n 'admin.email.logs.filters.title'}}{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}{{text-field value=filter.skipped_reason placeholderKey="admin.email.logs.filters.skipped_reason_placeholder"}}
{{format-date l.created_at}} + {{#if l.user}} + {{#link-to 'adminUser' l.user}}{{avatar l.user imageSize="tiny"}}{{/link-to}} + {{#link-to 'adminUser' l.user}}{{l.user.username}}{{/link-to}} + {{else}} + — + {{/if}} + {{l.to_address}}{{l.email_type}} + {{#if l.post_url}} + {{l.skipped_reason}} + {{else}} + {{l.skipped_reason}} + {{/if}} +
{{i18n 'admin.email.logs.none'}}
+{{/load-more}} + +{{conditional-loading-spinner condition=loading}} diff --git a/app/assets/javascripts/admin/templates/email.hbs b/app/assets/javascripts/admin/templates/email.hbs index 1a7d5bbfe7..509d5d2081 100644 --- a/app/assets/javascripts/admin/templates/email.hbs +++ b/app/assets/javascripts/admin/templates/email.hbs @@ -4,6 +4,7 @@ {{nav-item route='adminCustomizeEmailTemplates' label='admin.email.templates'}} {{nav-item route='adminEmail.sent' label='admin.email.sent'}} {{nav-item route='adminEmail.skipped' label='admin.email.skipped'}} + {{nav-item route='adminEmail.bounced' label='admin.email.bounced'}} {{nav-item route='adminEmail.received' label='admin.email.received'}} {{nav-item route='adminEmail.rejected' label='admin.email.rejected'}} {{/admin-nav}} diff --git a/app/assets/javascripts/admin/templates/reports.hbs b/app/assets/javascripts/admin/templates/reports.hbs index 7f0363a2f3..12303f27ef 100644 --- a/app/assets/javascripts/admin/templates/reports.hbs +++ b/app/assets/javascripts/admin/templates/reports.hbs @@ -3,7 +3,9 @@
{{i18n 'admin.dashboard.reports.start_date'}} {{date-picker-past value=startDate}} {{i18n 'admin.dashboard.reports.end_date'}} {{date-picker-past value=endDate}} - {{combo-box valueAttribute="value" content=categoryOptions value=categoryId}} + {{#if showCategoryOptions}} + {{combo-box valueAttribute="value" content=categoryOptions value=categoryId}} + {{/if}} {{#if showGroupOptions}} {{combo-box valueAttribute="value" content=groupOptions value=groupId}} {{/if}} diff --git a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 index c5ba44e27e..cb838a48fc 100644 --- a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 +++ b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 @@ -27,7 +27,7 @@ export default Ember.View.extend({ // force rerender this.rerender(); } - }, 150).observes("controller.model.@each"), + }, 150).observes("controller.model.[]"), render(buffer) { const formattedLogs = this.get("formattedLogs"); diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index f791de454c..063b805a49 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -8,9 +8,10 @@ define('ember', ['exports'], function(__exports__) { var _pluginCallbacks = []; -window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { +window.Discourse = Ember.Application.extend(Discourse.Ajax, { rootElement: '#main', _docTitle: document.title, + __TAGS_INCLUDED__: true, getURL: function(url) { if (!url) return url; @@ -106,7 +107,7 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { $('noscript').remove(); - Ember.keys(requirejs._eak_seen).forEach(function(key) { + Object.keys(requirejs._eak_seen).forEach(function(key) { if (/\/pre\-initializers\//.test(key)) { var module = require(key, null, null, true); if (!module) { throw new Error(key + ' must export an initializer.'); } @@ -114,7 +115,7 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { } }); - Ember.keys(requirejs._eak_seen).forEach(function(key) { + Object.keys(requirejs._eak_seen).forEach(function(key) { if (/\/initializers\//.test(key)) { var module = require(key, null, null, true); if (!module) { throw new Error(key + ' must export an initializer.'); } @@ -167,7 +168,7 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { return this.get("currentAssetVersion"); } }) -}); +}).create(); function RemovedObject(name) { this._removedName = name; diff --git a/app/assets/javascripts/discourse/adapters/tag-notification.js.es6 b/app/assets/javascripts/discourse/adapters/tag-notification.js.es6 new file mode 100644 index 0000000000..7df07c2b39 --- /dev/null +++ b/app/assets/javascripts/discourse/adapters/tag-notification.js.es6 @@ -0,0 +1,7 @@ +import RESTAdapter from 'discourse/adapters/rest'; + +export default RESTAdapter.extend({ + pathFor(store, type, id) { + return "/tags/" + id + "/notifications"; + } +}); diff --git a/app/assets/javascripts/discourse/components/basic-topic-list.js.es6 b/app/assets/javascripts/discourse/components/basic-topic-list.js.es6 index 53ebb014b6..75c4d370c0 100644 --- a/app/assets/javascripts/discourse/components/basic-topic-list.js.es6 +++ b/app/assets/javascripts/discourse/components/basic-topic-list.js.es6 @@ -13,7 +13,7 @@ export default Ember.Component.extend({ _topicListChanged: function() { this._initFromTopicList(this.get('topicList')); - }.observes('topicList.@each'), + }.observes('topicList.[]'), _initFromTopicList(topicList) { if (topicList !== null) { diff --git a/app/assets/javascripts/discourse/components/combo-box.js.es6 b/app/assets/javascripts/discourse/components/combo-box.js.es6 index 83b9290bf6..e023ad267c 100644 --- a/app/assets/javascripts/discourse/components/combo-box.js.es6 +++ b/app/assets/javascripts/discourse/components/combo-box.js.es6 @@ -46,7 +46,7 @@ export default Ember.Component.extend({ } }, - @observes('content.@each') + @observes('content.[]') _rerenderOnChange() { this.rerender(); }, diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 816a7e37de..89a45660d5 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -2,6 +2,7 @@ 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'; export default Ember.Component.extend({ classNames: ['wmd-controls'], @@ -27,6 +28,22 @@ export default Ember.Component.extend({ 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 { diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 47c6c068d0..01afbacd91 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -3,9 +3,10 @@ import loadScript from 'discourse/lib/load-script'; import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; import { showSelector } from "discourse/lib/emoji/emoji-toolbar"; import Category from 'discourse/models/category'; -import { SEPARATOR as categoryHashtagSeparator, - categoryHashtagTriggerRule - } from 'discourse/lib/category-hashtags'; +import { categoryHashtagTriggerRule } from 'discourse/lib/category-hashtags'; +import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; +import { search as searchCategoryTag } from 'discourse/lib/category-tag-search'; +import { SEPARATOR } from 'discourse/lib/category-hashtags'; // Our head can be a static string or a function that returns a string // based on input (like for numbered lists). @@ -217,7 +218,7 @@ export default Ember.Component.extend({ const mouseTrap = Mousetrap(this.$('.d-editor-input')[0]); const shortcuts = this.get('toolbar.shortcuts'); - Ember.keys(shortcuts).forEach(sc => { + Object.keys(shortcuts).forEach(sc => { const button = shortcuts[sc]; mouseTrap.bind(sc, () => { this.send(button.action, button); @@ -243,7 +244,7 @@ export default Ember.Component.extend({ this.appEvents.off('composer:insert-text'); const mouseTrap = this._mouseTrap; - Ember.keys(this.get('toolbar.shortcuts')).forEach(sc => mouseTrap.unbind(sc)); + Object.keys(this.get('toolbar.shortcuts')).forEach(sc => mouseTrap.unbind(sc)); this.$('.d-editor-preview').off('click.preview'); }, @@ -278,17 +279,22 @@ export default Ember.Component.extend({ Ember.run.debounce(this, this._updatePreview, 30); }, - _applyCategoryHashtagAutocomplete(container, $editorInput) { - const template = container.lookup('template:category-group-autocomplete.raw'); + _applyCategoryHashtagAutocomplete(container) { + const template = container.lookup('template:category-tag-autocomplete.raw'); + const siteSettings = this.siteSettings; - $editorInput.autocomplete({ + this.$('.d-editor-input').autocomplete({ template: template, key: '#', - transformComplete(category) { - return Category.slugFor(category, categoryHashtagSeparator); + transformComplete(obj) { + if (obj.model) { + return Category.slugFor(obj.model, SEPARATOR); + } else { + return `${obj.text}${TAG_HASHTAG_POSTFIX}`; + } }, dataSource(term) { - return Category.search(term); + return searchCategoryTag(term, siteSettings); }, triggerRule(textarea, opts) { return categoryHashtagTriggerRule(textarea, opts); diff --git a/app/assets/javascripts/discourse/components/date-picker.js.es6 b/app/assets/javascripts/discourse/components/date-picker.js.es6 index 21825d2ed8..ea86648784 100644 --- a/app/assets/javascripts/discourse/components/date-picker.js.es6 +++ b/app/assets/javascripts/discourse/components/date-picker.js.es6 @@ -9,24 +9,28 @@ export default Em.Component.extend({ @on("didInsertElement") _loadDatePicker() { const input = this.$(".date-picker")[0]; + const container = $("#" + this.get("containerId"))[0]; loadScript("/javascripts/pikaday.js").then(() => { - let default_opts = { - field: input, - container: this.$()[0], - format: "YYYY-MM-DD", - firstDay: moment.localeData().firstDayOfWeek(), - i18n: { - previousMonth: I18n.t('dates.previous_month'), - nextMonth: I18n.t('dates.next_month'), - months: moment.months(), - weekdays: moment.weekdays(), - weekdaysShort: moment.weekdaysShort() - }, - onSelect: date => this.set("value", moment(date).format("YYYY-MM-DD")) - }; + Ember.run.next(() => { + let default_opts = { + field: input, + container: container || this.$()[0], + bound: container === undefined, + format: "YYYY-MM-DD", + firstDay: moment.localeData().firstDayOfWeek(), + i18n: { + previousMonth: I18n.t('dates.previous_month'), + nextMonth: I18n.t('dates.next_month'), + months: moment.months(), + weekdays: moment.weekdays(), + weekdaysShort: moment.weekdaysShort() + }, + onSelect: date => this.set("value", moment(date).format("YYYY-MM-DD")) + }; - this._picker = new Pikaday(_.merge(default_opts, this._opts())); + this._picker = new Pikaday(_.merge(default_opts, this._opts())); + }); }); }, diff --git a/app/assets/javascripts/discourse/components/discourse-tag-bound.js.es6 b/app/assets/javascripts/discourse/components/discourse-tag-bound.js.es6 new file mode 100644 index 0000000000..08f0143256 --- /dev/null +++ b/app/assets/javascripts/discourse/components/discourse-tag-bound.js.es6 @@ -0,0 +1,13 @@ +export default Ember.Component.extend({ + tagName: 'a', + classNameBindings: [':discourse-tag', 'style', 'tagClass'], + attributeBindings: ['href'], + + tagClass: function() { + return "tag-" + this.get('tagRecord.id'); + }.property('tagRecord.id'), + + href: function() { + return '/tags/' + this.get('tagRecord.id'); + }.property('tagRecord.id'), +}); diff --git a/app/assets/javascripts/discourse/views/discovery-categories.js.es6 b/app/assets/javascripts/discourse/components/discovery-categories.js.es6 similarity index 82% rename from app/assets/javascripts/discourse/views/discovery-categories.js.es6 rename to app/assets/javascripts/discourse/components/discovery-categories.js.es6 index ee4b13d7dc..d4b969b56d 100644 --- a/app/assets/javascripts/discourse/views/discovery-categories.js.es6 +++ b/app/assets/javascripts/discourse/components/discovery-categories.js.es6 @@ -3,7 +3,9 @@ import { on } from 'ember-addons/ember-computed-decorators'; const CATEGORIES_LIST_BODY_CLASS = "categories-list"; -export default Ember.View.extend(UrlRefresh, { +export default Ember.Component.extend(UrlRefresh, { + classNames: ['contents'], + @on("didInsertElement") addBodyClass() { $('body').addClass(CATEGORIES_LIST_BODY_CLASS); diff --git a/app/assets/javascripts/discourse/views/discovery-topics.js.es6 b/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6 similarity index 68% rename from app/assets/javascripts/discourse/views/discovery-topics.js.es6 rename to app/assets/javascripts/discourse/components/discovery-topics-list.js.es6 index ef5e3712c8..5a154ca745 100644 --- a/app/assets/javascripts/discourse/views/discovery-topics.js.es6 +++ b/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6 @@ -1,27 +1,13 @@ -import UrlRefresh from 'discourse/mixins/url-refresh'; -import LoadMore from "discourse/mixins/load-more"; import { on, observes } from "ember-addons/ember-computed-decorators"; +import LoadMore from "discourse/mixins/load-more"; +import UrlRefresh from 'discourse/mixins/url-refresh'; -export default Ember.View.extend(LoadMore, UrlRefresh, { +const DiscoveryTopicsListComponent = Ember.Component.extend(UrlRefresh, LoadMore, { + classNames: ['contents'], eyelineSelector: '.topic-list-item', - actions: { - loadMore() { - const self = this; - Discourse.notifyTitle(0); - this.get('controller').loadMoreTopics().then(hasMoreResults => { - Ember.run.schedule('afterRender', () => self.saveScrollPosition()); - if (!hasMoreResults) { - this.get('eyeline').flushRest(); - } else if ($(window).height() >= $(document).height()) { - this.send("loadMore"); - } - }); - } - }, - @on("didInsertElement") - @observes("controller.model") + @observes("model") _readjustScrollPosition() { const scrollTo = this.session.get('topicListScrollPosition'); if (scrollTo && scrollTo >= 0) { @@ -31,19 +17,33 @@ export default Ember.View.extend(LoadMore, UrlRefresh, { } }, - @observes("controller.topicTrackingState.incomingCount") + @observes("incomingCount") _updateTitle() { - Discourse.notifyTitle(this.get('controller.topicTrackingState.incomingCount')); + Discourse.notifyTitle(this.get('incomingCount')); }, - // Remember where we were scrolled to saveScrollPosition() { this.session.set('topicListScrollPosition', $(window).scrollTop()); }, - // When the topic list is scrolled scrolled() { this._super(); this.saveScrollPosition(); + }, + + actions: { + loadMore() { + Discourse.notifyTitle(0); + this.get('model').loadMore().then(hasMoreResults => { + Ember.run.schedule('afterRender', () => this.saveScrollPosition()); + if (!hasMoreResults) { + this.get('eyeline').flushRest(); + } else if ($(window).height() >= $(document).height()) { + this.send("loadMore"); + } + }); + } } }); + +export default DiscoveryTopicsListComponent; diff --git a/app/assets/javascripts/discourse/components/global-notice.js.es6 b/app/assets/javascripts/discourse/components/global-notice.js.es6 index f61798163f..13994f28fb 100644 --- a/app/assets/javascripts/discourse/components/global-notice.js.es6 +++ b/app/assets/javascripts/discourse/components/global-notice.js.es6 @@ -17,6 +17,14 @@ export default Ember.Component.extend(StringBuffer, { notices.push([I18n.t("emails_are_disabled"), 'alert-emails-disabled']); } + if (this.currentUser && this.currentUser.get('staff') && this.siteSettings.bootstrap_mode_enabled) { + if (this.siteSettings.bootstrap_mode_min_users > 0) { + notices.push([I18n.t("bootstrap_mode_enabled", {min_users: this.siteSettings.bootstrap_mode_min_users}), 'alert-bootstrap-mode']); + } else { + notices.push([I18n.t("bootstrap_mode_disabled"), 'alert-bootstrap-mode']); + } + } + if (!_.isEmpty(this.siteSettings.global_notice)) { notices.push([this.siteSettings.global_notice, 'alert-global-notice']); } diff --git a/app/assets/javascripts/discourse/components/group-post-stream.js.es6 b/app/assets/javascripts/discourse/components/group-post-stream.js.es6 new file mode 100644 index 0000000000..fc83f2dbb6 --- /dev/null +++ b/app/assets/javascripts/discourse/components/group-post-stream.js.es6 @@ -0,0 +1,8 @@ +export default Ember.Component.extend({ + actions: { + // TODO: When on Ember 1.13, use a closure action + loadMore() { + this.sendAction('loadMore'); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 deleted file mode 100644 index e00b175819..0000000000 --- a/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 +++ /dev/null @@ -1,76 +0,0 @@ -import computed from 'ember-addons/ember-computed-decorators'; -import mobile from 'discourse/lib/mobile'; - -export default Ember.Component.extend({ - classNames: ['hamburger-panel'], - - @computed('currentUser.read_faq') - prioritizeFaq(readFaq) { - // If it's a custom FAQ never prioritize it - return Ember.isEmpty(this.siteSettings.faq_url) && !readFaq; - }, - - @computed() - showKeyboardShortcuts() { - return !this.site.mobileView && !this.capabilities.touch; - }, - - @computed() - showMobileToggle() { - return this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch); - }, - - @computed() - mobileViewLinkTextKey() { - return this.site.mobileView ? "desktop_view" : "mobile_view"; - }, - - @computed() - faqUrl() { - return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq'); - }, - - _lookupCount(type) { - const state = this.get('topicTrackingState'); - return state ? state.lookupCount(type) : 0; - }, - - @computed('topicTrackingState.messageCount') - newCount() { - return this._lookupCount('new'); - }, - - @computed('topicTrackingState.messageCount') - unreadCount() { - return this._lookupCount('unread'); - }, - - @computed() - categories() { - const hideUncategorized = !this.siteSettings.allow_uncategorized_topics; - const showSubcatList = this.siteSettings.show_subcategory_list; - const isStaff = Discourse.User.currentProp('staff'); - - return Discourse.Category.list().reject((c) => { - if (showSubcatList && c.get('parent_category_id')) { return true; } - if (hideUncategorized && c.get('isUncategorizedCategory') && !isStaff) { return true; } - return false; - }); - }, - - @computed() - showUserDirectoryLink() { - if (!this.siteSettings.enable_user_directory) return false; - if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) return false; - return true; - }, - - actions: { - keyboardShortcuts() { - this.sendAction('showKeyboardAction'); - }, - toggleMobileView() { - mobile.toggleMobileView(); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/header-dropdown.js.es6 b/app/assets/javascripts/discourse/components/header-dropdown.js.es6 deleted file mode 100644 index c747984855..0000000000 --- a/app/assets/javascripts/discourse/components/header-dropdown.js.es6 +++ /dev/null @@ -1,34 +0,0 @@ -import computed from 'ember-addons/ember-computed-decorators'; - -export default Ember.Component.extend({ - tagName: 'li', - classNameBindings: [':header-dropdown-toggle', 'active'], - - @computed('showUser', 'path') - href(showUser, path) { - return showUser ? this.currentUser.get('path') : Discourse.getURL(path); - }, - - active: Ember.computed.alias('toggleVisible'), - - actions: { - toggle() { - - if (this.siteSettings.login_required && !this.currentUser) { - this.sendAction('loginAction'); - } else { - if (this.site.mobileView && this.get('mobileAction')) { - this.sendAction('mobileAction'); - return; - } - - if (this.get('action')) { - this.sendAction('action'); - } else { - this.toggleProperty('toggleVisible'); - } - } - this.appEvents.trigger('dropdowns:closeAll'); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/header-extra-info.js.es6 b/app/assets/javascripts/discourse/components/header-extra-info.js.es6 index 2182f3ea34..66aaf82cb9 100644 --- a/app/assets/javascripts/discourse/components/header-extra-info.js.es6 +++ b/app/assets/javascripts/discourse/components/header-extra-info.js.es6 @@ -1,54 +1,3 @@ -import DiscourseURL from 'discourse/lib/url'; - -const TopicCategoryComponent = Ember.Component.extend({ - needsSecondRow: Ember.computed.gt('secondRowItems.length', 0), - secondRowItems: function() { return []; }.property(), - - pmPath: function() { - var currentUser = this.get('currentUser'); - return currentUser && currentUser.pmPath(this.get('topic')); - }.property('topic'), - - showPrivateMessageGlyph: function() { - return !this.get('topic.is_warning') && this.get('topic.isPrivateMessage'); - }.property('topic.is_warning', 'topic.isPrivateMessage'), - - actions: { - jumpToTopPost() { - const topic = this.get('topic'); - if (topic) { - DiscourseURL.routeTo(topic.get('firstPostUrl')); - } - } - } - -}); - -let id = 0; - -// Allow us (and plugins) to register themselves as needing a second -// row in the header. If there is at least one thing in the second row -// the style changes to accomodate it. -function needsSecondRowIf(prop, cb) { - const rowId = "_second_row_" + (id++), - methodHash = {}; - - methodHash[id] = function() { - const secondRowItems = this.get('secondRowItems'), - propVal = this.get(prop); - if (cb.call(this, propVal)) { - secondRowItems.addObject(rowId); - } else { - secondRowItems.removeObject(rowId); - } - }.observes(prop).on('init'); - - TopicCategoryComponent.reopen(methodHash); +export function needsSecondRowIf() { + Ember.warn("DEPRECATION: `needsSecondRowIf` is deprecated. Use widget hooks on `header-second-row`"); } - -needsSecondRowIf('topic.category', function(cat) { - return cat && (!cat.get('isUncategorizedCategory') || !this.siteSettings.suppress_uncategorized_badge); -}); - -export default TopicCategoryComponent; -export { needsSecondRowIf }; diff --git a/app/assets/javascripts/discourse/components/home-logo.js.es6 b/app/assets/javascripts/discourse/components/home-logo.js.es6 deleted file mode 100644 index 4798205e2e..0000000000 --- a/app/assets/javascripts/discourse/components/home-logo.js.es6 +++ /dev/null @@ -1,58 +0,0 @@ -import DiscourseURL from 'discourse/lib/url'; -import { iconHTML } from 'discourse/helpers/fa-icon'; -import { observes } from 'ember-addons/ember-computed-decorators'; - -export default Ember.Component.extend({ - widget: 'home-logo', - showMobileLogo: null, - linkUrl: null, - classNames: ['title'], - - init() { - this._super(); - this.showMobileLogo = this.site.mobileView && !Ember.isEmpty(this.siteSettings.mobile_logo_url); - this.linkUrl = this.get('targetUrl') || '/'; - }, - - @observes('minimized') - _updateLogo() { - // On mobile we don't minimize the logo - if (!this.site.mobileView) { - this.rerender(); - } - }, - - click(e) { - // if they want to open in a new tab, let it so - if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) { return true; } - - e.preventDefault(); - - DiscourseURL.routeTo(this.linkUrl); - return false; - }, - - render(buffer) { - const { siteSettings } = this; - const logoUrl = siteSettings.logo_url || ''; - const title = siteSettings.title; - - buffer.push(``); - if (!this.site.mobileView && this.get('minimized')) { - const logoSmallUrl = siteSettings.logo_small_url || ''; - if (logoSmallUrl.length) { - buffer.push(``); - } else { - buffer.push(iconHTML('home')); - } - } else if (this.showMobileLogo) { - buffer.push(``); - } else if (logoUrl.length) { - buffer.push(``); - } else { - buffer.push(``); - } - buffer.push(''); - } - -}); diff --git a/app/assets/javascripts/discourse/components/load-more.js.es6 b/app/assets/javascripts/discourse/components/load-more.js.es6 index 4b355a5b0a..ab7591f5e9 100644 --- a/app/assets/javascripts/discourse/components/load-more.js.es6 +++ b/app/assets/javascripts/discourse/components/load-more.js.es6 @@ -1,8 +1,6 @@ import LoadMore from "discourse/mixins/load-more"; export default Ember.Component.extend(LoadMore, { - _viaComponent: true, - init() { this._super(); this.set('eyelineSelector', this.get('selector')); diff --git a/app/assets/javascripts/discourse/components/menu-panel.js.es6 b/app/assets/javascripts/discourse/components/menu-panel.js.es6 deleted file mode 100644 index ca017a1171..0000000000 --- a/app/assets/javascripts/discourse/components/menu-panel.js.es6 +++ /dev/null @@ -1,224 +0,0 @@ -import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; -import { headerHeight } from 'discourse/views/header'; - -const PANEL_BODY_MARGIN = 30; -const mutationSupport = !Ember.testing && !!window['MutationObserver']; - -export default Ember.Component.extend({ - classNameBindings: [':menu-panel', 'visible::hidden', 'viewMode'], - _lastVisible: false, - - showClose: Ember.computed.equal('viewMode', 'slide-in'), - - _layoutComponent() { - if (!this.get('visible')) { return; } - - const $window = $(window); - let width = this.get('maxWidth') || 300; - const windowWidth = parseInt($window.width()); - - if ((windowWidth - width) < 50) { - width = windowWidth - 50; - } - - const viewMode = this.get('viewMode'); - const $panelBody = this.$('.panel-body'); - let contentHeight = parseInt(this.$('.panel-body-contents').height()); - - // We use a mutationObserver to check for style changes, so it's important - // we don't set it if it doesn't change. Same goes for the $panelBody! - const style = this.$().prop('style'); - - if (viewMode === 'drop-down') { - const $buttonPanel = $('header ul.icons'); - if ($buttonPanel.length === 0) { return; } - - // These values need to be set here, not in the css file - this is to deal with the - // possibility of the window being resized and the menu changing from .slide-in to .drop-down. - if (style.top !== '100%' || style.height !== 'auto') { - this.$().css({ top: '100%', height: 'auto' }); - } - - // adjust panel height - const fullHeight = parseInt($window.height()); - const offsetTop = this.$().offset().top; - const scrollTop = $window.scrollTop(); - - if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) { - contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN; - } - if ($panelBody.height() !== contentHeight) { - $panelBody.height(contentHeight); - } - $('body').addClass('drop-down-visible'); - } else { - const menuTop = headerHeight(); - - let height; - const winHeight = $(window).height() - 16; - if ((menuTop + contentHeight) < winHeight) { - height = contentHeight + "px"; - } else { - height = winHeight - menuTop; - } - - if ($panelBody.prop('style').height !== '100%') { - $panelBody.height('100%'); - } - if (style.top !== menuTop + "px" || style.height !== height) { - this.$().css({ top: menuTop + "px", height }); - } - $('body').removeClass('drop-down-visible'); - } - - this.$().width(width); - }, - - @computed('force') - viewMode() { - const force = this.get('force'); - if (force) { return force; } - - const headerWidth = $('#main-outlet .container').width() || 1100; - const screenWidth = $(window).width(); - const remaining = parseInt((screenWidth - headerWidth) / 2); - - return (remaining < 50) ? 'slide-in' : 'drop-down'; - }, - - @observes('viewMode', 'visible') - _visibleChanged() { - if (this.get('visible')) { - // Allow us to hook into things being shown - if (!this._lastVisible) { - Ember.run.scheduleOnce('afterRender', () => this.sendAction('onVisible')); - this._lastVisible = true; - } - - $('html').on('click.close-menu-panel', (e) => { - const $target = $(e.target); - if ($target.closest('.header-dropdown-toggle').length > 0) { return; } - if ($target.closest('.menu-panel').length > 0) { return; } - this.hide(); - }); - this.performLayout(); - this._watchSizeChanges(); - - // iOS does not handle scroll events well - if (!this.capabilities.isIOS) { - $(window).on('scroll.discourse-menu-panel', () => this.performLayout()); - } - } else if (this._lastVisible) { - this._lastVisible = false; - Ember.run.scheduleOnce('afterRender', () => this.sendAction('onHidden')); - $('html').off('click.close-menu-panel'); - $(window).off('scroll.discourse-menu-panel'); - this._stopWatchingSize(); - $('body').removeClass('drop-down-visible'); - } - }, - - @computed() - showKeyboardShortcuts() { - return !this.site.mobileView && !this.capabilities.touch; - }, - - @computed() - showMobileToggle() { - return this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch); - }, - - @computed() - mobileViewLinkTextKey() { - return this.site.mobileView ? "desktop_view" : "mobile_view"; - }, - - @computed() - faqUrl() { - return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq'); - }, - - performLayout() { - Ember.run.scheduleOnce('afterRender', this, this._layoutComponent); - }, - - _watchSizeChanges() { - if (mutationSupport) { - this._observer.disconnect(); - this._observer.observe(this.element, { childList: true, subtree: true, characterData: true, attributes: true }); - } else { - clearInterval(this._resizeInterval); - this._resizeInterval = setInterval(() => { - Ember.run(() => { - const $panelBodyContents = this.$('.panel-body-contents'); - if ($panelBodyContents && $panelBodyContents.length) { - const contentHeight = parseInt($panelBodyContents.height()); - if (contentHeight !== this._lastHeight) { this.performLayout(); } - this._lastHeight = contentHeight; - } - }); - }, 500); - } - }, - - _stopWatchingSize() { - if (mutationSupport) { - this._observer.disconnect(); - } else { - clearInterval(this._resizeInterval); - } - }, - - @on('didInsertElement') - _bindEvents() { - this.$().on('click.discourse-menu-panel', 'a', e => { - if (e.metaKey || e.ctrlKey || e.shiftKey) { return; } - const $target = $(e.target); - if ($target.data('ember-action') || $target.closest('.search-link').length > 0) { return; } - this.hide(); - }); - - this.appEvents.on('dropdowns:closeAll', this, this.hide); - this.appEvents.on('dom:clean', this, this.hide); - - $('body').on('keydown.discourse-menu-panel', e => { - if (e.which === 27) { - this.hide(); - } - }); - - $(window).on('resize.discourse-menu-panel', () => { - this.propertyDidChange('viewMode'); - this.performLayout(); - }); - - if (mutationSupport) { - this._observer = new MutationObserver(() => { - Ember.run.debounce(this, this.performLayout, 50); - }); - } - - this.propertyDidChange('viewMode'); - }, - - @on('willDestroyElement') - _removeEvents() { - this.appEvents.off('dom:clean', this, this.hide); - this.appEvents.off('dropdowns:closeAll', this, this.hide); - this.$().off('click.discourse-menu-panel'); - $('body').off('keydown.discourse-menu-panel'); - $('html').off('click.close-menu-panel'); - $(window).off('resize.discourse-menu-panel'); - $(window).off('scroll.discourse-menu-panel'); - }, - - hide() { - this.set('visible', false); - }, - - actions: { - close() { - this.hide(); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/mount-widget.js.es6 b/app/assets/javascripts/discourse/components/mount-widget.js.es6 index b7a8fdb020..da98973122 100644 --- a/app/assets/javascripts/discourse/components/mount-widget.js.es6 +++ b/app/assets/javascripts/discourse/components/mount-widget.js.es6 @@ -1,5 +1,6 @@ +import { keyDirty } from 'discourse/widgets/widget'; import { diff, patch } from 'virtual-dom'; -import { WidgetClickHook } from 'discourse/widgets/click-hook'; +import { WidgetClickHook } from 'discourse/widgets/hooks'; import { renderedKey, queryRegistry } from 'discourse/widgets/widget'; const _cleanCallbacks = {}; @@ -13,13 +14,20 @@ export default Ember.Component.extend({ _rootNode: null, _timeout: null, _widgetClass: null, - _afterRender: null, + _renderCallback: null, + _childEvents: null, init() { this._super(); const name = this.get('widget'); this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`); + + if (!this._widgetClass) { + console.error(`Error: Could not find widget: ${name}`); + } + + this._childEvents = []; this._connected = []; }, @@ -42,50 +50,68 @@ export default Ember.Component.extend({ }, willDestroyElement() { + this._childEvents.forEach(evt => this.appEvents.off(evt)); Ember.run.cancel(this._timeout); }, + afterRender() { + }, + + beforePatch() { + }, + + afterPatch() { + }, + + eventDispatched(eventName, key, refreshArg) { + const onRefresh = Ember.String.camelize(eventName.replace(/:/, '-')); + keyDirty(key, { onRefresh, refreshArg }); + this.queueRerender(); + }, + + dispatch(eventName, key) { + this._childEvents.push(eventName); + this.appEvents.on(eventName, refreshArg => { + this.eventDispatched(eventName, key, refreshArg); + }); + }, + queueRerender(callback) { - if (callback && !this._afterRender) { - this._afterRender = callback; + if (callback && !this._renderCallback) { + this._renderCallback = callback; } Ember.run.scheduleOnce('render', this, this.rerenderWidget); }, + buildArgs() { + }, + rerenderWidget() { Ember.run.cancel(this._timeout); if (this._rootNode) { + if (!this._widgetClass) { return; } + const t0 = new Date().getTime(); + const args = this.get('args') || this.buildArgs(); const opts = { model: this.get('model') }; - const newTree = new this._widgetClass(this.get('args'), this.container, opts); + const newTree = new this._widgetClass(args, this.container, opts); newTree._emberView = this; const patches = diff(this._tree || this._rootNode, newTree); - const $body = $(document); - const prevHeight = $body.height(); - const prevScrollTop = $body.scrollTop(); - + this.beforePatch(); this._rootNode = patch(this._rootNode, patches); - - const height = $body.height(); - const scrollTop = $body.scrollTop(); - - // This hack is for when swapping out many cloaked views at once - // when using keyboard navigation. It could suddenly move the - // scroll - if (prevHeight === height && scrollTop !== prevScrollTop) { - $body.scrollTop(prevScrollTop); - } + this.afterPatch(); this._tree = newTree; - if (this._afterRender) { - this._afterRender(); - this._afterRender = null; + if (this._renderCallback) { + this._renderCallback(); + this._renderCallback = null; } + this.afterRender(); renderedKey('*'); if (this.profileWidget) { diff --git a/app/assets/javascripts/discourse/components/notification-item.js.es6 b/app/assets/javascripts/discourse/components/notification-item.js.es6 deleted file mode 100644 index 64411038df..0000000000 --- a/app/assets/javascripts/discourse/components/notification-item.js.es6 +++ /dev/null @@ -1,105 +0,0 @@ -const LIKED_TYPE = 5; -const INVITED_TYPE = 8; -const GROUP_SUMMARY_TYPE = 16; - -export default Ember.Component.extend({ - tagName: 'li', - classNameBindings: ['notification.read', 'notification.is_warning'], - - name: function() { - var notificationType = this.get("notification.notification_type"); - var lookup = this.site.get("notificationLookup"); - return lookup[notificationType]; - }.property("notification.notification_type"), - - scope: function() { - if (this.get("name") === "custom") { - return this.get("notification.data.message"); - } else { - return "notifications." + this.get("name"); - } - }.property("name"), - - url: function() { - const it = this.get('notification'); - const badgeId = it.get("data.badge_id"); - if (badgeId) { - var badgeSlug = it.get("data.badge_slug"); - - if (!badgeSlug) { - const badgeName = it.get("data.badge_name"); - badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase(); - } - - var username = it.get('data.username'); - username = username ? "?username=" + username.toLowerCase() : ""; - return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug + username); - } - - const topicId = it.get('topic_id'); - if (topicId) { - return Discourse.Utilities.postUrl(it.get("slug"), topicId, it.get("post_number")); - } - - if (it.get('notification_type') === INVITED_TYPE) { - return Discourse.getURL('/users/' + it.get('data.display_username')); - } - - if (it.get('data.group_id')) { - return Discourse.getURL('/users/' + it.get('data.username') + '/messages/group/' + it.get('data.group_name')); - } - - }.property("notification.data.{badge_id,badge_name,display_username}", "model.slug", "model.topic_id", "model.post_number"), - - description: function() { - const badgeName = this.get("notification.data.badge_name"); - if (badgeName) { return Discourse.Utilities.escapeExpression(badgeName); } - - const title = this.get('notification.data.topic_title'); - return Ember.isEmpty(title) ? "" : Discourse.Utilities.escapeExpression(title); - }.property("notification.data.{badge_name,topic_title}"), - - _markRead: function(){ - this.$('a').click(() => { - this.set('notification.read', true); - Discourse.setTransientHeader("Discourse-Clear-Notifications", this.get('notification.id')); - if (document && document.cookie) { - document.cookie = `cn=${this.get('notification.id')}; expires=Fri, 31 Dec 9999 23:59:59 GMT`; - } - return true; - }); - }.on('didInsertElement'), - - render(buffer) { - const notification = this.get('notification'); - // since we are reusing views now sometimes this can be unset - if (!notification) { return; } - const description = this.get('description'); - const username = notification.get('data.display_username'); - var text; - if (notification.get('notification_type') === GROUP_SUMMARY_TYPE) { - const count = notification.get('data.inbox_count'); - const group_name = notification.get('data.group_name'); - text = I18n.t(this.get('scope'), {count, group_name}); - } else if (notification.get('notification_type') === LIKED_TYPE && notification.get("data.count") > 1) { - const count = notification.get('data.count') - 2; - const username2 = notification.get('data.username2'); - if (count===0) { - text = I18n.t('notifications.liked_2', {description, username, username2}); - } else { - text = I18n.t('notifications.liked_many', {description, username, username2, count}); - } - } - else { - text = I18n.t(this.get('scope'), {description, username}); - } - text = Discourse.Emoji.unescape(text); - - const url = this.get('url'); - if (url) { - buffer.push('' + text + ''); - } else { - buffer.push(text); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/preference-checkbox.js.es6 b/app/assets/javascripts/discourse/components/preference-checkbox.js.es6 index 6b76240951..352aeadac3 100644 --- a/app/assets/javascripts/discourse/components/preference-checkbox.js.es6 +++ b/app/assets/javascripts/discourse/components/preference-checkbox.js.es6 @@ -5,10 +5,10 @@ export default Em.Component.extend({ return I18n.t(this.get('labelKey')); }.property('labelKey'), - click() { + change() { const warning = this.get('warning'); - if (warning && !this.get('checked')) { + if (warning && this.get('checked')) { this.sendAction('warning'); return false; } diff --git a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 index 38ad5fea69..c5625a8109 100644 --- a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 +++ b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 @@ -37,6 +37,25 @@ export default MountWidget.extend({ 'searchService'); }).volatile(), + beforePatch() { + const $body = $(document); + this.prevHeight = $body.height(); + this.prevScrollTop = $body.scrollTop(); + }, + + afterPatch() { + const $body = $(document); + const height = $body.height(); + const scrollTop = $body.scrollTop(); + + // This hack is for when swapping out many cloaked views at once + // when using keyboard navigation. It could suddenly move the + // scroll + if (this.prevHeight === height && scrollTop !== this.prevScrollTop) { + $body.scrollTop(this.prevScrollTop); + } + }, + scrolled() { if (this.isDestroyed || this.isDestroying) { return; } if (isWorkaroundActive()) { return; } diff --git a/app/assets/javascripts/discourse/components/search-menu.js.es6 b/app/assets/javascripts/discourse/components/search-menu.js.es6 deleted file mode 100644 index be80d20a26..0000000000 --- a/app/assets/javascripts/discourse/components/search-menu.js.es6 +++ /dev/null @@ -1,162 +0,0 @@ -import {searchForTerm, searchContextDescription, isValidSearchTerm } from 'discourse/lib/search'; -import DiscourseURL from 'discourse/lib/url'; -import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; -import showModal from 'discourse/lib/show-modal'; - -let _dontSearch = false; -export default Ember.Component.extend({ - searchService: Ember.inject.service('search'), - classNames: ['search-menu'], - typeFilter: null, - - @observes('searchService.searchContext') - contextChanged: function() { - if (this.get('searchService.searchContextEnabled')) { - _dontSearch = true; - this.set('searchService.searchContextEnabled', false); - _dontSearch = false; - } - }, - - @computed('searchService.searchContext', 'searchService.term', 'searchService.searchContextEnabled') - fullSearchUrlRelative(searchContext, term, searchContextEnabled) { - - if (searchContextEnabled && Ember.get(searchContext, 'type') === 'topic') { - return null; - } - - let url = '/search?q=' + encodeURIComponent(this.get('searchService.term')); - if (searchContextEnabled) { - if (searchContext.id.toString().toLowerCase() === this.get('currentUser.username_lower') && - searchContext.type === "private_messages" - ) { - url += ' in:private'; - } else { - url += encodeURIComponent(" " + searchContext.type + ":" + searchContext.id); - } - } - - return url; - }, - - @computed('fullSearchUrlRelative') - fullSearchUrl(fullSearchUrlRelative) { - if (fullSearchUrlRelative) { - return Discourse.getURL(fullSearchUrlRelative); - } - }, - - @computed('searchService.searchContext') - searchContextDescription(ctx) { - return searchContextDescription(Em.get(ctx, 'type'), Em.get(ctx, 'user.username') || Em.get(ctx, 'category.name')); - }, - - @observes('searchService.searchContextEnabled') - searchContextEnabledChanged() { - if (_dontSearch) { return; } - this.newSearchNeeded(); - }, - - // If we need to perform another search - @observes('searchService.term', 'typeFilter') - newSearchNeeded() { - this.set('noResults', false); - const term = this.get('searchService.term'); - if (isValidSearchTerm(term)) { - this.set('loading', true); - Ember.run.debounce(this, 'searchTerm', term, this.get('typeFilter'), 400); - } else { - this.setProperties({ content: null }); - } - this.set('selectedIndex', 0); - }, - - searchTerm(term, typeFilter) { - // for cancelling debounced search - if (this._cancelSearch){ - this._cancelSearch = null; - return; - } - - if (this._search) { - this._search.abort(); - } - - const searchContext = this.get('searchService.searchContextEnabled') ? this.get('searchService.searchContext') : null; - this._search = searchForTerm(term, { typeFilter, searchContext, fullSearchUrl: this.get('fullSearchUrl') }); - - this._search.then((content) => { - this.setProperties({ noResults: !content, content }); - }).finally(() => { - this.set('loading', false); - this._search = null; - }); - }, - - @computed('typeFilter', 'loading') - showCancelFilter(typeFilter, loading) { - if (loading) { return false; } - return !Ember.isEmpty(typeFilter); - }, - - @observes('searchService.term') - termChanged() { - this.cancelTypeFilter(); - }, - - actions: { - fullSearch() { - const self = this; - - if (this._search) { - this._search.abort(); - } - - // maybe we are debounced and delayed - // stop that as well - this._cancelSearch = true; - Em.run.later(function() { - self._cancelSearch = false; - }, 400); - - const url = this.get('fullSearchUrlRelative'); - if (url) { - DiscourseURL.routeTo(url); - } - }, - - moreOfType(type) { - this.set('typeFilter', type); - }, - - cancelType() { - this.cancelTypeFilter(); - }, - - showedSearch() { - $('#search-term').focus().select(); - }, - - showSearchHelp() { - // TODO: @EvitTrout how do we get a loading indicator here? - Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then((model) => { - showModal('searchHelp', { model }); - }); - }, - - cancelHighlight() { - this.set('searchService.highlightTerm', null); - } - }, - - cancelTypeFilter() { - this.set('typeFilter', null); - }, - - keyDown(e) { - if (e.which === 13 && isValidSearchTerm(this.get('searchService.term'))) { - this.set('visible', false); - this.send('fullSearch'); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/search-result-category.js.es6 b/app/assets/javascripts/discourse/components/search-result-category.js.es6 deleted file mode 100644 index e23284ed16..0000000000 --- a/app/assets/javascripts/discourse/components/search-result-category.js.es6 +++ /dev/null @@ -1,2 +0,0 @@ -import SearchResult from 'discourse/components/search-result'; -export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result-post.js.es6 b/app/assets/javascripts/discourse/components/search-result-post.js.es6 deleted file mode 100644 index e23284ed16..0000000000 --- a/app/assets/javascripts/discourse/components/search-result-post.js.es6 +++ /dev/null @@ -1,2 +0,0 @@ -import SearchResult from 'discourse/components/search-result'; -export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result-topic.js.es6 b/app/assets/javascripts/discourse/components/search-result-topic.js.es6 deleted file mode 100644 index e23284ed16..0000000000 --- a/app/assets/javascripts/discourse/components/search-result-topic.js.es6 +++ /dev/null @@ -1,2 +0,0 @@ -import SearchResult from 'discourse/components/search-result'; -export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result-user.js.es6 b/app/assets/javascripts/discourse/components/search-result-user.js.es6 deleted file mode 100644 index e23284ed16..0000000000 --- a/app/assets/javascripts/discourse/components/search-result-user.js.es6 +++ /dev/null @@ -1,2 +0,0 @@ -import SearchResult from 'discourse/components/search-result'; -export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result.js.es6 b/app/assets/javascripts/discourse/components/search-result.js.es6 deleted file mode 100644 index dacf1f2696..0000000000 --- a/app/assets/javascripts/discourse/components/search-result.js.es6 +++ /dev/null @@ -1,11 +0,0 @@ -export default Ember.Component.extend({ - tagName: 'ul', - - _highlightOnInsert: function() { - const term = this.get('controller.term'); - if(!_.isEmpty(term)) { - this.$('.blurb').highlight(term.split(/\s+/), {className: 'search-highlight'}); - this.$('.topic-title').highlight(term.split(/\s+/), {className: 'search-highlight'} ); - } - }.on('didInsertElement') -}); diff --git a/app/assets/javascripts/discourse/components/site-header.js.es6 b/app/assets/javascripts/discourse/components/site-header.js.es6 new file mode 100644 index 0000000000..d8e87bb7d8 --- /dev/null +++ b/app/assets/javascripts/discourse/components/site-header.js.es6 @@ -0,0 +1,196 @@ +import MountWidget from 'discourse/components/mount-widget'; +import { observes } from 'ember-addons/ember-computed-decorators'; + +const _flagProperties = []; +function addFlagProperty(prop) { + _flagProperties.pushObject(prop); +} + +const PANEL_BODY_MARGIN = 30; + +const SiteHeaderComponent = MountWidget.extend({ + widget: 'header', + docAt: null, + dockedHeader: null, + _topic: null, + + // profileWidget: true, + // classNameBindings: ['editingTopic'], + + @observes('currentUser.unread_notifications', 'currentUser.unread_private_messages') + _notificationsChanged() { + this.queueRerender(); + }, + + examineDockHeader() { + const $body = $('body'); + + // Check the dock after the current run loop. While rendering, + // it's much slower to calculate `outlet.offset()` + Ember.run.next(() => { + if (this.docAt === null) { + const outlet = $('#main-outlet'); + if (!(outlet && outlet.length === 1)) return; + this.docAt = outlet.offset().top; + } + + const offset = window.pageYOffset || $('html').scrollTop(); + if (offset >= this.docAt) { + if (!this.dockedHeader) { + $body.addClass('docked'); + this.dockedHeader = true; + } + } else { + if (this.dockedHeader) { + $body.removeClass('docked'); + this.dockedHeader = false; + } + } + }); + }, + + setTopic(topic) { + this._topic = topic; + this.queueRerender(); + }, + + didInsertElement() { + this._super(); + $(window).bind('scroll.discourse-dock', () => this.examineDockHeader()); + $(document).bind('touchmove.discourse-dock', () => this.examineDockHeader()); + $(window).on('resize.discourse-menu-panel', () => this.afterRender()); + + this.appEvents.on('header:show-topic', topic => this.setTopic(topic)); + this.appEvents.on('header:hide-topic', () => this.setTopic(null)); + + this.dispatch('notifications:changed', 'user-notifications'); + this.dispatch('header:keyboard-trigger', 'header'); + + this.appEvents.on('dom:clean', () => { + // For performance, only trigger a re-render if any menu panels are visible + if (this.$('.menu-panel').length) { + this.eventDispatched('dom:clean', 'header'); + } + }); + + this.examineDockHeader(); + }, + + willDestroyElement() { + this._super(); + $(window).unbind('scroll.discourse-dock'); + $(document).unbind('touchmove.discourse-dock'); + $('body').off('keydown.header'); + this.appEvents.off('notifications:changed'); + $(window).off('resize.discourse-menu-panel'); + + this.appEvents.off('header:show-topic'); + this.appEvents.off('header:hide-topic'); + this.appEvents.off('dom:clean'); + }, + + buildArgs() { + return { + flagCount: _flagProperties.reduce((prev, cur) => prev + (this.get(cur) || 0), 0), + topic: this._topic, + canSignUp: this.get('canSignUp') + }; + }, + + afterRender() { + const $menuPanels = $('.menu-panel'); + if ($menuPanels.length === 0) { return; } + + const $window = $(window); + const windowWidth = parseInt($window.width()); + + + const headerWidth = $('#main-outlet .container').width() || 1100; + const remaining = parseInt((windowWidth - headerWidth) / 2); + const viewMode = (remaining < 50) ? 'slide-in' : 'drop-down'; + + $menuPanels.each((idx, panel) => { + const $panel = $(panel); + let width = parseInt($panel.attr('data-max-width') || 300); + if ((windowWidth - width) < 50) { + width = windowWidth - 50; + } + + $panel.removeClass('drop-down').removeClass('slide-in').addClass(viewMode); + + const $panelBody = $('.panel-body', $panel); + let contentHeight = parseInt($('.panel-body-contents', $panel).height()); + + // We use a mutationObserver to check for style changes, so it's important + // we don't set it if it doesn't change. Same goes for the $panelBody! + const style = $panel.prop('style'); + + if (viewMode === 'drop-down') { + const $buttonPanel = $('header ul.icons'); + if ($buttonPanel.length === 0) { return; } + + // These values need to be set here, not in the css file - this is to deal with the + // possibility of the window being resized and the menu changing from .slide-in to .drop-down. + if (style.top !== '100%' || style.height !== 'auto') { + $panel.css({ top: '100%', height: 'auto' }); + } + + // adjust panel height + const fullHeight = parseInt($window.height()); + const offsetTop = $panel.offset().top; + const scrollTop = $window.scrollTop(); + + if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) { + contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN; + } + if ($panelBody.height() !== contentHeight) { + $panelBody.height(contentHeight); + } + $('body').addClass('drop-down-visible'); + } else { + const menuTop = headerHeight(); + + let height; + const winHeight = $(window).height() - 16; + if ((menuTop + contentHeight) < winHeight) { + height = contentHeight + "px"; + } else { + height = winHeight - menuTop; + } + + if ($panelBody.prop('style').height !== '100%') { + $panelBody.height('100%'); + } + if (style.top !== menuTop + "px" || style.height !== height) { + $panel.css({ top: menuTop + "px", height }); + } + $('body').removeClass('drop-down-visible'); + } + + $panel.width(width); + }); + } +}); + +export default SiteHeaderComponent; + +function applyFlaggedProperties() { + const args = _flagProperties.slice(); + args.push(function() { + this.queueRerender(); + }.on('init')); + + SiteHeaderComponent.reopen({ _flagsChanged: Ember.observer.apply(this, args) }); +} + +addFlagProperty('currentUser.site_flagged_posts_count'); +addFlagProperty('currentUser.post_queue_new_count'); + +export { addFlagProperty, applyFlaggedProperties }; + +export function headerHeight() { + const $header = $('header.d-header'); + const headerOffset = $header.offset(); + const headerOffsetTop = (headerOffset) ? headerOffset.top : 0; + return parseInt($header.outerHeight() + headerOffsetTop - $(window).scrollTop()); +} diff --git a/app/assets/javascripts/discourse/components/tag-chooser.js.es6 b/app/assets/javascripts/discourse/components/tag-chooser.js.es6 new file mode 100644 index 0000000000..c942cf5993 --- /dev/null +++ b/app/assets/javascripts/discourse/components/tag-chooser.js.es6 @@ -0,0 +1,97 @@ +import renderTag from 'discourse/lib/render-tag'; + +function formatTag(t) { + return renderTag(t.id, {count: t.count}); +} + +export default Ember.TextField.extend({ + classNameBindings: [':tag-chooser'], + attributeBindings: ['tabIndex'], + + _setupTags: function() { + const tags = this.get('tags') || []; + this.set('value', tags.join(", ")); + }.on('init'), + + _valueChanged: function() { + const tags = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq(); + this.set('tags', tags); + }.observes('value'), + + _initializeTags: function() { + const site = this.site, + self = this, + filterRegexp = new RegExp(this.site.tags_filter_regexp, "g"); + + this.$().select2({ + tags: true, + placeholder: I18n.t('tagging.choose_for_topic'), + maximumInputLength: this.siteSettings.max_tag_length, + maximumSelectionSize: this.siteSettings.max_tags_per_topic, + initSelection(element, callback) { + const data = []; + + function splitVal(string, separator) { + var val, i, l; + if (string === null || string.length < 1) return []; + val = string.split(separator); + for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]); + return val; + } + + $(splitVal(element.val(), ",")).each(function () { + data.push({ + id: this, + text: this + }); + }); + + callback(data); + }, + createSearchChoice: function(term, data) { + term = term.replace(filterRegexp, '').trim(); + + // No empty terms, make sure the user has permission to create the tag + if (!term.length || !site.get('can_create_tag')) { return; } + + if ($(data).filter(function() { + return this.text.localeCompare(term) === 0; + }).length === 0) { + return { id: term, text: term }; + } + }, + createSearchChoicePosition: function(list, item) { + // Search term goes on the bottom + list.push(item); + }, + formatSelection: function (data) { + return data ? renderTag(this.text(data)) : undefined; + }, + formatSelectionCssClass: function(){ + return "discourse-tag-select2"; + }, + formatResult: formatTag, + multiple: true, + ajax: { + quietMillis: 200, + cache: true, + url: Discourse.getURL("/tags/filter/search"), + dataType: 'json', + data: function (term) { + return { q: term, limit: self.siteSettings.max_tag_search_results }; + }, + results: function (data) { + if (self.siteSettings.tags_sort_alphabetically) { + data.results = data.results.sort(function(a,b) { return a.id > b.id; }); + } + return data; + } + }, + }); + }.on('didInsertElement'), + + _destroyTags: function() { + this.$().select2('destroy'); + }.on('willDestroyElement') + +}); diff --git a/app/assets/javascripts/discourse/components/tag-drop-link.js.es6 b/app/assets/javascripts/discourse/components/tag-drop-link.js.es6 new file mode 100644 index 0000000000..8cfad3cd2a --- /dev/null +++ b/app/assets/javascripts/discourse/components/tag-drop-link.js.es6 @@ -0,0 +1,29 @@ +import DiscourseURL from 'discourse/lib/url'; + +export default Ember.Component.extend({ + tagName: 'a', + classNameBindings: [':tag-badge-wrapper', ':badge-wrapper', ':bullet', 'tagClass'], + attributeBindings: ['href'], + + href: function() { + var url = '/tags'; + if (this.get('category')) { + url += this.get('category.url'); + } + return url + '/' + this.get('tagId'); + }.property('tagId', 'category'), + + tagClass: function() { + return "tag-" + this.get('tagId'); + }.property('tagId'), + + render(buffer) { + buffer.push(Handlebars.Utils.escapeExpression(this.get('tagId'))); + }, + + click(e) { + e.preventDefault(); + DiscourseURL.routeTo(this.get('href')); + return true; + } +}); diff --git a/app/assets/javascripts/discourse/components/tag-drop.js.es6 b/app/assets/javascripts/discourse/components/tag-drop.js.es6 new file mode 100644 index 0000000000..23e48091b1 --- /dev/null +++ b/app/assets/javascripts/discourse/components/tag-drop.js.es6 @@ -0,0 +1,113 @@ +import { setting } from 'discourse/lib/computed'; + +export default Ember.Component.extend({ + classNameBindings: [':tag-drop', 'tag::no-category', 'tags:has-drop','categoryStyle','tagClass'], + categoryStyle: setting('category_style'), // match the category-drop style + currentCategory: Em.computed.or('secondCategory', 'firstCategory'), + showFilterByTag: setting('show_filter_by_tag'), + showTagDropdown: Em.computed.and('showFilterByTag', 'tags'), + tagId: null, + + tagName: 'li', + + tags: function() { + if (this.siteSettings.tags_sort_alphabetically && Discourse.Site.currentProp('top_tags')) { + return Discourse.Site.currentProp('top_tags').sort(); + } else { + return Discourse.Site.currentProp('top_tags'); + } + }.property('site.top_tags'), + + iconClass: function() { + if (this.get('expanded')) { return "fa fa-caret-down"; } + return "fa fa-caret-right"; + }.property('expanded'), + + tagClass: function() { + if (this.get('tagId')) { + return "tag-" + this.get('tagId'); + } else { + return "tag_all"; + } + }.property('tagId'), + + allTagsUrl: function() { + if (this.get('currentCategory')) { + return this.get('currentCategory.url') + "?allTags=1"; + } else { + return "/"; + } + }.property('firstCategory', 'secondCategory'), + + allTagsLabel: function() { + return I18n.t("tagging.selector_all_tags"); + }.property('tag'), + + dropdownButtonClass: function() { + var result = 'badge-category category-dropdown-button'; + if (Em.isNone(this.get('tag'))) { + result += ' home'; + } + return result; + }.property('tag'), + + clickEventName: function() { + return "click.tag-drop-" + (this.get('tag') || "all"); + }.property('tag'), + + actions: { + expand: function() { + var self = this; + + if(!this.get('renderTags')){ + this.set('renderTags',true); + Em.run.next(function(){ + self.send('expand'); + }); + return; + } + + if (this.get('expanded')) { + this.close(); + return; + } + + if (this.get('tags')) { + this.set('expanded', true); + } + var $dropdown = this.$()[0]; + + this.$('a[data-drop-close]').on('click.tag-drop', function() { + self.close(); + }); + + Em.run.next(function(){ + self.$('.cat a').add('html').on(self.get('clickEventName'), function(e) { + var $target = $(e.target), + closest = $target.closest($dropdown); + + if ($(e.currentTarget).hasClass('badge-wrapper')){ + self.close(); + } + + return ($(e.currentTarget).hasClass('badge-category') || (closest.length && closest[0] === $dropdown)) ? true : self.close(); + }); + }); + } + }, + + removeEvents: function(){ + $('html').off(this.get('clickEventName')); + this.$('a[data-drop-close]').off('click.tag-drop'); + }, + + close: function() { + this.removeEvents(); + this.set('expanded', false); + }, + + willDestroyElement: function() { + this.removeEvents(); + } + +}); diff --git a/app/assets/javascripts/discourse/components/tag-notifications-button.js.es6 b/app/assets/javascripts/discourse/components/tag-notifications-button.js.es6 new file mode 100644 index 0000000000..b4588df710 --- /dev/null +++ b/app/assets/javascripts/discourse/components/tag-notifications-button.js.es6 @@ -0,0 +1,11 @@ +import NotificationsButton from 'discourse/components/notifications-button'; + +export default NotificationsButton.extend({ + classNames: ['notification-options', 'tag-notification-menu'], + buttonIncludesText: false, + i18nPrefix: 'tagging.notifications', + + clicked(id) { + this.sendAction('action', id); + } +}); diff --git a/app/assets/javascripts/discourse/components/user-menu.js.es6 b/app/assets/javascripts/discourse/components/user-menu.js.es6 deleted file mode 100644 index d864f23ba4..0000000000 --- a/app/assets/javascripts/discourse/components/user-menu.js.es6 +++ /dev/null @@ -1,104 +0,0 @@ -import { url } from 'discourse/lib/computed'; -import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; -import { headerHeight } from 'discourse/views/header'; - -export default Ember.Component.extend({ - classNames: ['user-menu'], - notifications: null, - loadingNotifications: false, - notificationsPath: url('currentUser.path', '%@/notifications'), - bookmarksPath: url('currentUser.path', '%@/activity/bookmarks'), - messagesPath: url('currentUser.path', '%@/messages'), - preferencesPath: url('currentUser.path', '%@/preferences'), - - @computed('allowAnon', 'isAnon') - showEnableAnon(allowAnon, isAnon) { return allowAnon && !isAnon; }, - - @computed('allowAnon', 'isAnon') - showDisableAnon(allowAnon, isAnon) { return allowAnon && isAnon; }, - - @observes('visible') - _loadNotifications() { - if (this.get("visible")) { - this.refreshNotifications(); - } - }, - - @observes('currentUser.lastNotificationChange') - _resetCachedNotifications() { - const visible = this.get('visible'); - - if (!Discourse.get("hasFocus")) { - this.set('visible', false); - this.set('notifications', null); - return; - } - - if (visible) { - this.refreshNotifications(); - } else { - this.set('notifications', null); - } - }, - - refreshNotifications() { - if (this.get('loadingNotifications')) { return; } - - // estimate (poorly) the amount of notifications to return - var limit = Math.round(($(window).height() - headerHeight()) / 55); - // we REALLY don't want to be asking for negative counts of notifications - // less than 5 is also not that useful - if (limit < 5) { limit = 5; } - if (limit > 40) { limit = 40; } - - // TODO: It's a bit odd to use the store in a component, but this one really - // wants to reach out and grab notifications - const store = this.container.lookup('store:main'); - const stale = store.findStale('notification', {recent: true, limit }, {cacheKey: 'recent-notifications'}); - - if (stale.hasResults) { - const results = stale.results; - var content = results.get('content'); - - // we have to truncate to limit, otherwise we will render too much - if (content && (content.length > limit)) { - content = content.splice(0, limit); - results.set('content', content); - results.set('totalRows', limit); - } - - this.set('notifications', results); - } else { - this.set('loadingNotifications', true); - } - - stale.refresh().then((notifications) => { - this.set('currentUser.unread_notifications', 0); - this.set('notifications', notifications); - }).catch(() => { - this.set('notifications', null); - }).finally(() => { - this.set('loadingNotifications', false); - }); - }, - - @computed() - allowAnon() { - return this.siteSettings.allow_anonymous_posting && - (this.get("currentUser.trust_level") >= this.siteSettings.anonymous_posting_min_trust_level || - this.get("isAnon")); - }, - - isAnon: Ember.computed.alias('currentUser.is_anonymous'), - - actions: { - toggleAnon() { - Discourse.ajax("/users/toggle-anon", {method: 'POST'}).then(function(){ - window.location.reload(); - }); - }, - logout() { - this.sendAction('logoutAction'); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/user-notifications-large.js.es6 b/app/assets/javascripts/discourse/components/user-notifications-large.js.es6 new file mode 100644 index 0000000000..bfedea00f7 --- /dev/null +++ b/app/assets/javascripts/discourse/components/user-notifications-large.js.es6 @@ -0,0 +1,16 @@ +import MountWidget from 'discourse/components/mount-widget'; +import { observes } from "ember-addons/ember-computed-decorators"; + +export default MountWidget.extend({ + widget: 'user-notifications-large', + + init() { + this._super(); + this.args = { notifications: this.get('notifications') }; + }, + + @observes('notifications.length') + _triggerRefresh() { + this.queueRerender(); + } +}); diff --git a/app/assets/javascripts/discourse/components/user-stream.js.es6 b/app/assets/javascripts/discourse/components/user-stream.js.es6 new file mode 100644 index 0000000000..2afce0b978 --- /dev/null +++ b/app/assets/javascripts/discourse/components/user-stream.js.es6 @@ -0,0 +1,57 @@ +import LoadMore from "discourse/mixins/load-more"; +import ClickTrack from 'discourse/lib/click-track'; + +export default Ember.Component.extend(LoadMore, { + loading: false, + eyelineSelector: '.user-stream .item', + classNames: ['user-stream'], + + _scrollTopOnModelChange: function() { + Em.run.schedule('afterRender', () => $(document).scrollTop(0)); + }.observes('stream.user.id'), + + _inserted: function() { + this.bindScrolling({name: 'user-stream-view'}); + + $(window).on('resize.discourse-on-scroll', () => this.scrolled()); + + this.$().on('mouseup.discourse-redirect', '.excerpt a', function(e) { + // bypass if we are selecting stuff + const selection = window.getSelection && window.getSelection(); + if (selection.type === "Range" || selection.rangeCount > 0) { + if (Discourse.Utilities.selectedText() !== "") { + return true; + } + } + + const $target = $(e.target); + if ($target.hasClass('mention') || $target.parents('.expanded-embed').length) { return false; } + + return ClickTrack.trackClick(e); + }); + + }.on('didInsertElement'), + + // This view is being removed. Shut down operations + _destroyed: function() { + this.unbindScrolling('user-stream-view'); + $(window).unbind('resize.discourse-on-scroll'); + + // Unbind link tracking + this.$().off('mouseup.discourse-redirect', '.excerpt a'); + + }.on('willDestroyElement'), + + actions: { + loadMore() { + if (this.get('loading')) { return; } + + this.set('loading', true); + const stream = this.get('stream'); + stream.findItems().then(() => { + this.set('loading', false); + this.get('eyeline').flushRest(); + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 7333f6dd0a..b22330abba 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -81,6 +81,14 @@ export default Ember.Controller.extend({ this.set('similarTopics', []); }.on('init'), + @computed('model.canEditTitle', 'model.creatingPrivateMessage') + canEditTags(canEditTitle, creatingPrivateMessage) { + return !this.site.mobileView && + this.site.get('can_tag_topics') && + canEditTitle && + !creatingPrivateMessage; + }, + @computed('model.action') canWhisper(action) { const currentUser = this.currentUser; @@ -386,7 +394,7 @@ export default Ember.Controller.extend({ let message = this.get('similarTopicsMessage'); if (!message) { message = Discourse.ComposerMessage.create({ - templateName: 'composer/similar_topics', + templateName: 'composer/similar-topics', extraClass: 'similar-topics' }); this.set('similarTopicsMessage', message); diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index bc6ccf62a8..9ff65be628 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -127,7 +127,7 @@ export default Ember.Controller.extend(ModalFunctionality, { failed: true, reason: I18n.t('user.email.invalid') }); - }.property('accountEmail', 'rejectedEmails.@each'), + }.property('accountEmail', 'rejectedEmails.[]'), emailValidated: function() { return this.get('authOptions.email') === this.get("accountEmail") && this.get('authOptions.email_valid'); @@ -326,7 +326,7 @@ export default Ember.Controller.extend(ModalFunctionality, { ok: true, reason: I18n.t('user.password.ok') }); - }.property('accountPassword', 'rejectedPasswords.@each', 'accountUsername', 'accountEmail', 'isDeveloper'), + }.property('accountPassword', 'rejectedPasswords.[]', 'accountUsername', 'accountEmail', 'isDeveloper'), @on('init') fetchConfirmationValue() { diff --git a/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6 b/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6 index 8786828d7a..5ac8d4b733 100644 --- a/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6 @@ -12,7 +12,7 @@ export var queryParams = { // Basic controller options var controllerOpts = { needs: ['discovery/topics'], - queryParams: Ember.keys(queryParams), + queryParams: Object.keys(queryParams), }; // Aliases for the values diff --git a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 index 5e86950634..008ab29a3b 100644 --- a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 @@ -138,14 +138,11 @@ const controllerOpts = { return I18n.t("topics.none.educate." + split[0], { userPrefsUrl: Discourse.getURL("/users/") + (Discourse.User.currentProp("username_lower")) + "/preferences" }); - }.property('allLoaded', 'model.topics.length'), + }.property('allLoaded', 'model.topics.length') - loadMoreTopics() { - return this.get('model').loadMore(); - } }; -Ember.keys(queryParams).forEach(function(p) { +Object.keys(queryParams).forEach(function(p) { // If we don't have a default value, initialize it to null if (typeof controllerOpts[p] === 'undefined') { controllerOpts[p] = null; diff --git a/app/assets/javascripts/discourse/controllers/group-index.js.es6 b/app/assets/javascripts/discourse/controllers/group-index.js.es6 new file mode 100644 index 0000000000..7a5a5bf98f --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/group-index.js.es6 @@ -0,0 +1,27 @@ +import { fmt } from 'discourse/lib/computed'; + +export default Ember.ArrayController.extend({ + needs: ['group'], + loading: false, + emptyText: fmt('type', 'groups.empty.%@'), + + actions: { + loadMore() { + + if (this.get('loading')) { return; } + this.set('loading', true); + const posts = this.get('model'); + if (posts && posts.length) { + const beforePostId = posts[posts.length-1].get('id'); + const group = this.get('controllers.group.model'); + + const opts = { beforePostId, type: this.get('type') }; + group.findPosts(opts).then(newPosts => { + posts.addObjects(newPosts); + this.set('loading', false); + }); + } + } + } +}); + diff --git a/app/assets/javascripts/discourse/controllers/group/index.js.es6 b/app/assets/javascripts/discourse/controllers/group/index.js.es6 deleted file mode 100644 index 60df6a2cdf..0000000000 --- a/app/assets/javascripts/discourse/controllers/group/index.js.es6 +++ /dev/null @@ -1,28 +0,0 @@ -/** - Handles displaying posts within a group -**/ -export default Ember.ArrayController.extend({ - needs: ['group'], - loading: false, - - actions: { - loadMore: function() { - - if (this.get('loading')) { return; } - this.set('loading', true); - var posts = this.get('model'), - self = this; - if (posts && posts.length) { - var lastPostId = posts[posts.length-1].get('id'), - group = this.get('controllers.group.model'); - - var opts = {beforePostId: lastPostId, type: this.get('type')}; - group.findPosts(opts).then(function(newPosts) { - posts.addObjects(newPosts); - self.set('loading', false); - }); - } - } - } -}); - diff --git a/app/assets/javascripts/discourse/controllers/group/members.js.es6 b/app/assets/javascripts/discourse/controllers/group/members.js.es6 index 246a5f80cd..306ff8aa80 100644 --- a/app/assets/javascripts/discourse/controllers/group/members.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group/members.js.es6 @@ -1,12 +1,13 @@ import { popupAjaxError } from 'discourse/lib/ajax-error'; import computed from 'ember-addons/ember-computed-decorators'; +import Group from 'discourse/models/group'; export default Ember.Controller.extend({ loading: false, limit: null, offset: null, - @computed('model.owners.@each') + @computed('model.owners.[]') isOwner(owners) { if (this.get('currentUser.admin')) { return true; @@ -30,10 +31,7 @@ export default Ember.Controller.extend({ }, loadMore() { - const Group = require('discourse/models/group').default; - if (this.get("loading")) { return; } - // we've reached the end if (this.get("model.members.length") >= this.get("model.user_count")) { return; } this.set("loading", true); diff --git a/app/assets/javascripts/discourse/controllers/group/mentions.js.es6 b/app/assets/javascripts/discourse/controllers/group/mentions.js.es6 deleted file mode 100644 index 81fa5a8ffd..0000000000 --- a/app/assets/javascripts/discourse/controllers/group/mentions.js.es6 +++ /dev/null @@ -1,3 +0,0 @@ -import IndexController from 'discourse/controllers/group/index'; - -export default IndexController.extend({type: 'mentions'}); diff --git a/app/assets/javascripts/discourse/controllers/group/topics.js.es6 b/app/assets/javascripts/discourse/controllers/group/topics.js.es6 deleted file mode 100644 index 9423350320..0000000000 --- a/app/assets/javascripts/discourse/controllers/group/topics.js.es6 +++ /dev/null @@ -1,3 +0,0 @@ -import IndexController from 'discourse/controllers/group/index'; - -export default IndexController.extend({type: 'topics'}); diff --git a/app/assets/javascripts/discourse/controllers/header.js.es6 b/app/assets/javascripts/discourse/controllers/header.js.es6 index 57401135df..2665f2afbd 100644 --- a/app/assets/javascripts/discourse/controllers/header.js.es6 +++ b/app/assets/javascripts/discourse/controllers/header.js.es6 @@ -1,77 +1,6 @@ -import DiscourseURL from 'discourse/lib/url'; +import { addFlagProperty as realAddFlagProperty } from 'discourse/components/site-header'; -const HeaderController = Ember.Controller.extend({ - topic: null, - showExtraInfo: null, - hamburgerVisible: false, - searchVisible: false, - userMenuVisible: false, - needs: ['application'], - - canSignUp: Em.computed.alias('controllers.application.canSignUp'), - - showSignUpButton: function() { - return this.get('canSignUp') && !this.get('showExtraInfo'); - }.property('canSignUp', 'showExtraInfo'), - - showStarButton: function() { - return Discourse.User.current() && !this.get('topic.isPrivateMessage'); - }.property('topic.isPrivateMessage'), - - - actions: { - toggleSearch() { - this.toggleProperty('searchVisible'); - }, - showUserMenu() { - if (!this.get('userMenuVisible')) { - this.appEvents.trigger('dropdowns:closeAll'); - this.set('userMenuVisible', true); - } - }, - - fullPageSearch() { - const searchService = this.container.lookup('search-service:main'); - const context = searchService.get('searchContext'); - var params = ""; - - if (context) { - params = `?context=${context.type}&context_id=${context.id}&skip_context=true`; - } - - DiscourseURL.routeTo('/search' + params); - }, - toggleMenuPanel(visibleProp) { - this.toggleProperty(visibleProp); - this.appEvents.trigger('dropdowns:closeAll'); - }, - - toggleStar() { - const topic = this.get('topic'); - if (topic) topic.toggleStar(); - return false; - } - } -}); - -// Allow plugins to add to the sum of "flags" above the site map -const _flagProperties = []; -function addFlagProperty(prop) { - _flagProperties.pushObject(prop); +export function addFlagProperty(prop) { + Ember.warn("importing `addFlagProperty` is deprecated. Use the PluginAPI instead"); + realAddFlagProperty(prop); } - -function applyFlaggedProperties() { - const args = _flagProperties.slice(); - args.push(function() { - let sum = 0; - _flagProperties.forEach((fp) => sum += (this.get(fp) || 0)); - return sum; - }); - HeaderController.reopen({ flaggedPostsCount: Ember.computed.apply(this, args) }); -} - -addFlagProperty('currentUser.site_flagged_posts_count'); -addFlagProperty('currentUser.post_queue_new_count'); - -export { addFlagProperty, applyFlaggedProperties }; -export default HeaderController; diff --git a/app/assets/javascripts/discourse/controllers/history.js.es6 b/app/assets/javascripts/discourse/controllers/history.js.es6 index 52d0d1de8c..973ff24a45 100644 --- a/app/assets/javascripts/discourse/controllers/history.js.es6 +++ b/app/assets/javascripts/discourse/controllers/history.js.es6 @@ -3,6 +3,15 @@ import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import computed from 'ember-addons/ember-computed-decorators'; import { propertyGreaterThan, propertyLessThan } from 'discourse/lib/computed'; +function customTagArray(fieldName) { + return function() { + var val = this.get(fieldName); + if (!val) { return val; } + if (!Array.isArray(val)) { val = [val]; } + return val; + }.property(fieldName); +} + // This controller handles displaying of history export default Ember.Controller.extend(ModalFunctionality, { loading: true, @@ -13,6 +22,9 @@ export default Ember.Controller.extend(ModalFunctionality, { if (this.site.mobileView) { this.set("viewMode", "inline"); } }.on("init"), + previousTagChanges: customTagArray('model.tags_changes.previous'), + currentTagChanges: customTagArray('model.tags_changes.current'), + refresh(postId, postVersion) { this.set("loading", true); diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index 02969cf5e1..d85d449ac7 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -27,7 +27,7 @@ export default Ember.Controller.extend(ModalFunctionality, { **/ hasAtLeastOneLoginButton: function() { return Em.get("Discourse.LoginMethod.all").length > 0; - }.property("Discourse.LoginMethod.all.@each"), + }.property("Discourse.LoginMethod.all.[]"), loginButtonText: function() { return this.get('loggingIn') ? I18n.t('login.logging_in') : I18n.t('login.title'); diff --git a/app/assets/javascripts/discourse/controllers/rename-tag.js.es6 b/app/assets/javascripts/discourse/controllers/rename-tag.js.es6 new file mode 100644 index 0000000000..3c5a7bc292 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/rename-tag.js.es6 @@ -0,0 +1,25 @@ +import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import BufferedContent from 'discourse/mixins/buffered-content'; + +export default Ember.Controller.extend(ModalFunctionality, BufferedContent, { + + renameDisabled: function() { + const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g"), + newId = this.get('buffered.id').replace(filterRegexp, '').trim(); + + return (newId.length === 0) || (newId === this.get('model.id')); + }.property('buffered.id', 'id'), + + actions: { + performRename() { + const tag = this.get('model'), + self = this; + tag.update({ id: this.get('buffered.id') }).then(function() { + self.send('closeModal'); + self.transitionToRoute('tags.show', tag.get('id')); + }).catch(function() { + self.flash(I18n.t('generic_error'), 'error'); + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/tags-index.js.es6 b/app/assets/javascripts/discourse/controllers/tags-index.js.es6 new file mode 100644 index 0000000000..e2a49fee20 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/tags-index.js.es6 @@ -0,0 +1,15 @@ +export default Ember.Controller.extend({ + sortProperties: ['count:desc', 'id'], + + sortedTags: Ember.computed.sort('model', 'sortProperties'), + + actions: { + sortByCount() { + this.set('sortProperties', ['count:desc', 'id']); + }, + + sortById() { + this.set('sortProperties', ['id']); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/tags-show.js.es6 b/app/assets/javascripts/discourse/controllers/tags-show.js.es6 new file mode 100644 index 0000000000..34c4729228 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/tags-show.js.es6 @@ -0,0 +1,133 @@ +import BulkTopicSelection from "discourse/mixins/bulk-topic-selection"; +import { default as NavItem, extraNavItemProperties, customNavItemHref } from 'discourse/models/nav-item'; + +if (extraNavItemProperties) { + extraNavItemProperties(function(text, opts) { + if (opts && opts.tagId) { + return {tagId: opts.tagId}; + } else { + return {}; + } + }); +} + +if (customNavItemHref) { + customNavItemHref(function(navItem) { + if (navItem.get('tagId')) { + var name = navItem.get('name'); + + if ( !Discourse.Site.currentProp('filters').contains(name) ) { + return null; + } + + var path = "/tags/", + category = navItem.get("category"); + + if(category){ + path += "c/"; + path += Discourse.Category.slugFor(category); + if (navItem.get('noSubcategories')) { path += '/none'; } + path += "/"; + } + + path += navItem.get('tagId') + "/l/"; + return path + name.replace(' ', '-'); + } else { + return null; + } + }); +} + + +export default Ember.Controller.extend(BulkTopicSelection, { + needs: ["application"], + + tag: null, + list: null, + canAdminTag: Ember.computed.alias("currentUser.staff"), + filterMode: null, + navMode: 'latest', + loading: false, + canCreateTopic: false, + order: 'default', + ascending: false, + status: null, + state: null, + search: null, + max_posts: null, + q: null, + + queryParams: ['order', 'ascending', 'status', 'state', 'search', 'max_posts', 'q'], + + navItems: function() { + return NavItem.buildList(this.get('category'), {tagId: this.get('tag.id'), filterMode: this.get('filterMode')}); + }.property('category', 'tag.id', 'filterMode'), + + showTagFilter: function() { + return Discourse.SiteSettings.show_filter_by_tag; + }.property('category'), + + categories: function() { + return Discourse.Category.list(); + }.property(), + + showAdminControls: function() { + return this.get('canAdminTag') && !this.get('category'); + }.property('canAdminTag', 'category'), + + loadMoreTopics() { + return this.get("list").loadMore(); + }, + + _showFooter: function() { + this.set("controllers.application.showFooter", !this.get("list.canLoadMore")); + }.observes("list.canLoadMore"), + + footerMessage: function() { + if (this.get('loading') || this.get('list.topics.length') !== 0) { return; } + + if (this.get('list.topics.length') === 0) { + return I18n.t('tagging.topics.none.' + this.get('navMode'), {tag: this.get('tag.id')}); + } else { + return I18n.t('tagging.topics.bottom.' + this.get('navMode'), {tag: this.get('tag.id')}); + } + }.property('navMode', 'list.topics.length', 'loading'), + + actions: { + changeSort(sortBy) { + if (sortBy === this.get('order')) { + this.toggleProperty('ascending'); + } else { + this.setProperties({ order: sortBy, ascending: false }); + } + this.send('invalidateModel'); + }, + + refresh() { + const self = this; + // TODO: this probably doesn't work anymore + return this.store.findFiltered('topicList', {filter: 'tags/' + this.get('tag.id')}).then(function(list) { + self.set("list", list); + self.resetSelected(); + }); + }, + + deleteTag() { + const self = this; + bootbox.confirm(I18n.t("tagging.delete_confirm"), function(result) { + if (!result) { return; } + + self.get("tag").destroyRecord().then(function() { + self.transitionToRoute("tags.index"); + }).catch(function() { + bootbox.alert(I18n.t("generic_error")); + }); + }); + }, + + changeTagNotification(id) { + const tagNotification = this.get("tagNotification"); + tagNotification.update({ notification_level: id }); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 index 2dd79219e5..89a6b0e70a 100644 --- a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 @@ -14,11 +14,15 @@ addBulkButton('archiveTopics', 'archive_topics'); addBulkButton('showNotificationLevel', 'notification_level'); addBulkButton('resetRead', 'reset_read'); addBulkButton('unlistTopics', 'unlist_topics'); +addBulkButton('showTagTopics', 'change_tags'); // Modal for performing bulk actions on topics export default Ember.ArrayController.extend(ModalFunctionality, { + tags: null, buttonRows: null, + emptyTags: Ember.computed.empty('tags'), + onShow() { this.set('controllers.modal.modalClass', 'topic-bulk-actions-modal small'); @@ -73,6 +77,15 @@ export default Ember.ArrayController.extend(ModalFunctionality, { }, actions: { + showTagTopics() { + this.set('tags', ''); + this.send('changeBulkTemplate', 'bulk-tag'); + }, + + changeTags() { + this.performAndRefresh({type: 'change_tags', tags: this.get('tags')}); + }, + showChangeCategory() { this.send('changeBulkTemplate', 'modal/bulk_change_category'); this.set('controllers.modal.modalClass', 'topic-bulk-actions-modal full'); diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index f70f8877bc..1071f6d7f7 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -9,7 +9,7 @@ import Composer from 'discourse/models/composer'; import DiscourseURL from 'discourse/lib/url'; export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { - needs: ['header', 'modal', 'composer', 'quote-button', 'topic-progress', 'application'], + needs: ['modal', 'composer', 'quote-button', 'topic-progress', 'application'], multiSelect: false, allPostsSelected: false, editingTopic: false, @@ -108,6 +108,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { return post => this.postSelected(post); }.property(), + @computed('model.isPrivateMessage') + canEditTags(isPrivateMessage) { + return !isPrivateMessage && this.site.get('can_tag_topics'); + }, + actions: { fillGapBefore(args) { @@ -472,11 +477,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { this.get('content').toggleStatus('archived'); }, - // Toggle the star on the topic - toggleStar() { - this.get('content').toggleStar(); - }, - clearPin() { this.get('content').clearPin(); }, @@ -509,7 +509,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { }).then(q => { const postUrl = `${location.protocol}//${location.host}${post.get('url')}`; const postLink = `[${Handlebars.escapeExpression(self.get('model.title'))}](${postUrl})`; - composerController.get('model').appendText(`${I18n.t("post.continue_discussion", { postLink })}\n\n${q}`); + composerController.get('model').prependText(`${I18n.t("post.continue_discussion", { postLink })}\n\n${q}`, {new_line: true}); }); }, @@ -545,6 +545,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { changePostOwner(post) { this.get('selectedPosts').addObject(post); this.send('changeOwner'); + }, + + convertToPublicTopic() { + this.get('content').convertTopic("public"); + }, + + convertToPrivateMessage() { + this.get('content').convertTopic("private"); } }, @@ -625,10 +633,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { return false; }, - showStarButton: function() { - return Discourse.User.current() && !this.get('model.isPrivateMessage'); - }.property('model.isPrivateMessage'), - loadingHTML: function() { return spinnerHTML; }.property(), diff --git a/app/assets/javascripts/discourse/controllers/user-card.js.es6 b/app/assets/javascripts/discourse/controllers/user-card.js.es6 index 39d938b8c7..0001f1c23b 100644 --- a/app/assets/javascripts/discourse/controllers/user-card.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-card.js.es6 @@ -49,7 +49,7 @@ export default Ember.Controller.extend({ moreBadgesCount: function() { return this.get('user.badge_count') - this.get('user.featured_user_badges.length'); - }.property('user.badge_count', 'user.featured_user_badges.@each'), + }.property('user.badge_count', 'user.featured_user_badges.[]'), hasCardBadgeImage: function() { const img = this.get('user.card_badge.image'); diff --git a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 index c06137a9e1..b61c7d45f6 100644 --- a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 @@ -5,18 +5,16 @@ export default Ember.ArrayController.extend({ this.set("controllers.application.showFooter", !this.get("model.canLoadMore")); }.observes("model.canLoadMore"), - showDismissButton: Ember.computed.gt('user.total_unread_notifications', 0), - currentPath: Em.computed.alias('controllers.application.currentPath'), actions: { - resetNew: function() { + resetNew() { Discourse.ajax('/notifications/mark-read', { method: 'PUT' }).then(() => { this.setEach('read', true); }); }, - loadMore: function() { + loadMore() { this.get('model').loadMore(); } } diff --git a/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 b/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 index 06a1be30db..18f010adcc 100644 --- a/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 @@ -15,7 +15,7 @@ export default Ember.Controller.extend({ Discourse.User.currentProp('can_send_private_messages'); }.property('controllers.user.viewingSelf'), - @computed('selected.@each', 'bulkSelectEnabled') + @computed('selected.[]', 'bulkSelectEnabled') hasSelection(selected, bulkSelectEnabled){ return bulkSelectEnabled && selected && selected.length > 0; }, diff --git a/app/assets/javascripts/discourse/ember/resolver.js.es6 b/app/assets/javascripts/discourse/ember/resolver.js.es6 index d7124c0900..8f41a2c507 100644 --- a/app/assets/javascripts/discourse/ember/resolver.js.es6 +++ b/app/assets/javascripts/discourse/ember/resolver.js.es6 @@ -70,7 +70,7 @@ export default Ember.DefaultResolver.extend({ // If we end with the name we want, use it. This allows us to define components within plugins. const suffix = parsedName.type + 's/' + parsedName.fullNameWithoutType, dashed = Ember.String.dasherize(suffix), - moduleName = Ember.keys(requirejs.entries).find(function(e) { + moduleName = Object.keys(requirejs.entries).find(function(e) { return (e.indexOf(suffix, e.length - suffix.length) !== -1) || (e.indexOf(dashed, e.length - dashed.length) !== -1); }); @@ -151,11 +151,13 @@ export default Ember.DefaultResolver.extend({ const withoutType = parsedName.fullNameWithoutType, slashedType = withoutType.replace(/\./g, '/'), decamelized = withoutType.decamelize(), + dashed = decamelized.replace(/\./g, '-').replace(/\_/g, '-'), templates = Ember.TEMPLATES; return this._super(parsedName) || templates[slashedType] || templates[withoutType] || + templates[dashed] || templates[decamelized.replace(/\./, '/')] || templates[decamelized.replace(/\_/, '/')] || this.findAdminTemplate(parsedName) || diff --git a/app/assets/javascripts/discourse/helpers/discouse-tag.js.es6 b/app/assets/javascripts/discourse/helpers/discouse-tag.js.es6 new file mode 100644 index 0000000000..b6d9d35524 --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/discouse-tag.js.es6 @@ -0,0 +1,6 @@ +import registerUnbound from 'discourse/helpers/register-unbound'; +import renderTag from 'discourse/lib/render-tag'; + +export default registerUnbound('discourse-tag', function(name, params) { + return new Handlebars.SafeString(renderTag(name, params)); +}); diff --git a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 index 6da20c3423..f55beeba54 100644 --- a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 +++ b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 @@ -60,7 +60,7 @@ function findOutlets(collection, callback) { const disabledPlugins = Discourse.Site.currentProp('disabled_plugins') || []; - Ember.keys(collection).forEach(function(res) { + Object.keys(collection).forEach(function(res) { if (res.indexOf("/connectors/") !== -1) { // Skip any disabled plugins for (let i=0; i { + Object.keys(requirejs.entries).forEach(entry => { if ((/\/helpers\//).test(entry)) { require(entry, null, null, true); } diff --git a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 index 63ef5104b4..406b050d3b 100644 --- a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 @@ -10,7 +10,8 @@ export default { siteSettings = container.lookup('site-settings:main'), bus = container.lookup('message-bus:main'), keyValueStore = container.lookup('key-value-store:main'), - store = container.lookup('store:main'); + store = container.lookup('store:main'), + appEvents = container.lookup('app-events:main'); // clear old cached notifications, we used to store in local storage // TODO 2017 delete this line @@ -30,7 +31,7 @@ export default { }); } - bus.subscribe("/notification/" + user.get('id'), function(data) { + bus.subscribe(`/notification/${user.get('id')}`, function(data) { const oldUnread = user.get('unread_notifications'); const oldPM = user.get('unread_private_messages'); @@ -38,7 +39,7 @@ export default { user.set('unread_private_messages', data.unread_private_messages); if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) { - user.set('lastNotificationChange', new Date()); + appEvents.trigger('notifications:changed'); } const stale = store.findStale('notification', {}, {cacheKey: 'recent-notifications'}); diff --git a/app/assets/javascripts/discourse/lib/app-events.js.es6 b/app/assets/javascripts/discourse/lib/app-events.js.es6 index 597c21b83d..59257700fd 100644 --- a/app/assets/javascripts/discourse/lib/app-events.js.es6 +++ b/app/assets/javascripts/discourse/lib/app-events.js.es6 @@ -1,25 +1 @@ - -var id = 1; -function newKey() { - return "_view_app_event_" + (id++); -} - -function createViewListener(eventName, cb) { - var extension = {}; - extension[newKey()] = function() { - this.appEvents.on(eventName, this, cb); - }.on('didInsertElement'); - - extension[newKey()] = function() { - this.appEvents.off(eventName, this, cb); - }.on('willDestroyElement'); - - return extension; -} - -function listenForViewEvent(viewClass, eventName, cb) { - viewClass.reopen(createViewListener(eventName, cb)); -} - -export { listenForViewEvent, createViewListener }; export default Ember.Object.extend(Ember.Evented); diff --git a/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 b/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 new file mode 100644 index 0000000000..d7895ff47a --- /dev/null +++ b/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 @@ -0,0 +1,65 @@ +import { CANCELLED_STATUS } from 'discourse/lib/autocomplete'; +import Category from 'discourse/models/category'; + +var cache = {}; +var cacheTime; +var oldSearch; + +function updateCache(term, results) { + cache[term] = results; + cacheTime = new Date(); + return results; +} + +function searchTags(term, categories, limit) { + return new Ember.RSVP.Promise((resolve) => { + const clearPromise = setTimeout(() => { + resolve(CANCELLED_STATUS); + }, 5000); + + const debouncedSearch = _.debounce((q, cats, resultFunc) => { + oldSearch = $.ajax(Discourse.getURL("/tags/filter/search"), { + type: 'GET', + cache: true, + data: { limit: limit, q } + }); + + var returnVal = CANCELLED_STATUS; + + oldSearch.then((r) => { + var tags = r.results.map((tag) => { return { text: tag.text, count: tag.count }; }); + returnVal = cats.concat(tags); + }).always(() => { + oldSearch = null; + resultFunc(returnVal); + }); + }, 300); + + debouncedSearch(term, categories, (result) => { + clearTimeout(clearPromise); + resolve(updateCache(term, result)); + }); + }); +}; + +export function search(term, siteSettings) { + if (oldSearch) { + oldSearch.abort(); + oldSearch = null; + } + + if ((new Date() - cacheTime) > 30000) cache = {}; + const cached = cache[term]; + if (cached) return cached; + + const limit = 5; + var categories = Category.search(term, { limit }); + var numOfCategories = categories.length; + categories = categories.map((category) => { return { model: category }; }); + + if (numOfCategories !== limit && siteSettings.tagging_enabled) { + return searchTags(term, categories, limit - numOfCategories); + } else { + return updateCache(term, categories); + } +}; diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6 index 799c26ada8..775a638c7f 100644 --- a/app/assets/javascripts/discourse/lib/click-track.js.es6 +++ b/app/assets/javascripts/discourse/lib/click-track.js.es6 @@ -1,4 +1,5 @@ import DiscourseURL from 'discourse/lib/url'; +import { wantsNewWindow } from 'discourse/lib/intercept-click'; export function isValidLink($link) { return ($link.hasClass("track-link") || @@ -11,15 +12,24 @@ export default { if (Discourse.Utilities.selectedText() !== "") { return false; } var $link = $(e.currentTarget); - if ($link.hasClass('lightbox') || $link.hasClass('mention-group') || $link.hasClass('no-track-link')) { return true; } + + // don't track lightboxes, group mentions or links with disabled tracking + if ($link.hasClass('lightbox') || $link.hasClass('mention-group') || + $link.hasClass('no-track-link') || $link.hasClass('hashtag')) { + return true; + } + + // don't track links in quotes or in elided part + if ($link.parents('aside.quote,.elided').length) { return true; } var href = $link.attr('href') || $link.data('href'), - $article = $link.closest('article'), + $article = $link.closest('article,.excerpt,#revisions'), postId = $article.data('post-id'), - topicId = $('#topic').data('topic-id'), + topicId = $('#topic').data('topic-id') || $article.data('topic-id'), userId = $link.data('user-id'); - if (!href || href.trim().length === 0) { return; } + if (!href || href.trim().length === 0) { return false; } + if (href.indexOf("mailto:") === 0) { return true; } if (!userId) userId = $article.data('user-id'); @@ -52,7 +62,7 @@ export default { } // if they want to open in a new tab, do an AJAX request - if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) { + if (wantsNewWindow(e)) { Discourse.ajax("/clicks/track", { data: { url: href, diff --git a/app/assets/javascripts/discourse/lib/intercept-click.js.es6 b/app/assets/javascripts/discourse/lib/intercept-click.js.es6 index 6ce7a300f3..09367beca2 100644 --- a/app/assets/javascripts/discourse/lib/intercept-click.js.es6 +++ b/app/assets/javascripts/discourse/lib/intercept-click.js.es6 @@ -1,12 +1,16 @@ import DiscourseURL from 'discourse/lib/url'; +export function wantsNewWindow(e) { + return (e.isDefaultPrevented() || e.shiftKey || e.metaKey || e.ctrlKey || (e.button && e.button !== 0)); +} + /** Discourse does some server side rendering of HTML, such as the `cooked` contents of posts. The downside of this in an Ember app is the links will not go through the router. This jQuery code intercepts clicks on those links and routes them properly. **/ export default function interceptClick(e) { - if (e.isDefaultPrevented() || e.shiftKey || e.metaKey || e.ctrlKey) { return; } + if (wantsNewWindow(e)) { return; } const $currentTarget = $(e.currentTarget), href = $currentTarget.attr('href'); @@ -18,6 +22,7 @@ export default function interceptClick(e) { $currentTarget.data('auto-route') || $currentTarget.data('share-url') || $currentTarget.data('user-card') || + $currentTarget.hasClass('widget-link') || $currentTarget.hasClass('mention') || (!$currentTarget.hasClass('d-link') && $currentTarget.hasClass('ember-view')) || $currentTarget.hasClass('lightbox') || diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index 748099a740..ef505ebe8e 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -10,8 +10,8 @@ const bindings = { '.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics 'b': {handler: 'toggleBookmark'}, 'c': {handler: 'createTopic'}, - 'ctrl+f': {handler: 'showBuiltinSearch', anonymous: true}, - 'command+f': {handler: 'showBuiltinSearch', anonymous: true}, + 'ctrl+f': {handler: 'showPageSearch', anonymous: true}, + 'command+f': {handler: 'showPageSearch', anonymous: true}, 'd': {postAction: 'deletePost'}, 'e': {postAction: 'editPost'}, 'end': {handler: 'goToLastPost', anonymous: true}, @@ -142,32 +142,10 @@ export default { this._changeSection(-1); }, - showBuiltinSearch() { - if (this.container.lookup('controller:header').get('searchVisible')) { - this.toggleSearch(); - return true; - } - - this.searchService.set('searchContextEnabled', false); - - const currentPath = this.container.lookup('controller:application').get('currentPath'), - blacklist = [ /^discovery\.categories/ ], - whitelist = [ /^topic\./ ], - check = function(regex) { return !!currentPath.match(regex); }; - let showSearch = whitelist.any(check) && !blacklist.any(check); - - // If we're viewing a topic, only intercept search if there are cloaked posts - if (showSearch && currentPath.match(/^topic\./)) { - showSearch = $('.topic-post .cooked, .small-action:not(.time-gap)').length < this.container.lookup('controller:topic').get('model.postStream.stream.length'); - } - - if (showSearch) { - this.searchService.set('searchContextEnabled', true); - this.toggleSearch(); - return false; - } - - return true; + showPageSearch(event) { + Ember.run(() => { + this.appEvents.trigger('header:keyboard-trigger', {type: 'page-search', event}); + }); }, createTopic() { @@ -182,17 +160,16 @@ export default { this.container.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true}); }, - toggleSearch() { - this.container.lookup('controller:header').send('toggleSearch'); - return false; + toggleSearch(event) { + this.appEvents.trigger('header:keyboard-trigger', {type: 'search', event}); }, - toggleHamburgerMenu() { - this.container.lookup('controller:header').send('toggleMenuPanel', 'hamburgerVisible'); + toggleHamburgerMenu(event) { + this.appEvents.trigger('header:keyboard-trigger', {type: 'hamburger', event}); }, - showCurrentUser() { - this.container.lookup('controller:header').send('toggleMenuPanel', 'userMenuVisible'); + showCurrentUser(event) { + this.appEvents.trigger('header:keyboard-trigger', {type: 'user', event}); }, showHelpModal() { @@ -226,7 +203,13 @@ export default { const post = topicController.get('model.postStream.posts').findBy('id', selectedPostId); if (post) { // TODO: Use ember closure actions - const result = topicController._actions[action].call(topicController, post); + let actionMethod = topicController._actions[action]; + if (!actionMethod) { + const topicRoute = container.lookup('route:topic'); + actionMethod = topicRoute._actions[action]; + } + + const result = actionMethod.call(topicController, post); if (result && result.then) { this.appEvents.trigger('post-stream:refresh', { id: selectedPostId }); } diff --git a/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6 b/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6 new file mode 100644 index 0000000000..c37610ff03 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6 @@ -0,0 +1,52 @@ +import { replaceSpan } from 'discourse/lib/category-hashtags'; +import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; + +const validTagHashtags = {}; +const checkedTagHashtags = []; +const testedClass = 'tag-hashtag-tested'; + +function updateFound($hashtags, tagValues) { + Ember.run.schedule('afterRender', () => { + $hashtags.each((index, hashtag) => { + const tagValue = tagValues[index]; + const link = validTagHashtags[tagValue]; + const $hashtag = $(hashtag); + + if (link) { + replaceSpan($hashtag, tagValue, link); + } else if (checkedTagHashtags.indexOf(tagValue) !== -1) { + $hashtag.addClass(testedClass); + } + }); + }); +} + +export function linkSeenTagHashtags($elem) { + const $hashtags = $(`span.hashtag:not(.${testedClass})`, $elem); + const unseen = []; + + if ($hashtags.length) { + const tagValues = $hashtags.map((_, hashtag) => { + return $(hashtag).text().substr(1).replace(`${TAG_HASHTAG_POSTFIX}`, ""); + }); + + if (tagValues.length) { + _.uniq(tagValues).forEach((tagValue) => { + if (checkedTagHashtags.indexOf(tagValue) === -1) unseen.push(tagValue); + }); + } + updateFound($hashtags, tagValues); + } + + return unseen; +}; + +export function fetchUnseenTagHashtags(tagValues) { + return Discourse.ajax("/tags/check", { data: { tag_values: tagValues } }) + .then((response) => { + response.valid.forEach((tag) => { + validTagHashtags[tag.value] = tag.url; + }); + checkedTagHashtags.push.apply(checkedTagHashtags, tagValues); + }); +} diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index 178f48e5a9..028570517d 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -9,6 +9,7 @@ import { createWidget, decorateWidget, changeSetting } from 'discourse/widgets/w import { onPageChange } from 'discourse/lib/page-tracker'; import { preventCloak } from 'discourse/widgets/post-stream'; import { h } from 'virtual-dom'; +import { addFlagProperty } from 'discourse/components/site-header'; class PluginApi { constructor(version, container) { @@ -48,7 +49,7 @@ class PluginApi { if (!opts.onlyStream) { decorate(ComposerEditor, 'previewRefreshed', callback); - decorate(this.container.lookupFactory('view:user-stream'), 'didInsertElement', callback); + decorate(this.container.lookupFactory('component:user-stream'), 'didInsertElement', callback); } } @@ -284,11 +285,20 @@ class PluginApi { createWidget(name, args) { return createWidget(name, args); } + + /** + * Adds a property that can be summed for calculating the flag counter + **/ + addFlagProperty(property) { + return addFlagProperty(property); + } + } let _pluginv01; function getPluginApi(version) { - if (version === "0.1" || version === "0.2" || version === "0.3") { + version = parseFloat(version); + if (version <= 0.4) { if (!_pluginv01) { _pluginv01 = new PluginApi(version, Discourse.__container__); } @@ -299,7 +309,7 @@ function getPluginApi(version) { } /** - * withPluginApi(version, apiCode, noApi) + * withPluginApi(version, apiCodeCallback, opts) * * Helper to version our client side plugin API. Pass the version of the API that your * plugin is coded against. If that API is available, the `apiCodeCallback` function will diff --git a/app/assets/javascripts/discourse/lib/render-tag.js.es6 b/app/assets/javascripts/discourse/lib/render-tag.js.es6 new file mode 100644 index 0000000000..5b62798541 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/render-tag.js.es6 @@ -0,0 +1,37 @@ +import { h } from 'virtual-dom'; + +export default function renderTag(tag, params) { + params = params || {}; + tag = Handlebars.Utils.escapeExpression(tag); + const classes = ['tag-' + tag, 'discourse-tag']; + const tagName = params.tagName || "a"; + const href = tagName === "a" ? " href='" + Discourse.getURL("/tags/" + tag) + "' " : ""; + + if (Discourse.SiteSettings.tag_style || params.style) { + classes.push(params.style || Discourse.SiteSettings.tag_style); + } + + let val = "<" + tagName + href + " class='" + classes.join(" ") + "'>" + tag + ""; + + if (params.count) { + val += " x" + params.count + ""; + } + + return val; +}; + +export function tagNode(tag, params) { + const classes = ['tag-' + tag, 'discourse-tag']; + const tagName = params.tagName || "a"; + + if (Discourse.SiteSettings.tag_style || params.style) { + classes.push(params.style || Discourse.SiteSettings.tag_style); + } + + if (tagName === 'a') { + const href = Discourse.getURL(`/tags/${tag}`); + return h(tagName, { className: classes.join(' '), attributes: { href } }, tag); + } else { + return h(tagName, { className: classes.join(' ') }, tag); + } +} diff --git a/app/assets/javascripts/discourse/lib/tag-hashtags.js.es6 b/app/assets/javascripts/discourse/lib/tag-hashtags.js.es6 new file mode 100644 index 0000000000..13eb406545 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/tag-hashtags.js.es6 @@ -0,0 +1 @@ +export const TAG_HASHTAG_POSTFIX = '::tag'; diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index e0074df059..50ef5e45f6 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -2,7 +2,7 @@ let _jumpScheduled = false; const rewrites = []; -const DiscourseURL = Ember.Object.createWithMixins({ +const DiscourseURL = Ember.Object.extend({ // Used for matching a topic TOPIC_REGEXP: /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/, @@ -327,7 +327,6 @@ const DiscourseURL = Ember.Object.createWithMixins({ } }); } - -}); +}).create(); export default DiscourseURL; diff --git a/app/assets/javascripts/discourse/mixins/load-more.js.es6 b/app/assets/javascripts/discourse/mixins/load-more.js.es6 index 16c8734054..c422e1cd63 100644 --- a/app/assets/javascripts/discourse/mixins/load-more.js.es6 +++ b/app/assets/javascripts/discourse/mixins/load-more.js.es6 @@ -5,14 +5,6 @@ import { on } from 'ember-addons/ember-computed-decorators'; // Provides the ability to load more items for a view which is scrolled to the bottom. export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, { - init() { - this._super(); - if (!this._viaComponent) { - console.warn('Using `LoadMore` as a view mixin is deprecated. Use `{{load-more}}` instead'); - } - - }, - scrolled() { const eyeline = this.get('eyeline'); return eyeline && eyeline.update(); diff --git a/app/assets/javascripts/discourse/mixins/url-refresh.js.es6 b/app/assets/javascripts/discourse/mixins/url-refresh.js.es6 index e5ab51cb3d..bf1710d1c1 100644 --- a/app/assets/javascripts/discourse/mixins/url-refresh.js.es6 +++ b/app/assets/javascripts/discourse/mixins/url-refresh.js.es6 @@ -1,12 +1,16 @@ // A Mixin that a view can use to listen for 'url:refresh' when -// it is on screen, and will send an action to the controller to -// refresh its data. +// it is on screen, and will send an action to refresh its data. // // This is useful if you want to get around Ember's default // behavior of not refreshing when navigating to the same place. +export default { + didInsertElement() { + this._super(); + this.appEvents.on('url:refresh', () => this.sendAction('refresh')); + }, -import { createViewListener } from 'discourse/lib/app-events'; - -export default createViewListener('url:refresh', function() { - this.get('controller').send('refresh'); -}); + willDestroyElement() { + this._super(); + this.appEvents.off('url:refresh'); + } +}; diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 9107fe62f1..b7d5a318b5 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -28,12 +28,14 @@ const CLOSED = 'closed', archetype: 'archetypeId', target_usernames: 'targetUsernames', typing_duration_msecs: 'typingTime', - composer_open_duration_msecs: 'composerTime' + composer_open_duration_msecs: 'composerTime', + tags: 'tags' }, _edit_topic_serializer = { title: 'topic.title', - categoryId: 'topic.category.id' + categoryId: 'topic.category.id', + tags: 'topic.tags' }; const Composer = RestModel.extend({ @@ -358,6 +360,15 @@ const Composer = RestModel.extend({ return before.length + text.length; }, + prependText(text, opts) { + const reply = (this.get('reply') || ''); + + if (opts && opts.new_line && reply.length > 0) { + text = text.trim() + "\n\n"; + } + this.set('reply', text + reply); + }, + applyTopicTemplate(oldCategoryId, categoryId) { if (this.get('action') !== CREATE_TOPIC) { return; } let reply = this.get('reply'); diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index 1555118be5..75e9ef2382 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -40,7 +40,7 @@ export default RestModel.extend({ notLoading: Ember.computed.not('loading'), filteredPostsCount: Ember.computed.alias("stream.length"), - @computed('posts.@each') + @computed('posts.[]') hasPosts() { return this.get('posts.length') > 0; }, @@ -53,7 +53,7 @@ export default RestModel.extend({ canAppendMore: Ember.computed.and('notLoading', 'hasPosts', 'lastPostNotLoaded'), canPrependMore: Ember.computed.and('notLoading', 'hasPosts', 'firstPostNotLoaded'), - @computed('hasLoadedData', 'firstPostId', 'posts.@each') + @computed('hasLoadedData', 'firstPostId', 'posts.[]') firstPostPresent(hasLoadedData, firstPostId) { if (!hasLoadedData) { return false; } return !!this.get('posts').findProperty('id', firstPostId); @@ -101,7 +101,7 @@ export default RestModel.extend({ Returns the window of posts above the current set in the stream, bound to the top of the stream. This is the collection we'll ask for when scrolling upwards. **/ - @computed('posts.@each', 'stream.@each') + @computed('posts.[]', 'stream.[]') previousWindow() { // If we can't find the last post loaded, bail const firstPost = _.first(this.get('posts')); @@ -121,7 +121,7 @@ export default RestModel.extend({ Returns the window of posts below the current set in the stream, bound by the bottom of the stream. This is the collection we use when scrolling downwards. **/ - @computed('posts.lastObject', 'stream.@each') + @computed('posts.lastObject', 'stream.[]') nextWindow(lastLoadedPost) { // If we can't find the last post loaded, bail if (!lastLoadedPost) { return []; } diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index 37005bc993..c2403aafac 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -108,7 +108,7 @@ const Post = RestModel.extend({ // Put the metaData into the request if (metaData) { data.meta_data = {}; - Ember.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); }); + Object.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); }); } return data; diff --git a/app/assets/javascripts/discourse/models/site.js.es6 b/app/assets/javascripts/discourse/models/site.js.es6 index 988ba1eac7..37c1299049 100644 --- a/app/assets/javascripts/discourse/models/site.js.es6 +++ b/app/assets/javascripts/discourse/models/site.js.es6 @@ -15,7 +15,7 @@ const Site = RestModel.extend({ return result; }, - @computed("post_action_types.@each") + @computed("post_action_types.[]") flagTypes() { const postActionTypes = this.get('post_action_types'); if (!postActionTypes) return []; @@ -26,7 +26,7 @@ const Site = RestModel.extend({ categoriesByCount: Ember.computed.sort('categories', 'topicCountDesc'), // Sort subcategories under parents - @computed("categoriesByCount", "categories.@each") + @computed("categoriesByCount", "categories.[]") sortedCategories(cats) { const result = [], remaining = {}; @@ -41,7 +41,7 @@ const Site = RestModel.extend({ } }); - Ember.keys(remaining).forEach(parentCategoryId => { + Object.keys(remaining).forEach(parentCategoryId => { const category = result.findBy('id', parseInt(parentCategoryId, 10)), index = result.indexOf(category); diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index e6cd21304d..700e07aeb5 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -4,6 +4,7 @@ import { propertyEqual } from 'discourse/lib/computed'; import { longDate } from 'discourse/lib/formatter'; import computed from 'ember-addons/ember-computed-decorators'; import ActionSummary from 'discourse/models/action-summary'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; export function loadTopicView(topic, args) { const topicId = topic.get('id'); @@ -32,7 +33,7 @@ const Topic = RestModel.extend({ return poster && poster.user; }, - @computed('posters.@each') + @computed('posters.[]') lastPoster(posters) { var user; if (posters && posters.length > 0) { @@ -72,6 +73,24 @@ const Topic = RestModel.extend({ return this.store.createRecord('postStream', {id: this.get('id'), topic: this}); }.property(), + @computed('tags') + visibleListTags(tags) { + if (!tags || !Discourse.SiteSettings.suppress_overlapping_tags_in_list) { + return tags; + } + + const title = this.get('title'); + const newTags = []; + + tags.forEach(function(tag){ + if (title.toLowerCase().indexOf(tag) === -1 || Discourse.SiteSettings.staff_tags.indexOf(tag) !== -1) { + newTags.push(tag); + } + }); + + return newTags; + }, + replyCount: function() { return this.get('posts_count') - 1; }.property('posts_count'), @@ -428,8 +447,13 @@ const Topic = RestModel.extend({ }).finally(()=>this.set('archiving', false)); return promise; - } + }, + convertTopic(type) { + return Discourse.ajax(`/t/${this.get('id')}/convert-topic/${type}`, {type: 'PUT'}).then(() => { + window.location.reload(); + }).catch(popupAjaxError); + } }); Topic.reopenClass({ diff --git a/app/assets/javascripts/discourse/models/user-action.js.es6 b/app/assets/javascripts/discourse/models/user-action.js.es6 index 50af402544..a4e784bd24 100644 --- a/app/assets/javascripts/discourse/models/user-action.js.es6 +++ b/app/assets/javascripts/discourse/models/user-action.js.es6 @@ -147,10 +147,10 @@ const UserAction = RestModel.extend({ } return rval; }.property("childGroups", - "childGroups.likes.items", "childGroups.likes.items.@each", - "childGroups.stars.items", "childGroups.stars.items.@each", - "childGroups.edits.items", "childGroups.edits.items.@each", - "childGroups.bookmarks.items", "childGroups.bookmarks.items.@each"), + "childGroups.likes.items", "childGroups.likes.items.[]", + "childGroups.stars.items", "childGroups.stars.items.[]", + "childGroups.edits.items", "childGroups.edits.items.[]", + "childGroups.bookmarks.items", "childGroups.bookmarks.items.[]"), switchToActing() { this.setProperties({ diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index b74c2cd2cc..55456dfb1a 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -136,7 +136,7 @@ const User = RestModel.extend({ }, copy() { - return Discourse.User.create(this.getProperties(Ember.keys(this))); + return Discourse.User.create(this.getProperties(Object.keys(this))); }, save() { @@ -229,7 +229,7 @@ const User = RestModel.extend({ ua.action_type === UserAction.TYPES.topics; }, - @computed("groups.@each") + @computed("groups.[]") displayGroups() { const groups = this.get('groups'); const filtered = groups.filter(group => { diff --git a/app/assets/javascripts/discourse/router.js.es6 b/app/assets/javascripts/discourse/router.js.es6 index 950e78cb8e..0223dfe41b 100644 --- a/app/assets/javascripts/discourse/router.js.es6 +++ b/app/assets/javascripts/discourse/router.js.es6 @@ -15,7 +15,7 @@ export function mapRoutes() { // will be built automatically. You can supply a `resource` property to // automatically put it in that resource, such as `admin`. That way plugins // can define admin routes. - Ember.keys(requirejs._eak_seen).forEach(function(key) { + Object.keys(requirejs._eak_seen).forEach(function(key) { if (/route-map$/.test(key)) { var module = require(key, null, null, true); if (!module || !module.default) { throw new Error(key + ' must export a route map.'); } diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index f8c278db19..adc3436d9b 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -118,4 +118,16 @@ export default function() { this.resource('queued-posts', { path: '/queued-posts' }); this.route('full-page-search', {path: '/search'}); + + this.resource('tags', function() { + this.route('show', {path: '/:tag_id'}); + this.route('showCategory', {path: '/c/:category/:tag_id'}); + this.route('showParentCategory', {path: '/c/:parent_category/:category/:tag_id'}); + + Discourse.Site.currentProp('filters').forEach(filter => { + this.route('show' + filter.capitalize(), {path: '/:tag_id/l/' + filter}); + this.route('showCategory' + filter.capitalize(), {path: '/c/:category/:tag_id/l/' + filter}); + this.route('showParentCategory' + filter.capitalize(), {path: '/c/:parent_category/:category/:tag_id/l/' + filter}); + }); + }); } diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index b057bcba8c..7954ade89f 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -3,6 +3,7 @@ import logout from 'discourse/lib/logout'; import showModal from 'discourse/lib/show-modal'; import OpenComposer from "discourse/mixins/open-composer"; import Category from 'discourse/models/category'; +import mobile from 'discourse/lib/mobile'; function unlessReadOnly(method, message) { return function() { @@ -25,6 +26,22 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { actions: { + showSearchHelp() { + Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then(model => { + showModal('searchHelp', { model }); + }); + }, + + toggleAnonymous() { + Discourse.ajax("/users/toggle-anon", {method: 'POST'}).then(() => { + window.location.reload(); + }); + }, + + toggleMobileView() { + mobile.toggleMobileView(); + }, + logout: unlessReadOnly('_handleLogout', I18n.t("read_only_mode.logout_disabled")), _collectTitleTokens(tokens) { diff --git a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 index 990a882157..b2e5f3281e 100644 --- a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 @@ -4,7 +4,7 @@ import { queryParams } from 'discourse/controllers/discovery-sortable'; function filterQueryParams(params, defaultParams) { const findOpts = defaultParams || {}; if (params) { - Ember.keys(queryParams).forEach(function(opt) { + Object.keys(queryParams).forEach(function(opt) { if (params[opt]) { findOpts[opt] = params[opt]; } }); } @@ -38,7 +38,7 @@ function findTopicList(store, tracking, filter, filterParams, extras) { // Clean up any string parameters that might slip through filterParams = filterParams || {}; - Ember.keys(filterParams).forEach(function(k) { + Object.keys(filterParams).forEach(function(k) { const val = filterParams[k]; if (val === "undefined" || val === "null" || val === 'false') { filterParams[k] = undefined; diff --git a/app/assets/javascripts/discourse/routes/discourse.js.es6 b/app/assets/javascripts/discourse/routes/discourse.js.es6 index 5576fb422c..47fd25ed7d 100644 --- a/app/assets/javascripts/discourse/routes/discourse.js.es6 +++ b/app/assets/javascripts/discourse/routes/discourse.js.es6 @@ -75,7 +75,6 @@ const DiscourseRoute = Ember.Route.extend({ }); export function cleanDOM() { - if (window.MiniProfiler) { window.MiniProfiler.pageTransition(); } diff --git a/app/assets/javascripts/discourse/routes/group-index.js.es6 b/app/assets/javascripts/discourse/routes/group-index.js.es6 index bb8ecaeb03..a7baf27a15 100644 --- a/app/assets/javascripts/discourse/routes/group-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-index.js.es6 @@ -1,14 +1,24 @@ -export default Discourse.Route.extend({ - actions: { - didTransition() { return true; } - }, +export function buildIndex(type) { + return Discourse.Route.extend({ + type, - model() { - return this.modelFor("group").findPosts(); - }, + model() { + return this.modelFor("group").findPosts({ type }); + }, - setupController(controller, model) { - controller.set("model", model); - this.controllerFor("group").set("showing", "posts"); - } -}); + setupController(controller, model) { + this.controllerFor('group-index').setProperties({ model, type }); + this.controllerFor("group").set("showing", type); + }, + + renderTemplate() { + this.render('group-index'); + }, + + actions: { + didTransition() { return true; } + } + }); +} + +export default buildIndex('posts'); diff --git a/app/assets/javascripts/discourse/routes/group-members.js.es6 b/app/assets/javascripts/discourse/routes/group-members.js.es6 index 22e328cacf..c6e00e8028 100644 --- a/app/assets/javascripts/discourse/routes/group-members.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-members.js.es6 @@ -8,5 +8,4 @@ export default Discourse.Route.extend({ controller.set("model", model); model.findMembers(); } - }); diff --git a/app/assets/javascripts/discourse/routes/group-mentions.js.es6 b/app/assets/javascripts/discourse/routes/group-mentions.js.es6 index 1054fb18d3..d5c7471f51 100644 --- a/app/assets/javascripts/discourse/routes/group-mentions.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-mentions.js.es6 @@ -1,11 +1,3 @@ -export default Discourse.Route.extend({ +import { buildIndex } from 'discourse/routes/group-index'; - model() { - return this.modelFor("group").findPosts({type: 'mentions'}); - }, - - setupController(controller, model) { - controller.set("model", model); - this.controllerFor("group").set("showing", "mentions"); - } -}); +export default buildIndex('mentions'); diff --git a/app/assets/javascripts/discourse/routes/group-messages.js.es6 b/app/assets/javascripts/discourse/routes/group-messages.js.es6 index 34e077e362..df27a3f793 100644 --- a/app/assets/javascripts/discourse/routes/group-messages.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-messages.js.es6 @@ -1,11 +1,3 @@ -export default Discourse.Route.extend({ +import { buildIndex } from 'discourse/routes/group-index'; - model() { - return this.modelFor("group").findPosts({type: 'messages'}); - }, - - setupController(controller, model) { - controller.set("model", model); - this.controllerFor("group").set("showing", "messages"); - } -}); +export default buildIndex('messages'); diff --git a/app/assets/javascripts/discourse/routes/group-topics.js.es6 b/app/assets/javascripts/discourse/routes/group-topics.js.es6 index 397572e77b..991bb7475a 100644 --- a/app/assets/javascripts/discourse/routes/group-topics.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-topics.js.es6 @@ -1,11 +1,3 @@ -export default Discourse.Route.extend({ +import { buildIndex } from 'discourse/routes/group-index'; - model() { - return this.modelFor("group").findPosts({type: 'topics'}); - }, - - setupController(controller, model) { - controller.set("model", model); - this.controllerFor("group").set("showing", "topics"); - } -}); +export default buildIndex('topics'); diff --git a/app/assets/javascripts/discourse/routes/tags-index.js.es6 b/app/assets/javascripts/discourse/routes/tags-index.js.es6 new file mode 100644 index 0000000000..7fbd3d529e --- /dev/null +++ b/app/assets/javascripts/discourse/routes/tags-index.js.es6 @@ -0,0 +1,16 @@ +export default Discourse.Route.extend({ + model() { + return this.store.findAll('tag'); + }, + + titleToken() { + return I18n.t("tagging.tags"); + }, + + actions: { + didTransition() { + this.controllerFor("application").set("showFooter", true); + return true; + } + } +}); diff --git a/app/assets/javascripts/discourse/routes/tags-show.js.es6 b/app/assets/javascripts/discourse/routes/tags-show.js.es6 new file mode 100644 index 0000000000..ea8762745b --- /dev/null +++ b/app/assets/javascripts/discourse/routes/tags-show.js.es6 @@ -0,0 +1,137 @@ +import Composer from 'discourse/models/composer'; +import showModal from "discourse/lib/show-modal"; +import { findTopicList } from 'discourse/routes/build-topic-route'; + +export default Discourse.Route.extend({ + navMode: 'latest', + + renderTemplate() { + const controller = this.controllerFor('tags.show'); + this.render('tags.show', { controller }); + }, + + model(params) { + var tag = this.store.createRecord("tag", { id: Handlebars.Utils.escapeExpression(params.tag_id) }), + f = ''; + + if (params.category) { + f = 'c/'; + if (params.parent_category) { f += params.parent_category + '/'; } + f += params.category + '/l/'; + } + f += this.get('navMode'); + this.set('filterMode', f); + + if (params.category) { this.set('categorySlug', params.category); } + if (params.parent_category) { this.set('parentCategorySlug', params.parent_category); } + + if (this.get("currentUser")) { + // If logged in, we should get the tag"s user settings + return this.store.find("tagNotification", tag.get("id")).then(tn => { + this.set("tagNotification", tn); + return tag; + }); + } + + return tag; + }, + + afterModel(tag) { + const controller = this.controllerFor('tags.show'); + controller.set('loading', true); + + const params = controller.getProperties('order', 'ascending'); + + const categorySlug = this.get('categorySlug'); + const parentCategorySlug = this.get('parentCategorySlug'); + const filter = this.get('navMode'); + + if (categorySlug) { + var category = Discourse.Category.findBySlug(categorySlug, parentCategorySlug); + if (parentCategorySlug) { + params.filter = `tags/c/${parentCategorySlug}/${categorySlug}/${tag.id}/l/${filter}`; + } else { + params.filter = `tags/c/${categorySlug}/${tag.id}/l/${filter}`; + } + + this.set('category', category); + } else { + params.filter = `tags/${tag.id}/l/${filter}`; + this.set('category', null); + } + + return findTopicList(this.store, this.topicTrackingState, params.filter, params, {}).then(function(list) { + controller.set('list', list); + controller.set('canCreateTopic', list.get('can_create_topic')); + if (list.topic_list.tags) { + Discourse.Site.currentProp('top_tags', list.topic_list.tags); + } + controller.set('loading', false); + }); + }, + + titleToken() { + const filterText = I18n.t('filters.' + this.get('navMode').replace('/', '.') + '.title'), + controller = this.controllerFor('tags.show'); + + if (this.get('category')) { + return I18n.t('tagging.filters.with_category', { filter: filterText, tag: controller.get('model.id'), category: this.get('category.name')}); + } else { + return I18n.t('tagging.filters.without_category', { filter: filterText, tag: controller.get('model.id')}); + } + }, + + setupController(controller, model) { + this.controllerFor('tags.show').setProperties({ + model, + tag: model, + category: this.get('category'), + filterMode: this.get('filterMode'), + navMode: this.get('navMode'), + tagNotification: this.get('tagNotification') + }); + }, + + actions: { + invalidateModel() { + this.refresh(); + }, + + renameTag(tag) { + showModal("rename-tag", { model: tag }); + }, + + createTopic() { + var controller = this.controllerFor("tags.show"), + self = this; + + this.controllerFor('composer').open({ + categoryId: controller.get('category.id'), + action: Composer.CREATE_TOPIC, + draftKey: controller.get('list.draft_key'), + draftSequence: controller.get('list.draft_sequence') + }).then(function() { + // Pre-fill the tags input field + if (controller.get('model.id')) { + var c = self.controllerFor('composer').get('model'); + c.set('tags', [controller.get('model.id')]); + } + }); + }, + + didTransition() { + this.controllerFor("tags.show")._showFooter(); + return true; + }, + + willTransition(transition) { + if (!Discourse.SiteSettings.show_filter_by_tag) { return true; } + + if ((transition.targetName.indexOf("discovery.parentCategory") !== -1 || + transition.targetName.indexOf("discovery.category") !== -1) && !transition.queryParams.allTags ) { + this.transitionTo("/tags" + transition.intent.url + "/" + this.currentModel.get("id")); + } + return true; + } + } +}); diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 4297186f07..cd627ce46d 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -179,8 +179,9 @@ const TopicRoute = Discourse.Route.extend({ this.searchService.set('searchContext', null); this.controllerFor('user-card').set('visible', false); - const topicController = this.controllerFor('topic'), - postStream = topicController.get('model.postStream'); + const topicController = this.controllerFor('topic'); + const postStream = topicController.get('model.postStream'); + postStream.cancelFilter(); topicController.set('multiSelect', false); @@ -188,11 +189,7 @@ const TopicRoute = Discourse.Route.extend({ this.controllerFor('composer').set('topic', null); this.screenTrack.stop(); - const headerController = this.controllerFor('header'); - if (headerController) { - headerController.set('topic', null); - headerController.set('showExtraInfo', false); - } + this.appEvents.trigger('header:hide-topic'); }, setupController(controller, model) { @@ -207,7 +204,6 @@ const TopicRoute = Discourse.Route.extend({ TopicRoute.trigger('setupTopicController', this); - this.controllerFor('header').setProperties({ topic: model, showExtraInfo: false }); this.searchService.set('searchContext', model.get('searchContext')); this.controllerFor('composer').set('topic', model); diff --git a/app/assets/javascripts/discourse/templates/application.hbs b/app/assets/javascripts/discourse/templates/application.hbs index 99ef6ee552..479d0c2925 100644 --- a/app/assets/javascripts/discourse/templates/application.hbs +++ b/app/assets/javascripts/discourse/templates/application.hbs @@ -1,4 +1,11 @@ -{{render "header"}} +{{site-header canSignUp=canSignUp + showCreateAccount="showCreateAccount" + showLogin="showLogin" + showKeyboard="showKeyboardShortcutsHelp" + toggleMobileView="toggleMobileView" + toggleAnonymous="toggleAnonymous" + logout="logout" + showSearchHelp="showSearchHelp"}}
diff --git a/app/assets/javascripts/discourse/templates/bulk-tag.hbs b/app/assets/javascripts/discourse/templates/bulk-tag.hbs new file mode 100644 index 0000000000..1ffe345e30 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/bulk-tag.hbs @@ -0,0 +1,5 @@ +

{{i18n "topics.bulk.choose_new_tags"}}

+ +

{{tag-chooser tags=tags}}

+ +{{d-button action="changeTags" disabled=emptyTags label="topics.bulk.change_tags"}} diff --git a/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs new file mode 100644 index 0000000000..886c7bba73 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs @@ -0,0 +1,13 @@ +
+ +
diff --git a/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs b/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs index 756b976c33..f737c96976 100644 --- a/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs +++ b/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs @@ -4,6 +4,10 @@ {{category-drop category=secondCategory parentCategory=firstCategory categories=childCategories subCategory="true" noSubcategories=noSubcategories}} {{/if}} +{{#if siteSettings.tagging_enabled}} + {{tag-drop firstCategory=firstCategory secondCategory=secondCategory tagId=tagId}} +{{/if}} + {{plugin-outlet "bread-crumbs-right" tagName="li"}}
diff --git a/app/assets/javascripts/discourse/templates/components/date-picker.hbs b/app/assets/javascripts/discourse/templates/components/date-picker.hbs index 7d9e48080a..a2c89401ab 100644 --- a/app/assets/javascripts/discourse/templates/components/date-picker.hbs +++ b/app/assets/javascripts/discourse/templates/components/date-picker.hbs @@ -1 +1 @@ - + diff --git a/app/assets/javascripts/discourse/templates/components/discourse-tag-bound.hbs b/app/assets/javascripts/discourse/templates/components/discourse-tag-bound.hbs new file mode 100644 index 0000000000..d7eb83adb9 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/discourse-tag-bound.hbs @@ -0,0 +1 @@ +{{tagRecord.id}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/templates/components/group-post-stream.js.hbs b/app/assets/javascripts/discourse/templates/components/group-post-stream.js.hbs index 42af3ae454..9ab4c17c68 100644 --- a/app/assets/javascripts/discourse/templates/components/group-post-stream.js.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-post-stream.js.hbs @@ -1,7 +1,9 @@ -
- {{#each posts as |post|}} - {{group-post post=post}} - {{else}} -
{{i18n emptyText}}
- {{/each}} -
+{{#load-more selector=".user-stream .item" action="loadMore"}} +
+ {{#each posts as |post|}} + {{group-post post=post}} + {{else}} +
{{i18n emptyText}}
+ {{/each}} +
+{{/load-more}} diff --git a/app/assets/javascripts/discourse/templates/components/group-post.hbs b/app/assets/javascripts/discourse/templates/components/group-post.hbs index c661dbe345..45a1642281 100644 --- a/app/assets/javascripts/discourse/templates/components/group-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-post.hbs @@ -3,7 +3,7 @@
{{avatar post.user imageSize="large" extraClasses="actor" ignoreTitle="true"}}
{{format-date post.created_at leaveAgo="true"}} - {{post.topic.fancyTitle}} + {{{post.topic.fancyTitle}}} {{category-link post.category}}
diff --git a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs deleted file mode 100644 index f0bc9397ab..0000000000 --- a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs +++ /dev/null @@ -1,94 +0,0 @@ -{{#menu-panel visible=visible}} - {{#if prioritizeFaq}} - {{#menu-links}} -
  • - {{#d-link path=faqUrl class="faq-link"}} - {{i18n "faq"}} - {{i18n "new_item"}} - {{/d-link}} -
  • - {{/menu-links}} - {{/if}} - - {{#if currentUser.staff}} - {{#menu-links}} -
  • {{d-link route="admin" class="admin-link" icon="wrench" label="admin_title"}}
  • -
  • - {{#d-link route="adminFlags" class="flagged-posts-link"}} - {{fa-icon "flag"}} {{i18n 'flags_title'}} - {{#if currentUser.site_flagged_posts_count}} - {{currentUser.site_flagged_posts_count}} - {{/if}} - {{/d-link}} -
  • - - {{#if currentUser.show_queued_posts}} -
  • - {{#d-link route='queued-posts'}} - {{i18n "queue.title"}} - {{#if currentUser.post_queue_new_count}} - {{currentUser.post_queue_new_count}} - {{/if}} - {{/d-link}} -
  • - {{/if}} - {{#if currentUser.admin}} -
  • {{d-link route="adminSiteSettings" icon="gear" label="admin.site_settings.title"}}
  • - {{/if}} - - {{plugin-outlet "hamburger-admin"}} - {{/menu-links}} - {{/if}} - - {{#menu-links}} -
  • {{d-link route="discovery.latest" class="latest-topics-link" label="filters.latest.title"}}
  • - - {{#if currentUser}} -
  • - {{#if newCount}} - {{d-link route="discovery.new" class="new-topics-link" label="filters.new.title_with_count" count=newCount}} - {{else}} - {{d-link route="discovery.new" class="new-topics-link" label="filters.new.title"}} - {{/if}} -
  • -
  • - {{#if unreadCount}} - {{d-link route="discovery.unread" class="unread-topics-link" label="filters.unread.title_with_count" count=unreadCount}} - {{else}} - {{d-link route="discovery.unread" class="unread-topics-link" label="filters.unread.title"}} - {{/if}} -
  • - {{/if}} -
  • {{d-link route="discovery.top" class="top-topics-link" label="filters.top.title"}}
  • - - {{#if siteSettings.enable_badges}} -
  • {{d-link route="badges" class="badge-link" label="badges.title"}}
  • - {{/if}} - - {{#if showUserDirectoryLink}} -
  • {{d-link route="users" class="user-directory-link" label="directory.title"}}
  • - {{/if}} - - {{plugin-outlet "site-map-links"}} - - {{plugin-outlet "site-map-links-last"}} - {{/menu-links}} - - {{mount-widget widget='hamburger-categories' args=(as-hash categories=categories)}} -
    - - {{#menu-links omitRule="true"}} -
  • {{d-link route="about" class="about-link" label="about.simple_title"}}
  • - {{#unless prioritizeFaq}} -
  • {{d-link path=faqUrl class="faq-link" label="faq"}}
  • - {{/unless}} - - {{#if showKeyboardShortcuts}} -
  • {{d-link action="keyboardShortcuts" class="keyboard-shortcuts-link" label="keyboard_shortcuts_help.title"}}
  • - {{/if}} - - {{#if showMobileToggle}} -
  • {{d-link action="toggleMobileView" class="mobile-toggle-link" label=mobileViewLinkTextKey}}
  • - {{/if}} - {{/menu-links}} -{{/menu-panel}} diff --git a/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs b/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs deleted file mode 100644 index 6cb4af742b..0000000000 --- a/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs +++ /dev/null @@ -1,9 +0,0 @@ - - {{#if showUser}} - {{bound-avatar currentUser "medium"}} - {{else}} - {{fa-icon icon}} - {{/if}} - - -{{yield}} diff --git a/app/assets/javascripts/discourse/templates/components/header-extra-info.hbs b/app/assets/javascripts/discourse/templates/components/header-extra-info.hbs deleted file mode 100644 index 4d5b798753..0000000000 --- a/app/assets/javascripts/discourse/templates/components/header-extra-info.hbs +++ /dev/null @@ -1,21 +0,0 @@ -
    -
    -
    -

    - {{#if showPrivateMessageGlyph}} - - {{fa-icon "envelope"}} - - {{/if}} - - {{#if topic.details.loaded}} - {{topic-status topic=topic}} - {{{topic.fancyTitle}}} - {{/if}} -

    - {{#if topic.details.loaded}} - {{topic-category topic=topic}} - {{/if}} -
    -
    -
    diff --git a/app/assets/javascripts/discourse/templates/components/menu-links.hbs b/app/assets/javascripts/discourse/templates/components/menu-links.hbs deleted file mode 100644 index fe9b872fcb..0000000000 --- a/app/assets/javascripts/discourse/templates/components/menu-links.hbs +++ /dev/null @@ -1,7 +0,0 @@ - -{{#unless omitRule}} -
    -{{/unless}} diff --git a/app/assets/javascripts/discourse/templates/components/menu-panel.hbs b/app/assets/javascripts/discourse/templates/components/menu-panel.hbs deleted file mode 100644 index bbb72a5736..0000000000 --- a/app/assets/javascripts/discourse/templates/components/menu-panel.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{#if visible}} -
    -
    - {{yield}} -
    -
    -{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/search-menu.hbs b/app/assets/javascripts/discourse/templates/components/search-menu.hbs deleted file mode 100644 index 1f9cc018a9..0000000000 --- a/app/assets/javascripts/discourse/templates/components/search-menu.hbs +++ /dev/null @@ -1,40 +0,0 @@ -{{#menu-panel visible=visible onVisible="showedSearch" onHidden="cancelHighlight" maxWidth="500"}} - {{plugin-outlet "above-search"}} - {{search-text-field value=searchService.term id="search-term"}} - -
    - {{#if searchService.searchContext}} - - {{/if}} - {{i18n "show_help"}} -
    -
    - {{#if loading}} -
    {{loading-spinner}}
    - {{else}} -
    - {{#if noResults}} -
    - {{i18n "search.no_results"}} -
    - {{else}} - {{#each content.resultTypes as |resultType|}} -
      -
    • {{resultType.name}}
    • - {{component resultType.componentName results=resultType.results term=searchService.term}} -
    -
    - {{#if resultType.moreUrl}} - {{i18n "show_more"}} {{fa-icon "chevron-down"}} - {{/if}} - {{#if resultType.more}} - {{i18n "show_more"}} {{fa-icon "chevron-down"}} - {{/if}} -
    - {{/each}} - {{/if}} -
    - {{/if}} -{{/menu-panel}} diff --git a/app/assets/javascripts/discourse/templates/components/search-result-category.hbs b/app/assets/javascripts/discourse/templates/components/search-result-category.hbs deleted file mode 100644 index 071eba1841..0000000000 --- a/app/assets/javascripts/discourse/templates/components/search-result-category.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{#each results as |result|}} -
  • - - {{category-badge result}} - -
  • -{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/search-result-post.hbs b/app/assets/javascripts/discourse/templates/components/search-result-post.hbs deleted file mode 100644 index f89090ac24..0000000000 --- a/app/assets/javascripts/discourse/templates/components/search-result-post.hbs +++ /dev/null @@ -1,14 +0,0 @@ -{{#each results as |result|}} -
  • - - - {{i18n 'search.post_format' post_number=result.post_number username=result.username}} - - {{#unless site.mobileView}} - - {{{unbound result.blurb}}} - - {{/unless}} - -
  • -{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/search-result-topic.hbs b/app/assets/javascripts/discourse/templates/components/search-result-topic.hbs deleted file mode 100644 index 132e56360b..0000000000 --- a/app/assets/javascripts/discourse/templates/components/search-result-topic.hbs +++ /dev/null @@ -1,14 +0,0 @@ -{{#each results as |result|}} -
  • - - - {{topic-status topic=result.topic disableActions=true}}{{{unbound result.topic.fancyTitle}}}{{category-badge result.topic.category}}{{plugin-outlet "search-category"}} - - {{#unless site.mobileView}} - - {{format-age result.created_at}} - {{{unbound result.blurb}}} - - {{/unless}} - -
  • -{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/search-result-user.hbs b/app/assets/javascripts/discourse/templates/components/search-result-user.hbs deleted file mode 100644 index 9cf46a1519..0000000000 --- a/app/assets/javascripts/discourse/templates/components/search-result-user.hbs +++ /dev/null @@ -1,8 +0,0 @@ -{{#each results as |result|}} -
  • - - {{avatar result imageSize="small"}} - {{unbound result.username}} - -
  • -{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/stream-item.hbs b/app/assets/javascripts/discourse/templates/components/stream-item.hbs index 4c5609b88d..0018f8a2d8 100644 --- a/app/assets/javascripts/discourse/templates/components/stream-item.hbs +++ b/app/assets/javascripts/discourse/templates/components/stream-item.hbs @@ -13,7 +13,7 @@

    {{actionDescription}}

    {{/if}} -

    {{{item.excerpt}}}

    +

    {{{item.excerpt}}}

    {{#each item.children as |child|}}
    diff --git a/app/assets/javascripts/discourse/templates/components/tag-drop.hbs b/app/assets/javascripts/discourse/templates/components/tag-drop.hbs new file mode 100644 index 0000000000..46e4f66046 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/tag-drop.hbs @@ -0,0 +1,21 @@ +{{#if showTagDropdown}} + {{#if tagId}} + {{tagId}} + {{else}} + {{allTagsLabel}} + {{/if}} + + {{#if tags}} + +
    + + {{#if renderTags}} + {{#each t in tags}} +
    + {{tag-drop-link tagId=t category=currentCategory}} +
    + {{/each}} + {{/if}} +
    + {{/if}} +{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/templates/components/topic-category.hbs b/app/assets/javascripts/discourse/templates/components/topic-category.hbs index 8a914440e0..cf70e3e8a8 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-category.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-category.hbs @@ -2,5 +2,12 @@ {{bound-category-link topic.category.parentCategory}} {{/if}} {{bound-category-link topic.category hideParent=true}} +{{#if siteSettings.tagging_enabled}} +
    + {{#each t in topic.tags}} + {{discourse-tag t}} + {{/each}} +
    +{{/if}} {{plugin-outlet "topic-category"}} diff --git a/app/assets/javascripts/discourse/templates/components/user-menu.hbs b/app/assets/javascripts/discourse/templates/components/user-menu.hbs deleted file mode 100644 index 6f014d80d2..0000000000 --- a/app/assets/javascripts/discourse/templates/components/user-menu.hbs +++ /dev/null @@ -1,48 +0,0 @@ -{{#menu-panel visible=visible}} - - -
    - {{#conditional-loading-spinner condition=loadingNotifications containerClass="spinner-container"}} - {{#if notifications}} -
    -
      - {{#each notifications as |n|}} - {{notification-item notification=n}} - {{/each}} -
    • - {{#d-link path=notificationsPath}} - {{i18n 'notifications.more'}}… - {{/d-link}} -
    • -
    - {{/if}} - {{/conditional-loading-spinner}} -
    - {{plugin-outlet "user-menu-bottom"}} - -{{/menu-panel}} diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 06ebfdfbef..1ecbecab16 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -97,6 +97,9 @@ {{#if currentUser}}
    {{plugin-outlet "composer-fields-below"}} + {{#if canEditTags}} + {{tag-chooser tags=model.tags tabIndex="4"}} + {{/if}} {{i18n 'cancel'}} diff --git a/app/assets/javascripts/discourse/templates/composer/similar_topics.hbs b/app/assets/javascripts/discourse/templates/composer/similar-topics.hbs similarity index 64% rename from app/assets/javascripts/discourse/templates/composer/similar_topics.hbs rename to app/assets/javascripts/discourse/templates/composer/similar-topics.hbs index ed326e488e..257a51ebf1 100644 --- a/app/assets/javascripts/discourse/templates/composer/similar_topics.hbs +++ b/app/assets/javascripts/discourse/templates/composer/similar-topics.hbs @@ -2,5 +2,5 @@

    {{i18n 'composer.similar_topics'}}

      - {{search-result-topic results=similarTopics}} + {{mount-widget widget="search-result-topic" args=(as-hash results=similarTopics)}}
    diff --git a/app/assets/javascripts/discourse/templates/discovery/categories.hbs b/app/assets/javascripts/discourse/templates/discovery/categories.hbs index dcd5c6acfd..7d26d35ba2 100644 --- a/app/assets/javascripts/discourse/templates/discovery/categories.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/categories.hbs @@ -1,5 +1,5 @@ {{#if model.categories}} -
    + {{#discovery-categories refresh="refresh"}} @@ -54,6 +54,6 @@ {{/each}}
    -
    + {{/discovery-categories}}
    {{/if}} diff --git a/app/assets/javascripts/discourse/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/discovery/topics.hbs index 9b55d626ae..8bb13926fc 100644 --- a/app/assets/javascripts/discourse/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/topics.hbs @@ -15,7 +15,7 @@ {{bulk-select-button selected=selected refreshTarget=controller}} -
    +{{#discovery-topics-list model=model refresh="refresh" incomingCount=topicTrackingState.incomingCount}} {{#if top}}
    {{period-chooser period=period action="changePeriod"}} @@ -32,23 +32,23 @@ {{/if}} {{#if hasTopics}} - {{topic-list - showTopicPostBadges=showTopicPostBadges - showPosters=true - currentUser=currentUser - canBulkSelect=canBulkSelect - changeSort="changeSort" - toggleBulkSelect="toggleBulkSelect" - hideCategory=model.hideCategory - order=order - ascending=ascending - bulkSelectEnabled=bulkSelectEnabled - selected=selected - expandGloballyPinned=expandGloballyPinned - expandAllPinned=expandAllPinned - topics=model.topics}} + {{topic-list + showTopicPostBadges=showTopicPostBadges + showPosters=true + currentUser=currentUser + canBulkSelect=canBulkSelect + changeSort="changeSort" + toggleBulkSelect="toggleBulkSelect" + hideCategory=model.hideCategory + order=order + ascending=ascending + bulkSelectEnabled=bulkSelectEnabled + selected=selected + expandGloballyPinned=expandGloballyPinned + expandAllPinned=expandAllPinned + topics=model.topics}} {{/if}} -
    +{{/discovery-topics-list}}
    {{conditional-loading-spinner condition=model.loadingMore}} diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index e216f7ab3f..7c39f11aa5 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -70,6 +70,9 @@
    {{category-link result.topic.category}} + {{#each result.topic.tags as |tag|}} + {{discourse-tag tag}} + {{/each}} {{plugin-outlet "full-page-search-category"}}
    diff --git a/app/assets/javascripts/discourse/templates/group-index.hbs b/app/assets/javascripts/discourse/templates/group-index.hbs new file mode 100644 index 0000000000..1641286ad7 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/group-index.hbs @@ -0,0 +1 @@ +{{group-post-stream posts=controller emptyText=emptyText loadMore="loadMore"}} diff --git a/app/assets/javascripts/discourse/templates/group-members.hbs b/app/assets/javascripts/discourse/templates/group-members.hbs new file mode 100644 index 0000000000..41ad1fd922 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/group-members.hbs @@ -0,0 +1,45 @@ +{{#if model}} + {{#if isOwner}} +
    +
    + {{user-selector usernames=usernames placeholderKey="groups.selector_placeholder" id="user-search-selector" name="usernames"}} + {{d-button action="addMembers" class="add" icon="plus" label="groups.add"}} +
    +
    + {{/if}} + + {{#load-more selector=".group-members tr" action="loadMore"}} + + + + + {{#if isOwner}} + + {{/if}} + + {{#each model.members as |m|}} + + + + + {{#if isOwner}} + + {{/if}} + + {{/each}} +
    {{i18n 'last_post'}}{{i18n 'last_seen'}}
    + {{user-info user=m}} + {{#if m.owner}}{{i18n "groups.owner"}}{{/if}} + + {{bound-date m.last_posted_at}} + + {{bound-date m.last_seen_at}} + + {{#unless m.owner}} + + {{/unless}} +
    + {{/load-more}} +{{else}} +
    {{i18n "groups.empty.users"}}
    +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/group/index.hbs b/app/assets/javascripts/discourse/templates/group/index.hbs deleted file mode 100644 index 73ba496fa7..0000000000 --- a/app/assets/javascripts/discourse/templates/group/index.hbs +++ /dev/null @@ -1 +0,0 @@ -{{group-post-stream posts=controller emptyText="groups.empty.posts"}} diff --git a/app/assets/javascripts/discourse/templates/group/members.hbs b/app/assets/javascripts/discourse/templates/group/members.hbs deleted file mode 100644 index 2d3c9d80f5..0000000000 --- a/app/assets/javascripts/discourse/templates/group/members.hbs +++ /dev/null @@ -1,42 +0,0 @@ -{{#if model}} - {{#if isOwner}} -
    -
    - {{user-selector usernames=usernames placeholderKey="groups.selector_placeholder" id="user-search-selector" name="usernames"}} - {{d-button action="addMembers" class="add" icon="plus" label="groups.add"}} -
    -
    - {{/if}} - - - - - {{#if isOwner}} - - {{/if}} - - {{#each model.members as |m|}} - - - - - {{#if isOwner}} - - {{/if}} - - {{/each}} -
    {{i18n 'last_post'}}{{i18n 'last_seen'}}
    - {{user-info user=m}} - {{#if m.owner}}{{i18n "groups.owner"}}{{/if}} - - {{bound-date m.last_posted_at}} - - {{bound-date m.last_seen_at}} - - {{#unless m.owner}} - - {{/unless}} -
    -{{else}} -
    {{i18n "groups.empty.users"}}
    -{{/if}} diff --git a/app/assets/javascripts/discourse/templates/group/mentions.hbs b/app/assets/javascripts/discourse/templates/group/mentions.hbs deleted file mode 100644 index 3fa0965f26..0000000000 --- a/app/assets/javascripts/discourse/templates/group/mentions.hbs +++ /dev/null @@ -1 +0,0 @@ -{{group-post-stream posts=controller emptyText="groups.empty.mentions"}} diff --git a/app/assets/javascripts/discourse/templates/group/messages.hbs b/app/assets/javascripts/discourse/templates/group/messages.hbs deleted file mode 100644 index 9b457e64eb..0000000000 --- a/app/assets/javascripts/discourse/templates/group/messages.hbs +++ /dev/null @@ -1 +0,0 @@ -{{group-post-stream posts=controller emptyText="groups.empty.messages"}} diff --git a/app/assets/javascripts/discourse/templates/group/topics.hbs b/app/assets/javascripts/discourse/templates/group/topics.hbs deleted file mode 100644 index 2a9a4eed96..0000000000 --- a/app/assets/javascripts/discourse/templates/group/topics.hbs +++ /dev/null @@ -1 +0,0 @@ -{{group-post-stream posts=controller emptyText="groups.empty.topics"}} diff --git a/app/assets/javascripts/discourse/templates/header.hbs b/app/assets/javascripts/discourse/templates/header.hbs deleted file mode 100644 index ad1af73529..0000000000 --- a/app/assets/javascripts/discourse/templates/header.hbs +++ /dev/null @@ -1,66 +0,0 @@ -
    -
    - {{home-logo minimized=showExtraInfo}} - {{plugin-outlet "header-after-home-logo"}} - -
    - {{#unless currentUser}} - {{#if showSignUpButton}} - {{d-button action="showCreateAccount" class="btn-primary btn-small sign-up-button" label="sign_up"}} - {{/if}} - {{d-button action="showLogin" class="btn-primary btn-small login-button" icon="user" label="log_in"}} - {{/unless}} - - {{plugin-outlet "header-before-dropdowns"}} - {{user-menu visible=userMenuVisible logoutAction="logout"}} - {{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}} - {{search-menu visible=searchVisible}} -
    - - {{#if showExtraInfo}} - {{header-extra-info topic=topic}} - {{/if}} -
    -
    -{{plugin-outlet "header-under-content"}} diff --git a/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs b/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs index 67b03cfbd3..6efca18268 100644 --- a/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs @@ -11,6 +11,13 @@ {{#if controller.showTopicPostBadges}} {{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}} {{/if}} + {{#if topic.tags}} +
    + {{#each tag in topic.visibleListTags}} + {{discourse-tag tag}} + {{/each}} +
    + {{/if}} {{plugin-outlet "topic-list-tags"}} {{#if expandPinned}} {{raw "list/topic-excerpt" topic=topic}} diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs index 3355aef462..5d1a3c8e1b 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs @@ -1,4 +1,4 @@ -
    +{{#discovery-topics-list model=model refresh="refresh" incomingCount=topicTrackingState.incomingCount}} {{#if top}}
    {{period-chooser period=period action="changePeriod"}} @@ -19,7 +19,7 @@ hideCategory=model.hideCategory topics=model.topics}} {{/if}} -
    +{{/discovery-topics-list}}
    {{conditional-loading-spinner condition=model.loadingMore}} diff --git a/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs b/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs index 0c11907039..aff527a690 100644 --- a/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs @@ -27,6 +27,14 @@
    {{/unless}} + {{#if context.topic.tags}} +
    + {{#each tag in context.topic.visibleListTags}} + {{discourse-tag tag}} + {{/each}} +
    + {{/if}} + {{plugin-outlet "topic-list-tags"}}
    diff --git a/app/assets/javascripts/discourse/templates/modal/change-timestamp.hbs b/app/assets/javascripts/discourse/templates/modal/change-timestamp.hbs index c76c58e124..5114bd9cf9 100644 --- a/app/assets/javascripts/discourse/templates/modal/change-timestamp.hbs +++ b/app/assets/javascripts/discourse/templates/modal/change-timestamp.hbs @@ -1,4 +1,4 @@ -