diff --git a/.eslintignore b/.eslintignore index 54bd9d41ef..1806ae7c09 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,9 +6,6 @@ app/assets/javascripts/ember-addons/ app/assets/javascripts/discourse/lib/autosize.js.es6 lib/javascripts/locale/ lib/javascripts/messageformat.js -lib/javascripts/moment.js -lib/javascripts/moment-timezone-with-data.js -lib/javascripts/moment_locale/ lib/highlight_js/ plugins/**/lib/javascripts/locale public/javascripts/ diff --git a/.rspec b/.rspec index 53607ea52b..49e9bca297 100644 --- a/.rspec +++ b/.rspec @@ -1 +1,3 @@ --colour +--profile +--fail-fast diff --git a/Dangerfile b/Dangerfile index 2f6e4c3071..d1c3326dee 100644 --- a/Dangerfile +++ b/Dangerfile @@ -22,7 +22,10 @@ files = (git.added_files + git.modified_files) .select { |path| !path.start_with?("plugins/") } .select { |path| path.end_with?("es6") || path.end_with?("rb") } +js_test_files = files.select { |path| path.end_with?("-test.js.es6") } + super_offenses = [] +jquery_find_offenses = [] files.each do |path| diff = git.diff_for_file(path) @@ -34,9 +37,26 @@ files.each do |path| end end +js_test_files.each do |path| + diff = git.diff_for_file(path) + + next if !diff + + diff.patch.lines.grep(/^\+\s\s/).each do |added_line| + jquery_find_offenses << path if added_line['this.$('] + end +end + if !super_offenses.empty? warn(%{ When possible use `this._super(...arguments)` instead of `this._super()`\n #{super_offenses.uniq.map { |o| github.html_link(o) }.join("\n")} }) end + +if !jquery_find_offenses.empty? + warn(%{ +Use `find()` instead of `this.$` in js tests`\n +#{jquery_find_offenses.uniq.map { |o| github.html_link(o) }.join("\n")} + }) +end diff --git a/Gemfile b/Gemfile index 1bc558f68d..54797b5cf8 100644 --- a/Gemfile +++ b/Gemfile @@ -44,7 +44,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.77' +gem 'onebox', '1.8.79' gem 'http_accept_language', '~>2.0.5', require: false @@ -65,6 +65,7 @@ gem 'fast_xor', platform: :mri gem 'fastimage' gem 'aws-sdk-s3', require: false +gem 'aws-sdk-sns', require: false gem 'excon', require: false gem 'unf', require: false @@ -201,10 +202,11 @@ gem 'rchardet', require: false if ENV["IMPORT"] == "1" gem 'mysql2' gem 'redcarpet' - gem 'sqlite3', '~> 1.3.13' + gem 'sqlite3', '~> 1.3', '>= 1.3.13' gem 'ruby-bbcode-to-md', git: 'https://github.com/nlalonde/ruby-bbcode-to-md' gem 'reverse_markdown' gem 'tiny_tds' + gem 'csv', '~> 3.0' end gem 'webpush', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 9f6780d367..48f6a2610c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -44,19 +44,22 @@ GEM arel (9.0.0) ast (2.4.0) aws-eventstream (1.0.1) - aws-partitions (1.104.0) - aws-sdk-core (3.27.0) + aws-partitions (1.138.0) + aws-sdk-core (3.46.1) aws-eventstream (~> 1.0) aws-partitions (~> 1.0) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-kms (1.9.0) - aws-sdk-core (~> 3, >= 3.26.0) + aws-sdk-kms (1.13.0) + aws-sdk-core (~> 3, >= 3.39.0) aws-sigv4 (~> 1.0) - aws-sdk-s3 (1.19.0) - aws-sdk-core (~> 3, >= 3.26.0) + aws-sdk-s3 (1.30.1) + aws-sdk-core (~> 3, >= 3.39.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.0) + aws-sdk-sns (1.9.0) + aws-sdk-core (~> 3, >= 3.39.0) + aws-sigv4 (~> 1.0) aws-sigv4 (1.0.3) barber (0.12.0) ember-source (>= 1.0, < 3.1) @@ -183,7 +186,7 @@ GEM logstash-event (1.2.02) logstash-logger (0.26.1) logstash-event (~> 1.2) - logster (2.0.1) + logster (2.1.2) loofah (2.2.3) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -202,7 +205,7 @@ GEM libv8 (>= 6.3) mini_scheduler (0.9.1) sidekiq - mini_sql (0.1.10) + mini_sql (0.2.1) mini_suffix (0.3.0) ffi (~> 1.9) minitest (5.11.3) @@ -214,7 +217,7 @@ GEM multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) - mustache (1.0.5) + mustache (1.1.0) nap (1.1.0) no_proxy_fix (0.1.2) nokogiri (1.10.1) @@ -258,7 +261,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.77) + onebox (1.8.79) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -452,6 +455,7 @@ DEPENDENCIES activesupport (= 5.2.2) annotate aws-sdk-s3 + aws-sdk-sns barber better_errors binding_of_caller @@ -511,7 +515,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.77) + onebox (= 1.8.79) openid-redis-store pg pry-nav diff --git a/README.md b/README.md index a00c5b9643..ff02f5882e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - @@ -10,14 +10,14 @@ Discourse is the 100% open source discussion platform built for the next decade - discussion forum - long-form chat room -To learn more about the philosophy and goals of the project, [visit **discourse.org**](http://www.discourse.org). +To learn more about the philosophy and goals of the project, [visit **discourse.org**](https://www.discourse.org). ## Screenshots Boing Boing - + Mobile @@ -34,7 +34,7 @@ To get your environment setup, follow the community setup guide for your operati If you're familiar with how Rails works and are comfortable setting up your own environment, you can also try out the [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md), which is aimed primarily at Ubuntu and macOS environments. -Before you get started, ensure you have the following minimum versions: [Ruby 2.5+](http://www.ruby-lang.org/en/downloads/), [PostgreSQL 10+](http://www.postgresql.org/download/), [Redis 2.6+](http://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first! +Before you get started, ensure you have the following minimum versions: [Ruby 2.5+](https://www.ruby-lang.org/en/downloads/), [PostgreSQL 10+](https://www.postgresql.org/download/), [Redis 2.6+](https://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first! ## Setting up Discourse @@ -57,8 +57,8 @@ Discourse is built for the *next* 10 years of the Internet, so our requirements - [Ruby on Rails](https://github.com/rails/rails) — Our back end API is a Rails app. It responds to requests RESTfully in JSON. - [Ember.js](https://github.com/emberjs/ember.js) — Our front end is an Ember.js app that communicates with the Rails API. -- [PostgreSQL](http://www.postgresql.org/) — Our main data store is in Postgres. -- [Redis](http://redis.io/) — We use Redis as a cache and for transient data. +- [PostgreSQL](https://www.postgresql.org/) — Our main data store is in Postgres. +- [Redis](https://redis.io/) — We use Redis as a cache and for transient data. Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https://github.com/discourse/discourse/blob/master/Gemfile). @@ -71,11 +71,11 @@ accepts contributions from the public – including you! Before contributing to Discourse: -1. Please read the complete mission statements on [**discourse.org**](http://www.discourse.org). Yes we actually believe this stuff; you should too. -2. Read and sign the [**Electronic Discourse Forums Contribution License Agreement**](http://discourse.org/cla). +1. Please read the complete mission statements on [**discourse.org**](https://www.discourse.org). Yes we actually believe this stuff; you should too. +2. Read and sign the [**Electronic Discourse Forums Contribution License Agreement**](https://www.discourse.org/cla). 3. Dig into [**CONTRIBUTING.MD**](CONTRIBUTING.md), which covers submitting bugs, requesting new features, preparing your code for a pull request, etc. 4. Always strive to collaborate [with mutual respect](https://github.com/discourse/discourse/blob/master/docs/code-of-conduct.md). -5. Not sure what to work on? [**We've got some ideas.**](http://meta.discourse.org/t/so-you-want-to-help-out-with-discourse/3823) +5. Not sure what to work on? [**We've got some ideas.**](https://meta.discourse.org/t/so-you-want-to-help-out-with-discourse/3823) We look forward to seeing your pull requests! @@ -86,7 +86,7 @@ We take security very seriously at Discourse; all our code is 100% open source a ## The Discourse Team -The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/AUTHORS.md). For a complete list of the many individuals that contributed to the design and implementation of Discourse, please refer to [the official Discourse blog](http://blog.discourse.org/2013/02/the-discourse-team/) and [GitHub's list of contributors](https://github.com/discourse/discourse/contributors). +The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/AUTHORS.md). For a complete list of the many individuals that contributed to the design and implementation of Discourse, please refer to [the official Discourse blog](https://blog.discourse.org/2013/02/the-discourse-team/) and [GitHub's list of contributors](https://github.com/discourse/discourse/contributors). ## Copyright / License @@ -97,7 +97,7 @@ Licensed under the GNU General Public License Version 2.0 (or later); you may not use this work except in compliance with the License. You may obtain a copy of the License in the LICENSE file, or at: - http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt + https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -109,4 +109,4 @@ Discourse logo and “Discourse Forum” ®, Civilized Discourse Construction Ki ## Dedication -Discourse is built with [love, Internet style.](http://www.youtube.com/watch?v=Xe1TZaElTAs) +Discourse is built with [love, Internet style.](https://www.youtube.com/watch?v=Xe1TZaElTAs) diff --git a/app/assets/javascripts/admin/components/admin-theme-editor.js.es6 b/app/assets/javascripts/admin/components/admin-theme-editor.js.es6 new file mode 100644 index 0000000000..c9d4c803de --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-theme-editor.js.es6 @@ -0,0 +1,91 @@ +import { default as computed } from "ember-addons/ember-computed-decorators"; +import { fmt } from "discourse/lib/computed"; + +export default Ember.Component.extend({ + @computed("theme.targets", "onlyOverridden", "showAdvanced") + visibleTargets(targets, onlyOverridden, showAdvanced) { + return targets.filter(target => { + if (target.advanced && !showAdvanced) { + return false; + } + if (!onlyOverridden) { + return true; + } + return target.edited; + }); + }, + + @computed("currentTargetName", "onlyOverridden", "theme.fields") + visibleFields(targetName, onlyOverridden, fields) { + fields = fields[targetName]; + if (onlyOverridden) { + fields = fields.filter(field => field.edited); + } + return fields; + }, + + @computed("currentTargetName", "fieldName") + activeSectionMode(targetName, fieldName) { + if (["settings", "translations"].includes(targetName)) return "yaml"; + return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html"; + }, + + @computed("fieldName", "currentTargetName", "theme") + activeSection: { + get(fieldName, target, model) { + return model.getField(target, fieldName); + }, + set(value, fieldName, target, model) { + model.setField(target, fieldName, value); + return value; + } + }, + + editorId: fmt("fieldName", "currentTargetName", "%@|%@"), + + @computed("maximized") + maximizeIcon(maximized) { + return maximized ? "discourse-compress" : "discourse-expand"; + }, + + @computed("currentTargetName", "theme.targets") + showAddField(currentTargetName, targets) { + return targets.find(t => t.name === currentTargetName).customNames; + }, + + @computed("currentTargetName", "fieldName", "theme.theme_fields.@each.error") + error(target, fieldName) { + return this.get("theme").getError(target, fieldName); + }, + + actions: { + toggleShowAdvanced() { + this.toggleProperty("showAdvanced"); + }, + + toggleAddField() { + this.toggleProperty("addingField"); + }, + + cancelAddField() { + this.set("addingField", false); + }, + + addField(name) { + if (!name) return; + name = name.replace(/\W/g, ""); + this.get("theme").setField(this.get("currentTargetName"), name, ""); + this.setProperties({ newFieldName: "", addingField: false }); + this.fieldAdded(this.get("currentTargetName"), name); + }, + + toggleMaximize: function() { + this.toggleProperty("maximized"); + Ember.run.next(() => this.appEvents.trigger("ace:resize")); + }, + + onlyOverriddenChanged(value) { + this.onlyOverriddenChanged(value); + } + } +}); diff --git a/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 b/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 index bdb22e258c..c0ce3c9bb0 100644 --- a/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 +++ b/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 @@ -2,6 +2,12 @@ import UserField from "admin/models/user-field"; import { bufferedProperty } from "discourse/mixins/buffered-content"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { propertyEqual } from "discourse/lib/computed"; +import { i18n } from "discourse/lib/computed"; +import { + default as computed, + observes, + on +} from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend(bufferedProperty("userField"), { editing: Ember.computed.empty("userField.id"), @@ -10,58 +16,56 @@ export default Ember.Component.extend(bufferedProperty("userField"), { cantMoveUp: propertyEqual("userField", "firstField"), cantMoveDown: propertyEqual("userField", "lastField"), - userFieldsDescription: function() { - return I18n.t("admin.user_fields.description"); - }.property(), + userFieldsDescription: i18n("admin.user_fields.description"), - bufferedFieldType: function() { - return UserField.fieldTypeById(this.get("buffered.field_type")); - }.property("buffered.field_type"), + @computed("buffered.field_type") + bufferedFieldType(fieldType) { + return UserField.fieldTypeById(fieldType); + }, - _focusOnEdit: function() { + @on("didInsertElement") + @observes("editing") + _focusOnEdit() { if (this.get("editing")) { Ember.run.scheduleOnce("afterRender", this, "_focusName"); } - } - .observes("editing") - .on("didInsertElement"), + }, - _focusName: function() { + _focusName() { $(".user-field-name").select(); }, - fieldName: function() { - return UserField.fieldTypeById(this.get("userField.field_type")).get( - "name" - ); - }.property("userField.field_type"), + @computed("userField.field_type") + fieldName(fieldType) { + return UserField.fieldTypeById(fieldType).get("name"); + }, - flags: function() { - const ret = []; - if (this.get("userField.editable")) { - ret.push(I18n.t("admin.user_fields.editable.enabled")); - } - if (this.get("userField.required")) { - ret.push(I18n.t("admin.user_fields.required.enabled")); - } - if (this.get("userField.show_on_profile")) { - ret.push(I18n.t("admin.user_fields.show_on_profile.enabled")); - } - if (this.get("userField.show_on_user_card")) { - ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled")); - } - - return ret.join(", "); - }.property( + @computed( "userField.editable", "userField.required", "userField.show_on_profile", "userField.show_on_user_card" - ), + ) + flags(editable, required, showOnProfile, showOnUserCard) { + const ret = []; + if (editable) { + ret.push(I18n.t("admin.user_fields.editable.enabled")); + } + if (required) { + ret.push(I18n.t("admin.user_fields.required.enabled")); + } + if (showOnProfile) { + ret.push(I18n.t("admin.user_fields.show_on_profile.enabled")); + } + if (showOnUserCard) { + ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled")); + } + + return ret.join(", "); + }, actions: { save() { - const self = this; const buffered = this.get("buffered"); const attrs = buffered.getProperties( "name", @@ -76,9 +80,9 @@ export default Ember.Component.extend(bufferedProperty("userField"), { this.get("userField") .save(attrs) - .then(function() { - self.set("editing", false); - self.commitBuffer(); + .then(() => { + this.set("editing", false); + this.commitBuffer(); }) .catch(popupAjaxError); }, diff --git a/app/assets/javascripts/admin/components/install-theme-item.js.es6 b/app/assets/javascripts/admin/components/install-theme-item.js.es6 new file mode 100644 index 0000000000..c1f3f57a8c --- /dev/null +++ b/app/assets/javascripts/admin/components/install-theme-item.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + classNames: ["install-theme-item"] +}); diff --git a/app/assets/javascripts/admin/components/themes-list.js.es6 b/app/assets/javascripts/admin/components/themes-list.js.es6 index 5600600588..b97ecfd9b6 100644 --- a/app/assets/javascripts/admin/components/themes-list.js.es6 +++ b/app/assets/javascripts/admin/components/themes-list.js.es6 @@ -61,18 +61,6 @@ export default Ember.Component.extend({ } }, - didRender() { - this._super(...arguments); - - // hide scrollbar - const $container = this.$(".themes-list-container"); - const containerNode = $container[0]; - if (containerNode) { - const width = containerNode.offsetWidth - containerNode.clientWidth; - $container.css("width", `calc(100% + ${width}px)`); - } - }, - actions: { changeView(newTab) { if (newTab !== this.get("currentTab")) { diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 index 88ab2b8846..faf9d29a60 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 @@ -1,21 +1,25 @@ import showModal from "discourse/lib/show-modal"; +import { default as computed } from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend({ - baseColorScheme: function() { + @computed("model.@each.id") + baseColorScheme() { return this.get("model").findBy("is_base", true); - }.property("model.@each.id"), + }, - baseColorSchemes: function() { + @computed("model.@each.id") + baseColorSchemes() { return this.get("model").filterBy("is_base", true); - }.property("model.@each.id"), + }, - baseColors: function() { - var baseColorsHash = Ember.Object.create({}); - this.get("baseColorScheme.colors").forEach(color => { + @computed("baseColorScheme") + baseColors(baseColorScheme) { + const baseColorsHash = Ember.Object.create({}); + baseColorScheme.get("colors").forEach(color => { baseColorsHash.set(color.get("name"), color); }); return baseColorsHash; - }.property("baseColorScheme"), + }, actions: { newColorSchemeWithBase(baseKey) { @@ -24,8 +28,10 @@ export default Ember.Controller.extend({ baseKey ); const newColorScheme = Ember.copy(base, true); - newColorScheme.set("name", I18n.t("admin.customize.colors.new_name")); - newColorScheme.set("base_scheme_id", base.get("base_scheme_id")); + newColorScheme.setProperties({ + name: I18n.t("admin.customize.colors.new_name"), + base_scheme_id: base.get("base_scheme_id") + }); newColorScheme.save().then(() => { this.get("model").pushObject(newColorScheme); newColorScheme.set("savingStatus", null); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 index 807c21e768..9c33c27392 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 @@ -1,163 +1,28 @@ import { url } from "discourse/lib/computed"; -import { - default as computed, - observes -} from "ember-addons/ember-computed-decorators"; +import { default as computed } from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend({ section: null, currentTarget: 0, maximized: false, previewUrl: url("model.id", "/admin/themes/%@/preview"), - + showAdvanced: false, editRouteName: "adminCustomizeThemes.edit", - - targets: [ - { id: 0, name: "common" }, - { id: 1, name: "desktop" }, - { id: 2, name: "mobile" }, - { id: 3, name: "settings" }, - { id: 4, name: "translations" } - ], - - fieldsForTarget: function(target) { - const common = [ - "scss", - "head_tag", - "header", - "after_header", - "body_tag", - "footer" - ]; - switch (target) { - case "common": - return [...common, "embedded_scss"]; - case "desktop": - return common; - case "mobile": - return common; - case "settings": - return ["yaml"]; - } - }, - - @computed("onlyOverridden") - showCommon() { - return this.shouldShow("common"); - }, - - @computed("onlyOverridden") - showDesktop() { - return this.shouldShow("desktop"); - }, - - @computed("onlyOverridden") - showMobile() { - return this.shouldShow("mobile"); - }, - - @observes("onlyOverridden") - onlyOverriddenChanged() { - if (this.get("onlyOverridden")) { - if ( - !this.get("model").hasEdited( - this.get("currentTargetName"), - this.get("fieldName") - ) - ) { - let target = - (this.get("showCommon") && "common") || - (this.get("showDesktop") && "desktop") || - (this.get("showMobile") && "mobile"); - - let fields = this.get("model.theme_fields"); - let field = fields && fields.find(f => f.target === target); - this.replaceRoute( - this.get("editRouteName"), - this.get("model.id"), - target, - field && field.name - ); - } - } - }, - - shouldShow(target) { - if (!this.get("onlyOverridden")) { - return true; - } - return this.get("model").hasEdited(target); - }, + showRouteName: "adminCustomizeThemes.show", setTargetName: function(name) { - const target = this.get("targets").find(t => t.name === name); + const target = this.get("model.targets").find(t => t.name === name); this.set("currentTarget", target && target.id); }, @computed("currentTarget") currentTargetName(id) { - const target = this.get("targets").find(t => t.id === parseInt(id, 10)); + const target = this.get("model.targets").find( + t => t.id === parseInt(id, 10) + ); return target && target.name; }, - @computed("fieldName") - activeSectionMode(fieldName) { - if (fieldName === "yaml") return "yaml"; - return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html"; - }, - - @computed("currentTargetName", "fieldName", "saving") - error(target, fieldName) { - return this.get("model").getError(target, fieldName); - }, - - @computed("fieldName", "currentTargetName") - editorId(fieldName, currentTarget) { - return fieldName + "|" + currentTarget; - }, - - @computed("fieldName", "currentTargetName", "model") - activeSection: { - get(fieldName, target, model) { - return model.getField(target, fieldName); - }, - set(value, fieldName, target, model) { - model.setField(target, fieldName, value); - return value; - } - }, - - @computed("currentTargetName", "onlyOverridden") - fields(target, onlyOverridden) { - let fields = this.fieldsForTarget(target); - - if (onlyOverridden) { - const model = this.get("model"); - const targetName = this.get("currentTargetName"); - fields = fields.filter(name => model.hasEdited(targetName, name)); - } - - return fields.map(name => { - let hash = { - key: `admin.customize.theme.${name}.text`, - name: name - }; - - if (name.indexOf("_tag") > 0) { - hash.icon = "file-text-o"; - } - - hash.title = I18n.t(`admin.customize.theme.${name}.title`); - - return hash; - }); - }, - - @computed("maximized") - maximizeIcon(maximized) { - return maximized ? "discourse-compress" : "discourse-expand"; - }, - @computed("model.isSaving") saveButtonText(isSaving) { return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save"); @@ -178,11 +43,36 @@ export default Ember.Controller.extend({ }); }, - toggleMaximize: function() { - this.toggleProperty("maximized"); - Ember.run.next(() => { - this.appEvents.trigger("ace:resize"); - }); + fieldAdded(target, name) { + this.replaceRoute( + this.get("editRouteName"), + this.get("model.id"), + target, + name + ); + }, + + onlyOverriddenChanged(onlyShowOverridden) { + if (onlyShowOverridden) { + if ( + !this.get("model").hasEdited( + this.get("currentTargetName"), + this.get("fieldName") + ) + ) { + let firstTarget = this.get("model.targets").find(t => t.edited); + let firstField = this.get(`model.fields.${firstTarget.name}`).find( + f => f.edited + ); + + this.replaceRoute( + this.get("editRouteName"), + this.get("model.id"), + firstTarget.name, + firstField.name + ); + } + } } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 index f572813c8a..b54d1e9b09 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 @@ -147,6 +147,10 @@ export default Ember.Controller.extend({ "scss" ); }, + sourceIsHttp: Ember.computed.match( + "model.remote_theme.remote_url", + /^http(s)?:\/\// + ), actions: { updateToLatest() { this.set("updatingRemote", true); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 index 641ec62826..f38ae3ab74 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 @@ -12,5 +12,10 @@ export default Ember.Controller.extend({ @computed("model", "model.@each.component") childThemes(themes) { return themes.filter(t => t.get("component")); + }, + + @computed("model", "model.@each.component") + installedThemes(themes) { + return themes.map(t => t.name); } }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 index c2ec582af2..535fa4bca1 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 @@ -1,9 +1,8 @@ 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}") + this.loadLogs(); + }, 250).observes("filter.{status,user,address,type}") }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-incomings.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-incomings.js.es6 deleted file mode 100644 index 9dc59589fb..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-email-incomings.js.es6 +++ /dev/null @@ -1,25 +0,0 @@ -import IncomingEmail from "admin/models/incoming-email"; - -export default Ember.Controller.extend({ - loading: false, - - actions: { - loadMore() { - if (this.get("loading") || this.get("model.allLoaded")) { - return; - } - this.set("loading", true); - - IncomingEmail.findAll(this.get("filter"), this.get("model.length")) - .then(incoming => { - if (incoming.length < 50) { - this.get("model").set("allLoaded", true); - } - this.get("model").addObjects(incoming); - }) - .finally(() => { - this.set("loading", false); - }); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 index cbbd1563b0..0c36630625 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 @@ -33,7 +33,7 @@ export default Ember.Controller.extend({ data: { email_address: this.get("testEmailAddress") } }) .then(response => - this.set("sentTestEmailMessage", response.send_test_email_message) + this.set("sentTestEmailMessage", response.sent_test_email_message) ) .catch(e => { if (e.responseJSON && e.responseJSON.errors) { diff --git a/app/assets/javascripts/admin/controllers/admin-email-logs.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-logs.js.es6 index a86ae42111..b08a5115bd 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-logs.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-logs.js.es6 @@ -3,23 +3,34 @@ import EmailLog from "admin/models/email-log"; export default Ember.Controller.extend({ loading: false, + loadLogs(sourceModel, loadMore) { + if ((loadMore && this.get("loading")) || this.get("model.allLoaded")) { + return; + } + + this.set("loading", true); + + sourceModel = sourceModel || EmailLog; + + return sourceModel + .findAll(this.get("filter"), loadMore ? this.get("model.length") : null) + .then(logs => { + if (this.get("model") && loadMore && logs.length < 50) { + this.get("model").set("allLoaded", true); + } + + if (this.get("model") && loadMore) { + this.get("model").addObjects(logs); + } else { + this.set("model", logs); + } + }) + .finally(() => this.set("loading", false)); + }, + actions: { loadMore() { - if (this.get("loading") || this.get("model.allLoaded")) { - return; - } - - this.set("loading", true); - return EmailLog.findAll(this.get("filter"), this.get("model.length")) - .then(logs => { - if (logs.length < 50) { - this.get("model").set("allLoaded", true); - } - this.get("model").addObjects(logs); - }) - .finally(() => { - this.set("loading", false); - }); + this.loadLogs(EmailLog, true); } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-received.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-received.js.es6 index a5f240fdbf..7659e61edd 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-received.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-received.js.es6 @@ -1,11 +1,15 @@ -import AdminEmailIncomingsController from "admin/controllers/admin-email-incomings"; +import AdminEmailLogsController from "admin/controllers/admin-email-logs"; import debounce from "discourse/lib/debounce"; import IncomingEmail from "admin/models/incoming-email"; -export default AdminEmailIncomingsController.extend({ +export default AdminEmailLogsController.extend({ filterIncomingEmails: debounce(function() { - IncomingEmail.findAll(this.get("filter")).then(incomings => - this.set("model", incomings) - ); - }, 250).observes("filter.{from,to,subject}") + this.loadLogs(IncomingEmail); + }, 250).observes("filter.{status,from,to,subject}"), + + actions: { + loadMore() { + this.loadLogs(IncomingEmail, true); + } + } }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-rejected.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-rejected.js.es6 index b9341dd7e3..602bb052ce 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-rejected.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-rejected.js.es6 @@ -1,11 +1,15 @@ -import AdminEmailIncomingsController from "admin/controllers/admin-email-incomings"; +import AdminEmailLogsController from "admin/controllers/admin-email-logs"; import debounce from "discourse/lib/debounce"; import IncomingEmail from "admin/models/incoming-email"; -export default AdminEmailIncomingsController.extend({ +export default AdminEmailLogsController.extend({ filterIncomingEmails: debounce(function() { - IncomingEmail.findAll(this.get("filter")).then(incomings => - this.set("model", incomings) - ); - }, 250).observes("filter.{from,to,subject,error}") + this.loadLogs(IncomingEmail); + }, 250).observes("filter.{status,from,to,subject,error}"), + + actions: { + loadMore() { + this.loadLogs(IncomingEmail, true); + } + } }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 index 691b017c6f..83f52d3510 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 @@ -1,9 +1,8 @@ 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,reply_key}") + this.loadLogs(); + }, 250).observes("filter.{status,user,address,type,reply_key}") }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 index c2ec582af2..535fa4bca1 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 @@ -1,9 +1,8 @@ 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}") + this.loadLogs(); + }, 250).observes("filter.{status,user,address,type}") }); diff --git a/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 index a62c74001a..7ef5469649 100644 --- a/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 @@ -1,5 +1,4 @@ let lastSearch; -let lastOverridden; export default Ember.Controller.extend({ searching: false, @@ -8,7 +7,7 @@ export default Ember.Controller.extend({ queryParams: ["q", "overridden"], q: null, - overridden: null, + overridden: false, _performSearch() { this.store @@ -24,14 +23,18 @@ export default Ember.Controller.extend({ this.transitionToRoute("adminSiteText.edit", siteText.get("id")); }, - search(overridden) { - if (typeof overridden === "boolean") this.set("overridden", overridden); + toggleOverridden() { + this.toggleProperty("overridden"); + this.set("searching", true); + Ember.run.debounce(this, this._performSearch, 400); + }, + + search() { const q = this.get("q"); - if (q !== lastSearch || overridden !== lastOverridden) { + if (q !== lastSearch) { this.set("searching", true); Ember.run.debounce(this, this._performSearch, 400); lastSearch = q; - lastOverridden = overridden; } } } 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 b71b51631c..8c3b95bd13 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -48,7 +48,7 @@ export default Ember.Controller.extend(CanCheckEmails, { return automaticGroups .map(group => { const name = Ember.String.htmlSafe(group.name); - return `${name}`; + return `${name}`; }) .join(", "); }, @@ -143,9 +143,6 @@ export default Ember.Controller.extend(CanCheckEmails, { resetBounceScore() { return this.get("model").resetBounceScore(); }, - refreshBrowsers() { - return this.get("model").refreshBrowsers(); - }, approve() { return this.get("model").approve(); }, diff --git a/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 index 16b148f951..f80222cdc5 100644 --- a/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 @@ -1,12 +1,11 @@ import debounce from "discourse/lib/debounce"; import { i18n } from "discourse/lib/computed"; import AdminUser from "admin/models/admin-user"; -import { observes } from "ember-addons/ember-computed-decorators"; import CanCheckEmails from "discourse/mixins/can-check-emails"; export default Ember.Controller.extend(CanCheckEmails, { + model: null, query: null, - queryParams: ["order", "ascending"], order: null, ascending: null, showEmails: false, @@ -47,8 +46,7 @@ export default Ember.Controller.extend(CanCheckEmails, { this._refreshUsers(); }, 250).observes("listFilter"), - @observes("order", "ascending") - _refreshUsers: function() { + _refreshUsers() { this.set("refreshing", true); AdminUser.findAll(this.get("query"), { @@ -57,12 +55,8 @@ export default Ember.Controller.extend(CanCheckEmails, { order: this.get("order"), ascending: this.get("ascending") }) - .then(result => { - this.set("model", result); - }) - .finally(() => { - this.set("refreshing", false); - }); + .then(result => this.set("model", result)) + .finally(() => this.set("refreshing", false)); }, actions: { @@ -95,7 +89,7 @@ export default Ember.Controller.extend(CanCheckEmails, { showEmails: function() { this.set("showEmails", true); - this._refreshUsers(true); + this._refreshUsers(); } } }); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 index 2702362156..045a96c2aa 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 @@ -1,3 +1,4 @@ +import { default as computed } from "ember-addons/ember-computed-decorators"; import { escapeExpression } from "discourse/lib/utilities"; export default Ember.Controller.extend({ @@ -5,43 +6,42 @@ export default Ember.Controller.extend({ errors: Ember.computed.alias("model.errors"), count: Ember.computed.alias("model.grant_count"), - count_warning: function() { - if (this.get("count") <= 10) { - return this.get("sample.length") !== this.get("count"); + @computed("count", "sample.length") + countWarning(count, sampleLength) { + if (count <= 10) { + return sampleLength !== count; } else { - return this.get("sample.length") !== 10; + return sampleLength !== 10; } - }.property("count", "sample.length"), + }, - has_query_plan: function() { - return !!this.get("model.query_plan"); - }.property("model.query_plan"), + @computed("model.query_plan") + hasQueryPlan(queryPlan) { + return !!queryPlan; + }, - query_plan_html: function() { - var raw = this.get("model.query_plan"), - returned = "
";
+  @computed("model.query_plan")
+  queryPlanHtml(queryPlan) {
+    let output = `
`;
 
-    raw.forEach(linehash => {
-      returned += escapeExpression(linehash["QUERY PLAN"]);
-      returned += "
"; + queryPlan.forEach(linehash => { + output += escapeExpression(linehash["QUERY PLAN"]); + output += "
"; }); - returned += "
"; - return returned; - }.property("model.query_plan"), + output += "
"; + return output; + }, - processed_sample: Ember.computed.map("model.sample", function(grant) { - var i18nKey = "admin.badges.preview.grant.with", - i18nParams = { username: escapeExpression(grant.username) }; + processedSample: Ember.computed.map("model.sample", grant => { + let i18nKey = "admin.badges.preview.grant.with"; + const i18nParams = { username: escapeExpression(grant.username) }; if (grant.post_id) { i18nKey += "_post"; - i18nParams.link = - "" + - Handlebars.Utils.escapeExpression(grant.title) + - ""; + i18nParams.link = ` + ${Handlebars.Utils.escapeExpression(grant.title)} + `; } if (grant.granted_at) { diff --git a/app/assets/javascripts/admin/controllers/modals/admin-create-theme.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-create-theme.js.es6 deleted file mode 100644 index 4b25e006c2..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-create-theme.js.es6 +++ /dev/null @@ -1,65 +0,0 @@ -import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { default as computed } from "ember-addons/ember-computed-decorators"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { THEMES, COMPONENTS } from "admin/models/theme"; - -const MIN_NAME_LENGTH = 4; - -export default Ember.Controller.extend(ModalFunctionality, { - saving: false, - triggerError: false, - themesController: Ember.inject.controller("adminCustomizeThemes"), - types: [ - { name: I18n.t("admin.customize.theme.theme"), value: THEMES }, - { name: I18n.t("admin.customize.theme.component"), value: COMPONENTS } - ], - - @computed("triggerError", "nameTooShort") - showError(trigger, tooShort) { - return trigger && tooShort; - }, - - @computed("name") - nameTooShort(name) { - return !name || name.length < MIN_NAME_LENGTH; - }, - - @computed("component") - placeholder(component) { - if (component) { - return I18n.t("admin.customize.theme.component_name"); - } else { - return I18n.t("admin.customize.theme.theme_name"); - } - }, - - @computed("themesController.currentTab") - selectedType(tab) { - return tab; - }, - - @computed("selectedType") - component(type) { - return type === COMPONENTS; - }, - - actions: { - createTheme() { - if (this.get("nameTooShort")) { - this.set("triggerError", true); - return; - } - - this.set("saving", true); - const theme = this.store.createRecord("theme"); - theme - .save({ name: this.get("name"), component: this.get("component") }) - .then(() => { - this.get("themesController").send("addTheme", theme); - this.send("closeModal"); - }) - .catch(popupAjaxError) - .finally(() => this.set("saving", false)); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 index 1a195149b3..63dac2a1ec 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 @@ -1,22 +1,24 @@ import { ajax } from "discourse/lib/ajax"; import ModalFunctionality from "discourse/mixins/modal-functionality"; +import { observes } from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend(ModalFunctionality, { - modelChanged: function() { + @observes("model") + modelChanged() { const model = this.get("model"); const copy = Ember.A(); const store = this.store; if (model) { - model.forEach(function(o) { - copy.pushObject(store.createRecord("badge-grouping", o)); - }); + model.forEach(o => + copy.pushObject(store.createRecord("badge-grouping", o)) + ); } this.set("workingCopy", copy); - }.observes("model"), + }, - moveItem: function(item, delta) { + moveItem(item, delta) { const copy = this.get("workingCopy"); const index = copy.indexOf(item); if (index + delta < 0 || index + delta >= copy.length) { @@ -28,60 +30,51 @@ export default Ember.Controller.extend(ModalFunctionality, { }, actions: { - up: function(item) { + up(item) { this.moveItem(item, -1); }, - down: function(item) { + down(item) { this.moveItem(item, 1); }, - delete: function(item) { + delete(item) { this.get("workingCopy").removeObject(item); }, - cancel: function() { - this.set("model", null); - this.set("workingCopy", null); + cancel() { + this.setProperties({ model: null, workingCopy: null }); this.send("closeModal"); }, - edit: function(item) { + edit(item) { item.set("editing", true); }, - save: function(item) { + save(item) { item.set("editing", false); }, - add: function() { + add() { const obj = this.store.createRecord("badge-grouping", { editing: true, name: I18n.t("admin.badges.badge_grouping") }); this.get("workingCopy").pushObject(obj); }, - saveAll: function() { - const self = this; - var items = this.get("workingCopy"); - const groupIds = items.map(function(i) { - return i.get("id") || -1; - }); - const names = items.map(function(i) { - return i.get("name"); - }); + saveAll() { + let items = this.get("workingCopy"); + const groupIds = items.map(i => i.get("id") || -1); + const names = items.map(i => i.get("name")); ajax("/admin/badges/badge_groupings", { - data: { ids: groupIds, names: names }, + data: { ids: groupIds, names }, method: "POST" }).then( - function(data) { - items = self.get("model"); + data => { + items = this.get("model"); items.clear(); - data.badge_groupings.forEach(function(g) { - items.pushObject(self.store.createRecord("badge-grouping", g)); + data.badge_groupings.forEach(g => { + items.pushObject(this.store.createRecord("badge-grouping", g)); }); - self.set("model", null); - self.set("workingCopy", null); - self.send("closeModal"); + this.setProperties({ model: null, workingCopy: null }); + this.send("closeModal"); }, - function() { - bootbox.alert(I18n.t("generic_error")); - } + () => bootbox.alert(I18n.t("generic_error")) ); } } diff --git a/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 deleted file mode 100644 index 75514b409b..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 +++ /dev/null @@ -1,89 +0,0 @@ -import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { ajax } from "discourse/lib/ajax"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { - default as computed, - observes -} from "ember-addons/ember-computed-decorators"; - -export default Ember.Controller.extend(ModalFunctionality, { - local: Ember.computed.equal("selection", "local"), - remote: Ember.computed.equal("selection", "remote"), - selection: "local", - adminCustomizeThemes: Ember.inject.controller(), - loading: false, - keyGenUrl: "/admin/themes/generate_key_pair", - importUrl: "/admin/themes/import", - checkPrivate: Ember.computed.match("uploadUrl", /^git/), - localFile: null, - uploadUrl: null, - urlPlaceholder: "https://github.com/discourse/sample_theme", - - @computed("loading", "remote", "uploadUrl", "local", "localFile") - importDisabled(isLoading, isRemote, uploadUrl, isLocal, localFile) { - return isLoading || (isRemote && !uploadUrl) || (isLocal && !localFile); - }, - - @observes("privateChecked") - privateWasChecked() { - this.get("privateChecked") - ? this.set("urlPlaceholder", "git@github.com:discourse/sample_theme.git") - : this.set("urlPlaceholder", "https://github.com/discourse/sample_theme"); - - const checked = this.get("privateChecked"); - if (checked && !this._keyLoading) { - this._keyLoading = true; - ajax(this.get("keyGenUrl"), { method: "POST" }) - .then(pair => { - this.set("privateKey", pair.private_key); - this.set("publicKey", pair.public_key); - }) - .catch(popupAjaxError) - .finally(() => { - this._keyLoading = false; - }); - } - }, - - actions: { - uploadLocaleFile() { - this.set("localFile", $("#file-input")[0].files[0]); - }, - - importTheme() { - let options = { - type: "POST" - }; - - if (this.get("local")) { - options.processData = false; - options.contentType = false; - options.data = new FormData(); - options.data.append("theme", this.get("localFile")); - } else { - options.data = { - remote: this.get("uploadUrl"), - branch: this.get("branch") - }; - - if (this.get("privateChecked")) { - options.data.private_key = this.get("privateKey"); - } - } - - this.set("loading", true); - ajax(this.get("importUrl"), options) - .then(result => { - const theme = this.store.createRecord("theme", result.theme); - this.get("adminCustomizeThemes").send("addTheme", theme); - this.send("closeModal"); - }) - .then(() => { - this.set("privateKey", null); - this.set("publicKey", null); - }) - .catch(popupAjaxError) - .finally(() => this.set("loading", false)); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-install-theme.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-install-theme.js.es6 new file mode 100644 index 0000000000..d1e9fc71a3 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-install-theme.js.es6 @@ -0,0 +1,290 @@ +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { + default as computed, + observes +} from "ember-addons/ember-computed-decorators"; +import { THEMES, COMPONENTS } from "admin/models/theme"; + +const MIN_NAME_LENGTH = 4; + +// TODO: use a central repository for themes/components +const POPULAR_THEMES = [ + { + name: "Graceful", + value: "https://github.com/discourse/graceful", + preview: "https://theme-creator.discourse.org/theme/awesomerobot/graceful", + description: "A light and graceful theme for Discourse.", + meta_url: + "https://meta.discourse.org/t/a-graceful-theme-for-discourse/93040" + }, + { + name: "Material Design Theme", + value: "https://github.com/discourse/material-design-stock-theme", + preview: "https://newmaterial.trydiscourse.com", + description: + "Inspired by Material Design, this theme comes with several color palettes (incl. a dark one).", + meta_url: "https://meta.discourse.org/t/material-design-stock-theme/47142" + }, + { + name: "Minima", + value: "https://github.com/discourse/minima", + preview: "https://theme-creator.discourse.org/theme/awesomerobot/minima", + description: "A minimal theme with reduced UI elements and focus on text.", + meta_url: + "https://meta.discourse.org/t/minima-a-minimal-theme-for-discourse/108178" + }, + { + name: "Sam's Simple Theme", + value: "https://github.com/discourse/discourse-simple-theme", + preview: "https://theme-creator.discourse.org/theme/sam/simple", + description: + "Simplified front page design with classic colors and typography.", + meta_url: + "https://meta.discourse.org/t/sams-personal-minimal-topic-list-design/23552" + }, + { + name: "Vincent", + value: "https://github.com/discourse/discourse-vincent-theme", + preview: "https://theme-creator.discourse.org/theme/awesomerobot/vincent", + description: "An elegant dark theme with a few color palettes.", + meta_url: "https://meta.discourse.org/t/discourse-vincent-theme/76662" + }, + { + name: "Alternative Logos", + value: "https://github.com/discourse/discourse-alt-logo", + description: "Add alternative logos for dark / light themes.", + meta_url: + "https://meta.discourse.org/t/alternative-logo-for-dark-themes/88502", + component: true + }, + { + name: "Brand Header Theme Component", + value: "https://github.com/discourse/discourse-brand-header", + description: + "Add an extra top header with your logo, navigation links and social icons.", + meta_url: "https://meta.discourse.org/t/brand-header-theme-component/77977", + component: true + }, + { + name: "Custom Header Links", + value: "https://github.com/discourse/discourse-custom-header-links", + preview: + "https://theme-creator.discourse.org/theme/Johani/custom-header-links", + description: "Easily add custom text-based links to the header.", + meta_url: "https://meta.discourse.org/t/custom-header-links/90588", + component: true + }, + { + name: "Category Banners", + value: "https://github.com/discourse/discourse-category-banners", + preview: + "https://theme-creator.discourse.org/theme/awesomerobot/discourse-category-banners", + description: + "Show banners on category pages using your existing category details.", + meta_url: "https://meta.discourse.org/t/discourse-category-banners/86241", + component: true + }, + { + name: "Hamburger Theme Selector", + value: "https://github.com/discourse/discourse-hamburger-theme-selector", + description: + "Displays a theme selector in the hamburger menu provided there is more than one user-selectable theme.", + meta_url: "https://meta.discourse.org/t/hamburger-theme-selector/61210", + component: true + }, + { + name: "Header submenus", + value: "https://github.com/discourse/discourse-header-submenus", + preview: "https://theme-creator.discourse.org/theme/Johani/header-submenus", + description: "Lets you build a header menu with submenus (dropdowns).", + meta_url: "https://meta.discourse.org/t/header-submenus/94584", + component: true + } +]; + +export default Ember.Controller.extend(ModalFunctionality, { + popular: Ember.computed.equal("selection", "popular"), + local: Ember.computed.equal("selection", "local"), + remote: Ember.computed.equal("selection", "remote"), + create: Ember.computed.equal("selection", "create"), + selection: "popular", + adminCustomizeThemes: Ember.inject.controller(), + loading: false, + keyGenUrl: "/admin/themes/generate_key_pair", + importUrl: "/admin/themes/import", + recordType: "theme", + checkPrivate: Ember.computed.match("uploadUrl", /^git/), + localFile: null, + uploadUrl: null, + urlPlaceholder: "https://github.com/discourse/sample_theme", + advancedVisible: false, + themesController: Ember.inject.controller("adminCustomizeThemes"), + createTypes: [ + { name: I18n.t("admin.customize.theme.theme"), value: THEMES }, + { name: I18n.t("admin.customize.theme.component"), value: COMPONENTS } + ], + selectedType: Ember.computed.alias("themesController.currentTab"), + component: Ember.computed.equal("selectedType", COMPONENTS), + + @computed("themesController.installedThemes") + themes(installedThemes) { + return POPULAR_THEMES.map(t => { + if (installedThemes.includes(t.name)) { + Ember.set(t, "installed", true); + } + return t; + }); + }, + + @computed( + "loading", + "remote", + "uploadUrl", + "local", + "localFile", + "create", + "nameTooShort" + ) + installDisabled( + isLoading, + isRemote, + uploadUrl, + isLocal, + localFile, + isCreate, + nameTooShort + ) { + return ( + isLoading || + (isRemote && !uploadUrl) || + (isLocal && !localFile) || + (isCreate && nameTooShort) + ); + }, + + @observes("privateChecked") + privateWasChecked() { + this.get("privateChecked") + ? this.set("urlPlaceholder", "git@github.com:discourse/sample_theme.git") + : this.set("urlPlaceholder", "https://github.com/discourse/sample_theme"); + + const checked = this.get("privateChecked"); + if (checked && !this._keyLoading) { + this._keyLoading = true; + ajax(this.get("keyGenUrl"), { method: "POST" }) + .then(pair => { + this.setProperties({ + privateKey: pair.private_key, + publicKey: pair.public_key + }); + }) + .catch(popupAjaxError) + .finally(() => { + this._keyLoading = false; + }); + } + }, + + @computed("name") + nameTooShort(name) { + return !name || name.length < MIN_NAME_LENGTH; + }, + + @computed("component") + placeholder(component) { + if (component) { + return I18n.t("admin.customize.theme.component_name"); + } else { + return I18n.t("admin.customize.theme.theme_name"); + } + }, + + @computed("selection") + submitLabel(selection) { + return `admin.customize.theme.${ + selection === "create" ? "create" : "install" + }`; + }, + + @computed("privateChecked", "checkPrivate", "publicKey") + showPublicKey(privateChecked, checkPrivate, publicKey) { + return privateChecked && checkPrivate && publicKey; + }, + + actions: { + uploadLocaleFile() { + this.set("localFile", $("#file-input")[0].files[0]); + }, + + toggleAdvanced() { + this.toggleProperty("advancedVisible"); + }, + + installThemeFromList(url) { + this.set("uploadUrl", url); + this.send("installTheme"); + }, + + installTheme() { + if (this.get("create")) { + this.set("loading", true); + const theme = this.store.createRecord(this.get("recordType")); + theme + .save({ name: this.get("name"), component: this.get("component") }) + .then(() => { + this.get("themesController").send("addTheme", theme); + this.send("closeModal"); + }) + .catch(popupAjaxError) + .finally(() => this.set("loading", false)); + + return; + } + + let options = { + type: "POST" + }; + + if (this.get("local")) { + options.processData = false; + options.contentType = false; + options.data = new FormData(); + options.data.append("theme", this.get("localFile")); + } + + if (this.get("remote") || this.get("popular")) { + options.data = { + remote: this.get("uploadUrl"), + branch: this.get("branch") + }; + + if (this.get("privateChecked")) { + options.data.private_key = this.get("privateKey"); + } + } + + if (this.get("model.user_id")) { + // Used by theme-creator + options.data["user_id"] = this.get("model.user_id"); + } + + this.set("loading", true); + ajax(this.get("importUrl"), options) + .then(result => { + const theme = this.store.createRecord( + this.get("recordType"), + result.theme + ); + this.get("adminCustomizeThemes").send("addTheme", theme); + this.send("closeModal"); + }) + .then(() => { + this.setProperties({ privateKey: null, publicKey: null }); + }) + .catch(popupAjaxError) + .finally(() => this.set("loading", false)); + } + } +}); diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index b9a8fc7618..7e45667c25 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -226,14 +226,6 @@ const AdminUser = Discourse.User.extend({ .catch(popupAjaxError); }, - refreshBrowsers() { - return ajax("/admin/users/" + this.get("id") + "/refresh_browsers", { - type: "POST" - }).finally(() => - bootbox.alert(I18n.t("admin.user.refresh_browsers_message")) - ); - }, - approve() { const self = this; return ajax("/admin/users/" + this.get("id") + "/approve", { diff --git a/app/assets/javascripts/admin/models/backup.js.es6 b/app/assets/javascripts/admin/models/backup.js.es6 index 0f6663065d..b79230829e 100644 --- a/app/assets/javascripts/admin/models/backup.js.es6 +++ b/app/assets/javascripts/admin/models/backup.js.es6 @@ -1,4 +1,5 @@ import { ajax } from "discourse/lib/ajax"; +import { extractError } from "discourse/lib/ajax-error"; const Backup = Discourse.Model.extend({ destroy() { @@ -15,9 +16,16 @@ const Backup = Discourse.Model.extend({ Backup.reopenClass({ find() { - return ajax("/admin/backups.json").then(backups => - backups.map(backup => Backup.create(backup)) - ); + return ajax("/admin/backups.json") + .then(backups => backups.map(backup => Backup.create(backup))) + .catch(error => { + bootbox.alert( + I18n.t("admin.backups.backup_storage_error", { + error_message: extractError(error) + }) + ); + return []; + }); }, start(withUploads) { diff --git a/app/assets/javascripts/admin/models/color-scheme-color.js.es6 b/app/assets/javascripts/admin/models/color-scheme-color.js.es6 index de0bee512d..3cda49fe1d 100644 --- a/app/assets/javascripts/admin/models/color-scheme-color.js.es6 +++ b/app/assets/javascripts/admin/models/color-scheme-color.js.es6 @@ -1,48 +1,50 @@ -const ColorSchemeColor = Discourse.Model.extend({ - init: function() { - this._super(...arguments); - this.startTrackingChanges(); - }, +import { + default as computed, + observes, + on +} from "ember-addons/ember-computed-decorators"; +import { propertyNotEqual, i18n } from "discourse/lib/computed"; - startTrackingChanges: function() { +const ColorSchemeColor = Discourse.Model.extend({ + @on("init") + startTrackingChanges() { this.set("originals", { hex: this.get("hex") || "FFFFFF" }); - this.notifyPropertyChange("hex"); // force changed property to be recalculated + + // force changed property to be recalculated + this.notifyPropertyChange("hex"); }, // Whether value has changed since it was last saved. - changed: function() { - if (!this.originals) return false; - if (this.get("hex") !== this.originals["hex"]) return true; + @computed("hex") + changed(hex) { + if (!this.get("originals")) return false; + if (hex !== this.get("originals").hex) return true; + return false; - }.property("hex"), + }, // Whether the current value is different than Discourse's default color scheme. - overridden: function() { - return this.get("hex") !== this.get("default_hex"); - }.property("hex", "default_hex"), + overridden: propertyNotEqual("hex", "default_hex"), // Whether the saved value is different than Discourse's default color scheme. - savedIsOverriden: function() { - return this.get("originals").hex !== this.get("default_hex"); - }.property("hex", "default_hex"), + @computed("default_hex", "hex") + savedIsOverriden(defaultHex) { + return this.get("originals").hex !== defaultHex; + }, - revert: function() { + revert() { this.set("hex", this.get("default_hex")); }, - undo: function() { - if (this.originals) this.set("hex", this.originals["hex"]); + undo() { + if (this.get("originals")) { + this.set("hex", this.get("originals").hex); + } }, - translatedName: function() { - return I18n.t("admin.customize.colors." + this.get("name") + ".name"); - }.property("name"), + translatedName: i18n("name", "admin.customize.colors.%@.name"), - description: function() { - return I18n.t( - "admin.customize.colors." + this.get("name") + ".description" - ); - }.property("name"), + description: i18n("name", "admin.customize.colors.%@.description"), /** brightness returns a number between 0 (darkest) to 255 (brightest). @@ -50,8 +52,8 @@ const ColorSchemeColor = Discourse.Model.extend({ @property brightness **/ - brightness: function() { - var hex = this.get("hex"); + @computed("hex") + brightness(hex) { if (hex.length === 6 || hex.length === 3) { if (hex.length === 3) { hex = @@ -69,9 +71,10 @@ const ColorSchemeColor = Discourse.Model.extend({ 1000 ); } - }.property("hex"), + }, - hexValueChanged: function() { + @observes("hex") + hexValueChanged() { if (this.get("hex")) { this.set( "hex", @@ -80,11 +83,12 @@ const ColorSchemeColor = Discourse.Model.extend({ .replace(/[^0-9a-fA-F]/g, "") ); } - }.observes("hex"), + }, - valid: function() { - return this.get("hex").match(/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/) !== null; - }.property("hex") + @computed("hex") + valid(hex) { + return hex.match(/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/) !== null; + } }); export default ColorSchemeColor; diff --git a/app/assets/javascripts/admin/models/theme.js.es6 b/app/assets/javascripts/admin/models/theme.js.es6 index a3ae230820..d72ac47e4b 100644 --- a/app/assets/javascripts/admin/models/theme.js.es6 +++ b/app/assets/javascripts/admin/models/theme.js.es6 @@ -9,11 +9,87 @@ export const COMPONENTS = "components"; const SETTINGS_TYPE_ID = 5; const Theme = RestModel.extend({ - FIELDS_IDS: [0, 1], + FIELDS_IDS: [0, 1, 5], isActive: Ember.computed.or("default", "user_selectable"), isPendingUpdates: Ember.computed.gt("remote_theme.commits_behind", 0), hasEditedFields: Ember.computed.gt("editedFields.length", 0), + @computed("theme_fields.[]") + targets() { + return [ + { id: 0, name: "common" }, + { id: 1, name: "desktop", icon: "desktop" }, + { id: 2, name: "mobile", icon: "mobile-alt" }, + { id: 3, name: "settings", icon: "cog", advanced: true }, + { + id: 4, + name: "translations", + icon: "globe", + advanced: true, + customNames: true + } + ].map(target => { + target["edited"] = this.hasEdited(target.name); + target["error"] = this.hasError(target.name); + return target; + }); + }, + + @computed("theme_fields.[]") + fieldNames() { + const common = [ + "scss", + "head_tag", + "header", + "after_header", + "body_tag", + "footer" + ]; + + return { + common: [...common, "embedded_scss"], + desktop: common, + mobile: common, + settings: ["yaml"], + translations: [ + "en", + ...(this.get("theme_fields") || []) + .filter(f => f.target === "translations" && f.name !== "en") + .map(f => f.name) + ] + }; + }, + + @computed("fieldNames", "theme_fields.[]", "theme_fields.@each.error") + fields(fieldNames) { + const hash = {}; + Object.keys(fieldNames).forEach(target => { + hash[target] = fieldNames[target].map(fieldName => { + const field = { + name: fieldName, + edited: this.hasEdited(target, fieldName), + error: this.hasError(target, fieldName) + }; + + if (target === "translations") { + field.translatedName = fieldName; + } else { + field.translatedName = I18n.t( + `admin.customize.theme.${fieldName}.text` + ); + field.title = I18n.t(`admin.customize.theme.${fieldName}.title`); + } + + if (fieldName.indexOf("_tag") > 0) { + field.icon = "far-file-alt"; + } + + return field; + }); + }); + return hash; + }, + @computed("theme_fields") themeFields(fields) { if (!fields) { @@ -76,6 +152,12 @@ const Theme = RestModel.extend({ } }, + hasError(target, name) { + return this.get("theme_fields") + .filter(f => f.target === target && (!name || name === f.name)) + .any(f => f.error); + }, + getError(target, name) { let themeFields = this.get("themeFields"); let key = this.getKey({ target, name }); @@ -114,7 +196,7 @@ const Theme = RestModel.extend({ existing.value = value; existing.upload_id = upload_id; } else { - fields.push(field); + fields.pushObject(field); } return; } @@ -123,10 +205,19 @@ const Theme = RestModel.extend({ let key = this.getKey({ target, name }); let existingField = themeFields[key]; if (!existingField) { - this.theme_fields.push(field); + this.theme_fields.pushObject(field); themeFields[key] = field; } else { + const changed = + (Ember.isEmpty(existingField.value) && !Ember.isEmpty(value)) || + (Ember.isEmpty(value) && !Ember.isEmpty(existingField.value)); + existingField.value = value; + if (changed) { + // Observing theme_fields.@each.value is too slow, so manually notify + // if the value goes to/from blank + this.notifyPropertyChange("theme_fields.[]"); + } } }, diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 index 5bab290091..818a75e6e4 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 @@ -21,7 +21,7 @@ export default Ember.Route.extend({ }, setupController(controller, wrapper) { - const fields = controller.fieldsForTarget(wrapper.target); + const fields = wrapper.model.get("fields")[wrapper.target].map(f => f.name); if (!fields.includes(wrapper.field_name)) { this.transitionTo( "adminCustomizeThemes.edit", diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 index 49a74bc281..91c7ec5b76 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 @@ -11,17 +11,13 @@ export default Ember.Route.extend({ }, actions: { - importModal() { - showModal("admin-import-theme", { admin: true }); + installModal() { + showModal("admin-install-theme", { admin: true }); }, addTheme(theme) { this.refresh(); this.transitionTo("adminCustomizeThemes.show", theme.get("id")); - }, - - showCreateModal() { - showModal("admin-create-theme", { admin: true }); } } }); diff --git a/app/assets/javascripts/admin/routes/admin-email-logs.js.es6 b/app/assets/javascripts/admin/routes/admin-email-logs.js.es6 index b61b63088c..35d2c51e8a 100644 --- a/app/assets/javascripts/admin/routes/admin-email-logs.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-email-logs.js.es6 @@ -1,12 +1,8 @@ -import EmailLog from "admin/models/email-log"; - export default Discourse.Route.extend({ - model() { - return EmailLog.findAll({ status: this.get("status") }); - }, - - setupController(controller, model) { - controller.set("model", model); - controller.set("filter", { status: this.get("status") }); + setupController(controller) { + controller.setProperties({ + loading: true, + filter: { status: this.get("status") } + }); } }); diff --git a/app/assets/javascripts/admin/routes/admin-users-list-show.js.es6 b/app/assets/javascripts/admin/routes/admin-users-list-show.js.es6 index 88fb204954..e893139fc4 100644 --- a/app/assets/javascripts/admin/routes/admin-users-list-show.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-users-list-show.js.es6 @@ -1,17 +1,28 @@ -import AdminUser from "admin/models/admin-user"; - export default Discourse.Route.extend({ - model: function(params) { - this.userFilter = params.filter; - return AdminUser.findAll(params.filter); + queryParams: { + order: { refreshModel: true }, + ascending: { refreshModel: true } }, - setupController: function(controller, model) { - controller.setProperties({ - model: model, - query: this.userFilter, - showEmails: false, - refreshing: false - }); + // TODO: this has been introduced to fix a bug in admin-users-list-show + // loading AdminUser model multiple times without refactoring the controller + beforeModel(transition) { + const routeName = "adminUsersList.show"; + + if (transition.targetName === routeName) { + const params = transition.params[routeName]; + const controller = this.controllerFor(routeName); + if (controller) { + controller.setProperties({ + order: transition.queryParams.order, + ascending: transition.queryParams.ascending, + query: params.filter, + showEmails: false, + refreshing: false + }); + + controller._refreshUsers(); + } + } } }); diff --git a/app/assets/javascripts/admin/templates/components/admin-theme-editor.hbs b/app/assets/javascripts/admin/templates/components/admin-theme-editor.hbs new file mode 100644 index 0000000000..049563bd55 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-theme-editor.hbs @@ -0,0 +1,94 @@ +
+ +
+ +
+ +
+ + +{{#if error}} +
{{error}}
+{{/if}} + +{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true"}} diff --git a/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs b/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs index fef8cb074d..a23d21f6e9 100644 --- a/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs @@ -18,19 +18,19 @@ {{/if}} {{#admin-form-row wrapLabel="true"}} - {{input type="checkbox" checked=buffered.editable}} {{i18n 'admin.user_fields.editable.title'}} + {{input type="checkbox" checked=buffered.editable}} {{i18n "admin.user_fields.editable.title"}} {{/admin-form-row}} {{#admin-form-row wrapLabel="true"}} - {{input type="checkbox" checked=buffered.required}} {{i18n 'admin.user_fields.required.title'}} + {{input type="checkbox" checked=buffered.required}} {{i18n "admin.user_fields.required.title"}} {{/admin-form-row}} {{#admin-form-row wrapLabel="true"}} - {{input type="checkbox" checked=buffered.show_on_profile}} {{i18n 'admin.user_fields.show_on_profile.title'}} + {{input type="checkbox" checked=buffered.show_on_profile}} {{i18n "admin.user_fields.show_on_profile.title"}} {{/admin-form-row}} {{#admin-form-row wrapLabel="true"}} - {{input type="checkbox" checked=buffered.show_on_user_card}} {{i18n 'admin.user_fields.show_on_user_card.title'}} + {{input type="checkbox" checked=buffered.show_on_user_card}} {{i18n "admin.user_fields.show_on_user_card.title"}} {{/admin-form-row}} {{#admin-form-row}} @@ -39,15 +39,14 @@ {{/admin-form-row}} {{else}}
-
+
{{userField.name}}
{{{userField.description}}}
-
{{fieldName}}
-
+
{{fieldName}}
+
{{d-button action=(action "edit") class="btn-default" icon="pencil-alt" label="admin.user_fields.edit"}} - {{d-button action=destroyAction actionParam=userField class="btn-danger" icon="far-trash-alt" label="admin.user_fields.delete"}} {{d-button action=moveUpAction actionParam=userField class="btn-default" icon="arrow-up" disabled=cantMoveUp}} {{d-button action=moveDownAction actionParam=userField class="btn-default" icon="arrow-down" disabled=cantMoveDown}} @@ -55,4 +54,4 @@
{{flags}}
{{/if}} -
+
diff --git a/app/assets/javascripts/admin/templates/components/admin-web-hook-event.hbs b/app/assets/javascripts/admin/templates/components/admin-web-hook-event.hbs index a98f8684f0..a73ddbcfb0 100644 --- a/app/assets/javascripts/admin/templates/components/admin-web-hook-event.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-web-hook-event.hbs @@ -7,7 +7,7 @@
{{d-button icon="ellipsis-v" action=(action "toggleRequest") label="admin.web_hooks.events.request"}} {{d-button icon="ellipsis-v" action=(action "toggleResponse") label="admin.web_hooks.events.response"}} - {{d-button icon="refresh" action=(action "redeliver") label="admin.web_hooks.events.redeliver"}} + {{d-button icon="sync" action=(action "redeliver") label="admin.web_hooks.events.redeliver"}}
{{#if expandDetails}}
diff --git a/app/assets/javascripts/admin/templates/components/flag-counts.hbs b/app/assets/javascripts/admin/templates/components/flag-counts.hbs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/assets/javascripts/admin/templates/components/install-theme-item.hbs b/app/assets/javascripts/admin/templates/components/install-theme-item.hbs new file mode 100644 index 0000000000..ed4ddd242f --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/install-theme-item.hbs @@ -0,0 +1,8 @@ +{{radio-button name="install-items" id=value value=value selection=selection}} + +{{d-icon 'caret-right'}} diff --git a/app/assets/javascripts/admin/templates/components/permalinks-list.hbs b/app/assets/javascripts/admin/templates/components/permalinks-list.hbs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/assets/javascripts/admin/templates/components/themes-list.hbs b/app/assets/javascripts/admin/templates/components/themes-list.hbs index eff5372714..1f0c7fc2d5 100644 --- a/app/assets/javascripts/admin/templates/components/themes-list.hbs +++ b/app/assets/javascripts/admin/templates/components/themes-list.hbs @@ -1,8 +1,8 @@
- {{d-icon "cube"}} {{I18n "admin.customize.theme.title"}}
+ {{d-icon "puzzle-piece"}} {{I18n "admin.customize.theme.components"}}
@@ -37,4 +37,10 @@ {{I18n "admin.customize.theme.empty"}}
{{/if}} -
\ No newline at end of file + + +
+ +
+ {{d-button action=installModal icon="upload" label="admin.customize.install" class="btn-primary"}} +
diff --git a/app/assets/javascripts/admin/templates/customize-colors.hbs b/app/assets/javascripts/admin/templates/customize-colors.hbs index b09fc1855a..01716958c8 100644 --- a/app/assets/javascripts/admin/templates/customize-colors.hbs +++ b/app/assets/javascripts/admin/templates/customize-colors.hbs @@ -1,15 +1,24 @@ -
-

{{i18n 'admin.customize.colors.long_title'}}

+
+

{{i18n "admin.customize.colors.long_title"}}

+
    {{#each model as |scheme|}} {{#unless scheme.is_base}} -
  • - {{#link-to 'adminCustomize.colors.show' scheme replace=true}}{{d-icon 'paint-brush'}}{{scheme.description}}{{/link-to}} -
  • +
  • + {{#link-to "adminCustomize.colors.show" scheme replace=true}} + {{d-icon "paint-brush"}} + {{scheme.description}} + {{/link-to}} +
  • {{/unless}} {{/each}}
- + + {{d-button + class="btn-default" + action=(action "newColorScheme") + icon="plus" + label="admin.customize.new"}}
{{outlet}} diff --git a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs index 4807209639..d17ab8d2f3 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs @@ -1,66 +1,17 @@
-

{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to 'adminCustomizeThemes.show' model.id replace=true}}{{model.name}}{{/link-to}}

- - {{#if error}} -
{{error}}
- {{/if}} - -
- -
- -
-
-
- -
- -
- - {{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true"}} +

{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to showRouteName model.id replace=true}}{{model.name}}{{/link-to}}

+ {{admin-theme-editor + theme=model + editRouteName=editRouteName + currentTargetName=currentTargetName + fieldName=fieldName + fieldAdded=(action 'fieldAdded') + maximized=maximized + onlyOverriddenChanged=(action 'onlyOverriddenChanged') + }} + {{/unless}} @@ -38,7 +38,11 @@ {{#if model.remote_theme}} {{#if model.remote_theme.remote_url}} - {{i18n "admin.customize.theme.source_url"}} {{d-icon "link"}} + {{#if sourceIsHttp}} + {{i18n "admin.customize.theme.source_url"}} {{d-icon "link"}} + {{else}} +
{{model.remote_theme.remote_url}}
+ {{/if}} {{/if}} {{#if model.remote_theme.about_url}} {{i18n "admin.customize.theme.about_theme"}} {{d-icon "link"}} @@ -67,13 +71,13 @@ {{model.remoteError}}
{{/if}} - + {{#if model.remote_theme.commits_behind}} {{#d-button action=(action "updateToLatest") icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}} {{else}} - {{#d-button action=(action "checkForThemeUpdates") icon="refresh" class="btn-default"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}} + {{#d-button action=(action "checkForThemeUpdates") icon="sync" class="btn-default"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}} {{/if}} - + {{#if updatingRemote}} {{i18n 'admin.customize.theme.updating'}} @@ -104,11 +108,14 @@
{{i18n "admin.customize.theme.color_scheme"}}
{{i18n "admin.customize.theme.color_scheme_select"}}
-
{{combo-box content=colorSchemes - filterable=true - forceEscape=true - value=colorSchemeId - icon="paint-brush"}} +
+ {{color-palettes + content=colorSchemes + filterable=true + forceEscape=true + value=colorSchemeId + icon="paint-brush"}} + {{#if colorSchemeChanged}} {{d-button action=(action "changeScheme") class="btn-primary submit-edit" icon="check"}} {{d-button action=(action "cancelChangeScheme") class="btn-default cancel-edit" icon="times"}} @@ -190,7 +197,10 @@ {{#if availableChildThemes}}
-
{{i18n "admin.customize.theme.theme_components"}}
+
+ {{d-icon "puzzle-piece"}} + {{i18n "admin.customize.theme.theme_components"}} +
{{#if model.childThemes.length}}
    {{#each model.childThemes as |child|}} diff --git a/app/assets/javascripts/admin/templates/customize-themes.hbs b/app/assets/javascripts/admin/templates/customize-themes.hbs index 268febd435..31d81941df 100644 --- a/app/assets/javascripts/admin/templates/customize-themes.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes.hbs @@ -1,12 +1,9 @@ {{#unless editingTheme}} -
    - - -
    - {{d-button label="admin.customize.new" icon="plus" action=(route-action "showCreateModal") class="btn-primary"}} - {{d-button action=(route-action "importModal") icon="upload" label="admin.customize.import" class="btn-default"}} -
    -
    - {{themes-list themes=fullThemes components=childThemes currentTab=currentTab}} + {{themes-list + themes=fullThemes + components=childThemes + currentTab=currentTab + installModal=(route-action "installModal")}} {{/unless}} + {{outlet}} diff --git a/app/assets/javascripts/admin/templates/dashboard-problems.hbs b/app/assets/javascripts/admin/templates/dashboard-problems.hbs index 901b6eb8d7..0f0b1f9d29 100644 --- a/app/assets/javascripts/admin/templates/dashboard-problems.hbs +++ b/app/assets/javascripts/admin/templates/dashboard-problems.hbs @@ -18,7 +18,7 @@

- {{d-button action=(action "refreshProblems") class="btn-default" icon="refresh" label="admin.dashboard.refresh_problems"}} + {{d-button action=(action "refreshProblems") class="btn-default" icon="sync" label="admin.dashboard.refresh_problems"}} {{i18n 'admin.dashboard.last_checked'}}: {{problemsTimestamp}}

{{/conditional-loading-section}} diff --git a/app/assets/javascripts/admin/templates/email-bounced.hbs b/app/assets/javascripts/admin/templates/email-bounced.hbs index dc9c2e2b7f..bbb812b07f 100644 --- a/app/assets/javascripts/admin/templates/email-bounced.hbs +++ b/app/assets/javascripts/admin/templates/email-bounced.hbs @@ -31,7 +31,9 @@ {{l.email_type}} {{else}} - {{i18n 'admin.email.logs.none'}} + {{#unless loading}} + {{i18n 'admin.email.logs.none'}} + {{/unless}} {{/each}} diff --git a/app/assets/javascripts/admin/templates/email-sent.hbs b/app/assets/javascripts/admin/templates/email-sent.hbs index 83bb5b56d8..fe3712b986 100644 --- a/app/assets/javascripts/admin/templates/email-sent.hbs +++ b/app/assets/javascripts/admin/templates/email-sent.hbs @@ -44,7 +44,9 @@ {{else}} - {{i18n 'admin.email.logs.none'}} + {{#unless loading}} + {{i18n 'admin.email.logs.none'}} + {{/unless}} {{/each}} diff --git a/app/assets/javascripts/admin/templates/email-skipped.hbs b/app/assets/javascripts/admin/templates/email-skipped.hbs index 0ca7fed0a7..ee80534382 100644 --- a/app/assets/javascripts/admin/templates/email-skipped.hbs +++ b/app/assets/javascripts/admin/templates/email-skipped.hbs @@ -40,7 +40,9 @@ {{else}} - {{i18n 'admin.email.logs.none'}} + {{#unless loading}} + {{i18n 'admin.email.logs.none'}} + {{/unless}} {{/each}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-badge-preview.hbs b/app/assets/javascripts/admin/templates/modal/admin-badge-preview.hbs index 70b0c1c3fc..7911b91ab8 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-badge-preview.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-badge-preview.hbs @@ -1,49 +1,55 @@ {{#d-modal-body title="admin.badges.preview.modal_title" class="badge-query-preview"}} {{#if errors}} -

{{i18n 'admin.badges.preview.sql_error_header'}}

+

{{i18n "admin.badges.preview.sql_error_header"}}

{{errors}}
+ {{else}}

{{#if count}} - {{{i18n 'admin.badges.preview.grant_count' count=count}}} + {{{i18n "admin.badges.preview.grant_count" count=count}}} {{else}} - {{{i18n 'admin.badges.preview.no_grant_count'}}} + {{{i18n "admin.badges.preview.no_grant_count"}}} {{/if}}

- {{#if count_warning}} + {{#if countWarning}}
-

{{d-icon "warning"}} {{i18n 'admin.badges.preview.bad_count_warning.header'}}

-

{{i18n 'admin.badges.preview.bad_count_warning.text'}}

+

+ {{d-icon "warning"}} + {{i18n "admin.badges.preview.bad_count_warning.header"}} +

+

+ {{i18n "admin.badges.preview.bad_count_warning.text"}} +

{{/if}} {{#if sample}}

- {{i18n 'admin.badges.preview.sample'}} + {{i18n "admin.badges.preview.sample"}}

    - {{#each processed_sample as |html|}} + {{#each processedSample as |html|}}
  • {{{html}}}
  • {{/each}}
{{/if}} - {{#if has_query_plan}} -
- {{{query_plan_html}}} -
+ {{#if hasQueryPlan}} +
+ {{{queryPlanHtml}}} +
{{/if}} {{/if}} {{/d-modal-body}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-create-theme.hbs b/app/assets/javascripts/admin/templates/modal/admin-create-theme.hbs deleted file mode 100644 index 788aa03a8d..0000000000 --- a/app/assets/javascripts/admin/templates/modal/admin-create-theme.hbs +++ /dev/null @@ -1,30 +0,0 @@ -{{#d-modal-body class="create-theme-modal" title="admin.customize.theme.create"}} -
- - {{I18n "admin.customize.theme.create_name"}} - - - {{input value=name placeholder=placeholder}} - -
- -
- - {{I18n "admin.customize.theme.create_type"}} - - - {{combo-box valueAttribute="value" content=types value=selectedType}} - -
- {{#if showError}} -
- {{d-icon "warning"}} - {{I18n "admin.customize.theme.name_too_short"}} -
- {{/if}} -{{/d-modal-body}} - - diff --git a/app/assets/javascripts/admin/templates/modal/admin-edit-badge-groupings.hbs b/app/assets/javascripts/admin/templates/modal/admin-edit-badge-groupings.hbs index 9e639f74a9..a5fba68993 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-edit-badge-groupings.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-edit-badge-groupings.hbs @@ -1,30 +1,34 @@ {{#d-modal-body title="admin.badges.badge_groupings.modal_title" class="badge-groupings-modal"}}
-
    +
      {{#each workingCopy as |wc|}}
    • {{#if wc.editing}} {{input value=wc.name class="badge-grouping-name-input"}} - + {{d-button action=(action "save" wc) icon="check"}} {{else}} {{wc.displayName}} {{/if}}
      -
      - - - - +
      + {{d-button action=(action "edit" wc) disabled=wc.system icon="pencil-alt"}} + {{d-button action=(action "up" wc) icon="chevron-up"}} + {{d-button action=(action "down" wc) icon="chevron-down"}} + {{d-button action=(action "delete" wc) disabled=wc.system icon="times"}}
    • {{/each}}
- + {{d-button action=(action "add") label="admin.badges.new"}} {{/d-modal-body}} + \ No newline at end of file +
diff --git a/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs b/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs deleted file mode 100644 index 9d7c9a6db0..0000000000 --- a/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs +++ /dev/null @@ -1,49 +0,0 @@ -{{#d-modal-body class='upload-selector' title="admin.customize.theme.import_theme"}} -
- {{radio-button name="upload" id="local" value="local" selection=selection}} - - {{#if local}} -
-
- {{i18n 'admin.customize.theme.import_file_tip'}} -
- {{/if}} -
-
- {{radio-button name="upload" id="remote" value="remote" selection=selection}} - - {{#if remote}} -
-
- {{input value=uploadUrl placeholder=urlPlaceholder}} - {{i18n 'admin.customize.theme.import_web_tip'}} -
-
- {{input value=branch placeholder="master"}} - {{i18n 'admin.customize.theme.remote_branch'}} -
-
- -
- {{#if checkPrivate}} - {{#if privateChecked}} - {{#if publicKey}} -
- {{i18n 'admin.customize.theme.public_key'}} - {{textarea readonly=true value=publicKey}} -
- {{/if}} - {{/if}} - {{/if}} -
- {{/if}} -
-{{/d-modal-body}} - - diff --git a/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs b/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs new file mode 100644 index 0000000000..153dbc9e8f --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs @@ -0,0 +1,104 @@ +{{#d-modal-body class='upload-selector install-theme' title="admin.customize.theme.install"}} +
+ {{install-theme-item value="popular" selection=selection label="admin.customize.theme.install_popular"}} + {{install-theme-item value="local" selection=selection label="admin.customize.theme.install_upload"}} + {{install-theme-item value="remote" selection=selection label="admin.customize.theme.install_git_repo"}} + {{install-theme-item value="create" selection=selection label="admin.customize.theme.install_create" showIcon=true}} +
+
+ {{#if popular}} + + {{/if}} + + {{#if local}} +
+
+ {{i18n 'admin.customize.theme.import_file_tip'}} +
+ {{/if}} + + {{#if remote}} +
+
+
{{i18n 'admin.customize.theme.import_web_tip'}}
+ {{input value=uploadUrl placeholder=urlPlaceholder}} +
+ + {{d-button + class="btn-small advanced-repo" + action=(action "toggleAdvanced") + label='admin.customize.theme.import_web_advanced'}} + + {{#if advancedVisible}} +
+
{{i18n 'admin.customize.theme.remote_branch'}}
+ {{input value=branch placeholder="master"}} +
+ +
+ +
+ {{#if showPublicKey}} +
+
{{i18n 'admin.customize.theme.public_key'}}
+ {{textarea readonly=true value=publicKey}} +
+ {{/if}} + {{/if}} +
+ {{/if}} + + {{#if create}} +
+
{{I18n "admin.customize.theme.create_name"}}
+ {{input value=name placeholder=placeholder}} + +
{{I18n "admin.customize.theme.create_type"}}
+ {{combo-box valueAttribute="value" content=createTypes value=selectedType}} +
+ {{/if}} +
+ +{{/d-modal-body}} + +{{#unless popular}} + +{{/unless}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-theme-item.hbs b/app/assets/javascripts/admin/templates/modal/admin-theme-item.hbs new file mode 100644 index 0000000000..e1b6bd0fe8 --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin-theme-item.hbs @@ -0,0 +1,19 @@ + diff --git a/app/assets/javascripts/admin/templates/site-text-index.hbs b/app/assets/javascripts/admin/templates/site-text-index.hbs index c84233348f..78f2c1735e 100644 --- a/app/assets/javascripts/admin/templates/site-text-index.hbs +++ b/app/assets/javascripts/admin/templates/site-text-index.hbs @@ -8,7 +8,10 @@ key-up=(action "search")}}
- {{d-checkbox label="admin.site_text.show_overriden" checked=overridden change=(action "search")}} +
diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index 5e5dd9b482..dc69ee5a7a 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -148,10 +148,6 @@
{{model.ip_address}}
{{#if currentUser.staff}} - {{d-button - class="btn-default" - action=(action "refreshBrowsers") - label="admin.user.refresh_browsers"}} {{ip-lookup ip=model.ip_address userId=model.id}} {{/if}}
diff --git a/app/assets/javascripts/discourse-common/lib/helpers.js.es6 b/app/assets/javascripts/discourse-common/lib/helpers.js.es6 index bbaaf6200a..907ae24d8c 100644 --- a/app/assets/javascripts/discourse-common/lib/helpers.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/helpers.js.es6 @@ -32,7 +32,11 @@ function resolveParams(ctx, options) { if (options.hashTypes) { Object.keys(hash).forEach(function(k) { const type = options.hashTypes[k]; - if (type === "STRING" || type === "StringLiteral") { + if ( + type === "STRING" || + type === "StringLiteral" || + type === "SubExpression" + ) { params[k] = hash[k]; } else if (type === "ID" || type === "PathExpression") { params[k] = get(ctx, hash[k], options); diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index 100f33dba0..9bc0004e12 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -167,7 +167,7 @@ const fa4Replacements = { "eye-slash": "far-eye-slash", eyedropper: "eye-dropper", fa: "fab-font-awesome", - facebook: "fab-facebook-f", + facebook: "fab-facebook", "facebook-f": "fab-facebook-f", "facebook-official": "fab-facebook", "facebook-square": "fab-facebook-square", diff --git a/app/assets/javascripts/discourse/components/backup-uploader.js.es6 b/app/assets/javascripts/discourse/components/backup-uploader.js.es6 index 04588987be..939777f0a7 100644 --- a/app/assets/javascripts/discourse/components/backup-uploader.js.es6 +++ b/app/assets/javascripts/discourse/components/backup-uploader.js.es6 @@ -1,4 +1,5 @@ import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; import computed from "ember-addons/ember-computed-decorators"; import UploadMixin from "discourse/mixins/upload"; @@ -39,14 +40,12 @@ export default Ember.Component.extend(UploadMixin, { $upload.on("fileuploadadd", (e, data) => { ajax("/admin/backups/upload_url", { data: { filename: data.files[0].name } - }).then(result => { - if (!result.success) { - bootbox.alert(result.message); - } else { + }) + .then(result => { data.url = result.url; data.submit(); - } - }); + }) + .catch(popupAjaxError); }); }.on("didInsertElement") }); diff --git a/app/assets/javascripts/discourse/components/bread-crumbs.js.es6 b/app/assets/javascripts/discourse/components/bread-crumbs.js.es6 index ded546ec17..3ef5c99af7 100644 --- a/app/assets/javascripts/discourse/components/bread-crumbs.js.es6 +++ b/app/assets/javascripts/discourse/components/bread-crumbs.js.es6 @@ -1,7 +1,10 @@ +import { default as computed } from "ember-addons/ember-computed-decorators"; + // A breadcrumb including category drop downs export default Ember.Component.extend({ classNameBindings: ["hidden:hidden", ":category-breadcrumb"], tagName: "ol", + parentCategory: Ember.computed.alias("category.parentCategory"), parentCategories: Ember.computed.filter("categories", function(c) { @@ -12,42 +15,44 @@ export default Ember.Component.extend({ // Don't show "uncategorized" if allow_uncategorized_topics setting is false. return false; } + return !c.get("parentCategory"); }), - parentCategoriesSorted: function() { - let cats = this.get("parentCategories"); + @computed("parentCategories") + parentCategoriesSorted(parentCategories) { if (this.siteSettings.fixed_category_positions) { - return cats; + return parentCategories; } - return cats.sortBy("totalTopicCount").reverse(); - }.property("parentCategories"), + return parentCategories.sortBy("totalTopicCount").reverse(); + }, - hidden: function() { - return this.site.mobileView && !this.get("category"); - }.property("category"), + @computed("category") + hidden(category) { + return this.site.mobileView && !category; + }, - firstCategory: function() { - return this.get("parentCategory") || this.get("category"); - }.property("parentCategory", "category"), + firstCategory: Ember.computed.or("{parentCategory,category}"), - secondCategory: function() { - if (this.get("parentCategory")) return this.get("category"); + @computed("category", "parentCategory") + secondCategory(category, parentCategory) { + if (parentCategory) return category; return null; - }.property("category", "parentCategory"), + }, - childCategories: function() { - if (this.get("hideSubcategories")) { + @computed("firstCategory", "hideSubcategories") + childCategories(firstCategory, hideSubcategories) { + if (hideSubcategories) { return []; } - var firstCategory = this.get("firstCategory"); + if (!firstCategory) { return []; } - return this.get("categories").filter(function(c) { - return c.get("parentCategory") === firstCategory; - }); - }.property("firstCategory", "hideSubcategories") + return this.get("categories").filter( + c => c.get("parentCategory") === firstCategory + ); + } }); diff --git a/app/assets/javascripts/discourse/components/composer-body.js.es6 b/app/assets/javascripts/discourse/components/composer-body.js.es6 index 5d194eb511..c5365650f9 100644 --- a/app/assets/javascripts/discourse/components/composer-body.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-body.js.es6 @@ -8,6 +8,17 @@ import positioningWorkaround from "discourse/lib/safari-hacks"; import { headerHeight } from "discourse/components/site-header"; import KeyEnterEscape from "discourse/mixins/key-enter-escape"; +const START_EVENTS = "touchstart mousedown"; +const DRAG_EVENTS = "touchmove mousemove"; +const END_EVENTS = "touchend mouseup"; + +const MIN_COMPOSER_SIZE = 240; +const THROTTLE_RATE = 20; + +function mouseYPos(e) { + return e.clientY || (e.touches && e.touches[0] && e.touches[0].clientY); +} + export default Ember.Component.extend(KeyEnterEscape, { elementId: "reply-control", @@ -84,17 +95,53 @@ export default Ember.Component.extend(KeyEnterEscape, { } }, + setupComposerResizeEvents() { + const $composer = this.$(); + const $grippie = this.$(".grippie"); + const $document = Ember.$(document); + let origComposerSize = 0; + let lastMousePos = 0; + + const performDrag = event => { + $composer.trigger("div-resizing"); + $composer.addClass("clear-transitions"); + const currentMousePos = mouseYPos(event); + let size = origComposerSize + (lastMousePos - currentMousePos); + + const winHeight = Ember.$(window).height(); + size = Math.min(size, winHeight - headerHeight()); + size = Math.max(size, MIN_COMPOSER_SIZE); + const sizePx = `${size}px`; + this.movePanels(sizePx); + $composer.height(sizePx); + }; + + const throttledPerformDrag = (event => { + event.preventDefault(); + Ember.run.throttle(this, performDrag, event, THROTTLE_RATE); + }).bind(this); + + const endDrag = () => { + $document.off(DRAG_EVENTS, throttledPerformDrag); + $document.off(END_EVENTS, endDrag); + $composer.removeClass("clear-transitions"); + $composer.focus(); + }; + + $grippie.on(START_EVENTS, event => { + event.preventDefault(); + origComposerSize = $composer.height(); + lastMousePos = mouseYPos(event); + $document.on(DRAG_EVENTS, throttledPerformDrag); + $document.on(END_EVENTS, endDrag); + }); + }, + didInsertElement() { this._super(...arguments); - const $replyControl = $("#reply-control"); + this.setupComposerResizeEvents(); + const resize = () => Ember.run(() => this.resize()); - - $replyControl.DivResizer({ - resize, - maxHeight: winHeight => winHeight - headerHeight(), - onDrag: sizePx => this.movePanels(sizePx) - }); - const triggerOpen = () => { if (this.get("composer.composeState") === Composer.OPEN) { this.appEvents.trigger("composer:opened"); @@ -102,13 +149,11 @@ export default Ember.Component.extend(KeyEnterEscape, { }; triggerOpen(); - afterTransition($replyControl, () => { + afterTransition(this.$(), () => { resize(); triggerOpen(); }); positioningWorkaround(this.$()); - - this.appEvents.on("composer:resize", this, this.resize); }, willDestroyElement() { diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index db6cde6c0f..c27db9ed14 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -30,7 +30,8 @@ import { validateUploadedFiles, authorizesOneOrMoreImageExtensions, formatUsername, - clipboardData + clipboardData, + safariHacksDisabled } from "discourse/lib/utilities"; import { cacheShortUploadUrl, @@ -108,6 +109,13 @@ export default Ember.Component.extend({ this._resetUpload(true); }, + @observes("focusTarget") + setFocus() { + if (this.get("focusTarget") === "editor") { + this.$("textarea").putCursorAtEnd(); + } + }, + @computed markdownOptions() { return { @@ -185,8 +193,19 @@ export default Ember.Component.extend({ ); } + if (!this.site.mobileView) { + $preview + .off("touchstart mouseenter", "img") + .on("touchstart mouseenter", "img", () => { + this._placeImageScaleButtons($preview); + }); + } + // Focus on the body unless we have a title - if (!this.get("composer.canEditTitle") && !this.capabilities.isIOS) { + if ( + !this.get("composer.canEditTitle") && + (!this.capabilities.isIOS || safariHacksDisabled()) + ) { this.$(".d-editor-input").putCursorAtEnd(); } @@ -767,6 +786,116 @@ export default Ember.Component.extend({ } }, + _appendImageScaleButtons($images, imageScaleRegex) { + const buttonScales = [100, 75, 50]; + const imageWrapperTemplate = `
`; + const buttonWrapperTemplate = `
`; + const scaleButtonTemplate = ``; + + $images.each((i, e) => { + const $e = $(e); + + const matches = this.get("composer.reply").match(imageScaleRegex); + + // ignore previewed upload markdown in codeblock + if (!matches || $e.hasClass("codeblock-image")) return; + + if (!$e.parent().hasClass("image-wrapper")) { + const match = matches[i]; + const matchingPlaceholder = imageScaleRegex.exec(match); + + if (!matchingPlaceholder) return; + + const currentScale = matchingPlaceholder[2] || 100; + + $e.data("index", i).wrap(imageWrapperTemplate); + $e.parent().append( + $(buttonWrapperTemplate).attr("data-image-index", i) + ); + + buttonScales.forEach((buttonScale, buttonIndex) => { + const activeClass = + parseInt(currentScale, 10) === buttonScale ? "active" : ""; + + const $scaleButton = $(scaleButtonTemplate) + .addClass(activeClass) + .attr("data-scale", buttonScale) + .text(`${buttonScale}%`); + + const $buttonWrapper = $e.parent().find(".button-wrapper"); + $buttonWrapper.append($scaleButton); + + if (buttonIndex !== buttonScales.length - 1) { + $buttonWrapper.append(` | `); + } + }); + } + }); + }, + + _registerImageScaleButtonClick($preview, imageScaleRegex) { + $preview.off("click", ".scale-btn").on("click", ".scale-btn", e => { + const index = parseInt( + $(e.target) + .parent() + .attr("data-image-index") + ); + + const scale = e.target.attributes["data-scale"].value; + const matchingPlaceholder = this.get("composer.reply").match( + imageScaleRegex + ); + + if (matchingPlaceholder) { + const match = matchingPlaceholder[index]; + if (!match) { + return; + } + + const replacement = match.replace(imageScaleRegex, `$1,${scale}%$3`); + this.appEvents.trigger( + "composer:replace-text", + matchingPlaceholder[index], + replacement, + { regex: imageScaleRegex, index } + ); + } + }); + }, + + _placeImageScaleButtons($preview) { + // regex matches only upload placeholders with size defined, + // which is required for resizing + + // original string `![28|690x226,5%](upload://ceEfx3vO7bx7Cecv2co1SrnoTpW.png)` + // match 1 `![28|690x226` + // match 2 `5` + // match 3 `](upload://ceEfx3vO7bx7Cecv2co1SrnoTpW.png)` + const imageScaleRegex = /(!\[(?:\S*?(?=\|)\|)*?(?:\d{1,6}x\d{1,6})+?)(?:,?(\d{1,3})?%?)?(\]\(upload:\/\/\S*?\))/g; + + // wraps previewed upload markdown in a codeblock in its own class to keep a track + // of indexes later on to replace the correct upload placeholder in the composer + if ($preview.find(".codeblock-image").length === 0) { + this.$(".d-editor-preview *") + .contents() + .filter(function() { + return this.nodeType === 3; // TEXT_NODE + }) + .each(function() { + $(this).replaceWith( + $(this) + .text() + .replace(imageScaleRegex, "$&") + ); + }); + } + + const $images = $preview.find("img.resizable, span.codeblock-image"); + + this._appendImageScaleButtons($images, imageScaleRegex); + this._registerImageScaleButtonClick($preview, imageScaleRegex); + }, + @on("willDestroyElement") _unbindUploadTarget() { this._validUploads = 0; @@ -804,6 +933,12 @@ export default Ember.Component.extend({ this.storeToolbarState(toolbarEvent); }, + showPreview() { + const $preview = this.$(".d-editor-preview-wrapper"); + this._placeImageScaleButtons($preview); + this.send("togglePreview"); + }, + actions: { importQuote(toolbarEvent) { this.importQuote(toolbarEvent); @@ -852,7 +987,7 @@ export default Ember.Component.extend({ group: "mobileExtras", icon: "television", title: "composer.show_preview", - sendAction: this.get("togglePreview") + sendAction: this.showPreview.bind(this) }); } }, @@ -960,6 +1095,10 @@ export default Ember.Component.extend({ ); } + if (this.site.mobileView && $preview.is(":visible")) { + this._placeImageScaleButtons($preview); + } + this.trigger("previewRefreshed", $preview); this.afterRefresh($preview); } diff --git a/app/assets/javascripts/discourse/components/create-account.js.es6 b/app/assets/javascripts/discourse/components/create-account.js.es6 index f7b30205a8..e13fd4ca1d 100644 --- a/app/assets/javascripts/discourse/components/create-account.js.es6 +++ b/app/assets/javascripts/discourse/components/create-account.js.es6 @@ -16,10 +16,22 @@ export default Ember.Component.extend({ return false; } }); + + this.$().on("click.dropdown-user-field-label", "[for]", event => { + const $element = $(event.target); + const $target = $(`#${$element.attr("for")}`); + + if ($target.is(".select-kit")) { + event.preventDefault(); + $target.find(".select-kit-header").trigger("click"); + } + }); }, willDestroyElement() { this._super(...arguments); + this.$().off("keydown.discourse-create-account"); + this.$().off("click.dropdown-user-field-label"); } }); diff --git a/app/assets/javascripts/discourse/components/d-button.js.es6 b/app/assets/javascripts/discourse/components/d-button.js.es6 index 8b964299d8..7fbf32bc18 100644 --- a/app/assets/javascripts/discourse/components/d-button.js.es6 +++ b/app/assets/javascripts/discourse/components/d-button.js.es6 @@ -4,9 +4,12 @@ export default Ember.Component.extend({ // subclasses need this layoutName: "components/d-button", + form: null, + tagName: "button", classNameBindings: [":btn", "noText", "btnType"], attributeBindings: [ + "form", "disabled", "translatedTitle:title", "translatedLabel:aria-label", diff --git a/app/assets/javascripts/discourse/components/d-checkbox.js.es6 b/app/assets/javascripts/discourse/components/d-checkbox.js.es6 deleted file mode 100644 index 7bbb682582..0000000000 --- a/app/assets/javascripts/discourse/components/d-checkbox.js.es6 +++ /dev/null @@ -1,25 +0,0 @@ -import { on } from "ember-addons/ember-computed-decorators"; - -export default Ember.Component.extend({ - tagName: "label", - - @on("didInsertElement") - _init() { - const checked = this.get("checked"); - if (checked && checked !== "false") { - this.$("input").prop("checked", true); - } - - // In Ember 13.3 we can use action on the checkbox `{{input}}` but not in 1.11 - this.$("input").on("click.d-checkbox", () => { - Ember.run.scheduleOnce("afterRender", () => - this.change(this.$("input").prop("checked")) - ); - }); - }, - - @on("willDestroyElement") - _clear() { - this.$("input").off("click.d-checkbox"); - } -}); diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index fca09f4288..bc9240d53e 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -12,7 +12,8 @@ import { findRawTemplate } from "discourse/lib/raw-templates"; import { siteDir } from "discourse/lib/text-direction"; import { determinePostReplaceSelection, - clipboardData + clipboardData, + safariHacksDisabled } from "discourse/lib/utilities"; import toMarkdown from "discourse/lib/to-markdown"; import deprecated from "discourse-common/lib/deprecated"; @@ -80,7 +81,7 @@ class Toolbar { icon: "italic", label: getButtonLabel("composer.italic_label", "I"), shortcut: "I", - perform: e => e.applySurround("_", "_", "italic_text") + perform: e => e.applySurround("*", "*", "italic_text") }); if (opts.showLink) { @@ -256,15 +257,6 @@ export default Ember.Component.extend({ const mouseTrap = Mousetrap(this.$(".d-editor-input")[0]); const shortcuts = this.get("toolbar.shortcuts"); - // for some reason I am having trouble bubbling this so hack it in - mouseTrap.bind(["ctrl+alt+f"], event => { - this.appEvents.trigger("header:keyboard-trigger", { - type: "search", - event - }); - return true; - }); - Object.keys(shortcuts).forEach(sc => { const button = shortcuts[sc]; mouseTrap.bind(sc, () => { @@ -304,8 +296,8 @@ export default Ember.Component.extend({ this.appEvents.on("composer:insert-text", (text, options) => this._addText(this._getSelected(), text, options) ); - this.appEvents.on("composer:replace-text", (oldVal, newVal) => - this._replaceText(oldVal, newVal) + this.appEvents.on("composer:replace-text", (oldVal, newVal, opts) => + this._replaceText(oldVal, newVal, opts) ); } this._mouseTrap = mouseTrap; @@ -323,7 +315,6 @@ export default Ember.Component.extend({ Object.keys(this.get("toolbar.shortcuts")).forEach(sc => mouseTrap.unbind(sc) ); - mouseTrap.unbind("ctrl+/", "command+/"); this.$(".d-editor-preview").off("click.preview"); }, @@ -534,7 +525,7 @@ export default Ember.Component.extend({ const $textarea = this.$("textarea.d-editor-input"); const textarea = $textarea[0]; const oldScrollPos = $textarea.scrollTop(); - if (!this.capabilities.isIOS) { + if (!this.capabilities.isIOS || safariHacksDisabled()) { $textarea.focus(); } textarea.selectionStart = from; @@ -669,7 +660,7 @@ export default Ember.Component.extend({ } }, - _replaceText(oldVal, newVal) { + _replaceText(oldVal, newVal, opts) { const val = this.get("value"); const needleStart = val.indexOf(oldVal); @@ -687,8 +678,17 @@ export default Ember.Component.extend({ replacement: { start: needleStart, end: needleStart + newVal.length } }); - // Replace value (side effect: cursor at the end). - this.set("value", val.replace(oldVal, newVal)); + if (opts && opts.index && opts.regex) { + let i = -1; + const newValue = val.replace(opts.regex, match => { + i++; + return i === opts.index ? newVal : match; + }); + this.set("value", newValue); + } else { + // Replace value (side effect: cursor at the end). + this.set("value", val.replace(oldVal, newVal)); + } if ($("textarea.d-editor-input").is(":focus")) { // Restore cursor. diff --git a/app/assets/javascripts/discourse/components/d-modal.js.es6 b/app/assets/javascripts/discourse/components/d-modal.js.es6 index 44792ecfc0..51c5824e51 100644 --- a/app/assets/javascripts/discourse/components/d-modal.js.es6 +++ b/app/assets/javascripts/discourse/components/d-modal.js.es6 @@ -1,9 +1,17 @@ import { on } from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - classNameBindings: [":modal", ":d-modal", "modalClass", "modalStyle"], + classNameBindings: [ + ":modal", + ":d-modal", + "modalClass", + "modalStyle", + "hasPanels" + ], attributeBindings: ["data-keyboard"], dismissable: true, + title: null, + subtitle: null, init() { this._super(...arguments); diff --git a/app/assets/javascripts/discourse/components/emoji-uploader.js.es6 b/app/assets/javascripts/discourse/components/emoji-uploader.js.es6 index c9027c7151..f3273d8322 100644 --- a/app/assets/javascripts/discourse/components/emoji-uploader.js.es6 +++ b/app/assets/javascripts/discourse/components/emoji-uploader.js.es6 @@ -7,6 +7,12 @@ export default Ember.Component.extend(UploadMixin, { hasName: Ember.computed.notEmpty("name"), addDisabled: Ember.computed.not("hasName"), + uploadOptions() { + return { + sequentialUploads: true + }; + }, + @computed("hasName", "name") data(hasName, name) { return hasName ? { name } : {}; diff --git a/app/assets/javascripts/discourse/components/group-card-contents.js.es6 b/app/assets/javascripts/discourse/components/group-card-contents.js.es6 index 763d8fe2c5..2a73474d67 100644 --- a/app/assets/javascripts/discourse/components/group-card-contents.js.es6 +++ b/app/assets/javascripts/discourse/components/group-card-contents.js.es6 @@ -35,7 +35,7 @@ export default Ember.Component.extend(CardContentsBase, CleansUp, { @computed("group") groupPath(group) { - return `${Discourse.BaseUri}/groups/${group.name}`; + return `${Discourse.BaseUri}/g/${group.name}`; }, _showCallback(username, $target) { diff --git a/app/assets/javascripts/discourse/components/group-selector.js.es6 b/app/assets/javascripts/discourse/components/group-selector.js.es6 index 8746b01fd3..dc3db23567 100644 --- a/app/assets/javascripts/discourse/components/group-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/group-selector.js.es6 @@ -30,6 +30,7 @@ export default Ember.Component.extend({ ? [] : [groupNames], single: this.get("single"), + fullWidthWrap: this.get("fullWidthWrap"), updateData: opts && opts.updateData ? opts.updateData : false, onChangeItems: items => { selectedGroups = items; diff --git a/app/assets/javascripts/discourse/components/image-uploader.js.es6 b/app/assets/javascripts/discourse/components/image-uploader.js.es6 index db580fb71f..8ed486ade9 100644 --- a/app/assets/javascripts/discourse/components/image-uploader.js.es6 +++ b/app/assets/javascripts/discourse/components/image-uploader.js.es6 @@ -1,8 +1,22 @@ import computed from "ember-addons/ember-computed-decorators"; import UploadMixin from "discourse/mixins/upload"; +import lightbox from "discourse/lib/lightbox"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; export default Ember.Component.extend(UploadMixin, { classNames: ["image-uploader"], + loadingLightbox: false, + + init() { + this._super(...arguments); + this._applyLightbox(); + }, + + willDestroyElement() { + this._super(...arguments); + $("a.lightbox").magnificPopup("close"); + }, @computed("imageUrl") backgroundStyle(imageUrl) { @@ -13,6 +27,12 @@ export default Ember.Component.extend(UploadMixin, { return `background-image: url(${imageUrl})`.htmlSafe(); }, + @computed("imageUrl") + imageBaseName(imageUrl) { + if (Ember.isEmpty(imageUrl)) return; + return imageUrl.split("/").slice(-1)[0]; + }, + @computed("backgroundStyle") hasBackgroundStyle(backgroundStyle) { return !Ember.isEmpty(backgroundStyle.string); @@ -23,14 +43,56 @@ export default Ember.Component.extend(UploadMixin, { }, uploadDone(upload) { - this.setProperties({ imageUrl: upload.url, imageId: upload.id }); + this.setProperties({ + imageUrl: upload.url, + imageId: upload.id, + imageFilesize: upload.human_filesize, + imageFilename: upload.original_filename, + imageWidth: upload.width, + imageHeight: upload.height + }); + + this._applyLightbox(); if (this.onUploadDone) { this.onUploadDone(upload); } }, + _openLightbox() { + Ember.run.next(() => this.$("a.lightbox").magnificPopup("open")); + }, + + _applyLightbox() { + if (this.get("imageUrl")) Ember.run.next(() => lightbox(this.$())); + }, + actions: { + toggleLightbox() { + if (this.get("imageFilename")) { + this._openLightbox(); + } else { + this.set("loadingLightbox", true); + + ajax(`/uploads/lookup-metadata`, { + type: "POST", + data: { url: this.get("imageUrl") } + }) + .then(json => { + this.setProperties({ + imageFilename: json.original_filename, + imageFilesize: json.human_filesize, + imageWidth: json.width, + imageHeight: json.height + }); + + this._openLightbox(); + this.set("loadingLightbox", false); + }) + .catch(popupAjaxError); + } + }, + trash() { this.setProperties({ imageUrl: null, imageId: null }); diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/components/invite-panel.js.es6 similarity index 66% rename from app/assets/javascripts/discourse/controllers/invite.js.es6 rename to app/assets/javascripts/discourse/components/invite-panel.js.es6 index b6144dd7b0..2f0fc23539 100644 --- a/app/assets/javascripts/discourse/controllers/invite.js.es6 +++ b/app/assets/javascripts/discourse/components/invite-panel.js.es6 @@ -1,34 +1,30 @@ -import ModalFunctionality from "discourse/mixins/modal-functionality"; import { emailValid } from "discourse/lib/utilities"; import computed from "ember-addons/ember-computed-decorators"; import Group from "discourse/models/group"; import Invite from "discourse/models/invite"; +import { i18n } from "discourse/lib/computed"; -export default Ember.Controller.extend(ModalFunctionality, { - userInvitedShow: Ember.inject.controller("user-invited-show"), +export default Ember.Component.extend({ + tagName: null, - // If this isn't defined, it will proxy to the user model on the preferences + inviteModel: Ember.computed.alias("panel.model.inviteModel"), + userInvitedShow: Ember.computed.alias("panel.model.userInvitedShow"), + + // If this isn't defined, it will proxy to the user topic on the preferences // page which is wrong. emailOrUsername: null, hasCustomMessage: false, + hasCustomMessage: false, customMessage: null, inviteIcon: "envelope", invitingExistingUserToTopic: false, - @computed("isMessage", "invitingToTopic") - title(isMessage, invitingToTopic) { - if (isMessage) { - return "topic.invite_private.title"; - } else if (invitingToTopic) { - return "topic.invite_reply.title"; - } else { - return "user.invited.create"; - } - }, + isAdmin: Ember.computed.alias("currentUser.admin"), - @computed - isAdmin() { - return this.currentUser.admin; + willDestroyElement() { + this._super(...arguments); + + this.reset(); }, @computed( @@ -36,9 +32,9 @@ export default Ember.Controller.extend(ModalFunctionality, { "emailOrUsername", "invitingToTopic", "isPrivateTopic", - "model.groupNames", - "model.saving", - "model.details.can_invite_to" + "topic.groupNames", + "topic.saving", + "topic.details.can_invite_to" ) disabled( isAdmin, @@ -51,26 +47,39 @@ export default Ember.Controller.extend(ModalFunctionality, { ) { if (saving) return true; if (Ember.isEmpty(emailOrUsername)) return true; + const emailTrimmed = emailOrUsername.trim(); // when inviting to forum, email must be valid - if (!invitingToTopic && !emailValid(emailTrimmed)) return true; - // normal users (not admin) can't invite users to private topic via email - if (!isAdmin && isPrivateTopic && emailValid(emailTrimmed)) return true; - // when inviting to private topic via email, group name must be specified - if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(emailTrimmed)) + if (!invitingToTopic && !emailValid(emailTrimmed)) { return true; + } + + // normal users (not admin) can't invite users to private topic via email + if (!isAdmin && isPrivateTopic && emailValid(emailTrimmed)) { + return true; + } + + // when inviting to private topic via email, group name must be specified + if ( + isPrivateTopic && + Ember.isEmpty(groupNames) && + emailValid(emailTrimmed) + ) { + return true; + } if (can_invite_to) return false; + return false; }, @computed( "isAdmin", "emailOrUsername", - "model.saving", + "inviteModel.saving", "isPrivateTopic", - "model.groupNames", + "inviteModel.groupNames", "hasCustomMessage" ) disabledCopyLink( @@ -84,54 +93,65 @@ export default Ember.Controller.extend(ModalFunctionality, { if (hasCustomMessage) return true; if (saving) return true; if (Ember.isEmpty(emailOrUsername)) return true; + const email = emailOrUsername.trim(); + // email must be valid - if (!emailValid(email)) return true; - // normal users (not admin) can't invite users to private topic via email - if (!isAdmin && isPrivateTopic && emailValid(email)) return true; - // when inviting to private topic via email, group name must be specified - if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(email)) + if (!emailValid(email)) { return true; + } + + // normal users (not admin) can't invite users to private topic via email + if (!isAdmin && isPrivateTopic && emailValid(email)) { + return true; + } + + // when inviting to private topic via email, group name must be specified + if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(email)) { + return true; + } + return false; }, - @computed("model.saving") + @computed("inviteModel.saving") buttonTitle(saving) { return saving ? "topic.inviting" : "topic.invite_reply.action"; }, - // We are inviting to a topic if the model isn't the current user. + // We are inviting to a topic if the topic isn't the current user. // The current user would mean we are inviting to the forum in general. - @computed("model") - invitingToTopic(model) { - return model !== this.currentUser; + @computed("inviteModel") + invitingToTopic(inviteModel) { + return inviteModel !== this.currentUser; }, - @computed("model", "model.details.can_invite_via_email") - canInviteViaEmail(model, can_invite_via_email) { - return this.get("model") === this.currentUser ? true : can_invite_via_email; + @computed("inviteModel", "inviteModel.details.can_invite_via_email") + canInviteViaEmail(inviteModel, canInviteViaEmail) { + return this.get("inviteModel") === this.currentUser + ? true + : canInviteViaEmail; }, - @computed("isMessage", "canInviteViaEmail") - showCopyInviteButton(isMessage, canInviteViaEmail) { - return canInviteViaEmail && !isMessage; + @computed("isPM", "canInviteViaEmail") + showCopyInviteButton(isPM, canInviteViaEmail) { + return canInviteViaEmail && !isPM; }, - topicId: Ember.computed.alias("model.id"), + topicId: Ember.computed.alias("inviteModel.id"), - // Is Private Topic? (i.e. visible only to specific group members) + // eg: visible only to specific group members isPrivateTopic: Ember.computed.and( "invitingToTopic", - "model.category.read_restricted" + "inviteModel.category.read_restricted" ), - // Is Private Message? - isMessage: Ember.computed.equal("model.archetype", "private_message"), + isPM: Ember.computed.equal("inviteModel.archetype", "private_message"), - // Allow Existing Members? (username autocomplete) + // scope to allowed usernames allowExistingMembers: Ember.computed.alias("invitingToTopic"), - @computed("isAdmin", "model.group_users") + @computed("isAdmin", "inviteModel.group_users") isGroupOwnerOrAdmin(isAdmin, groupUsers) { return ( isAdmin || (groupUsers && groupUsers.some(groupUser => groupUser.owner)) @@ -143,7 +163,7 @@ export default Ember.Controller.extend(ModalFunctionality, { "isGroupOwnerOrAdmin", "emailOrUsername", "isPrivateTopic", - "isMessage", + "isPM", "invitingToTopic", "canInviteViaEmail" ) @@ -151,14 +171,14 @@ export default Ember.Controller.extend(ModalFunctionality, { isGroupOwnerOrAdmin, emailOrUsername, isPrivateTopic, - isMessage, + isPM, invitingToTopic, canInviteViaEmail ) { return ( isGroupOwnerOrAdmin && canInviteViaEmail && - !isMessage && + !isPM && (emailValid(emailOrUsername) || isPrivateTopic || !invitingToTopic) ); }, @@ -166,13 +186,14 @@ export default Ember.Controller.extend(ModalFunctionality, { @computed("emailOrUsername") showCustomMessage(emailOrUsername) { return ( - this.get("model") === this.currentUser || emailValid(emailOrUsername) + this.get("inviteModel") === this.currentUser || + emailValid(emailOrUsername) ); }, // Instructional text for the modal. @computed( - "isMessage", + "isPM", "invitingToTopic", "emailOrUsername", "isPrivateTopic", @@ -180,7 +201,7 @@ export default Ember.Controller.extend(ModalFunctionality, { "canInviteViaEmail" ) inviteInstructions( - isMessage, + isPM, invitingToTopic, emailOrUsername, isPrivateTopic, @@ -190,7 +211,7 @@ export default Ember.Controller.extend(ModalFunctionality, { if (!canInviteViaEmail) { // can't invite via email, only existing users return I18n.t("topic.invite_reply.sso_enabled"); - } else if (isMessage) { + } else if (isPM) { // inviting to a message return I18n.t("topic.invite_private.email_or_username"); } else if (invitingToTopic) { @@ -222,14 +243,14 @@ export default Ember.Controller.extend(ModalFunctionality, { }, groupFinder(term) { - return Group.findAll({ term: term, ignore_automatic: true }); + return Group.findAll({ term, ignore_automatic: true }); }, - @computed("isMessage", "emailOrUsername", "invitingExistingUserToTopic") - successMessage(isMessage, emailOrUsername, invitingExistingUserToTopic) { + @computed("isPM", "emailOrUsername", "invitingExistingUserToTopic") + successMessage(isPM, emailOrUsername, invitingExistingUserToTopic) { if (this.get("hasGroups")) { return I18n.t("topic.invite_private.success_group"); - } else if (isMessage) { + } else if (isPM) { return I18n.t("topic.invite_private.success"); } else if (invitingExistingUserToTopic) { return I18n.t("topic.invite_reply.success_existing_email", { @@ -242,9 +263,9 @@ export default Ember.Controller.extend(ModalFunctionality, { } }, - @computed("isMessage") - errorMessage(isMessage) { - return isMessage + @computed("isPM") + errorMessage(isPM) { + return isPM ? I18n.t("topic.invite_private.error") : I18n.t("topic.invite_reply.error"); }, @@ -256,18 +277,18 @@ export default Ember.Controller.extend(ModalFunctionality, { : "topic.invite_reply.username_placeholder"; }, - @computed - customMessagePlaceholder() { - return I18n.t("invite.custom_message_placeholder"); - }, + customMessagePlaceholder: i18n("invite.custom_message_placeholder"), // Reset the modal to allow a new user to be invited. reset() { - this.set("emailOrUsername", null); - this.set("hasCustomMessage", false); - this.set("customMessage", null); - this.set("invitingExistingUserToTopic", false); - this.get("model").setProperties({ + this.setProperties({ + emailOrUsername: null, + hasCustomMessage: false, + customMessage: null, + invitingExistingUserToTopic: false + }); + + this.get("inviteModel").setProperties({ groupNames: null, error: false, saving: false, @@ -278,24 +299,23 @@ export default Ember.Controller.extend(ModalFunctionality, { actions: { createInvite() { - const self = this; if (this.get("disabled")) { return; } - const groupNames = this.get("model.groupNames"), - userInvitedController = this.get("userInvitedShow"), - model = this.get("model"); + const groupNames = this.get("inviteModel.groupNames"); + const userInvitedController = this.get("userInvitedShow"); + const model = this.get("inviteModel"); model.setProperties({ saving: true, error: false }); const onerror = e => { if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { - self.set("errorMessage", e.jqXHR.responseJSON.errors[0]); + this.set("errorMessage", e.jqXHR.responseJSON.errors[0]); } else { - self.set( + this.set( "errorMessage", - self.get("isMessage") + this.get("isPM") ? I18n.t("topic.invite_private.error") : I18n.t("topic.invite_reply.error") ); @@ -304,18 +324,18 @@ export default Ember.Controller.extend(ModalFunctionality, { }; if (this.get("hasGroups")) { - return this.get("model") + return this.get("inviteModel") .createGroupInvite(this.get("emailOrUsername").trim()) .then(data => { model.setProperties({ saving: false, finished: true }); - this.get("model.details.allowed_groups").pushObject( + this.get("inviteModel.details.allowed_groups").pushObject( Ember.Object.create(data.group) ); this.appEvents.trigger("post-stream:refresh"); }) .catch(onerror); } else { - return this.get("model") + return this.get("inviteModel") .createInvite( this.get("emailOrUsername").trim(), groupNames, @@ -323,19 +343,18 @@ export default Ember.Controller.extend(ModalFunctionality, { ) .then(result => { model.setProperties({ saving: false, finished: true }); - if (!this.get("invitingToTopic")) { + if (!this.get("invitingToTopic") && userInvitedController) { Invite.findInvitedBy( this.currentUser, userInvitedController.get("filter") - ).then(invite_model => { - userInvitedController.set("model", invite_model); - userInvitedController.set( - "totalInvites", - invite_model.invites.length - ); + ).then(inviteModel => { + userInvitedController.setProperties({ + model: inviteModel, + totalInvites: inviteModel.invites.length + }); }); - } else if (this.get("isMessage") && result && result.user) { - this.get("model.details.allowed_users").pushObject( + } else if (this.get("isPM") && result && result.user) { + this.get("inviteModel.details.allowed_users").pushObject( Ember.Object.create(result.user) ); this.appEvents.trigger("post-stream:refresh"); @@ -353,24 +372,21 @@ export default Ember.Controller.extend(ModalFunctionality, { }, generateInvitelink() { - const self = this; - if (this.get("disabled")) { return; } - const groupNames = this.get("model.groupNames"), - userInvitedController = this.get("userInvitedShow"), - model = this.get("model"); - - var topicId = null; - if (this.get("invitingToTopic")) { - topicId = this.get("model.id"); - } - + const groupNames = this.get("inviteModel.groupNames"); + const userInvitedController = this.get("userInvitedShow"); + const model = this.get("inviteModel"); model.setProperties({ saving: true, error: false }); - return this.get("model") + let topicId; + if (this.get("invitingToTopic")) { + topicId = this.get("inviteModel.id"); + } + + return model .generateInviteLink( this.get("emailOrUsername").trim(), groupNames, @@ -382,24 +398,26 @@ export default Ember.Controller.extend(ModalFunctionality, { finished: true, inviteLink: result }); - Invite.findInvitedBy( - this.currentUser, - userInvitedController.get("filter") - ).then(invite_model => { - userInvitedController.set("model", invite_model); - userInvitedController.set( - "totalInvites", - invite_model.invites.length - ); - }); + + if (userInvitedController) { + Invite.findInvitedBy( + this.currentUser, + userInvitedController.get("filter") + ).then(inviteModel => { + userInvitedController.setProperties({ + model: inviteModel, + totalInvites: inviteModel.invites.length + }); + }); + } }) - .catch(function(e) { + .catch(e => { if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { - self.set("errorMessage", e.jqXHR.responseJSON.errors[0]); + this.set("errorMessage", e.jqXHR.responseJSON.errors[0]); } else { - self.set( + this.set( "errorMessage", - self.get("isMessage") + this.get("isPM") ? I18n.t("topic.invite_private.error") : I18n.t("topic.invite_reply.error") ); @@ -411,7 +429,7 @@ export default Ember.Controller.extend(ModalFunctionality, { showCustomMessageBox() { this.toggleProperty("hasCustomMessage"); if (this.get("hasCustomMessage")) { - if (this.get("model") === this.currentUser) { + if (this.get("inviteModel") === this.currentUser) { this.set( "customMessage", I18n.t("invite.custom_message_template_forum") diff --git a/app/assets/javascripts/discourse/components/modal-panel.js.es6 b/app/assets/javascripts/discourse/components/modal-panel.js.es6 new file mode 100644 index 0000000000..b441457a7d --- /dev/null +++ b/app/assets/javascripts/discourse/components/modal-panel.js.es6 @@ -0,0 +1,11 @@ +import { fmt } from "discourse/lib/computed"; + +export default Ember.Component.extend({ + panel: null, + + panelComponent: fmt("panel.id", "%@-panel"), + + classNameBindings: ["panel.id"], + + classNames: ["modal-panel"] +}); diff --git a/app/assets/javascripts/discourse/components/modal-tab.js.es6 b/app/assets/javascripts/discourse/components/modal-tab.js.es6 new file mode 100644 index 0000000000..275581a293 --- /dev/null +++ b/app/assets/javascripts/discourse/components/modal-tab.js.es6 @@ -0,0 +1,17 @@ +import { propertyEqual } from "discourse/lib/computed"; + +export default Ember.Component.extend({ + tagName: "li", + classNames: ["modal-tab"], + panel: null, + selectedPanel: null, + panelsLength: null, + classNameBindings: ["isActive", "singleTab", "panel.id"], + singleTab: Ember.computed.equal("panelsLength", 1), + title: Ember.computed.alias("panel.title"), + isActive: propertyEqual("panel.id", "selectedPanel.id"), + + click() { + this.onSelectPanel(this.get("panel")); + } +}); diff --git a/app/assets/javascripts/discourse/components/second-factor-form.js.es6 b/app/assets/javascripts/discourse/components/second-factor-form.js.es6 index f930a4cb39..7c75b0edc1 100644 --- a/app/assets/javascripts/discourse/components/second-factor-form.js.es6 +++ b/app/assets/javascripts/discourse/components/second-factor-form.js.es6 @@ -16,17 +16,23 @@ export default Ember.Component.extend({ : I18n.t("login.second_factor_backup_description"); }, - @computed("secondFactorMethod") - linkText(secondFactorMethod) { - return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP - ? "login.second_factor_backup" - : "login.second_factor"; + @computed("secondFactorMethod", "isLogin") + linkText(secondFactorMethod, isLogin) { + if (isLogin) { + return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP + ? "login.second_factor_backup" + : "login.second_factor"; + } else { + return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP + ? "user.second_factor_backup.use" + : "user.second_factor.use"; + } }, actions: { toggleSecondFactorMethod() { const secondFactorMethod = this.get("secondFactorMethod"); - this.set("loginSecondFactor", ""); + this.set("secondFactorToken", ""); if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) { this.set("secondFactorMethod", SECOND_FACTOR_METHODS.BACKUP_CODE); } else { diff --git a/app/assets/javascripts/discourse/components/share-panel.js.es6 b/app/assets/javascripts/discourse/components/share-panel.js.es6 new file mode 100644 index 0000000000..18a4801114 --- /dev/null +++ b/app/assets/javascripts/discourse/components/share-panel.js.es6 @@ -0,0 +1,75 @@ +import { escapeExpression } from "discourse/lib/utilities"; +import { default as computed } from "ember-addons/ember-computed-decorators"; +import Sharing from "discourse/lib/sharing"; + +export default Ember.Component.extend({ + tagName: null, + + type: Ember.computed.alias("panel.model.type"), + + topic: Ember.computed.alias("panel.model.topic"), + + @computed + sources() { + return Sharing.activeSources(this.siteSettings.share_links); + }, + + @computed("type", "topic.title") + shareTitle(type, topicTitle) { + topicTitle = escapeExpression(topicTitle); + return I18n.t("share.topic_html", { topicTitle }); + }, + + @computed("panel.model.shareUrl", "topic.shareUrl") + shareUrl(forcedShareUrl, shareUrl) { + shareUrl = forcedShareUrl || shareUrl; + + if (Ember.isEmpty(shareUrl)) { + return; + } + + // Relative urls + if (shareUrl.indexOf("/") === 0) { + const location = window.location; + shareUrl = `${location.protocol}//${location.host}${shareUrl}`; + } + + return encodeURI(shareUrl); + }, + + didInsertElement() { + this._super(...arguments); + + const shareUrl = this.get("shareUrl"); + const $linkInput = this.$(".topic-share-url"); + const $linkForTouch = this.$(".topic-share-url-for-touch a"); + + Ember.run.schedule("afterRender", () => { + if (!this.capabilities.touch) { + $linkForTouch.parent().remove(); + + $linkInput + .val(shareUrl) + .select() + .focus(); + } else { + $linkInput.remove(); + + $linkForTouch.attr("href", shareUrl).text(shareUrl); + + const range = window.document.createRange(); + range.selectNode($linkForTouch[0]); + window.getSelection().addRange(range); + } + }); + }, + + actions: { + share(source) { + Sharing.shareSource(source, { + url: this.get("shareUrl"), + title: this.get("topic.title") + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/share-popup.js.es6 b/app/assets/javascripts/discourse/components/share-popup.js.es6 index 29f3f482ce..5b0ff9745e 100644 --- a/app/assets/javascripts/discourse/components/share-popup.js.es6 +++ b/app/assets/javascripts/discourse/components/share-popup.js.es6 @@ -2,6 +2,7 @@ import { wantsNewWindow } from "discourse/lib/intercept-click"; import { longDateNoYear } from "discourse/lib/formatter"; import computed from "ember-addons/ember-computed-decorators"; import Sharing from "discourse/lib/sharing"; +import { nativeShare } from "discourse/lib/pwa-utils"; export default Ember.Component.extend({ elementId: "share-link", @@ -87,11 +88,6 @@ export default Ember.Component.extend({ Ember.run.scheduleOnce("afterRender", this, this._focusUrl); }, - _webShare(url) { - // We can pass title and text too, but most share targets do their own oneboxing - return navigator.share({ url }); - }, - didInsertElement() { this._super(...arguments); @@ -126,12 +122,10 @@ export default Ember.Component.extend({ this.setProperties({ postNumber, date, postId }); // use native webshare only when the user clicks on the "chain" icon - // navigator.share needs HTTPS, returns undefined on HTTP - if (navigator.share && !$currentTarget.hasClass("post-date")) { - this._webShare(url).catch(() => { - // if navigator fails for unexpected reason fallback to popup - this._showUrl($currentTarget, url); - }); + if (!$currentTarget.hasClass("post-date")) { + nativeShare({ url }).then(null, () => + this._showUrl($currentTarget, url) + ); } else { this._showUrl($currentTarget, url); } @@ -160,15 +154,6 @@ export default Ember.Component.extend({ }, actions: { - replyAsNewTopic() { - const postStream = this.get("topic.postStream"); - const postId = - this.get("postId") || postStream.findPostIdForPostNumber(1); - const post = postStream.findLoadedPost(postId); - this.get("replyAsNewTopic")(post); - this.send("close"); - }, - close() { this.setProperties({ link: null, @@ -179,17 +164,10 @@ export default Ember.Component.extend({ }, share(source) { - const url = source.generateUrl(this.get("link"), this.get("topic.title")); - if (source.shouldOpenInPopup) { - window.open( - url, - "", - "menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=600,height=" + - (source.popupHeight || 315) - ); - } else { - window.open(url, "_blank"); - } + Sharing.shareSource(source, { + url: this.get("link"), + title: this.get("topic.title") + }); } } }); diff --git a/app/assets/javascripts/discourse/components/site-header.js.es6 b/app/assets/javascripts/discourse/components/site-header.js.es6 index 85913e6a71..28627bff78 100644 --- a/app/assets/javascripts/discourse/components/site-header.js.es6 +++ b/app/assets/javascripts/discourse/components/site-header.js.es6 @@ -15,7 +15,7 @@ function addFlagProperty(prop) { const PANEL_BODY_MARGIN = 30; //android supports pulling in from the screen edges -const SCREEN_EDGE_MARGIN = 30; +const SCREEN_EDGE_MARGIN = 20; const SCREEN_OFFSET = 300; const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, { @@ -228,6 +228,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, { didInsertElement() { this._super(...arguments); + const { isAndroid } = this.capabilities; $(window).on("resize.discourse-menu-panel", () => this.afterRender()); this.appEvents.on("header:show-topic", topic => this.setTopic(topic)); @@ -244,11 +245,16 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, { } }); - this.addTouchListeners($("body")); + // Only add listeners for opening menus by swiping them in on Android devices + // iOS will respond to these events, but also does swiping for back/forward + if (isAndroid) { + this.addTouchListeners($("body")); + } }, willDestroyElement() { this._super(...arguments); + const { isAndroid } = this.capabilities; $("body").off("keydown.header"); $(window).off("resize.discourse-menu-panel"); @@ -256,7 +262,9 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, { this.appEvents.off("header:hide-topic"); this.appEvents.off("dom:clean"); - this.removeTouchListeners($("body")); + if (isAndroid) { + this.removeTouchListeners($("body")); + } Ember.run.cancel(this._scheduledRemoveAnimate); window.cancelAnimationFrame(this._scheduledMovingAnimation); diff --git a/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 b/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 index a46a9a5eae..a92f22bfc8 100644 --- a/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 @@ -30,10 +30,7 @@ export default Ember.Component.extend({ return !isPM || this.siteSettings.enable_personal_messages; }, - @computed("topic.details.can_invite_to") - canInviteTo(result) { - return !this.site.mobileView && result; - }, + canInviteTo: Ember.computed.alias("topic.details.can_invite_to"), inviteDisabled: Ember.computed.or( "topic.archived", diff --git a/app/assets/javascripts/discourse/components/topic-progress.js.es6 b/app/assets/javascripts/discourse/components/topic-progress.js.es6 index d77d7f7cbe..3990124b66 100644 --- a/app/assets/javascripts/discourse/components/topic-progress.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-progress.js.es6 @@ -155,14 +155,16 @@ export default Ember.Component.extend({ const $wrapper = this.$(); if (!$wrapper || $wrapper.length === 0) return; - const offset = window.pageYOffset || $("html").scrollTop(), + const $html = $("html"), + offset = window.pageYOffset || $html.scrollTop(), progressHeight = this.site.mobileView ? 0 : $("#topic-progress").height(), maximumOffset = $("#topic-bottom").offset().top + progressHeight, windowHeight = $(window).height(), bodyHeight = $("body").height(), composerHeight = $("#reply-control").height() || 0, isDocked = offset >= maximumOffset - windowHeight + composerHeight, - bottom = bodyHeight - maximumOffset; + bottom = bodyHeight - maximumOffset, + wrapperDir = $html.hasClass("rtl") ? "left" : "right"; if (composerHeight > 0) { $wrapper.css("bottom", isDocked ? bottom : composerHeight); @@ -174,9 +176,9 @@ export default Ember.Component.extend({ const $replyArea = $("#reply-control .reply-area"); if ($replyArea && $replyArea.length > 0) { - $wrapper.css("right", `${$replyArea.offset().left}px`); + $wrapper.css(wrapperDir, `${$replyArea.offset().left}px`); } else { - $wrapper.css("right", "1em"); + $wrapper.css(wrapperDir, "1em"); } // switch mobile scroll logo at the very bottom of topics diff --git a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 index d13fcd101c..4849a917af 100644 --- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 +++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 @@ -195,6 +195,16 @@ export default Ember.Component.extend( this._close(); }, + ignoreUser() { + this.get("user").ignore(); + this._close(); + }, + + watchUser() { + this.get("user").watch(); + this._close(); + }, + showUser() { this.showUser(this.get("user")); this._close(); diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index e07f06ccf4..1ad76ba6ab 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -34,7 +34,8 @@ export default TextField.extend({ single = bool("single"), allowAny = bool("allowAny"), disabled = bool("disabled"), - disallowEmails = bool("disallowEmails"); + allowEmails = bool("allowEmails"), + fullWidthWrap = bool("fullWidthWrap"); function excludedUsernames() { // hack works around some issues with allowAny eventing @@ -54,6 +55,7 @@ export default TextField.extend({ single: single, allowAny: allowAny, updateData: opts && opts.updateData ? opts.updateData : false, + fullWidthWrap, dataSource(term) { var results = userSearch({ @@ -65,7 +67,7 @@ export default TextField.extend({ includeMentionableGroups, includeMessageableGroups, group: self.get("group"), - disallowEmails + allowEmails }); return results; }, diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 6760e0279b..8156df8e42 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -12,7 +12,8 @@ import { getOwner } from "discourse-common/lib/get-owner"; import { escapeExpression, uploadIcon, - authorizesOneOrMoreExtensions + authorizesOneOrMoreExtensions, + safariHacksDisabled } from "discourse/lib/utilities"; import { emojiUnescape } from "discourse/lib/text"; import { shortDate } from "discourse/lib/formatter"; @@ -133,10 +134,11 @@ export default Ember.Controller.extend({ @computed( "model.replyingToTopic", "model.creatingPrivateMessage", - "model.targetUsernames" + "model.targetUsernames", + "model.composeState" ) - focusTarget(replyingToTopic, creatingPM, usernames) { - if (this.capabilities.isIOS) { + focusTarget(replyingToTopic, creatingPM, usernames, composeState) { + if (this.capabilities.isIOS && !safariHacksDisabled()) { return "none"; } @@ -153,6 +155,10 @@ export default Ember.Controller.extend({ return "reply"; } + if (composeState === Composer.FULLSCREEN) { + return "editor"; + } + return "title"; }, @@ -542,18 +548,19 @@ export default Ember.Controller.extend({ ) { groups.forEach(group => { let body; + const groupLink = Discourse.getURL(`/g/${group.name}/members`); if (group.max_mentions < group.user_count) { body = I18n.t("composer.group_mentioned_limit", { group: "@" + group.name, max: group.max_mentions, - group_link: Discourse.getURL(`/groups/${group.name}/members`) + group_link: groupLink }); } else { body = I18n.t("composer.group_mentioned", { group: "@" + group.name, count: group.user_count, - group_link: Discourse.getURL(`/groups/${group.name}/members`) + group_link: groupLink }); } @@ -783,7 +790,11 @@ export default Ember.Controller.extend({ }); // Scope the categories drop down to the category we opened the composer with. - if (opts.categoryId && opts.draftKey !== "reply_as_new_topic") { + if ( + opts.categoryId && + opts.draftKey !== "reply_as_new_topic" && + !opts.disableScopedCategory + ) { const category = this.site.categories.findBy("id", opts.categoryId); if (category) { this.set("scopedCategoryId", opts.categoryId); diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index 34ca143444..d5e6042c7b 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -106,7 +106,7 @@ export default Ember.Controller.extend(ModalFunctionality, { data: { login: this.get("loginName"), password: this.get("loginPassword"), - second_factor_token: this.get("loginSecondFactor"), + second_factor_token: this.get("secondFactorToken"), second_factor_method: this.get("secondFactorMethod") } }).then( diff --git a/app/assets/javascripts/discourse/controllers/move-to-topic.js.es6 b/app/assets/javascripts/discourse/controllers/move-to-topic.js.es6 index 5e47258f7c..ce56fa15fe 100644 --- a/app/assets/javascripts/discourse/controllers/move-to-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/move-to-topic.js.es6 @@ -66,10 +66,12 @@ export default Ember.Controller.extend(ModalFunctionality, { }); const isPrivateMessage = this.get("model.isPrivateMessage"); - const canSplitTopic = this.get("canSplitTopic"); if (isPrivateMessage) { - this.set("selection", canSplitTopic ? "new_message" : "existing_message"); - } else if (!canSplitTopic) { + this.set( + "selection", + this.get("canSplitToPM") ? "new_message" : "existing_message" + ); + } else if (!this.get("canSplitTopic")) { this.set("selection", "existing_topic"); Ember.run.next(() => $("#choose-topic-title").focus()); } @@ -85,6 +87,11 @@ export default Ember.Controller.extend(ModalFunctionality, { ); }, + @computed("canSplitTopic") + canSplitToPM(canSplitTopic) { + return canSplitTopic && (this.currentUser && this.currentUser.admin); + }, + actions: { performMove() { this.get("moveTypes").forEach(type => { diff --git a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 index 7e2ed248b5..2c74c8f208 100644 --- a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 +++ b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 @@ -9,7 +9,7 @@ export default Ember.Controller.extend(PasswordValidation, { isDeveloper: Ember.computed.alias("model.is_developer"), admin: Ember.computed.alias("model.admin"), secondFactorRequired: Ember.computed.alias("model.second_factor_required"), - backupEnabled: Ember.computed.alias("model.second_factor_backup_enabled"), + backupEnabled: Ember.computed.alias("model.backup_enabled"), secondFactorMethod: SECOND_FACTOR_METHODS.TOTP, passwordRequired: true, errorMessage: null, @@ -38,7 +38,7 @@ export default Ember.Controller.extend(PasswordValidation, { type: "PUT", data: { password: this.get("accountPassword"), - second_factor_token: this.get("secondFactor"), + second_factor_token: this.get("secondFactorToken"), second_factor_method: this.get("secondFactorMethod") } }) diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 index 1bd1beb6ea..b3c09ef6b9 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 @@ -92,9 +92,9 @@ export default Ember.Controller.extend( return userId !== this.get("currentUser.id"); }, - @computed("model.second_factor_enabled") - canUpdateAssociatedAccounts(secondFactorEnabled) { - if (secondFactorEnabled) { + @computed("model.second_factor_enabled", "canCheckEmails") + canUpdateAssociatedAccounts(secondFactorEnabled, canCheckEmails) { + if (secondFactorEnabled || !canCheckEmails) { return false; } @@ -106,9 +106,15 @@ export default Ember.Controller.extend( @computed("showAllAuthTokens", "model.user_auth_tokens") authTokens(showAllAuthTokens, tokens) { - tokens.sort((a, b) => - a.is_active ? -1 : b.is_active ? 1 : b.seen_at.localeCompare(a.seen_at) - ); + tokens.sort((a, b) => { + if (a.is_active) { + return -1; + } else if (b.is_active) { + return 1; + } else { + return b.seen_at.localeCompare(a.seen_at); + } + }); return showAllAuthTokens ? tokens diff --git a/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 index 864e577741..cdd21a6151 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 @@ -20,12 +20,12 @@ export default Ember.Controller.extend(PreferencesTabController, { return [].concat(watched, watchedFirst, tracked, muted).filter(t => t); }, - canSave: function() { - return ( - this.get("currentUser.id") === this.get("model.id") || - this.get("currentUser.admin") - ); - }.property(), + @computed + canSee() { + return this.get("currentUser.id") === this.get("model.id"); + }, + + canSave: Ember.computed.or("canSee", "currentUser.admin"), actions: { save() { diff --git a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 index a418cea7fa..3b9d6e6a63 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 @@ -5,12 +5,12 @@ import { observes } from "ember-addons/ember-computed-decorators"; import { - currentThemeId, listThemes, previewTheme, setLocalTheme } from "discourse/lib/theme-selector"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { safariHacksDisabled, isiPad } from "discourse/lib/utilities"; const USER_HOMES = { 1: "latest", @@ -46,19 +46,22 @@ export default Ember.Controller.extend(PreferencesTabController, { }, preferencesController: Ember.inject.controller("preferences"), - makeThemeDefault: true, - makeTextSizeDefault: true, + + @computed() + isiPad() { + return isiPad(); + }, + + @computed() + disableSafariHacks() { + return safariHacksDisabled(); + }, @computed() availableLocales() { return JSON.parse(this.siteSettings.available_locales); }, - @computed() - themeId() { - return currentThemeId(); - }, - @computed textSizes() { return TEXT_SIZES.map(value => { @@ -81,6 +84,16 @@ export default Ember.Controller.extend(PreferencesTabController, { previewTheme([id]); }, + @computed("model.user_option.theme_ids", "themeId") + showThemeSetDefault(userOptionThemes, selectedTheme) { + return !userOptionThemes || userOptionThemes[0] !== selectedTheme; + }, + + @computed("model.user_option.text_size", "textSize") + showTextSetDefault(userOptionTextSize, selectedTextSize) { + return userOptionTextSize !== selectedTextSize; + }, + homeChanged() { const siteHome = this.siteSettings.top_menu.split("|")[0].split(",")[0]; const userHome = USER_HOMES[this.get("model.user_option.homepage_id")]; @@ -120,17 +133,31 @@ export default Ember.Controller.extend(PreferencesTabController, { .then(() => { this.set("saved", true); - if (!makeThemeDefault) { + if (makeThemeDefault) { + setLocalTheme([]); + } else { setLocalTheme( [this.get("themeId")], this.get("model.user_option.theme_key_seq") ); } - if (!makeTextSizeDefault) { + if (makeTextSizeDefault) { + this.get("model").updateTextSizeCookie(null); + } else { this.get("model").updateTextSizeCookie(this.get("textSize")); } this.homeChanged(); + + if (this.get("isiPad")) { + if (safariHacksDisabled() !== this.get("disableSafariHacks")) { + Discourse.set("assetVersion", "forceRefresh"); + } + localStorage.setItem( + "safari-hacks-disabled", + this.get("disableSafariHacks").toString() + ); + } }) .catch(popupAjaxError); }, diff --git a/app/assets/javascripts/discourse/controllers/preferences/second-factor-backup.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/second-factor-backup.js.es6 index e0b703b52d..5f41061f97 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/second-factor-backup.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/second-factor-backup.js.es6 @@ -1,6 +1,7 @@ import { default as computed } from "ember-addons/ember-computed-decorators"; import { default as DiscourseURL, userPath } from "discourse/lib/url"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { SECOND_FACTOR_METHODS } from "discourse/models/user"; export default Ember.Controller.extend({ loading: false, @@ -11,10 +12,15 @@ export default Ember.Controller.extend({ "model.second_factor_remaining_backup_codes" ), backupCodes: null, + secondFactorMethod: SECOND_FACTOR_METHODS.TOTP, - @computed("secondFactorToken") - isValidSecondFactorToken(secondFactorToken) { - return secondFactorToken && secondFactorToken.length === 6; + @computed("secondFactorToken", "secondFactorMethod") + isValidSecondFactorToken(secondFactorToken, secondFactorMethod) { + if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) { + return secondFactorToken && secondFactorToken.length === 6; + } else if (secondFactorMethod === SECOND_FACTOR_METHODS.BACKUP_CODE) { + return secondFactorToken && secondFactorToken.length === 16; + } }, @computed("isValidSecondFactorToken", "backupEnabled", "loading") @@ -59,7 +65,12 @@ export default Ember.Controller.extend({ this.set("loading", true); this.get("model") - .toggleSecondFactor(this.get("secondFactorToken"), false, 2) + .toggleSecondFactor( + this.get("secondFactorToken"), + this.get("secondFactorMethod"), + SECOND_FACTOR_METHODS.BACKUP_CODE, + false + ) .then(response => { if (response.error) { this.set("errorMessage", response.error); @@ -79,7 +90,10 @@ export default Ember.Controller.extend({ if (!this.get("secondFactorToken")) return; this.set("loading", true); this.get("model") - .generateSecondFactorCodes(this.get("secondFactorToken")) + .generateSecondFactorCodes( + this.get("secondFactorToken"), + this.get("secondFactorMethod") + ) .then(response => { if (response.error) { this.set("errorMessage", response.error); diff --git a/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 index af6e78eb6d..662eb31adc 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 @@ -2,6 +2,7 @@ import { default as computed } from "ember-addons/ember-computed-decorators"; import { default as DiscourseURL, userPath } from "discourse/lib/url"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { findAll } from "discourse/models/login-method"; +import { SECOND_FACTOR_METHODS } from "discourse/models/user"; export default Ember.Controller.extend({ loading: false, @@ -13,6 +14,8 @@ export default Ember.Controller.extend({ showSecondFactorKey: false, errorMessage: null, newUsername: null, + backupEnabled: Ember.computed.alias("model.second_factor_backup_enabled"), + secondFactorMethod: SECOND_FACTOR_METHODS.TOTP, loaded: Ember.computed.and("secondFactorImage", "secondFactorKey"), @@ -41,7 +44,12 @@ export default Ember.Controller.extend({ this.set("loading", true); this.get("model") - .toggleSecondFactor(this.get("secondFactorToken"), enable, 1) + .toggleSecondFactor( + this.get("secondFactorToken"), + this.get("secondFactorMethod"), + SECOND_FACTOR_METHODS.TOTP, + enable + ) .then(response => { if (response.error) { this.set("errorMessage", response.error); diff --git a/app/assets/javascripts/discourse/controllers/tags-show.js.es6 b/app/assets/javascripts/discourse/controllers/tags-show.js.es6 index 59ad9db32f..65062dfbc4 100644 --- a/app/assets/javascripts/discourse/controllers/tags-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/tags-show.js.es6 @@ -1,4 +1,7 @@ -import { default as computed } from "ember-addons/ember-computed-decorators"; +import { + default as computed, + observes +} from "ember-addons/ember-computed-decorators"; import BulkTopicSelection from "discourse/mixins/bulk-topic-selection"; import { default as NavItem, @@ -19,14 +22,14 @@ if (extraNavItemProperties) { if (customNavItemHref) { customNavItemHref(function(navItem) { if (navItem.get("tagId")) { - var name = navItem.get("name"); + const name = navItem.get("name"); if (!Discourse.Site.currentProp("filters").includes(name)) { return null; } - var path = "/tags/", - category = navItem.get("category"); + let path = "/tags/"; + const category = navItem.get("category"); if (category) { path += "c/"; @@ -37,8 +40,8 @@ if (customNavItemHref) { path += "/"; } - path += navItem.get("tagId") + "/l/"; - return path + name.replace(" ", "-"); + path += `${navItem.get("tagId")}/l/`; + return `${path}${name.replace(" ", "-")}`; } else { return null; } @@ -66,9 +69,10 @@ export default Ember.Controller.extend(BulkTopicSelection, { categories: Ember.computed.alias("site.categoriesList"), - createTopicLabel: function() { - return this.get("list.draft") ? "topic.open_draft" : "topic.create"; - }.property("list", "list.draft"), + @computed("list", "list.draft") + createTopicLabel(list, listDraft) { + return listDraft ? "topic.open_draft" : "topic.create"; + }, @computed("canCreateTopic", "category", "canCreateTopicOnCategory") createTopicDisabled(canCreateTopic, category, canCreateTopicOnCategory) { @@ -85,92 +89,87 @@ export default Ember.Controller.extend(BulkTopicSelection, { "q" ], - navItems: function() { - return NavItem.buildList(this.get("category"), { - tagId: this.get("tag.id"), - filterMode: this.get("filterMode") + @computed("category", "tag.id", "filterMode") + navItems(category, tagId, filterMode) { + return NavItem.buildList(category, { + tagId, + filterMode }); - }.property("category", "tag.id", "filterMode"), + }, - showTagFilter: function() { + @computed("category") + showTagFilter() { return Discourse.SiteSettings.show_filter_by_tag; - }.property("category"), + }, - showAdminControls: function() { - return ( - !this.get("additionalTags") && - this.get("canAdminTag") && - !this.get("category") - ); - }.property("additionalTags", "canAdminTag", "category"), + @computed("additionalTags", "canAdminTag", "category") + showAdminControls(additionalTags, canAdminTag, category) { + return !additionalTags && canAdminTag && !category; + }, loadMoreTopics() { return this.get("list").loadMore(); }, - _showFooter: function() { + @observes("list.canLoadMore") + _showFooter() { this.set("application.showFooter", !this.get("list.canLoadMore")); - }.observes("list.canLoadMore"), + }, - footerMessage: function() { - if (this.get("loading") || this.get("list.topics.length") !== 0) { + @computed("navMode", "list.topics.length", "loading") + footerMessage(navMode, listTopicsLength, loading) { + if (loading || listTopicsLength !== 0) { return; } - if (this.get("list.topics.length") === 0) { - return I18n.t("tagging.topics.none." + this.get("navMode"), { + if (listTopicsLength === 0) { + return I18n.t(`tagging.topics.none.${navMode}`, { tag: this.get("tag.id") }); } else { - return I18n.t("tagging.topics.bottom." + this.get("navMode"), { + return I18n.t(`tagging.topics.bottom.${navMode}`, { tag: this.get("tag.id") }); } - }.property("navMode", "list.topics.length", "loading"), + }, actions: { - changeSort(sortBy) { - if (sortBy === this.get("order")) { + changeSort(order) { + if (order === this.get("order")) { this.toggleProperty("ascending"); } else { - this.setProperties({ order: sortBy, ascending: false }); + this.setProperties({ order, 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(); + .then(list => { + this.set("list", list); + this.resetSelected(); }); }, deleteTag() { - const self = this; const numTopics = this.get("list.topic_list.tags.firstObject.topic_count") || 0; + const confirmText = numTopics === 0 ? I18n.t("tagging.delete_confirm_no_topics") : I18n.t("tagging.delete_confirm", { count: numTopics }); - bootbox.confirm(confirmText, function(result) { - if (!result) { - return; - } - self - .get("tag") + bootbox.confirm(confirmText, result => { + if (!result) return; + + this.get("tag") .destroyRecord() - .then(function() { - self.transitionToRoute("tags.index"); - }) - .catch(function() { - bootbox.alert(I18n.t("generic_error")); - }); + .then(() => this.transitionToRoute("tags.index")) + .catch(() => bootbox.alert(I18n.t("generic_error"))); }); }, diff --git a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 index 2707013a88..f68788f478 100644 --- a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 @@ -1,8 +1,11 @@ import Invite from "discourse/models/invite"; import debounce from "discourse/lib/debounce"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { + default as computed, + observes +} from "ember-addons/ember-computed-decorators"; -// This controller handles actions related to a user's invitations export default Ember.Controller.extend({ user: null, model: null, @@ -13,84 +16,67 @@ export default Ember.Controller.extend({ invitesLoading: false, reinvitedAll: false, rescindedAll: false, + searchTerm: null, - init: function() { + init() { this._super(...arguments); + this.set("searchTerm", ""); }, - /** - Observe the search term box with a debouncer and change the results. - - @observes searchTerm - **/ + @observes("searchTearm") _searchTermChanged: debounce(function() { - var self = this; Invite.findInvitedBy( - self.get("user"), + this.get("user"), this.get("filter"), this.get("searchTerm") - ).then(function(invites) { - self.set("model", invites); - }); - }, 250).observes("searchTerm"), + ).then(invites => this.set("model", invites)); + }, 250), inviteRedeemed: Ember.computed.equal("filter", "redeemed"), - showBulkActionButtons: function() { + @computed("filter") + showBulkActionButtons(filter) { return ( - this.get("filter") === "pending" && + filter === "pending" && this.get("model").invites.length > 4 && this.currentUser.get("staff") ); - }.property("filter"), + }, - /** - Can the currently logged in user invite users to the site - - @property canInviteToForum - **/ - canInviteToForum: function() { + @computed + canInviteToForum() { return Discourse.User.currentProp("can_invite_to_forum"); - }.property(), + }, - /** - Can the currently logged in user bulk invite users to the site (only Admin is allowed to perform this operation) - - @property canBulkInvite - **/ - canBulkInvite: function() { + @computed + canBulkInvite() { return Discourse.User.currentProp("admin"); - }.property(), + }, - /** - Should the search filter input box be displayed? + showSearch: Ember.computed.gte("totalInvites", 10), - @property showSearch - **/ - showSearch: function() { - return this.get("totalInvites") > 9; - }.property("totalInvites"), - - pendingLabel: function() { - if (this.get("invitesCount.total") > 50) { + @computed("invitesCount.total", "invitesCount.pending}") + pendingLabel(invitesCountTotal, invitesCountPending) { + if (invitesCountTotal > 50) { return I18n.t("user.invited.pending_tab_with_count", { - count: this.get("invitesCount.pending") + count: invitesCountPending }); } else { return I18n.t("user.invited.pending_tab"); } - }.property("invitesCount"), + }, - redeemedLabel: function() { - if (this.get("invitesCount.total") > 50) { + @computed("invitesCount.total", "invitesCount.redeemed") + redeemedLabel(invitesCountTotal, invitesCountRedeemed) { + if (invitesCountTotal > 50) { return I18n.t("user.invited.redeemed_tab_with_count", { - count: this.get("invitesCount.redeemed") + count: invitesCountRedeemed }); } else { return I18n.t("user.invited.redeemed_tab"); } - }.property("invitesCount"), + }, actions: { rescind(invite) { @@ -120,34 +106,31 @@ export default Ember.Controller.extend({ bootbox.confirm(I18n.t("user.invited.reinvite_all_confirm"), confirm => { if (confirm) { Invite.reinviteAll() - .then(() => { - this.set("reinvitedAll", true); - }) + .then(() => this.set("reinvitedAll", true)) .catch(popupAjaxError); } }); }, loadMore() { - var self = this; - var model = self.get("model"); + const model = this.get("model"); - if (self.get("canLoadMore") && !self.get("invitesLoading")) { - self.set("invitesLoading", true); + if (this.get("canLoadMore") && !this.get("invitesLoading")) { + this.set("invitesLoading", true); Invite.findInvitedBy( - self.get("user"), - self.get("filter"), - self.get("searchTerm"), + this.get("user"), + this.get("filter"), + this.get("searchTerm"), model.invites.length - ).then(function(invite_model) { - self.set("invitesLoading", false); + ).then(invite_model => { + this.set("invitesLoading", false); model.invites.pushObjects(invite_model.invites); if ( invite_model.invites.length === 0 || invite_model.invites.length < Discourse.SiteSettings.invites_per_page ) { - self.set("canLoadMore", false); + this.set("canLoadMore", false); } }); } diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index e0b31bd172..d6f577fe37 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -145,6 +145,16 @@ export default Ember.Controller.extend(CanCheckEmails, { adminDelete() { this.get("adminTools").deleteUser(this.get("model.id")); + }, + + ignoreUser() { + const user = this.get("model"); + user.ignore().then(() => user.set("ignored", true)); + }, + + watchUser() { + const user = this.get("model"); + user.watch().then(() => user.set("ignored", false)); } } }); diff --git a/app/assets/javascripts/discourse/initializers/localization.js.es6 b/app/assets/javascripts/discourse/initializers/localization.js.es6 index a5f96b83a6..fc9eee1536 100644 --- a/app/assets/javascripts/discourse/initializers/localization.js.es6 +++ b/app/assets/javascripts/discourse/initializers/localization.js.es6 @@ -51,5 +51,12 @@ export default { node[segs[segs.length - 1]] = v; } }); + + bootbox.addLocale(I18n.currentLocale(), { + OK: I18n.t("composer.modal_ok"), + CANCEL: I18n.t("composer.modal_cancel"), + CONFIRM: I18n.t("composer.modal_ok") + }); + bootbox.setLocale(I18n.currentLocale()); } }; diff --git a/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 b/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 index 61d84ab3ed..74bbcf4fe3 100644 --- a/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 +++ b/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 @@ -1,3 +1,5 @@ +import showModal from "discourse/lib/show-modal"; +import { nativeShare } from "discourse/lib/pwa-utils"; import { registerTopicFooterButton } from "discourse/lib/register-topic-footer-button"; export default { @@ -5,26 +7,63 @@ export default { initialize() { registerTopicFooterButton({ - id: "share", + id: "share-and-invite", icon: "link", priority: 999, label: "topic.share.title", title: "topic.share.help", action() { - this.appEvents.trigger( - "share:url", - this.get("topic.shareUrl"), - $("#topic-footer-buttons") - ); + const modal = () => { + const panels = [ + { + id: "share", + title: "topic.share.extended_title", + model: { + topic: this.get("topic") + } + } + ]; + + if (this.get("canInviteTo") && !this.get("inviteDisabled")) { + let invitePanelTitle; + + if (this.get("isPM")) { + invitePanelTitle = "topic.invite_private.title"; + } else if (this.get("invitingToTopic")) { + invitePanelTitle = "topic.invite_reply.title"; + } else { + invitePanelTitle = "user.invited.create"; + } + + panels.push({ + id: "invite", + title: invitePanelTitle, + model: { + inviteModel: this.get("topic") + } + }); + } + + showModal("share-and-invite", { + modalClass: "share-and-invite", + panels + }); + }; + + nativeShare({ url: this.get("topic.shareUrl") }).then(null, modal); }, dropdown() { return this.site.mobileView; }, - classNames: ["share"], - dependentKeys: ["topic.shareUrl", "topic.isPrivateMessage"], - displayed() { - return !this.get("topic.isPrivateMessage"); - } + classNames: ["share-and-invite"], + dependentKeys: [ + "topic.shareUrl", + "topic.isPrivateMessage", + "canInviteTo", + "inviteDisabled", + "isPM", + "invitingToTopic" + ] }); registerTopicFooterButton({ @@ -47,26 +86,6 @@ export default { } }); - registerTopicFooterButton({ - id: "invite", - icon: "users", - priority: 997, - label: "topic.invite_reply.title", - title: "topic.invite_reply.help", - action: "showInvite", - dropdown() { - return this.site.mobileView; - }, - classNames: ["invite-topic"], - dependentKeys: ["canInviteTo", "inviteDisabled"], - displayed() { - return this.get("canInviteTo"); - }, - disabled() { - return this.get("inviteDisabled"); - } - }); - registerTopicFooterButton({ dependentKeys: ["topic.bookmarked", "topic.isPrivateMessage"], id: "bookmark", @@ -97,7 +116,7 @@ export default { registerTopicFooterButton({ id: "archive", - priority: 1001, + priority: 996, icon() { return this.get("archiveIcon"); }, diff --git a/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 b/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 index 56febf3be6..dad0137574 100644 --- a/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 +++ b/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 @@ -10,6 +10,8 @@ export default { // URL rewrites (usually due to refactoring) DiscourseURL.rewrite(/^\/category\//, "/c/"); DiscourseURL.rewrite(/^\/group\//, "/groups/"); + DiscourseURL.rewrite(/^\/groups$/, "/g"); + DiscourseURL.rewrite(/^\/groups\//, "/g/"); DiscourseURL.rewrite(/\/private-messages\/$/, "/messages/"); DiscourseURL.rewrite(/^\/users$/, "/u"); DiscourseURL.rewrite(/^\/users\//, "/u/"); diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index 939eef6a90..a75568731f 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -201,7 +201,10 @@ export default function(options) { wrap = this.wrap( "
" ).parent(); - wrap.width(width); + + if (!options.fullWidthWrap) { + wrap.width(width); + } } if (options.single && !options.width) { diff --git a/app/assets/javascripts/discourse/lib/intercept-click.js.es6 b/app/assets/javascripts/discourse/lib/intercept-click.js.es6 index 1fe999912f..31a3c4666d 100644 --- a/app/assets/javascripts/discourse/lib/intercept-click.js.es6 +++ b/app/assets/javascripts/discourse/lib/intercept-click.js.es6 @@ -6,7 +6,8 @@ export function wantsNewWindow(e) { e.shiftKey || e.metaKey || e.ctrlKey || - (e.button && e.button !== 0) + (e.button && e.button !== 0) || + (e.target && e.target.target === "_blank") ); } diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index fb690805c6..af8443d157 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -7,7 +7,7 @@ const bindings = { "!": { postAction: "showFlags" }, "#": { handler: "goToPost", anonymous: true }, "/": { handler: "toggleSearch", anonymous: true }, - "ctrl+alt+f": { handler: "toggleSearch", anonymous: true }, + "ctrl+alt+f": { handler: "toggleSearch", anonymous: true, global: true }, "=": { handler: "toggleHamburgerMenu", anonymous: true }, "?": { handler: "showHelpModal", anonymous: true }, ".": { click: ".alert.alert-info.clickable", anonymous: true }, // show incoming/updated topics @@ -67,7 +67,7 @@ const bindings = { "shift+s": { click: "#topic-footer-buttons button.share", anonymous: true }, // share topic "shift+u": { handler: "goToUnreadPost" }, "shift+z shift+z": { handler: "logout" }, - "shift+f11": { handler: "fullscreenComposer" }, + "shift+f11": { handler: "fullscreenComposer", global: true }, t: { postAction: "replyAsNewTopic" }, u: { handler: "goBack", anonymous: true }, "x r": { @@ -101,7 +101,12 @@ export default { if (binding.path) { this._bindToPath(binding.path, key); } else if (binding.handler) { - this._bindToFunction(binding.handler, key); + if (binding.global) { + // global shortcuts will trigger even while focusing on input/textarea + this._globalBindToFunction(binding.handler, key); + } else { + this._bindToFunction(binding.handler, key); + } } else if (binding.postAction) { this._bindToSelectedPost(binding.postAction, key); } else if (binding.click) { @@ -399,6 +404,12 @@ export default { }); }, + _globalBindToFunction(func, binding) { + if (typeof this[func] === "function") { + this.keyTrapper.bindGlobal(binding, _.bind(this[func], this)); + } + }, + _bindToFunction(func, binding) { if (typeof this[func] === "function") { this.keyTrapper.bind(binding, _.bind(this[func], this)); @@ -406,53 +417,112 @@ export default { }, _moveSelection(direction) { + // Pressing a move key (J/K) very quick (i.e. keeping J or K pressed) will + // move fast by disabling smooth page scrolling. + const now = +new Date(); + const fast = this._lastMoveTime && now - this._lastMoveTime < 150; + this._lastMoveTime = now; + const $articles = this._findArticles(); + if ($articles === undefined) { + return; + } - if (typeof $articles === "undefined") return; + let $selected = $articles.filter(".selected"); + if ($selected.length === 0) { + $selected = $articles.filter("[data-islastviewedtopic=true]"); + } - const $selected = - $articles.filter(".selected").length !== 0 - ? $articles.filter(".selected") - : $articles.filter("[data-islastviewedtopic=true]"); + // If still nothing is selected, select the first post that is + // visible and cancel move operation. + if ($selected.length === 0) { + const offset = minimumOffset(); + $selected = $articles + .toArray() + .find(article => article.getBoundingClientRect().top > offset); + direction = 0; + } - let index = $articles.index($selected); + const index = $articles.index($selected); + let $article = $articles.eq(index); + + /* + * Try doing a page scroll in the context of current post. + */ + + if (!fast && direction !== 0 && $article.length > 0) { + /** @var Begin and end offsets for current article + * The beginning of first article is the beginning of the page. + */ + const beginArticle = + $article.is(".topic-post") && $article.find("#post_1").length + ? 0 + : $article.offset().top; + const endArticle = + $article.offset().top + $article[0].getBoundingClientRect().height; + + /** @var Begin and end offsets for screen */ + const beginScreen = $(window).scrollTop(); + const endScreen = beginScreen + window.innerHeight; + + if (direction < 0 && beginScreen > beginArticle) { + return this._scrollTo( + Math.max( + beginScreen - window.innerHeight + 3 * minimumOffset(), // page up + beginArticle - minimumOffset() // beginning of article + ) + ); + } else if (direction > 0 && endScreen < endArticle - minimumOffset()) { + return this._scrollTo( + Math.min( + endScreen - 3 * minimumOffset(), // page down + endArticle - window.innerHeight // end of article + ) + ); + } + } + + /* + * Try scrolling to post above or below. + */ if ($selected.length !== 0) { if (direction === -1 && index === 0) return; if (direction === 1 && index === $articles.length - 1) return; } - // when nothing is selected - if ($selected.length === 0) { - // select the first post with its top visible - const offset = minimumOffset(); - index = $articles - .toArray() - .findIndex(article => article.getBoundingClientRect().top > offset); - direction = 0; - } - - const $article = $articles.eq(index + direction); - + $article = $articles.eq(index + direction); if ($article.length > 0) { $articles.removeClass("selected"); $article.addClass("selected"); - if ($article.is(".topic-post")) { - $("a.tabLoc", $article).focus(); - this._scrollToPost($article); - } else { - this._scrollList($article, direction); + const articleRect = $article[0].getBoundingClientRect(); + if (!fast && direction < 0 && articleRect.height > window.innerHeight) { + // Scrolling to the last "page" of the previous post if post has multiple + // "pages" (if its height does not fit in the screen). + return this._scrollTo( + $article.offset().top + articleRect.height - window.innerHeight + ); + } else if ($article.is(".topic-post")) { + return this._scrollTo( + $article.find("#post_1").length > 0 + ? 0 + : $article.offset().top - minimumOffset(), + () => $("a.tabLoc", $article).focus() + ); } + + /* + * Otherwise scroll through the suggested topic list. + */ + this._scrollList($article, direction); } }, - _scrollToPost($article) { - if ($article.find("#post_1").length > 0) { - $(window).scrollTop(0); - } else { - $(window).scrollTop($article.offset().top - minimumOffset()); - } + _scrollTo(scrollTop, complete) { + $("html, body") + .stop(true, true) + .animate({ scrollTop }, { duration: 100, complete }); }, _scrollList($article) { diff --git a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 index c58797d0bf..16bc7304c1 100644 --- a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 +++ b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 @@ -16,7 +16,7 @@ function replaceSpan($e, username, opts) { extraClass = "notify"; } $e.replaceWith( - `@${username}` ); } else { diff --git a/app/assets/javascripts/discourse/lib/mobile.js.es6 b/app/assets/javascripts/discourse/lib/mobile.js.es6 index c05fd2c0f3..df164b031c 100644 --- a/app/assets/javascripts/discourse/lib/mobile.js.es6 +++ b/app/assets/javascripts/discourse/lib/mobile.js.es6 @@ -23,6 +23,9 @@ const Mobile = { if (window.location.search.match(/mobile_view=0/)) { localStorage.mobileView = false; } + if (window.location.search.match(/mobile_view=auto/)) { + localStorage.removeItem("mobileView"); + } if (localStorage.mobileView) { var savedValue = localStorage.mobileView === "true"; if (savedValue !== this.mobileView) { diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index e174b581a3..910b55fbe4 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -31,6 +31,7 @@ import { replaceIcon } from "discourse-common/lib/icon-library"; import { replaceCategoryLinkRenderer } from "discourse/helpers/category-link"; +import { replaceTagRenderer } from "discourse/lib/render-tag"; import { addNavItem } from "discourse/models/nav-item"; import { replaceFormatter } from "discourse/lib/utilities"; import { modifySelectKit } from "select-kit/mixins/plugin-api"; @@ -42,7 +43,7 @@ import Sharing from "discourse/lib/sharing"; import { addComposerUploadHandler } from "discourse/components/composer-editor"; // If you add any methods to the API ensure you bump up this number -const PLUGIN_API_VERSION = "0.8.28"; +const PLUGIN_API_VERSION = "0.8.29"; class PluginApi { constructor(version, container) { @@ -830,6 +831,21 @@ class PluginApi { replaceCategoryLinkRenderer(fn); } + /** + * Registers a renderer that overrides the display of a tag. + * + * Example: + * + * function testTagRenderer(tag, params) { + * const visibleName = Handlebars.Utils.escapeExpression(tag); + * return `testing: ${visibleName}`; + * } + * api.replaceTagRenderer(testTagRenderer); + **/ + replaceTagRenderer(fn) { + replaceTagRenderer(fn); + } + /** * Registers custom languages for use with HighlightJS. * diff --git a/app/assets/javascripts/discourse/lib/pwa-utils.js.es6 b/app/assets/javascripts/discourse/lib/pwa-utils.js.es6 new file mode 100644 index 0000000000..f44665af3d --- /dev/null +++ b/app/assets/javascripts/discourse/lib/pwa-utils.js.es6 @@ -0,0 +1,21 @@ +export function nativeShare(data) { + return new Ember.RSVP.Promise((resolve, reject) => { + if ( + window.location.protocol === "https:" && + typeof window.navigator.share !== "undefined" + ) { + window.navigator + .share(data) + .then(resolve) + .catch(e => { + if (e.message === "Share canceled") { + // closing share panel do nothing + } else { + reject(); + } + }); + } else { + reject(); + } + }); +} diff --git a/app/assets/javascripts/discourse/lib/render-tag.js.es6 b/app/assets/javascripts/discourse/lib/render-tag.js.es6 index 8420638b3b..4e0597d4a4 100644 --- a/app/assets/javascripts/discourse/lib/render-tag.js.es6 +++ b/app/assets/javascripts/discourse/lib/render-tag.js.es6 @@ -1,4 +1,10 @@ -export default function renderTag(tag, params) { +let _renderer = defaultRenderTag; + +export function replaceTagRenderer(fn) { + _renderer = fn; +} + +function defaultRenderTag(tag, params) { params = params || {}; const visibleName = Handlebars.Utils.escapeExpression(tag); tag = visibleName.toLowerCase(); @@ -41,3 +47,7 @@ export default function renderTag(tag, params) { return val; } + +export default function renderTag(tag, params) { + return _renderer(tag, params); +} diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 index 8c7c9df7d5..026826c540 100644 --- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 +++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 @@ -1,4 +1,4 @@ -import { isAppleDevice } from "discourse/lib/utilities"; +import { isAppleDevice, safariHacksDisabled } from "discourse/lib/utilities"; // we can't tell what the actual visible window height is // because we cannot account for the height of the mobile keyboard @@ -65,7 +65,7 @@ export function isWorkaroundActive() { // per http://stackoverflow.com/questions/29001977/safari-in-ios8-is-scrolling-screen-when-fixed-elements-get-focus/29064810 function positioningWorkaround($fixedElement) { - if (!isAppleDevice()) { + if (!isAppleDevice() || safariHacksDisabled()) { return; } diff --git a/app/assets/javascripts/discourse/lib/sharing.js.es6 b/app/assets/javascripts/discourse/lib/sharing.js.es6 index cdd6ec506e..4e57b9dfa5 100644 --- a/app/assets/javascripts/discourse/lib/sharing.js.es6 +++ b/app/assets/javascripts/discourse/lib/sharing.js.es6 @@ -47,6 +47,27 @@ export default { _sources[source.id] = source; }, + shareSource(source, data) { + const url = source.generateUrl(data.url, data.title); + const options = { + menubar: "no", + toolbar: "no", + resizable: "yes", + scrollbars: "yes", + width: 600, + height: source.popupHeight || 315 + }; + const stringOptions = Object.keys(options) + .map(k => `${k}=${options[k]}`) + .join(","); + + if (source.shouldOpenInPopup) { + window.open(url, "", stringOptions); + } else { + window.open(url, "_blank"); + } + }, + activeSources(linksSetting = "") { return linksSetting .split("|") diff --git a/app/assets/javascripts/discourse/lib/show-modal.js.es6 b/app/assets/javascripts/discourse/lib/show-modal.js.es6 index 675a0e37f8..3fa2b31fc1 100644 --- a/app/assets/javascripts/discourse/lib/show-modal.js.es6 +++ b/app/assets/javascripts/discourse/lib/show-modal.js.es6 @@ -64,6 +64,24 @@ export default function(name, opts) { modalController.set("title", I18n.t(opts.title)); } + if (opts.panels) { + modalController.setProperties({ + panels: opts.panels, + selectedPanel: opts.panels[0] + }); + + if (controller.actions.onSelectPanel) { + modalController.set("onSelectPanel", controller.actions.onSelectPanel); + } + + modalController.set( + "modalClass", + `${modalController.get("modalClass")} has-tabs` + ); + } else { + modalController.setProperties({ panels: [], selectedPanel: null }); + } + controller.set("modal", modalController); const model = opts.model; if (model) { diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6 index a560d1ba70..c0371aa157 100644 --- a/app/assets/javascripts/discourse/lib/transform-post.js.es6 +++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6 @@ -114,7 +114,8 @@ export default function transformPost( postAtts.isAutoGenerated = post.is_auto_generated; postAtts.isModeratorAction = postType === postTypes.moderator_action; postAtts.isWhisper = postType === postTypes.whisper; - postAtts.isSmallAction = postType === postTypes.small_action; + postAtts.isSmallAction = + postType === postTypes.small_action || post.action_code === "split_topic"; postAtts.canBookmark = !!currentUser; postAtts.canManage = currentUser && currentUser.get("canManageTopic"); postAtts.canViewRawEmail = diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6 index e666a72a1b..953aeba7ae 100644 --- a/app/assets/javascripts/discourse/lib/user-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/user-search.js.es6 @@ -23,7 +23,11 @@ function performSearch( resultsFn(cached); return; } - if (term === "") { + + // I am not strongly against unconditionally returning + // however this allows us to return a list of probable + // users we want to mention, early on a topic + if (term === "" && !topicId) { return []; } @@ -81,7 +85,7 @@ function organizeResults(r, options) { }); } - if (!options.disallowEmails && emailValid(options.term)) { + if (options.allowEmails && emailValid(options.term)) { let e = { username: options.term }; emails = [e]; results.push(e); @@ -108,6 +112,22 @@ function organizeResults(r, options) { return results; } +// all punctuations except for -, _ and . which are allowed in usernames +// note: these are valid in names, but will end up tripping search anyway so just skip +// this means searching for `sam saffron` is OK but if my name is `sam$ saffron` autocomplete +// will not find me, which is a reasonable compromise +// +// we also ignore if we notice a double space or a string that is only a space +const ignoreRegex = /([\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\/:;<=>?\[\]^`{|}~])|\s\s|^\s$/; + +function skipSearch(term, allowEmails) { + if (term.indexOf("@") > -1 && !allowEmails) { + return true; + } + + return !!term.match(ignoreRegex); +} + export default function userSearch(options) { if (options.term && options.term.length > 0 && options.term[0] === "@") { options.term = options.term.substring(1); @@ -139,6 +159,11 @@ export default function userSearch(options) { resolve(CANCELLED_STATUS); }, 5000); + if (skipSearch(term, options.allowEmails)) { + resolve([]); + return; + } + debouncedSearch( term, topicId, diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index e71223f337..60fbf6d7d2 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -282,7 +282,7 @@ export function validateUploadedFile(file, opts) { return true; } -const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)/i; +const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|svg|ico)/i; function extensionsToArray(exts) { return exts @@ -348,7 +348,7 @@ export function authorizedExtensions() { export function authorizedImagesExtensions() { return authorizesAllExtensions() - ? "png, jpg, jpeg, gif, bmp, tiff, svg, webp, ico" + ? "png, jpg, jpeg, gif, svg, ico" : imagesExtensions().join(", "); } @@ -376,7 +376,7 @@ export function authorizesOneOrMoreImageExtensions() { } export function isAnImage(path) { - return /\.(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)$/i.test(path); + return /\.(png|jpe?g|gif|svg|ico)$/i.test(path); } function uploadTypeFromFileName(fileName) { @@ -550,6 +550,26 @@ export function isAppleDevice() { ); } +let iPadDetected = undefined; + +export function isiPad() { + if (iPadDetected === undefined) { + iPadDetected = + navigator.userAgent.match(/iPad/g) && + !navigator.userAgent.match(/Trident/g); + } + return iPadDetected; +} + +export function safariHacksDisabled() { + let pref = localStorage.getItem("safari-hacks-disabled"); + let result = false; + if (pref !== null) { + result = pref === "true"; + } + return result; +} + const toArray = items => { items = items || []; diff --git a/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6 b/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6 index 954fcafa0c..ce110da855 100644 --- a/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6 +++ b/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6 @@ -97,6 +97,10 @@ export default Ember.Mixin.create({ } this._close(); + + if (this.site.mobileView) { + return false; + } } return true; diff --git a/app/assets/javascripts/discourse/mixins/key-enter-escape.js.es6 b/app/assets/javascripts/discourse/mixins/key-enter-escape.js.es6 index 8019870114..d3949f9581 100644 --- a/app/assets/javascripts/discourse/mixins/key-enter-escape.js.es6 +++ b/app/assets/javascripts/discourse/mixins/key-enter-escape.js.es6 @@ -1,11 +1,19 @@ +import { isiPad } from "discourse/lib/utilities"; + // A mixin where hitting ESC calls `cancelled` and ctrl+enter calls `save. export default { keyDown(e) { if (e.which === 27) { this.cancelled(); return false; - } else if (e.which === 13 && (e.ctrlKey || e.metaKey)) { + } else if ( + e.which === 13 && + (e.ctrlKey || e.metaKey || (isiPad() && e.altKey)) + ) { // CTRL+ENTER or CMD+ENTER + // + // iPad physical keyboard does not offer Command or Control detection + // so use ALT-ENTER this.save(); return false; } diff --git a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 index ac024919ee..0caef43943 100644 --- a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 +++ b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 @@ -16,6 +16,11 @@ export default Ember.Mixin.create({ actions: { closeModal() { this.get("modal").send("closeModal"); + this.set("panels", []); + }, + + onSelectPanel(panel) { + this.set("selectedPanel", panel); } } }); diff --git a/app/assets/javascripts/discourse/mixins/pan-events.js.es6 b/app/assets/javascripts/discourse/mixins/pan-events.js.es6 index 840a6aa31d..423fb1c8a5 100644 --- a/app/assets/javascripts/discourse/mixins/pan-events.js.es6 +++ b/app/assets/javascripts/discourse/mixins/pan-events.js.es6 @@ -4,7 +4,8 @@ **/ export const SWIPE_VELOCITY = 40; export const SWIPE_DISTANCE_THRESHOLD = 50; -export const SWIPE_VELOCITY_THRESHOLD = 0.1; +export const SWIPE_VELOCITY_THRESHOLD = 0.12; +export const MINIMUM_SWIPE_DISTANCE = 5; export default Ember.Mixin.create({ //velocity is pixels per ms @@ -120,7 +121,7 @@ export default Ember.Mixin.create({ } const previousState = this.get("_panState"); const newState = this._calculateNewPanState(previousState, e); - if (previousState.start && newState.distance < 5) { + if (previousState.start && newState.distance < MINIMUM_SWIPE_DISTANCE) { return; } this.set("_panState", newState); diff --git a/app/assets/javascripts/discourse/models/category.js.es6 b/app/assets/javascripts/discourse/models/category.js.es6 index c8531986d4..2e6928abfe 100644 --- a/app/assets/javascripts/discourse/models/category.js.es6 +++ b/app/assets/javascripts/discourse/models/category.js.es6 @@ -99,7 +99,7 @@ const Category = RestModel.extend({ color: this.get("color"), text_color: this.get("text_color"), secure: this.get("secure"), - permissions: this.get("permissionsForUpdate"), + permissions: this._permissionsForUpdate(), auto_close_hours: this.get("auto_close_hours"), auto_close_based_on_last_post: this.get( "auto_close_based_on_last_post" @@ -135,8 +135,8 @@ const Category = RestModel.extend({ }); }, - @computed("permissions") - permissionsForUpdate(permissions) { + _permissionsForUpdate() { + const permissions = this.get("permissions"); let rval = {}; permissions.forEach(p => (rval[p.group_name] = p.permission.id)); return rval; diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index e67e10515f..22c356239e 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -62,7 +62,7 @@ export const SAVE_LABELS = { }; export const SAVE_ICONS = { - [EDIT]: "pencil", + [EDIT]: "pencil-alt", [EDIT_SHARED_DRAFT]: "clipboard", [REPLY]: "reply", [CREATE_TOPIC]: "plus", @@ -369,8 +369,7 @@ const Composer = RestModel.extend({ return ( canCategorize && !categoryId && - !this.siteSettings.allow_uncategorized_topics && - !this.user.get("admin") + !this.siteSettings.allow_uncategorized_topics ); }, diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index 8cc53ea0a7..4188998531 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -907,10 +907,13 @@ export default RestModel.extend({ }, fetchNextWindow(postNumber, asc, callback) { + let includeSuggested = !this.get("topic.suggested_topics"); + const url = `/t/${this.get("topic.id")}/posts.json`; let data = { post_number: postNumber, - asc: asc + asc: asc, + include_suggested: includeSuggested }; data = _.merge(data, this.get("streamFilters")); @@ -950,8 +953,10 @@ export default RestModel.extend({ return Ember.RSVP.resolve([]); } + let includeSuggested = !this.get("topic.suggested_topics"); + const url = "/t/" + this.get("topic.id") + "/posts.json"; - const data = { post_ids: postIds }; + const data = { post_ids: postIds, include_suggested: includeSuggested }; const store = this.store; return ajax(url, { data }).then(result => { diff --git a/app/assets/javascripts/discourse/models/tag.js.es6 b/app/assets/javascripts/discourse/models/tag.js.es6 index 572463e5bd..c9665111ac 100644 --- a/app/assets/javascripts/discourse/models/tag.js.es6 +++ b/app/assets/javascripts/discourse/models/tag.js.es6 @@ -5,5 +5,10 @@ export default RestModel.extend({ @computed("count", "pm_count") totalCount(count, pmCount) { return count + pmCount; + }, + + @computed("count", "pm_count") + pmOnly(count, pmCount) { + return count === 0 && pmCount > 0; } }); diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 489333e588..70ae169286 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -1,30 +1,34 @@ import { ajax } from "discourse/lib/ajax"; import { flushMap } from "discourse/models/store"; import RestModel from "discourse/models/rest"; -import { propertyEqual } from "discourse/lib/computed"; +import { propertyEqual, fmt } from "discourse/lib/computed"; import { longDate } from "discourse/lib/formatter"; import { isRTL } from "discourse/lib/text-direction"; -import computed from "ember-addons/ember-computed-decorators"; import ActionSummary from "discourse/models/action-summary"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { censor } from "pretty-text/censored-words"; import { emojiUnescape } from "discourse/lib/text"; import PreloadStore from "preload-store"; import { userPath } from "discourse/lib/url"; +import { + default as computed, + observes, + on +} from "ember-addons/ember-computed-decorators"; export function loadTopicView(topic, args) { const topicId = topic.get("id"); const data = _.merge({}, args); - const url = Discourse.getURL("/t/") + topicId; + const url = `${Discourse.getURL("/t/")}${topicId}`; const jsonUrl = (data.nearPost ? `${url}/${data.nearPost}` : url) + ".json"; delete data.nearPost; delete data.__type; delete data.store; - return PreloadStore.getAndRemove(`topic_${topicId}`, () => { - return ajax(jsonUrl, { data }); - }).then(json => { + return PreloadStore.getAndRemove(`topic_${topicId}`, () => + ajax(jsonUrl, { data }) + ).then(json => { topic.updateFromJson(json); return json; }); @@ -101,44 +105,44 @@ const Topic = RestModel.extend({ ); if (Discourse.SiteSettings.support_mixed_text_direction) { - let titleDir = isRTL(title) ? "rtl" : "ltr"; + const titleDir = isRTL(title) ? "rtl" : "ltr"; return `${fancyTitle}`; } return fancyTitle; }, // returns createdAt if there's no bumped date - bumpedAt: function() { - const bumpedAt = this.get("bumped_at"); - if (bumpedAt) { - return new Date(bumpedAt); + @computed("bumped_at", "createdAt") + bumpedAt(bumped_at, createdAt) { + if (bumped_at) { + return new Date(bumped_at); } else { - return this.get("createdAt"); + return createdAt; } - }.property("bumped_at", "createdAt"), + }, - bumpedAtTitle: function() { - return ( - I18n.t("first_post") + - ": " + - longDate(this.get("createdAt")) + - "\n" + - I18n.t("last_post") + - ": " + - longDate(this.get("bumpedAt")) - ); - }.property("bumpedAt"), + @computed("bumpedAt", "createdAt") + bumpedAtTitle(bumpedAt, createdAt) { + const firstPost = I18n.t("first_post"); + const lastPost = I18n.t("last_post"); + const createdAtDate = longDate(createdAt); + const bumpedAtDate = longDate(bumpedAt); - createdAt: function() { - return new Date(this.get("created_at")); - }.property("created_at"), + return `${firstPost}: ${createdAtDate}\n${lastPost}: ${bumpedAtDate}`; + }, - postStream: function() { + @computed("created_at") + createdAt(created_at) { + return new Date(created_at); + }, + + @computed + postStream() { return this.store.createRecord("postStream", { id: this.get("id"), topic: this }); - }.property(), + }, @computed("tags") visibleListTags(tags) { @@ -165,9 +169,7 @@ const Topic = RestModel.extend({ return this.set( "related_messages", - relatedMessages.map(st => { - return store.createRecord("topic", st); - }) + relatedMessages.map(st => store.createRecord("topic", st)) ); } }, @@ -179,115 +181,116 @@ const Topic = RestModel.extend({ return this.set( "suggested_topics", - suggestedTopics.map(st => { - return store.createRecord("topic", st); - }) + suggestedTopics.map(st => store.createRecord("topic", st)) ); } }, - replyCount: function() { - return this.get("posts_count") - 1; - }.property("posts_count"), + @computed("posts_count") + replyCount(postsCount) { + return postsCount - 1; + }, - details: function() { + @computed + details() { return this.store.createRecord("topicDetails", { id: this.get("id"), topic: this }); - }.property(), + }, invisible: Ember.computed.not("visible"), deleted: Ember.computed.notEmpty("deleted_at"), - searchContext: function() { - return { type: "topic", id: this.get("id") }; - }.property("id"), + @computed("id") + searchContext(id) { + return { type: "topic", id }; + }, - _categoryIdChanged: function() { + @on("init") + @observes("category_id") + _categoryIdChanged() { this.set("category", Discourse.Category.findById(this.get("category_id"))); - } - .observes("category_id") - .on("init"), + }, - _categoryNameChanged: function() { + @observes("categoryName") + _categoryNameChanged() { const categoryName = this.get("categoryName"); let category; if (categoryName) { category = this.site.get("categories").findBy("name", categoryName); } this.set("category", category); - }.observes("categoryName"), - - categoryClass: function() { - return "category-" + this.get("category.fullSlug"); - }.property("category.fullSlug"), - - shareUrl: function() { - const user = Discourse.User.current(); - return this.get("url") + (user ? "?u=" + user.get("username_lower") : ""); - }.property("url"), - - @computed("url") - printUrl(url) { - return url + "/print"; }, - url: function() { - let slug = this.get("slug") || ""; + categoryClass: fmt("category.fullSlug", "category-%@"), + + @computed("url") + shareUrl(url) { + const user = Discourse.User.current(); + const userQueryString = user ? `?u=${user.get("username_lower")}` : ""; + return `${url}${userQueryString}`; + }, + + printUrl: fmt("url", "%@/print"), + + @computed("id", "slug") + url(id, slug) { + slug = slug || ""; if (slug.trim().length === 0) { slug = "topic"; } - return Discourse.getURL("/t/") + slug + "/" + this.get("id"); - }.property("id", "slug"), + return `${Discourse.getURL("/t/")}${slug}/${id}`; + }, // Helper to build a Url with a post number urlForPostNumber(postNumber) { let url = this.get("url"); if (postNumber && postNumber > 0) { - url += "/" + postNumber; + url += `/${postNumber}`; } return url; }, - totalUnread: function() { - const count = (this.get("unread") || 0) + (this.get("new_posts") || 0); + @computed("new_posts", "unread") + totalUnread(newPosts, unread) { + const count = (unread || 0) + (newPosts || 0); return count > 0 ? count : null; - }.property("new_posts", "unread"), + }, - lastReadUrl: function() { - return this.urlForPostNumber(this.get("last_read_post_number")); - }.property("url", "last_read_post_number"), + @computed("last_read_post_number", "url") + lastReadUrl(lastReadPostNumber) { + return this.urlForPostNumber(lastReadPostNumber); + }, - lastUnreadUrl: function() { - const highest = this.get("highest_post_number"); - const lastRead = this.get("last_read_post_number"); - - if (highest <= lastRead) { + @computed("last_read_post_number", "highest_post_number", "url") + lastUnreadUrl(lastReadPostNumber, highestPostNumber) { + if (highestPostNumber <= lastReadPostNumber) { if (this.get("category.navigate_to_first_post_after_read")) { return this.urlForPostNumber(1); } else { - return this.urlForPostNumber(lastRead + 1); + return this.urlForPostNumber(lastReadPostNumber + 1); } } else { - return this.urlForPostNumber(lastRead + 1); + return this.urlForPostNumber(lastReadPostNumber + 1); } - }.property("url", "last_read_post_number", "highest_post_number"), + }, - lastPostUrl: function() { - return this.urlForPostNumber(this.get("highest_post_number")); - }.property("url", "highest_post_number"), + @computed("highest_post_number", "url") + lastPostUrl(highestPostNumber) { + return this.urlForPostNumber(highestPostNumber); + }, - firstPostUrl: function() { + @computed("url") + firstPostUrl() { return this.urlForPostNumber(1); - }.property("url"), + }, - summaryUrl: function() { - return ( - this.urlForPostNumber(1) + - (this.get("has_summary") ? "?filter=summary" : "") - ); - }.property("url"), + @computed("url") + summaryUrl() { + const summaryQueryString = this.get("has_summary") ? "?filter=summary" : ""; + return `${this.urlForPostNumber(1)}${summaryQueryString}`; + }, @computed("last_poster.username") lastPosterUrl(username) { @@ -297,39 +300,40 @@ const Topic = RestModel.extend({ // The amount of new posts to display. It might be different than what the server // tells us if we are still asynchronously flushing our "recently read" data. // So take what the browser has seen into consideration. - displayNewPosts: function() { - const highestSeen = Discourse.Session.currentProp("highestSeenByTopic")[ - this.get("id") - ]; + @computed("new_posts", "id") + displayNewPosts(newPosts, id) { + const highestSeen = Discourse.Session.currentProp("highestSeenByTopic")[id]; if (highestSeen) { - let delta = highestSeen - this.get("last_read_post_number"); + const delta = highestSeen - this.get("last_read_post_number"); if (delta > 0) { - let result = this.get("new_posts") - delta; + let result = newPosts - delta; if (result < 0) { result = 0; } return result; } } - return this.get("new_posts"); - }.property("new_posts", "id"), + return newPosts; + }, - viewsHeat: function() { - const v = this.get("views"); - if (v >= Discourse.SiteSettings.topic_views_heat_high) + @computed("views") + viewsHeat(v) { + if (v >= Discourse.SiteSettings.topic_views_heat_high) { return "heatmap-high"; - if (v >= Discourse.SiteSettings.topic_views_heat_medium) + } + if (v >= Discourse.SiteSettings.topic_views_heat_medium) { return "heatmap-med"; - if (v >= Discourse.SiteSettings.topic_views_heat_low) return "heatmap-low"; + } + if (v >= Discourse.SiteSettings.topic_views_heat_low) { + return "heatmap-low"; + } return null; - }.property("views"), + }, - archetypeObject: function() { - return Discourse.Site.currentProp("archetypes").findBy( - "id", - this.get("archetype") - ); - }.property("archetype"), + @computed("archetype") + archetypeObject(archetype) { + return Discourse.Site.currentProp("archetypes").findBy("id", archetype); + }, isPrivateMessage: Ember.computed.equal("archetype", "private_message"), isBanner: Ember.computed.equal("archetype", "banner"), @@ -343,32 +347,26 @@ const Topic = RestModel.extend({ if (property === "closed") { this.incrementProperty("posts_count"); } - return ajax(this.get("url") + "/status", { + return ajax(`${this.get("url")}/status`, { type: "PUT", data: { status: property, enabled: !!value, - until: until + until } }); }, makeBanner() { - const self = this; - return ajax("/t/" + this.get("id") + "/make-banner", { type: "PUT" }).then( - function() { - self.set("archetype", "banner"); - } + return ajax(`/t/${this.get("id")}/make-banner`, { type: "PUT" }).then(() => + this.set("archetype", "banner") ); }, removeBanner() { - const self = this; - return ajax("/t/" + this.get("id") + "/remove-banner", { + return ajax(`/t/${this.get("id")}/remove-banner`, { type: "PUT" - }).then(function() { - self.set("archetype", "regular"); - }); + }).then(() => this.set("archetype", "regular")); }, toggleBookmark() { @@ -432,23 +430,23 @@ const Topic = RestModel.extend({ }, createGroupInvite(group) { - return ajax("/t/" + this.get("id") + "/invite-group", { + return ajax(`/t/${this.get("id")}/invite-group`, { type: "POST", data: { group } }); }, createInvite(user, group_names, custom_message) { - return ajax("/t/" + this.get("id") + "/invite", { + return ajax(`/t/${this.get("id")}/invite`, { type: "POST", data: { user, group_names, custom_message } }); }, - generateInviteLink: function(email, groupNames, topicId) { + generateInviteLink(email, groupNames, topicId) { return ajax("/invites/link", { type: "POST", - data: { email: email, group_names: groupNames, topic_id: topicId } + data: { email, group_names: groupNames, topic_id: topicId } }); }, @@ -492,28 +490,25 @@ const Topic = RestModel.extend({ }, reload() { - return ajax(`/t/${this.get("id")}`, { type: "GET" }).then(topic_json => { - this.updateFromJson(topic_json); - }); + return ajax(`/t/${this.get("id")}`, { type: "GET" }).then(topic_json => + this.updateFromJson(topic_json) + ); }, - isPinnedUncategorized: function() { - return this.get("pinned") && this.get("category.isUncategorizedCategory"); - }.property("pinned", "category.isUncategorizedCategory"), + isPinnedUncategorized: Ember.computed.and( + "pinned", + "category.isUncategorizedCategory" + ), clearPin() { - const topic = this; - // Clear the pin optimistically from the object - topic.set("pinned", false); - topic.set("unpinned", true); + this.setProperties({ pinned: false, unpinned: true }); - ajax("/t/" + this.get("id") + "/clear-pin", { + ajax(`/t/${this.get("id")}/clear-pin`, { type: "PUT" - }).then(null, function() { + }).then(null, () => { // On error, put the pin back - topic.set("pinned", true); - topic.set("unpinned", false); + this.setProperties({ pinned: true, unpinned: false }); }); }, @@ -526,18 +521,14 @@ const Topic = RestModel.extend({ }, rePin() { - const topic = this; - // Clear the pin optimistically from the object - topic.set("pinned", true); - topic.set("unpinned", false); + this.setProperties({ pinned: true, unpinned: false }); - ajax("/t/" + this.get("id") + "/re-pin", { + ajax(`/t/${this.get("id")}/re-pin`, { type: "PUT" - }).then(null, function() { + }).then(null, () => { // On error, put the pin back - topic.set("pinned", true); - topic.set("unpinned", false); + this.setProperties({ pinned: true, unpinned: false }); }); }, @@ -548,17 +539,19 @@ const Topic = RestModel.extend({ hasExcerpt: Ember.computed.notEmpty("excerpt"), - excerptTruncated: function() { - const e = this.get("excerpt"); - return e && e.substr(e.length - 8, 8) === "…"; - }.property("excerpt"), + @computed("excerpt") + excerptTruncated(excerpt) { + return excerpt && excerpt.substr(excerpt.length - 8, 8) === "…"; + }, readLastPost: propertyEqual("last_read_post_number", "highest_post_number"), canClearPin: Ember.computed.and("pinned", "readLastPost"), archiveMessage() { this.set("archiving", true); - var promise = ajax(`/t/${this.get("id")}/archive-message`, { type: "PUT" }); + const promise = ajax(`/t/${this.get("id")}/archive-message`, { + type: "PUT" + }); promise .then(msg => { @@ -574,7 +567,7 @@ const Topic = RestModel.extend({ moveToInbox() { this.set("archiving", true); - var promise = ajax(`/t/${this.get("id")}/move-to-inbox`, { type: "PUT" }); + const promise = ajax(`/t/${this.get("id")}/move-to-inbox`, { type: "PUT" }); promise .then(msg => { @@ -593,9 +586,7 @@ const Topic = RestModel.extend({ type: "PUT", data: this.getProperties("destination_category_id") }) - .then(() => { - this.set("destination_category_id", null); - }) + .then(() => this.set("destination_category_id", null)) .catch(popupAjaxError); }, @@ -609,9 +600,7 @@ const Topic = RestModel.extend({ convertTopic(type) { return ajax(`/t/${this.get("id")}/convert-topic/${type}`, { type: "PUT" }) - .then(() => { - window.location.reload(); - }) + .then(() => window.location.reload()) .catch(popupAjaxError); }, @@ -633,7 +622,7 @@ Topic.reopenClass({ createActionSummary(result) { if (result.actions_summary) { const lookup = Ember.Object.create(); - result.actions_summary = result.actions_summary.map(function(a) { + result.actions_summary = result.actions_summary.map(a => { a.post = result; a.actionType = Discourse.Site.current().postActionTypeById(a.id); const actionSummary = ActionSummary.create(a); @@ -664,13 +653,11 @@ Topic.reopenClass({ Object.keys(props).forEach(function(k) { const v = props[k]; if (v instanceof Array && v.length === 0) { - props[k + "_empty_array"] = true; + props[`${k}_empty_array`] = true; } }); - return ajax(topic.get("url"), { type: "PUT", data: props }).then(function( - result - ) { + return ajax(topic.get("url"), { type: "PUT", data: props }).then(result => { // The title can be cleaned up server side props.title = result.basic_topic.title; props.fancy_title = result.basic_topic.fancy_title; @@ -688,7 +675,7 @@ Topic.reopenClass({ find(topicId, opts) { let url = Discourse.getURL("/t/") + topicId; if (opts.nearPost) { - url += "/" + opts.nearPost; + url += `/${opts.nearPost}`; } const data = {}; @@ -716,14 +703,14 @@ Topic.reopenClass({ } // Check the preload store. If not, load it via JSON - return ajax(url + ".json", { data: data }); + return ajax(`${url}.json`, { data }); }, changeOwners(topicId, opts) { - const promise = ajax("/t/" + topicId + "/change-owner", { + const promise = ajax(`/t/${topicId}/change-owner`, { type: "POST", data: opts - }).then(function(result) { + }).then(result => { if (result.success) return result; promise.reject(new Error("error changing ownership of posts")); }); @@ -731,10 +718,10 @@ Topic.reopenClass({ }, changeTimestamp(topicId, timestamp) { - const promise = ajax("/t/" + topicId + "/change-timestamp", { + const promise = ajax(`/t/${topicId}/change-timestamp`, { type: "PUT", - data: { timestamp: timestamp } - }).then(function(result) { + data: { timestamp } + }).then(result => { if (result.success) return result; promise.reject(new Error("error updating timestamp of topic")); }); @@ -745,20 +732,18 @@ Topic.reopenClass({ return ajax("/topics/bulk", { type: "PUT", data: { - topic_ids: topics.map(function(t) { - return t.get("id"); - }), - operation: operation + topic_ids: topics.map(t => t.get("id")), + operation } }); }, bulkOperationByFilter(filter, operation, categoryId) { - const data = { filter: filter, operation: operation }; - if (categoryId) data["category_id"] = categoryId; + const data = { filter, operation }; + if (categoryId) data.category_id = categoryId; return ajax("/topics/bulk", { type: "PUT", - data: data + data }); }, @@ -767,7 +752,7 @@ Topic.reopenClass({ }, idForSlug(slug) { - return ajax("/t/id_for/" + slug); + return ajax(`/t/id_for/${slug}`); } }); @@ -781,13 +766,13 @@ function moveResult(result) { } export function movePosts(topicId, data) { - return ajax("/t/" + topicId + "/move-posts", { type: "POST", data }).then( + return ajax(`/t/${topicId}/move-posts`, { type: "POST", data }).then( moveResult ); } export function mergeTopic(topicId, data) { - return ajax("/t/" + topicId + "/merge-topic", { type: "POST", data }).then( + return ajax(`/t/${topicId}/merge-topic`, { type: "POST", data }).then( moveResult ); } diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 1de31d7ba9..ab7f31538d 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -367,20 +367,24 @@ const User = RestModel.extend({ }); }, - toggleSecondFactor(token, enable, method) { + toggleSecondFactor(authToken, authMethod, targetMethod, enable) { return ajax("/u/second_factor.json", { data: { - second_factor_token: token, - second_factor_method: method, + second_factor_token: authToken, + second_factor_method: authMethod, + second_factor_target: targetMethod, enable }, type: "PUT" }); }, - generateSecondFactorCodes(token) { + generateSecondFactorCodes(authToken, authMethod) { return ajax("/u/second_factors_backup.json", { - data: { second_factor_token: token }, + data: { + second_factor_token: authToken, + second_factor_method: authMethod + }, type: "PUT" }); }, @@ -611,6 +615,20 @@ const User = RestModel.extend({ } }, + ignore() { + return ajax(`${userPath(this.get("username"))}/ignore.json`, { + type: "PUT", + data: { ignored_user_id: this.get("id") } + }); + }, + + watch() { + return ajax(`${userPath(this.get("username"))}/ignore.json`, { + type: "DELETE", + data: { ignored_user_id: this.get("id") } + }); + }, + dismissBanner(bannerKey) { this.set("dismissed_banner_key", bannerKey); ajax(userPath(this.get("username") + ".json"), { @@ -719,11 +737,15 @@ const User = RestModel.extend({ }, updateTextSizeCookie(newSize) { - const seq = this.get("user_option.text_size_seq"); - $.cookie("text_size", `${newSize}|${seq}`, { - path: "/", - expires: 9999 - }); + if (newSize) { + const seq = this.get("user_option.text_size_seq"); + $.cookie("text_size", `${newSize}|${seq}`, { + path: "/", + expires: 9999 + }); + } else { + $.removeCookie("text_size", { path: "/", expires: 1 }); + } } }); 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 1e2e6ad338..160a72938b 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -60,36 +60,32 @@ export default function() { this.route("categoryWithID", { path: "/c/:parentSlug/:slug/:id" }); }); - this.route("groups", { resetNamespace: true }, function() { + this.route("groups", { resetNamespace: true, path: "/g" }, function() { this.route("new", { path: "custom/new" }); }); - this.route( - "group", - { path: "/groups/:name", resetNamespace: true }, - function() { + this.route("group", { path: "/g/:name", resetNamespace: true }, function() { + this.route("members"); + + this.route("activity", function() { + this.route("posts"); + this.route("topics"); + this.route("mentions"); + }); + + this.route("manage", function() { + this.route("profile"); + this.route("membership"); + this.route("interaction"); this.route("members"); + this.route("logs"); + }); - this.route("activity", function() { - this.route("posts"); - this.route("topics"); - this.route("mentions"); - }); - - this.route("manage", function() { - this.route("profile"); - this.route("membership"); - this.route("interaction"); - this.route("members"); - this.route("logs"); - }); - - this.route("messages", function() { - this.route("inbox"); - this.route("archive"); - }); - } - ); + this.route("messages", function() { + this.route("inbox"); + this.route("archive"); + }); + }); // User routes this.route("users", { resetNamespace: true, path: "/u" }); diff --git a/app/assets/javascripts/discourse/routes/new-message.js.es6 b/app/assets/javascripts/discourse/routes/new-message.js.es6 index 92d6265541..abdf17e24b 100644 --- a/app/assets/javascripts/discourse/routes/new-message.js.es6 +++ b/app/assets/javascripts/discourse/routes/new-message.js.es6 @@ -3,12 +3,11 @@ import Group from "discourse/models/group"; export default Discourse.Route.extend({ beforeModel(transition) { - const self = this; const params = transition.queryParams; const groupName = params.groupname || params.group_name; - if (self.currentUser) { - self.replaceWith("discovery.latest").then(e => { + if (this.currentUser) { + this.replaceWith("discovery.latest").then(e => { if (params.username) { // send a message to a user User.findByUsername(params.username) @@ -28,9 +27,7 @@ export default Discourse.Route.extend({ ); } }) - .catch(function() { - bootbox.alert(I18n.t("generic_error")); - }); + .catch(() => bootbox.alert(I18n.t("generic_error"))); } else if (groupName) { // send a message to a group Group.messageable(groupName) @@ -50,9 +47,7 @@ export default Discourse.Route.extend({ ); } }) - .catch(function() { - bootbox.alert(I18n.t("generic_error")); - }); + .catch(() => bootbox.alert(I18n.t("generic_error"))); } }); } else { @@ -60,7 +55,7 @@ export default Discourse.Route.extend({ if (Discourse.showingSignup) { Discourse.showingSignup = false; } else { - self.replaceWith("login"); + this.replaceWith("login"); } } } diff --git a/app/assets/javascripts/discourse/routes/preferences-interface.js.es6 b/app/assets/javascripts/discourse/routes/preferences-interface.js.es6 index ddd1fe4dab..34976bbd82 100644 --- a/app/assets/javascripts/discourse/routes/preferences-interface.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-interface.js.es6 @@ -1,4 +1,5 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; +import { currentThemeId } from "discourse/lib/theme-selector"; export default RestrictedUserRoute.extend({ showFooter: true, @@ -6,7 +7,13 @@ export default RestrictedUserRoute.extend({ setupController(controller, user) { controller.setProperties({ model: user, - textSize: user.get("currentTextSize") + textSize: user.get("currentTextSize"), + themeId: currentThemeId(), + makeThemeDefault: + !user.get("user_option.theme_ids") || + currentThemeId() === user.get("user_option.theme_ids")[0], + makeTextSizeDefault: + user.get("currentTextSize") === user.get("user_option.text_size") }); } }); diff --git a/app/assets/javascripts/discourse/routes/tags-show.js.es6 b/app/assets/javascripts/discourse/routes/tags-show.js.es6 index c4d7edd244..88ec2a05d4 100644 --- a/app/assets/javascripts/discourse/routes/tags-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/tags-show.js.es6 @@ -17,10 +17,10 @@ export default Discourse.Route.extend({ }, model(params) { - var tag = this.store.createRecord("tag", { - id: Handlebars.Utils.escapeExpression(params.tag_id) - }), - f = ""; + const tag = this.store.createRecord("tag", { + id: Handlebars.Utils.escapeExpression(params.tag_id) + }); + let f = ""; if (params.additional_tags) { this.set( @@ -38,9 +38,9 @@ export default Discourse.Route.extend({ if (params.category) { f = "c/"; if (params.parent_category) { - f += params.parent_category + "/"; + f += `${params.parent_category}/`; } - f += params.category + "/l/"; + f += `${params.category}/l/`; } f += this.get("navMode"); this.set("filterMode", f); @@ -76,29 +76,29 @@ export default Discourse.Route.extend({ const categorySlug = this.get("categorySlug"); const parentCategorySlug = this.get("parentCategorySlug"); const filter = this.get("navMode"); - const tag_id = tag ? tag.id.toLowerCase() : "none"; + const tagId = tag ? tag.id.toLowerCase() : "none"; if (categorySlug) { - var category = Discourse.Category.findBySlug( + const category = Discourse.Category.findBySlug( categorySlug, parentCategorySlug ); if (parentCategorySlug) { - params.filter = `tags/c/${parentCategorySlug}/${categorySlug}/${tag_id}/l/${filter}`; + params.filter = `tags/c/${parentCategorySlug}/${categorySlug}/${tagId}/l/${filter}`; } else { - params.filter = `tags/c/${categorySlug}/${tag_id}/l/${filter}`; + params.filter = `tags/c/${categorySlug}/${tagId}/l/${filter}`; } if (category) { category.setupGroupsAndPermissions(); this.set("category", category); } } else if (this.get("additionalTags")) { - params.filter = `tags/intersection/${tag_id}/${this.get( + params.filter = `tags/intersection/${tagId}/${this.get( "additionalTags" ).join("/")}`; this.set("category", null); } else { - params.filter = `tags/${tag_id}/l/${filter}`; + params.filter = `tags/${tagId}/l/${filter}`; this.set("category", null); } @@ -109,11 +109,12 @@ export default Discourse.Route.extend({ params, {} ).then(list => { - if (list.topic_list.tags) { - tag.set("id", list.topic_list.tags[0].name); // Update name of tag (case might be different) + if (list.topic_list.tags && list.topic_list.tags.length === 1) { + // Update name of tag (case might be different) + tag.set("id", list.topic_list.tags[0].name); } controller.setProperties({ - list: list, + list, canCreateTopic: list.get("can_create_topic"), loading: false, canCreateTopicOnCategory: @@ -124,9 +125,9 @@ export default Discourse.Route.extend({ titleToken() { const filterText = I18n.t( - "filters." + this.get("navMode").replace("/", ".") + ".title" - ), - controller = this.controllerFor("tags.show"); + `filters.${this.get("navMode").replace("/", ".")}.title` + ); + const controller = this.controllerFor("tags.show"); if (controller.get("model.id")) { if (this.get("category")) { @@ -177,8 +178,7 @@ export default Discourse.Route.extend({ }, createTopic() { - var controller = this.controllerFor("tags.show"), - self = this; + const controller = this.controllerFor("tags.show"); if (controller.get("list.draft")) { this.openTopicDraft(controller.get("list")); @@ -190,11 +190,12 @@ export default Discourse.Route.extend({ draftKey: controller.get("list.draft_key"), draftSequence: controller.get("list.draft_sequence") }) - .then(function() { + .then(() => { // Pre-fill the tags input field if (controller.get("model.id")) { - var c = self.controllerFor("composer").get("model"); - c.set( + const composerModel = this.controllerFor("composer").get("model"); + + composerModel.set( "tags", _.compact( _.flatten([ diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index c6e4cf4208..06518c1e16 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -48,6 +48,31 @@ const TopicRoute = Discourse.Route.extend({ }, actions: { + showInvite() { + let invitePanelTitle; + + if (this.get("isPM")) { + invitePanelTitle = "topic.invite_private.title"; + } else if (this.get("invitingToTopic")) { + invitePanelTitle = "topic.invite_reply.title"; + } else { + invitePanelTitle = "user.invited.create"; + } + + showModal("share-and-invite", { + modalClass: "share-and-invite", + panels: [ + { + id: "invite", + title: invitePanelTitle, + model: { + inviteModel: this.modelFor("topic") + } + } + ] + }); + }, + showFlags(model) { let controller = showModal("flag", { model }); controller.setProperties({ flagTopic: false }); @@ -92,11 +117,6 @@ const TopicRoute = Discourse.Route.extend({ this.controllerFor("feature_topic").reset(); }, - showInvite() { - showModal("invite", { model: this.modelFor("topic") }); - this.controllerFor("invite").reset(); - }, - showHistory(model, revision) { showModal("history", { model }); const historyController = this.controllerFor("history"); diff --git a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 index 31005cd3d9..d7e321fcb9 100644 --- a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 @@ -3,12 +3,11 @@ import showModal from "discourse/lib/show-modal"; export default Discourse.Route.extend({ model(params) { - const self = this; - Invite.findInvitedCount(self.modelFor("user")).then(function(result) { - self.set("invitesCount", result); - }); - self.inviteFilter = params.filter; - return Invite.findInvitedBy(self.modelFor("user"), params.filter); + Invite.findInvitedCount(this.modelFor("user")).then(result => + this.set("invitesCount", result) + ); + this.inviteFilter = params.filter; + return Invite.findInvitedBy(this.modelFor("user"), params.filter); }, afterModel(model) { @@ -19,7 +18,7 @@ export default Discourse.Route.extend({ setupController(controller, model) { controller.setProperties({ - model: model, + model, user: this.controllerFor("user").get("model"), filter: this.inviteFilter, searchTerm: "", @@ -30,8 +29,19 @@ export default Discourse.Route.extend({ actions: { showInvite() { - showModal("invite", { model: this.currentUser }); - this.controllerFor("invite").reset(); + showModal("share-and-invite", { + modalClass: "share-and-invite", + panels: [ + { + id: "invite", + title: "user.invited.create", + model: { + inviteModel: this.currentUser, + userInvitedShow: this.controllerFor("user-invited-show") + } + } + ] + }); } } }); diff --git a/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs b/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs index cee547d6b4..02d6869065 100644 --- a/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs +++ b/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs @@ -13,9 +13,12 @@ {{/if}} {{#if siteSettings.tagging_enabled}} - {{tag-drop firstCategory=firstCategory secondCategory=secondCategory tagId=tagId}} + {{tag-drop + firstCategory=firstCategory + secondCategory=secondCategory + tagId=tagId}} {{/if}} {{plugin-outlet name="bread-crumbs-right" connectorTagName="li"}} -
+
diff --git a/app/assets/javascripts/discourse/templates/components/composer-title.hbs b/app/assets/javascripts/discourse/templates/components/composer-title.hbs index 63d81f88a6..76f63dac1f 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-title.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-title.hbs @@ -4,6 +4,6 @@ maxLength=titleMaxLength placeholderKey=composer.titlePlaceholder disabled=composer.loading - autocomplete="off"}} + autocomplete="discourse"}} {{popup-input-tip validation=validation}} diff --git a/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs b/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs index 62473a5a82..9a9b1cfcf8 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs @@ -7,7 +7,8 @@ tabindex="1" usernames=usernames hasGroups=hasGroups - autocomplete="off"}} + allowEmails='true' + autocomplete="discourse"}} {{else}}
{{limitedUsernames}} diff --git a/app/assets/javascripts/discourse/templates/components/d-checkbox.hbs b/app/assets/javascripts/discourse/templates/components/d-checkbox.hbs deleted file mode 100644 index 8ec5979a1f..0000000000 --- a/app/assets/javascripts/discourse/templates/components/d-checkbox.hbs +++ /dev/null @@ -1,2 +0,0 @@ - -{{i18n label}} diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs index 41694e0b9d..b0aec1ede6 100644 --- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs @@ -39,7 +39,7 @@
{{conditional-loading-spinner condition=loading}} - {{textarea autocomplete="off" tabindex=tabindex value=value class="d-editor-input" placeholder=placeholderTranslated disabled=disabled}} + {{textarea autocomplete="discourse" tabindex=tabindex value=value class="d-editor-input" placeholder=placeholderTranslated disabled=disabled}} {{popup-input-tip validation=validation}} {{plugin-outlet name="after-d-editor" tagName="" args=outletArgs}}
diff --git a/app/assets/javascripts/discourse/templates/components/d-modal.hbs b/app/assets/javascripts/discourse/templates/components/d-modal.hbs index 374868f5e8..4e7cd0dca2 100644 --- a/app/assets/javascripts/discourse/templates/components/d-modal.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-modal.hbs @@ -10,13 +10,25 @@
{{/if}} -
-

{{title}}

+ {{#if panels}} + + {{else}} +
+

{{title}}

- {{#if subtitle}} -

{{subtitle}}

- {{/if}} -
+ {{#if subtitle}} +

{{subtitle}}

+ {{/if}} +
+ {{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs index e454439156..ea241adae6 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs @@ -27,7 +27,7 @@ content=category.availablePermissions value=selectedPermission}} {{/if}} {{else}} diff --git a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs index 13db8aa100..c795a851a5 100644 --- a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs @@ -4,9 +4,34 @@ {{d-icon "far-image"}} + {{#if hasBackgroundStyle}} {{/if}} + + {{#if imageUrl}} + {{d-button icon="discourse-expand" + title='expand' + class="btn image-uploader-lightbox-btn no-text" + action=(action "toggleLightbox") + disabled=loadingLightbox}} + {{/if}} + {{i18n 'upload_selector.uploading'}} {{uploadProgress}}%
+ + + {{#if imageUrl}} + + +
+ + {{imageWidth}}x{{imageHeight}} {{imageFilesize}} + +
+
+ {{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/invite-panel.hbs b/app/assets/javascripts/discourse/templates/components/invite-panel.hbs new file mode 100644 index 0000000000..c72a0d188d --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/invite-panel.hbs @@ -0,0 +1,94 @@ +{{#if inviteModel.error}} +
+ +
+ {{{errorMessage}}} +
+
+{{/if}} + +
+ {{#if inviteModel.finished}} + {{#if inviteModel.inviteLink}} + {{generated-invite-link link=inviteModel.inviteLink email=emailOrUsername}} + {{else}} +
+ {{{successMessage}}} +
+ {{/if}} + {{else}} +
+ + {{#if allowExistingMembers}} + {{user-selector + fullWidthWrap=true + single=true + allowAny=true + excludeCurrentUser=true + includeMessageableGroups=isPM + hasGroups=hasGroups + usernames=emailOrUsername + placeholderKey=placeholderKey + allowEmails=true + class="invite-user-input" + autocomplete="discourse"}} + {{else}} + {{text-field + class="email-or-username-input" + value=emailOrUsername + placeholderKey="topic.invite_reply.email_placeholder"}} + {{/if}} +
+ + {{#if showGroups}} +
+ + {{group-selector + fullWidthWrap=true + groupFinder=groupFinder + groupNames=inviteModel.groupNames + placeholderKey="topic.invite_private.group_name"}} +
+ {{/if}} + + {{#if showCustomMessage}} +
+ + {{#if hasCustomMessage}} + {{textarea value=customMessage placeholder=customMessagePlaceholder}} + {{/if}} +
+ {{/if}} + {{/if}} +
+ + diff --git a/app/assets/javascripts/discourse/templates/components/modal-panel.hbs b/app/assets/javascripts/discourse/templates/components/modal-panel.hbs new file mode 100644 index 0000000000..52c0d9abda --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/modal-panel.hbs @@ -0,0 +1 @@ +{{component panelComponent panel=panel close=(route-action "closeModal")}} diff --git a/app/assets/javascripts/discourse/templates/components/modal-tab.hbs b/app/assets/javascripts/discourse/templates/components/modal-tab.hbs new file mode 100644 index 0000000000..c598795d8d --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/modal-tab.hbs @@ -0,0 +1 @@ +{{i18n title}} diff --git a/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs b/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs index d0d7beb42b..f2ccf505df 100644 --- a/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs +++ b/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs @@ -1,5 +1,8 @@

{{secondFactorTitle}}

+ {{#if optionalText}} +

{{{optionalText}}}

+ {{/if}}

{{secondFactorDescription}}

{{yield}} {{#if backupEnabled}} diff --git a/app/assets/javascripts/discourse/templates/components/share-panel.hbs b/app/assets/javascripts/discourse/templates/components/share-panel.hbs new file mode 100644 index 0000000000..b166c42071 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/share-panel.hbs @@ -0,0 +1,14 @@ +
+

{{{shareTitle}}}

+
+ +
+ {{textarea value=shareUrl class="topic-share-url"}} + + +
+ {{#each sources as |source|}} + {{share-source source=source title=topic.title action=(action "share")}} + {{/each}} +
+
diff --git a/app/assets/javascripts/discourse/templates/components/share-popup.hbs b/app/assets/javascripts/discourse/templates/components/share-popup.hbs index 6b4adbe0ca..84b946fcbb 100644 --- a/app/assets/javascripts/discourse/templates/components/share-popup.hbs +++ b/app/assets/javascripts/discourse/templates/components/share-popup.hbs @@ -1,28 +1,24 @@ -

{{shareTitle}}

+
+

{{{shareTitle}}}

-{{#if date}} - {{displayDate}} -{{/if}} + {{#if date}} + {{displayDate}} + {{/if}} +
- +
-{{#each sources as |s|}} - {{share-source source=s title=model.title action=(action "share")}} -{{/each}} +
+ {{#each sources as |s|}} + {{share-source source=s title=model.title action=(action "share")}} + {{/each}} -{{#if topic.details.can_reply_as_new_topic}} -
- {{#if topic.isPrivateMessage}} - {{d-icon "plus"}}{{i18n 'user.new_private_message'}} - {{else}} - {{d-icon "plus"}}{{i18n 'topic.create'}} - {{/if}} + -{{/if}} - - diff --git a/app/assets/javascripts/discourse/templates/components/tag-list.hbs b/app/assets/javascripts/discourse/templates/components/tag-list.hbs index 7be97e2439..24adf8aa0e 100644 --- a/app/assets/javascripts/discourse/templates/components/tag-list.hbs +++ b/app/assets/javascripts/discourse/templates/components/tag-list.hbs @@ -9,7 +9,7 @@ {{/if}} {{#each sortedTags as |tag|}}
- {{discourse-tag tag.id isPrivateMessage=isPrivateMessage tagsForUser=tagsForUser}} {{#if tag.pm_count}}{{d-icon "envelope"}}{{/if}}{{#if tag.totalCount}} x {{tag.totalCount}}{{/if}} + {{discourse-tag tag.id isPrivateMessage=isPrivateMessage tagsForUser=tagsForUser}} {{#if tag.pmOnly}}{{d-icon "far-envelope"}}{{/if}}{{#if tag.totalCount}} x {{tag.totalCount}}{{/if}}
{{/each}}
diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs index a7fc8cac35..8df8e1e5cd 100644 --- a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs @@ -48,6 +48,21 @@ icon="envelope" label="user.private_message"}} + {{#if user.can_ignore_user}} +
  • + {{#if user.ignored}} + {{d-button class="btn-default" + action=(action "watchUser") + icon="far-eye" + label="user.watch"}} + {{else}} + {{d-button class="btn-danger" + action=(action "ignoreUser") + icon="far-eye-slash" + label="user.ignore"}} + {{/if}} +
  • + {{/if}} {{/if}} {{#if showFilter}} @@ -99,7 +114,7 @@ {{#if hasLocationOrWebsite}}
    {{#if user.location}} - {{d-icon "map-marker"}} {{user.location}} + {{d-icon "map-marker-alt"}} {{user.location}} {{/if}} {{#if user.website_name}} diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 4f313982e7..834f65d84c 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -4,7 +4,7 @@ typed=(action "typed") cancelled=(action "cancelled") save=(action "save")}} - +
    {{#if visible}} {{composer-messages composer=model messageCount=messageCount @@ -111,7 +111,8 @@ importQuote=(action "importQuote") togglePreview=(action "togglePreview") showToolbar=showToolbar - afterRefresh=(action "afterRefresh")}} + afterRefresh=(action "afterRefresh") + focusTarget=focusTarget}}
    {{plugin-outlet name="composer-fields-below" args=(hash model=model)}} diff --git a/app/assets/javascripts/discourse/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/discovery/topics.hbs index fa7de39f28..ff8ca998aa 100644 --- a/app/assets/javascripts/discourse/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/topics.hbs @@ -80,10 +80,10 @@ {{#if latest}} {{#if canCreateTopicOnCategory}}{{i18n 'topic.suggest_create_topic'}}{{/if}} {{else if top}} - {{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}}. + {{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}} {{top-period-buttons period=period action=(action "changePeriod")}} {{else}} - {{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}}. + {{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{/if}} {{/footer-message}} diff --git a/app/assets/javascripts/discourse/templates/emoji-picker.raw.hbs.erb b/app/assets/javascripts/discourse/templates/emoji-picker.raw.hbs.erb index 40c14709db..dd04f149d3 100644 --- a/app/assets/javascripts/discourse/templates/emoji-picker.raw.hbs.erb +++ b/app/assets/javascripts/discourse/templates/emoji-picker.raw.hbs.erb @@ -19,7 +19,7 @@
    {{d-icon 'search'}} - + diff --git a/app/assets/javascripts/discourse/templates/invites/show.hbs b/app/assets/javascripts/discourse/templates/invites/show.hbs index e8877238d3..6b3b461a0c 100644 --- a/app/assets/javascripts/discourse/templates/invites/show.hbs +++ b/app/assets/javascripts/discourse/templates/invites/show.hbs @@ -24,7 +24,7 @@
    - {{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="off"}} + {{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="discourse"}}  {{input-tip validation=usernameValidation id="username-validation"}}
    {{i18n 'user.username.instructions'}}
    diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs index b85e297bec..6945a02df9 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs @@ -40,12 +40,12 @@ {{#footer-message education=footerEducation message=footerMessage}} {{#if latest}} - {{#if canCreateTopicOnCategory}}{{i18n 'topic.suggest_create_topic'}}.{{/if}} + {{#if canCreateTopicOnCategory}}{{i18n 'topic.suggest_create_topic'}}{{/if}} {{else if top}} - {{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}}. + {{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}} {{top-period-buttons period=period action=(action "changePeriod")}} {{else}} - {{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}}. + {{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{/if}} {{/footer-message}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs b/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs index 907d36744e..ad6101bea8 100644 --- a/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs @@ -1,4 +1,4 @@ -{{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword loginSecondFactor=loginSecondFactor action=(action "login")}} +{{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword secondFactorToken=secondFactorToken action=(action "login")}} {{#d-modal-body title="login.title" class="login-modal"}} {{#if showLoginButtons}} {{login-buttons @@ -36,8 +36,12 @@
    - {{#second-factor-form secondFactorMethod=secondFactorMethod loginSecondFactor=loginSecondFactor backupEnabled=backupEnabled class=secondFactorClass}} - {{second-factor-input value=loginSecondFactor inputId='login-second-factor' secondFactorMethod=secondFactorMethod}} + {{#second-factor-form + secondFactorMethod=secondFactorMethod + secondFactorToken=secondFactorToken + class=secondFactorClass + isLogin=true}} + {{second-factor-input value=secondFactorToken inputId='login-second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} {{/second-factor-form}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/modal.hbs b/app/assets/javascripts/discourse/templates/modal.hbs index 953578f6ef..d266b5fc1f 100644 --- a/app/assets/javascripts/discourse/templates/modal.hbs +++ b/app/assets/javascripts/discourse/templates/modal.hbs @@ -2,10 +2,11 @@ modalClass=modalClass title=title subtitle=subtitle + panels=panels + selectedPanel=selectedPanel + onSelectPanel=onSelectPanel class="hidden" errors=errors closeModal=(route-action "closeModal")}} - {{outlet "modalBody"}} - {{/d-modal}} diff --git a/app/assets/javascripts/discourse/templates/modal/auth-token.hbs b/app/assets/javascripts/discourse/templates/modal/auth-token.hbs index 6b163a7642..7b239543c4 100644 --- a/app/assets/javascripts/discourse/templates/modal/auth-token.hbs +++ b/app/assets/javascripts/discourse/templates/modal/auth-token.hbs @@ -8,7 +8,7 @@

    {{i18n 'user.auth_tokens.details'}}

    • {{d-icon "far-clock"}} {{format-date model.seen_at}}
    • -
    • {{d-icon "map-marker"}} {{model.location}}
    • +
    • {{d-icon "map-marker-alt"}} {{model.location}}
    • {{d-icon model.icon}} {{i18n "user.auth_tokens.browser_and_device" browser=model.browser device=model.device}}
    diff --git a/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs b/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs index 2c5de674dd..26713e0149 100644 --- a/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs +++ b/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs @@ -19,7 +19,7 @@ {{d-button action=(action "refreshGravatar") title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled - icon="refresh" + icon="sync" class="btn-default avatar-selector-refresh-gravatar"}} {{#if gravatarFailed}} diff --git a/app/assets/javascripts/discourse/templates/modal/change-owner.hbs b/app/assets/javascripts/discourse/templates/modal/change-owner.hbs index 62b55d40e3..ecec3312b5 100644 --- a/app/assets/javascripts/discourse/templates/modal/change-owner.hbs +++ b/app/assets/javascripts/discourse/templates/modal/change-owner.hbs @@ -6,7 +6,7 @@ {{user-selector single="true" usernames=new_user placeholderKey="topic.change_owner.placeholder" - autocomplete="off"}} + autocomplete="discourse"}} {{/d-modal-body}} diff --git a/app/assets/javascripts/discourse/templates/modal/create-account.hbs b/app/assets/javascripts/discourse/templates/modal/create-account.hbs index 7b084f376b..29daa50b7e 100644 --- a/app/assets/javascripts/discourse/templates/modal/create-account.hbs +++ b/app/assets/javascripts/discourse/templates/modal/create-account.hbs @@ -28,7 +28,7 @@ - {{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="off"}} + {{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="discourse"}} diff --git a/app/assets/javascripts/discourse/templates/modal/group-add-members.hbs b/app/assets/javascripts/discourse/templates/modal/group-add-members.hbs index c07c81a1d7..ae13193ea1 100644 --- a/app/assets/javascripts/discourse/templates/modal/group-add-members.hbs +++ b/app/assets/javascripts/discourse/templates/modal/group-add-members.hbs @@ -9,8 +9,7 @@ class="input-xxlarge" usernames=model.usernames placeholderKey="groups.selector_placeholder" - id="group-add-members-user-selector" - disallowEmails=true}} + id="group-add-members-user-selector"}}
    {{#if currentUser.admin}} diff --git a/app/assets/javascripts/discourse/templates/modal/history.hbs b/app/assets/javascripts/discourse/templates/modal/history.hbs index 969e526265..97444c9a25 100644 --- a/app/assets/javascripts/discourse/templates/modal/history.hbs +++ b/app/assets/javascripts/discourse/templates/modal/history.hbs @@ -132,7 +132,7 @@ {{/if}} {{#if displayShow}} - {{d-button action=(action "showVersion") icon="eye" label="post.revisions.controls.show" class="btn-default" disabled=loading}} + {{d-button action=(action "showVersion") icon="far-eye" label="post.revisions.controls.show" class="btn-default" disabled=loading}} {{/if}} {{#if displayEdit}} diff --git a/app/assets/javascripts/discourse/templates/modal/invite.hbs b/app/assets/javascripts/discourse/templates/modal/invite.hbs deleted file mode 100644 index 40deca7ac3..0000000000 --- a/app/assets/javascripts/discourse/templates/modal/invite.hbs +++ /dev/null @@ -1,52 +0,0 @@ -{{#d-modal-body id="invite-modal" title=title}} - {{#if model.error}} -
    - - {{{errorMessage}}} -
    - {{/if}} - {{#if model.finished}} - {{#if model.inviteLink}} - {{generated-invite-link link=model.inviteLink email=emailOrUsername}} - {{else}} - {{{successMessage}}} - {{/if}} - {{else}} - - {{#if allowExistingMembers}} - {{user-selector - single=true - allowAny=true - excludeCurrentUser=true - includeMessageableGroups=isMessage - hasGroups=hasGroups - usernames=emailOrUsername - placeholderKey=placeholderKey - autocomplete="off"}} - {{else}} - {{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}} - {{/if}} - - {{#if showGroups}} - - {{group-selector groupFinder=groupFinder groupNames=model.groupNames placeholderKey="topic.invite_private.group_name"}} - {{/if}} - - {{#if showCustomMessage}} - - {{#if hasCustomMessage}}{{textarea value=customMessage placeholder=customMessagePlaceholder}}{{/if}} - {{/if}} - - {{/if}} -{{/d-modal-body}} - - diff --git a/app/assets/javascripts/discourse/templates/modal/login.hbs b/app/assets/javascripts/discourse/templates/modal/login.hbs index ef9742be6e..e8272134a9 100644 --- a/app/assets/javascripts/discourse/templates/modal/login.hbs +++ b/app/assets/javascripts/discourse/templates/modal/login.hbs @@ -1,4 +1,4 @@ -{{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword loginSecondFactor=loginSecondFactor action=(action "login")}} +{{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword secondFactorToken=secondFactorToken action=(action "login")}} {{#d-modal-body title="login.title" class=(concat "login-modal" " " (if hasAtLeastOneLoginButton "has-alt-auth"))}} {{#if canLoginLocal}} @@ -21,8 +21,13 @@
    - {{#second-factor-form secondFactorMethod=secondFactorMethod loginSecondFactor=loginSecondFactor backupEnabled=backupEnabled class=secondFactorClass}} - {{second-factor-input value=loginSecondFactor inputId='login-second-factor' secondFactorMethod=secondFactorMethod}} + {{#second-factor-form + secondFactorMethod=secondFactorMethod + secondFactorToken=secondFactorToken + class=secondFactorClass + backupEnabled=backupEnabled + isLogin=true}} + {{second-factor-input value=secondFactorToken inputId='login-second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} {{/second-factor-form}} {{/if}} @@ -37,11 +42,13 @@ diff --git a/app/assets/javascripts/discourse/templates/preferences-second-factor-backup.hbs b/app/assets/javascripts/discourse/templates/preferences-second-factor-backup.hbs index 3511363614..eb65b54a46 100644 --- a/app/assets/javascripts/discourse/templates/preferences-second-factor-backup.hbs +++ b/app/assets/javascripts/discourse/templates/preferences-second-factor-backup.hbs @@ -1,12 +1,7 @@
    -
    -

    {{i18n "user.second_factor_backup.title"}}

    - {{#if backupEnabled}} -

    {{{i18n "user.second_factor_backup.remaining_codes" count=remainingCodes}}}

    - {{/if}} -
    +
    {{#if successMessage}}
    @@ -22,17 +17,17 @@
    -
    - {{second-factor-input - value=secondFactorToken - maxlength=6 - inputId="second-factor-token"}} - -
    - {{i18n "user.second_factor.disable_description"}} -
    + {{#second-factor-form + secondFactorMethod=secondFactorMethod + backupEnabled=backupEnabled + secondFactorToken=secondFactorToken + secondFactorTitle=(i18n 'user.second_factor_backup.title') + optionalText=(if backupEnabled (i18n "user.second_factor_backup.remaining_codes" count=remainingCodes)) + isLogin=false}} + {{second-factor-input value=secondFactorToken inputId='second-factor-token' secondFactorMethod=secondFactorMethod}} + {{/second-factor-form}}
    diff --git a/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs b/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs index 797135fcdc..ad50e77095 100644 --- a/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs +++ b/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs @@ -1,12 +1,6 @@
    -
    -
    -

    {{i18n 'user.second_factor.title'}}

    -
    -
    - {{#if errorMessage}}
    @@ -16,15 +10,16 @@ {{/if}} {{#if model.second_factor_enabled}} - -
    - {{second-factor-input maxlength=6 value=secondFactorToken inputId='second-factor-token'}} -
    - -
    - {{i18n 'user.second_factor.disable_description'}} + {{#second-factor-form + secondFactorMethod=secondFactorMethod + backupEnabled=backupEnabled + secondFactorToken=secondFactorToken + secondFactorTitle=(i18n 'user.second_factor.title') + isLogin=false}} + {{second-factor-input value=secondFactorToken inputId='second-factor-token' secondFactorMethod=secondFactorMethod}} + {{/second-factor-form}}
    @@ -91,15 +86,16 @@
    - {{text-field value=password +
    + {{text-field value=password id="password" type="password" classNames="input-xxlarge" autofocus="autofocus"}} -
    - -
    - {{i18n 'user.second_factor.confirm_password_description'}} +
    +
    + {{i18n 'user.second_factor.confirm_password_description'}} +
    diff --git a/app/assets/javascripts/discourse/templates/preferences/categories.hbs b/app/assets/javascripts/discourse/templates/preferences/categories.hbs index fa29ec5cd1..6c9740c231 100644 --- a/app/assets/javascripts/discourse/templates/preferences/categories.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/categories.hbs @@ -6,18 +6,22 @@ {{category-selector categories=model.watchedCategories blacklist=selectedCategories}}
    {{i18n 'user.watched_categories_instructions'}}
    - + {{#if canSee}} + + {{/if}}
    {{category-selector categories=model.trackedCategories blacklist=selectedCategories}}
    {{i18n 'user.tracked_categories_instructions'}}
    - + {{#if canSee}} + + {{/if}}
    @@ -30,10 +34,11 @@ {{category-selector categories=model.mutedCategories blacklist=selectedCategories}}
    {{i18n 'user.muted_categories_instructions'}}
    - - + {{#if canSee}} + + {{/if}}
    {{plugin-outlet name="user-preferences-categories" args=(hash model=model save=(action "save"))}} diff --git a/app/assets/javascripts/discourse/templates/preferences/interface.hbs b/app/assets/javascripts/discourse/templates/preferences/interface.hbs index 6b396fec1a..a74f42b7eb 100644 --- a/app/assets/javascripts/discourse/templates/preferences/interface.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/interface.hbs @@ -4,9 +4,11 @@
    {{combo-box content=userSelectableThemes value=themeId}}
    -
    - {{preference-checkbox labelKey="user.theme_default_on_all_devices" checked=makeThemeDefault}} -
    + {{#if showThemeSetDefault}} +
    + {{preference-checkbox labelKey="user.theme_default_on_all_devices" checked=makeThemeDefault}} +
    + {{/if}}
    {{/if}} @@ -15,9 +17,11 @@
    {{combo-box valueAttribute="value" content=textSizes value=textSize onSelect=(action "selectTextSize")}}
    -
    - {{preference-checkbox labelKey="user.text_size_default_on_all_devices" checked=makeTextSizeDefault}} -
    + {{#if showTextSetDefault}} +
    + {{preference-checkbox labelKey="user.text_size_default_on_all_devices" checked=makeTextSizeDefault}} +
    + {{/if}}
    {{#if siteSettings.allow_user_locale}} @@ -51,6 +55,9 @@ {{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}} {{/if}} {{preference-checkbox labelKey="user.hide_profile_and_presence" checked=model.user_option.hide_profile_and_presence}} + {{#if isiPad}} + {{preference-checkbox labelKey="user.enable_physical_keyboard" checked=disableSafariHacks}} + {{/if}}
    {{plugin-outlet name="user-preferences-interface" args=(hash model=model save=(action "save"))}} diff --git a/app/assets/javascripts/discourse/templates/queued-posts.hbs b/app/assets/javascripts/discourse/templates/queued-posts.hbs index 22e24e0142..a5984c1add 100644 --- a/app/assets/javascripts/discourse/templates/queued-posts.hbs +++ b/app/assets/javascripts/discourse/templates/queued-posts.hbs @@ -6,6 +6,6 @@

    {{i18n "queue.none"}}

    {{/each}} - {{d-button action=(route-action "refresh") label="refresh" icon="refresh" disabled=model.refreshing class="btn-default" id='refresh-queued'}} + {{d-button action=(route-action "refresh") label="refresh" icon="sync" disabled=model.refreshing class="btn-default" id='refresh-queued'}}
    diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index 4a64ecf385..61dd9e936d 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -192,7 +192,6 @@ toggleSummary=(action "toggleSummary") removeAllowedUser=(action "removeAllowedUser") removeAllowedGroup=(action "removeAllowedGroup") - showInvite=(route-action "showInvite") topVisibleChanged=(action "topVisibleChanged") currentPostChanged=(action "currentPostChanged") currentPostScrolled=(action "currentPostScrolled") @@ -201,7 +200,8 @@ selectReplies=(action "selectReplies") selectBelow=(action "selectBelow") fillGapBefore=(action "fillGapBefore") - fillGapAfter=(action "fillGapAfter")}} + fillGapAfter=(action "fillGapAfter") + showInvite=(route-action "showInvite")}} {{/unless}} {{conditional-loading-spinner condition=model.postStream.loadingBelow}} @@ -264,11 +264,9 @@ convertToPrivateMessage=(action "convertToPrivateMessage") toggleBookmark=(action "toggleBookmark") showFlagTopic=(route-action "showFlagTopic") - showInvite=(route-action "showInvite") toggleArchiveMessage=(action "toggleArchiveMessage") editFirstPost=(action "editFirstPost") - replyToPost=(action "replyToPost") - }} + replyToPost=(action "replyToPost")}} {{else}} {{conditional-loading-spinner condition=retrying}} @@ -320,7 +318,7 @@ {{/if}} - {{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}} + {{share-popup topic=model}} {{#if embedQuoteButton}} {{quote-button quoteState=quoteState selectText=(action "selectText")}} diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs index 9c27ab5847..dcd09ac589 100644 --- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs @@ -14,7 +14,7 @@
    - {{d-button icon="plus" action=(route-action "showInvite") label="user.invited.create" class="btn"}} + {{d-button icon="plus" action=(route-action "showInvite") label="user.invited.create"}} {{#if canBulkInvite}} {{csv-uploader uploading=uploading}} {{d-icon "question-circle"}} @@ -23,12 +23,12 @@ {{#if rescindedAll}} {{i18n 'user.invited.rescinded_all'}} {{else}} - {{d-button icon="times" action=(action "rescindAll") class="btn" label="user.invited.rescind_all"}} + {{d-button icon="times" action=(action "rescindAll") label="user.invited.rescind_all"}} {{/if}} {{#if reinvitedAll}} {{i18n 'user.invited.reinvited_all'}} {{else}} - {{d-button icon="refresh" action=(action "reinviteAll") class="btn" label="user.invited.reinvite_all"}} + {{d-button icon="sync" action=(action "reinviteAll") label="user.invited.reinvite_all"}} {{/if}} {{/if}}
    @@ -72,9 +72,11 @@ {{number invite.user.topics_entered}} {{number invite.user.posts_read_count}} {{{format-duration invite.user.time_read}}} - {{{unbound invite.user.days_visited}}} + + {{{unbound invite.user.days_visited}}} / - {{{unbound invite.user.days_since_created}}} + {{{unbound invite.user.days_since_created}}} + {{/if}} {{else}} {{unbound invite.email}} @@ -87,13 +89,13 @@ {{#if invite.rescinded}} {{i18n 'user.invited.rescinded'}} {{else}} - {{d-button icon="times" action=(action "rescind") actionParam=invite class="btn" label="user.invited.rescind"}} + {{d-button icon="times" action=(action "rescind") actionParam=invite label="user.invited.rescind"}} {{/if}}      {{#if invite.reinvited}} {{i18n 'user.invited.reinvited'}} {{else}} - {{d-button icon="refresh" action=(action "reinvite") actionParam=invite class="btn" label="user.invited.reinvite"}} + {{d-button icon="sync" action=(action "reinvite") actionParam=invite label="user.invited.reinvite"}} {{/if}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index 0376714d9a..02ff00914c 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -43,12 +43,27 @@