");
+ $div.append(bannerHtml);
+ $div.find("[id^='heading--']").removeAttr("id");
+ return $div.html();
+ },
+
+ @discourseComputed("user.dismissed_banner_key", "banner.key", "hide")
visible(dismissedBannerKey, bannerKey, hide) {
dismissedBannerKey =
dismissedBannerKey || this.keyValueStore.get("dismissed_banner_key");
diff --git a/app/assets/javascripts/discourse/components/discourse-linked-text.js.es6 b/app/assets/javascripts/discourse/components/discourse-linked-text.js.es6
index 17398e4a21..55240d4ee2 100644
--- a/app/assets/javascripts/discourse/components/discourse-linked-text.js.es6
+++ b/app/assets/javascripts/discourse/components/discourse-linked-text.js.es6
@@ -1,9 +1,10 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "span",
- @computed("text")
+ @discourseComputed("text")
translatedText(text) {
if (text) return I18n.t(text);
},
diff --git a/app/assets/javascripts/discourse/components/discourse-tag-bound.js.es6 b/app/assets/javascripts/discourse/components/discourse-tag-bound.js.es6
index 9f151dc1fe..13c8798631 100644
--- a/app/assets/javascripts/discourse/components/discourse-tag-bound.js.es6
+++ b/app/assets/javascripts/discourse/components/discourse-tag-bound.js.es6
@@ -1,17 +1,18 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "a",
classNameBindings: [":discourse-tag", "style", "tagClass"],
attributeBindings: ["href"],
- @computed("tagRecord.id")
+ @discourseComputed("tagRecord.id")
tagClass(tagRecordId) {
return "tag-" + tagRecordId;
},
- @computed("tagRecord.id")
+ @discourseComputed("tagRecord.id")
href(tagRecordId) {
- return Discourse.getURL("/tags/" + tagRecordId);
+ return Discourse.getURL("/tag/" + tagRecordId);
}
});
diff --git a/app/assets/javascripts/discourse/components/discourse-topic.js.es6 b/app/assets/javascripts/discourse/components/discourse-topic.js.es6
index dc84102a30..83e7bf6fa1 100644
--- a/app/assets/javascripts/discourse/components/discourse-topic.js.es6
+++ b/app/assets/javascripts/discourse/components/discourse-topic.js.es6
@@ -1,9 +1,15 @@
+import { alias } from "@ember/object/computed";
+import { throttle } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import { later } from "@ember/runloop";
+import Component from "@ember/component";
import DiscourseURL from "discourse/lib/url";
import AddArchetypeClass from "discourse/mixins/add-archetype-class";
import ClickTrack from "discourse/lib/click-track";
import Scrolling from "discourse/mixins/scrolling";
import MobileScrollDirection from "discourse/mixins/mobile-scroll-direction";
-import { observes } from "ember-addons/ember-computed-decorators";
+import { observes } from "discourse-common/utils/decorators";
const MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE = 300;
@@ -14,12 +20,12 @@ function highlight(postNumber) {
$contents.on("animationend", () => $contents.removeClass("highlighted"));
}
-export default Ember.Component.extend(
+export default Component.extend(
AddArchetypeClass,
Scrolling,
MobileScrollDirection,
{
- userFilters: Ember.computed.alias("topic.userFilters"),
+ userFilters: alias("topic.userFilters"),
classNameBindings: [
"multiSelect",
"topic.archetype",
@@ -32,8 +38,8 @@ export default Ember.Component.extend(
menuVisible: true,
SHORT_POST: 1200,
- postStream: Ember.computed.alias("topic.postStream"),
- archetype: Ember.computed.alias("topic.archetype"),
+ postStream: alias("topic.postStream"),
+ archetype: alias("topic.archetype"),
dockAt: 0,
_lastShowTopic: null,
@@ -49,13 +55,13 @@ export default Ember.Component.extend(
const enteredAt = this.enteredAt;
if (enteredAt && this.lastEnteredAt !== enteredAt) {
this._lastShowTopic = null;
- Ember.run.schedule("afterRender", () => this.scrolled());
+ schedule("afterRender", () => this.scrolled());
this.set("lastEnteredAt", enteredAt);
}
},
_highlightPost(postNumber) {
- Ember.run.scheduleOnce("afterRender", null, highlight, postNumber);
+ scheduleOnce("afterRender", null, highlight, postNumber);
},
_hideTopicInHeader() {
@@ -77,7 +83,7 @@ export default Ember.Component.extend(
this.pauseHeaderTopicUpdate = true;
this._lastShowTopic = true;
- Ember.run.later(() => {
+ later(() => {
this._lastShowTopic = false;
this.pauseHeaderTopicUpdate = false;
}, debounceDuration);
@@ -102,7 +108,7 @@ export default Ember.Component.extend(
$(window).on("resize.discourse-on-scroll", () => this.scrolled());
- this.$().on(
+ $(this.element).on(
"click.discourse-redirect",
".cooked a, a.track-link",
function(e) {
@@ -110,6 +116,7 @@ export default Ember.Component.extend(
}
);
+ this.appEvents.on("discourse:focus-changed", this, "gotFocus");
this.appEvents.on("post:highlight", this, "_highlightPost");
this.appEvents.on("header:update-topic", this, "_updateTopic");
},
@@ -120,19 +127,22 @@ export default Ember.Component.extend(
$(window).unbind("resize.discourse-on-scroll");
// Unbind link tracking
- this.$().off("click.discourse-redirect", ".cooked a, a.track-link");
+ $(this.element).off(
+ "click.discourse-redirect",
+ ".cooked a, a.track-link"
+ );
this.resetExamineDockCache();
// this happens after route exit, stuff could have trickled in
this._hideTopicInHeader();
+ this.appEvents.off("discourse:focus-changed", this, "gotFocus");
this.appEvents.off("post:highlight", this, "_highlightPost");
this.appEvents.off("header:update-topic", this, "_updateTopic");
},
- @observes("Discourse.hasFocus")
- gotFocus() {
- if (Discourse.get("hasFocus")) {
+ gotFocus(hasFocus) {
+ if (hasFocus) {
this.scrolled();
}
},
@@ -187,7 +197,7 @@ export default Ember.Component.extend(
// at the start of the scroll. This feels a lot more snappy compared to waiting
// for the scroll to end if we debounce.
if (this.site.mobileView && this.hasScrolled) {
- Ember.run.throttle(
+ throttle(
this,
this.calculateDirection,
offset,
diff --git a/app/assets/javascripts/discourse/components/discovery-categories.js.es6 b/app/assets/javascripts/discourse/components/discovery-categories.js.es6
index 97bbc33009..bd4f34a2e7 100644
--- a/app/assets/javascripts/discourse/components/discovery-categories.js.es6
+++ b/app/assets/javascripts/discourse/components/discovery-categories.js.es6
@@ -1,9 +1,10 @@
+import Component from "@ember/component";
import UrlRefresh from "discourse/mixins/url-refresh";
-import { on } from "ember-addons/ember-computed-decorators";
+import { on } from "discourse-common/utils/decorators";
const CATEGORIES_LIST_BODY_CLASS = "categories-list";
-export default Ember.Component.extend(UrlRefresh, {
+export default Component.extend(UrlRefresh, {
classNames: ["contents"],
@on("didInsertElement")
diff --git a/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6 b/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6
index 907c213599..f4329ba2f2 100644
--- a/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6
+++ b/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6
@@ -1,55 +1,52 @@
-import { on, observes } from "ember-addons/ember-computed-decorators";
+import { schedule } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import Component from "@ember/component";
+import { on, observes } from "discourse-common/utils/decorators";
import LoadMore from "discourse/mixins/load-more";
import UrlRefresh from "discourse/mixins/url-refresh";
-const DiscoveryTopicsListComponent = Ember.Component.extend(
- UrlRefresh,
- LoadMore,
- {
- classNames: ["contents"],
- eyelineSelector: ".topic-list-item",
+const DiscoveryTopicsListComponent = Component.extend(UrlRefresh, LoadMore, {
+ classNames: ["contents"],
+ eyelineSelector: ".topic-list-item",
- @on("didInsertElement")
- @observes("model")
- _readjustScrollPosition() {
- const scrollTo = this.session.get("topicListScrollPosition");
- if (scrollTo && scrollTo >= 0) {
- Ember.run.schedule("afterRender", () =>
- $(window).scrollTop(scrollTo + 1)
- );
- } else {
- Ember.run.scheduleOnce("afterRender", this, this.loadMoreUnlessFull);
- }
- },
+ @on("didInsertElement")
+ @observes("model")
+ _readjustScrollPosition() {
+ const scrollTo = this.session.get("topicListScrollPosition");
+ if (scrollTo && scrollTo >= 0) {
+ schedule("afterRender", () => $(window).scrollTop(scrollTo + 1));
+ } else {
+ scheduleOnce("afterRender", this, this.loadMoreUnlessFull);
+ }
+ },
- @observes("incomingCount")
- _updateTitle() {
- Discourse.updateContextCount(this.incomingCount);
- },
+ @observes("topicTrackingState.states")
+ _updateTopics() {
+ this.topicTrackingState.updateTopics(this.model.topics);
+ },
- saveScrollPosition() {
- this.session.set("topicListScrollPosition", $(window).scrollTop());
- },
+ @observes("incomingCount")
+ _updateTitle() {
+ Discourse.updateContextCount(this.incomingCount);
+ },
- scrolled() {
- this._super(...arguments);
- this.saveScrollPosition();
- },
+ saveScrollPosition() {
+ this.session.set("topicListScrollPosition", $(window).scrollTop());
+ },
- actions: {
- loadMore() {
- Discourse.updateContextCount(0);
- this.model.loadMore().then(hasMoreResults => {
- Ember.run.schedule("afterRender", () => this.saveScrollPosition());
- if (!hasMoreResults) {
- this.eyeline.flushRest();
- } else if ($(window).height() >= $(document).height()) {
- this.send("loadMore");
- }
- });
- }
+ actions: {
+ loadMore() {
+ Discourse.updateContextCount(0);
+ this.model.loadMore().then(hasMoreResults => {
+ schedule("afterRender", () => this.saveScrollPosition());
+ if (!hasMoreResults) {
+ this.eyeline.flushRest();
+ } else if ($(window).height() >= $(document).height()) {
+ this.send("loadMore");
+ }
+ });
}
}
-);
+});
export default DiscoveryTopicsListComponent;
diff --git a/app/assets/javascripts/discourse/components/edit-category-general.js.es6 b/app/assets/javascripts/discourse/components/edit-category-general.js.es6
index 9988120e59..9d3e548a20 100644
--- a/app/assets/javascripts/discourse/components/edit-category-general.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-general.js.es6
@@ -1,7 +1,9 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import { not } from "@ember/object/computed";
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
import { categoryBadgeHTML } from "discourse/helpers/category-link";
import Category from "discourse/models/category";
-import computed from "ember-addons/ember-computed-decorators";
export default buildCategoryPanel("general", {
init() {
@@ -10,9 +12,7 @@ export default buildCategoryPanel("general", {
this.foregroundColors = ["FFFFFF", "000000"];
},
- canSelectParentCategory: Ember.computed.not(
- "category.isUncategorizedCategory"
- ),
+ canSelectParentCategory: not("category.isUncategorizedCategory"),
uncategorizedSiteSettingLink: Discourse.getURL(
"/admin/site_settings/category/all_results?filter=allow_uncategorized_topics"
),
@@ -21,7 +21,7 @@ export default buildCategoryPanel("general", {
),
// background colors are available as a pipe-separated string
- @computed
+ @discourseComputed
backgroundColors() {
const categories = this.site.get("categoriesList");
return this.siteSettings.category_colors
@@ -37,12 +37,12 @@ export default buildCategoryPanel("general", {
.uniq();
},
- @computed
+ @discourseComputed
noCategoryStyle() {
return this.siteSettings.category_style === "none";
},
- @computed("category.id", "category.color")
+ @discourseComputed("category.id", "category.color")
usedBackgroundColors(categoryId, categoryColor) {
const categories = this.site.get("categoriesList");
@@ -57,14 +57,14 @@ export default buildCategoryPanel("general", {
.compact();
},
- @computed
+ @discourseComputed
parentCategories() {
return this.site
.get("categoriesList")
- .filter(c => !c.get("parentCategory"));
+ .filter(c => c.level + 1 < Discourse.SiteSettings.max_category_nesting);
},
- @computed(
+ @discourseComputed(
"category.parent_category_id",
"category.categoryName",
"category.color",
@@ -76,22 +76,22 @@ export default buildCategoryPanel("general", {
name,
color,
text_color: textColor,
- parent_category_id: parseInt(parentCategoryId),
+ parent_category_id: parseInt(parentCategoryId, 10),
read_restricted: category.get("read_restricted")
});
return categoryBadgeHTML(c, { link: false });
},
// We can change the parent if there are no children
- @computed("category.id")
+ @discourseComputed("category.id")
subCategories(categoryId) {
- if (Ember.isEmpty(categoryId)) {
+ if (isEmpty(categoryId)) {
return null;
}
return Category.list().filterBy("parent_category_id", categoryId);
},
- @computed("category.isUncategorizedCategory", "category.id")
+ @discourseComputed("category.isUncategorizedCategory", "category.id")
showDescription(isUncategorizedCategory, categoryId) {
return !isUncategorizedCategory && categoryId;
},
diff --git a/app/assets/javascripts/discourse/components/edit-category-images.js.es6 b/app/assets/javascripts/discourse/components/edit-category-images.js.es6
index f7b03702ec..953a1a62e7 100644
--- a/app/assets/javascripts/discourse/components/edit-category-images.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-images.js.es6
@@ -1,13 +1,14 @@
+import EmberObject from "@ember/object";
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
export default buildCategoryPanel("images").extend({
- @computed("category.uploaded_background.url")
+ @discourseComputed("category.uploaded_background.url")
backgroundImageUrl(uploadedBackgroundUrl) {
return uploadedBackgroundUrl || "";
},
- @computed("category.uploaded_logo.url")
+ @discourseComputed("category.uploaded_logo.url")
logoImageUrl(uploadedLogoUrl) {
return uploadedLogoUrl || "";
},
@@ -33,7 +34,7 @@ export default buildCategoryPanel("images").extend({
_deleteUpload(path) {
this.set(
path,
- Ember.Object.create({
+ EmberObject.create({
id: null,
url: null
})
@@ -43,7 +44,7 @@ export default buildCategoryPanel("images").extend({
_setFromUpload(path, upload) {
this.set(
path,
- Ember.Object.create({
+ EmberObject.create({
url: upload.url,
id: upload.id
})
diff --git a/app/assets/javascripts/discourse/components/edit-category-panel.js.es6 b/app/assets/javascripts/discourse/components/edit-category-panel.js.es6
index 6a45839cf7..3161e31f35 100644
--- a/app/assets/javascripts/discourse/components/edit-category-panel.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-panel.js.es6
@@ -1,11 +1,13 @@
-const EditCategoryPanel = Ember.Component.extend({});
+import { equal } from "@ember/object/computed";
+import Component from "@ember/component";
+const EditCategoryPanel = Component.extend({});
export default EditCategoryPanel;
export function buildCategoryPanel(tab, extras) {
return EditCategoryPanel.extend(
{
- activeTab: Ember.computed.equal("selectedTab", tab),
+ activeTab: equal("selectedTab", tab),
classNameBindings: [
":modal-tab",
"activeTab::hide",
diff --git a/app/assets/javascripts/discourse/components/edit-category-security.js.es6 b/app/assets/javascripts/discourse/components/edit-category-security.js.es6
index 129fc82793..ddabd80588 100644
--- a/app/assets/javascripts/discourse/components/edit-category-security.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-security.js.es6
@@ -1,12 +1,53 @@
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
import PermissionType from "discourse/models/permission-type";
+import { on } from "discourse-common/utils/decorators";
export default buildCategoryPanel("security", {
editingPermissions: false,
selectedGroup: null,
selectedPermission: null,
+ showPendingGroupChangesAlert: false,
+ interactedWithDropdowns: false,
+
+ @on("init")
+ _setup() {
+ this.setProperties({
+ selectedGroup: this.get("category.availableGroups.firstObject"),
+ selectedPermission: this.get(
+ "category.availablePermissions.firstObject.id"
+ )
+ });
+ },
+
+ @on("init")
+ _registerValidator() {
+ this.registerValidator(() => {
+ if (
+ !this.showPendingGroupChangesAlert &&
+ this.interactedWithDropdowns &&
+ this.activeTab
+ ) {
+ this.set("showPendingGroupChangesAlert", true);
+ return true;
+ }
+ });
+ },
actions: {
+ onSelectGroup(selectedGroup) {
+ this.setProperties({
+ interactedWithDropdowns: true,
+ selectedGroup
+ });
+ },
+
+ onSelectPermission(selectedPermission) {
+ this.setProperties({
+ interactedWithDropdowns: true,
+ selectedPermission
+ });
+ },
+
editPermissions() {
if (!this.get("category.is_special")) {
this.set("editingPermissions", true);
@@ -17,14 +58,15 @@ export default buildCategoryPanel("security", {
if (!this.get("category.is_special")) {
this.category.addPermission({
group_name: group + "",
- permission: PermissionType.create({ id: parseInt(id) })
+ permission: PermissionType.create({ id: parseInt(id, 10) })
});
}
- this.set(
- "selectedGroup",
- this.get("category.availableGroups.firstObject")
- );
+ this.setProperties({
+ selectedGroup: this.get("category.availableGroups.firstObject"),
+ showPendingGroupChangesAlert: false,
+ interactedWithDropdowns: false
+ });
},
removePermission(permission) {
diff --git a/app/assets/javascripts/discourse/components/edit-category-settings.js.es6 b/app/assets/javascripts/discourse/components/edit-category-settings.js.es6
index c32f8b0553..6ee6d77ffa 100644
--- a/app/assets/javascripts/discourse/components/edit-category-settings.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-settings.js.es6
@@ -1,6 +1,7 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { empty, and } from "@ember/object/computed";
import { setting } from "discourse/lib/computed";
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
-import computed from "ember-addons/ember-computed-decorators";
import { searchPriorities } from "discourse/components/concerns/category-search-priorities";
import Group from "discourse/models/group";
@@ -12,14 +13,17 @@ export function addCategorySortCriteria(criteria) {
export default buildCategoryPanel("settings", {
emailInEnabled: setting("email_in"),
showPositionInput: setting("fixed_category_positions"),
- isParentCategory: Ember.computed.empty("category.parent_category_id"),
- showSubcategoryListStyle: Ember.computed.and(
+ @discourseComputed("category.isParent", "category.parent_category_id")
+ isParentCategory(isParent, parentCategoryId) {
+ return isParent || !parentCategoryId;
+ },
+ showSubcategoryListStyle: and(
"category.show_subcategory_list",
"isParentCategory"
),
- isDefaultSortOrder: Ember.computed.empty("category.sort_order"),
+ isDefaultSortOrder: empty("category.sort_order"),
- @computed
+ @discourseComputed
availableSubcategoryListStyles() {
return [
{ name: I18n.t("category.subcategory_list_styles.rows"), value: "rows" },
@@ -46,7 +50,7 @@ export default buildCategoryPanel("settings", {
return Group.findAll({ term, ignore_automatic: true });
},
- @computed
+ @discourseComputed
availableViews() {
return [
{ name: I18n.t("filters.latest.title"), value: "latest" },
@@ -54,7 +58,7 @@ export default buildCategoryPanel("settings", {
];
},
- @computed
+ @discourseComputed
availableTopPeriods() {
return ["all", "yearly", "quarterly", "monthly", "weekly", "daily"].map(
p => {
@@ -63,7 +67,7 @@ export default buildCategoryPanel("settings", {
);
},
- @computed
+ @discourseComputed
searchPrioritiesOptions() {
const options = [];
@@ -79,7 +83,7 @@ export default buildCategoryPanel("settings", {
return options;
},
- @computed
+ @discourseComputed
availableSorts() {
return [
"likes",
@@ -96,7 +100,7 @@ export default buildCategoryPanel("settings", {
.sort((a, b) => a.name.localeCompare(b.name));
},
- @computed
+ @discourseComputed
sortAscendingOptions() {
return [
{ name: I18n.t("category.sort_ascending"), value: "true" },
diff --git a/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 b/app/assets/javascripts/discourse/components/edit-category-tab.js.es6
index c36ec0175a..9fa7984533 100644
--- a/app/assets/javascripts/discourse/components/edit-category-tab.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-tab.js.es6
@@ -1,25 +1,27 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { scheduleOnce } from "@ember/runloop";
+import Component from "@ember/component";
import { propertyEqual } from "discourse/lib/computed";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "li",
classNameBindings: ["active", "tabClassName"],
- @computed("tab")
+ @discourseComputed("tab")
tabClassName(tab) {
return "edit-category-" + tab;
},
active: propertyEqual("selectedTab", "tab"),
- @computed("tab")
+ @discourseComputed("tab")
title(tab) {
return I18n.t("category." + tab.replace("-", "_"));
},
didInsertElement() {
this._super(...arguments);
- Ember.run.scheduleOnce("afterRender", this, this._addToCollection);
+ scheduleOnce("afterRender", this, this._addToCollection);
},
_addToCollection: function() {
@@ -27,7 +29,7 @@ export default Ember.Component.extend({
},
_resetModalScrollState() {
- const $modalBody = this.$()
+ const $modalBody = $(this.element)
.parents("#discourse-modal")
.find(".modal-body");
if ($modalBody.length === 1) {
diff --git a/app/assets/javascripts/discourse/components/edit-category-tags.js.es6 b/app/assets/javascripts/discourse/components/edit-category-tags.js.es6
index 665975cf6b..aaca4b0e49 100644
--- a/app/assets/javascripts/discourse/components/edit-category-tags.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-tags.js.es6
@@ -1,10 +1,8 @@
+import { empty, and } from "@ember/object/computed";
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
export default buildCategoryPanel("tags", {
- allowedTagsEmpty: Ember.computed.empty("category.allowed_tags"),
- allowedTagGroupsEmpty: Ember.computed.empty("category.allowed_tag_groups"),
- disableAllowGlobalTags: Ember.computed.and(
- "allowedTagsEmpty",
- "allowedTagGroupsEmpty"
- )
+ allowedTagsEmpty: empty("category.allowed_tags"),
+ allowedTagGroupsEmpty: empty("category.allowed_tag_groups"),
+ disableAllowGlobalTags: and("allowedTagsEmpty", "allowedTagGroupsEmpty")
});
diff --git a/app/assets/javascripts/discourse/components/edit-category-topic-template.js.es6 b/app/assets/javascripts/discourse/components/edit-category-topic-template.js.es6
index ab643885a4..9a5d7d7ab1 100644
--- a/app/assets/javascripts/discourse/components/edit-category-topic-template.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-topic-template.js.es6
@@ -1,11 +1,14 @@
+import { scheduleOnce } from "@ember/runloop";
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
+import { observes } from "discourse-common/utils/decorators";
export default buildCategoryPanel("topic-template", {
+ @observes("activeTab")
_activeTabChanged: function() {
if (this.activeTab) {
- Ember.run.scheduleOnce("afterRender", () =>
- this.$(".d-editor-input").focus()
+ scheduleOnce("afterRender", () =>
+ this.element.querySelector(".d-editor-input").focus()
);
}
- }.observes("activeTab")
+ }
});
diff --git a/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6
index 1f65b8797d..4d2be762d3 100644
--- a/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6
@@ -1,9 +1,11 @@
-import {
- default as computed,
+import { isEmpty } from "@ember/utils";
+import { equal, or, readOnly } from "@ember/object/computed";
+import { schedule } from "@ember/runloop";
+import Component from "@ember/component";
+import discourseComputed, {
observes,
on
-} from "ember-addons/ember-computed-decorators";
-
+} from "discourse-common/utils/decorators";
import {
PUBLISH_TO_CATEGORY_STATUS_TYPE,
OPEN_STATUS_TYPE,
@@ -13,39 +15,26 @@ import {
BUMP_TYPE
} from "discourse/controllers/edit-topic-timer";
-export default Ember.Component.extend({
- selection: Ember.computed.alias("topicTimer.status_type"),
- autoOpen: Ember.computed.equal("selection", OPEN_STATUS_TYPE),
- autoClose: Ember.computed.equal("selection", CLOSE_STATUS_TYPE),
- autoDelete: Ember.computed.equal("selection", DELETE_STATUS_TYPE),
- autoBump: Ember.computed.equal("selection", BUMP_TYPE),
- publishToCategory: Ember.computed.equal(
- "selection",
- PUBLISH_TO_CATEGORY_STATUS_TYPE
- ),
- reminder: Ember.computed.equal("selection", REMINDER_TYPE),
- showTimeOnly: Ember.computed.or(
- "autoOpen",
- "autoDelete",
- "reminder",
- "autoBump"
- ),
+export default Component.extend({
+ selection: readOnly("topicTimer.status_type"),
+ autoOpen: equal("selection", OPEN_STATUS_TYPE),
+ autoClose: equal("selection", CLOSE_STATUS_TYPE),
+ autoDelete: equal("selection", DELETE_STATUS_TYPE),
+ autoBump: equal("selection", BUMP_TYPE),
+ publishToCategory: equal("selection", PUBLISH_TO_CATEGORY_STATUS_TYPE),
+ reminder: equal("selection", REMINDER_TYPE),
+ showTimeOnly: or("autoOpen", "autoDelete", "reminder", "autoBump"),
- @computed(
+ @discourseComputed(
"topicTimer.updateTime",
- "loading",
"publishToCategory",
"topicTimer.category_id"
)
- saveDisabled(updateTime, loading, publishToCategory, topicTimerCategoryId) {
- return (
- Ember.isEmpty(updateTime) ||
- loading ||
- (publishToCategory && !topicTimerCategoryId)
- );
+ saveDisabled(updateTime, publishToCategory, topicTimerCategoryId) {
+ return isEmpty(updateTime) || (publishToCategory && !topicTimerCategoryId);
},
- @computed("topic.visible")
+ @discourseComputed("topic.visible")
excludeCategoryId(visible) {
if (visible) return this.get("topic.category_id");
},
@@ -72,9 +61,29 @@ export default Ember.Component.extend({
@observes("selection")
_updateBasedOnLastPost() {
if (!this.autoClose) {
- Ember.run.schedule("afterRender", () => {
+ schedule("afterRender", () => {
this.set("topicTimer.based_on_last_post", false);
});
}
+ },
+
+ didReceiveAttrs() {
+ this._super(...arguments);
+
+ // TODO: get rid of this hack
+ schedule("afterRender", () => {
+ if (!this.get("topicTimer.status_type")) {
+ this.set(
+ "topicTimer.status_type",
+ this.get("timerTypes.firstObject.id")
+ );
+ }
+ });
+ },
+
+ actions: {
+ onChangeTimerType(value) {
+ this.set("topicTimer.status_type", value);
+ }
}
});
diff --git a/app/assets/javascripts/discourse/components/emoji-picker.js.es6 b/app/assets/javascripts/discourse/components/emoji-picker.js.es6
index 2970f014ce..fccf3c597f 100644
--- a/app/assets/javascripts/discourse/components/emoji-picker.js.es6
+++ b/app/assets/javascripts/discourse/components/emoji-picker.js.es6
@@ -1,30 +1,26 @@
-import { on, observes } from "ember-addons/ember-computed-decorators";
+import { inject as service } from "@ember/service";
+import Component from "@ember/component";
+import { on, observes } from "discourse-common/utils/decorators";
import { findRawTemplate } from "discourse/lib/raw-templates";
import { emojiUrlFor } from "discourse/lib/text";
-import KeyValueStore from "discourse/lib/key-value-store";
import {
extendedEmojiList,
isSkinTonableEmoji,
emojiSearch
} from "pretty-text/emoji";
import { safariHacksDisabled } from "discourse/lib/utilities";
+import ENV from "discourse-common/config/environment";
+
const { run } = Ember;
-const keyValueStore = new KeyValueStore("discourse_emojis_");
-const EMOJI_USAGE = "emojiUsage";
-const EMOJI_SELECTED_DIVERSITY = "emojiSelectedDiversity";
const PER_ROW = 11;
const customEmojis = _.keys(extendedEmojiList()).map(code => {
return { code, src: emojiUrlFor(code) };
});
-export function resetCache() {
- keyValueStore.setObject({ key: EMOJI_USAGE, value: [] });
- keyValueStore.setObject({ key: EMOJI_SELECTED_DIVERSITY, value: 1 });
-}
-
-export default Ember.Component.extend({
+export default Component.extend({
automaticPositioning: true,
+ emojiStore: service("emoji-store"),
close() {
this._unbindEvents();
@@ -46,11 +42,10 @@ export default Ember.Component.extend({
this.$results = this.$picker.find(".results");
this.$list = this.$picker.find(".list");
- this.set(
- "selectedDiversity",
- keyValueStore.getObject(EMOJI_SELECTED_DIVERSITY) || 1
- );
- this.set("recentEmojis", keyValueStore.getObject(EMOJI_USAGE) || []);
+ this.setProperties({
+ selectedDiversity: this.emojiStore.diversity,
+ recentEmojis: this.emojiStore.favorites
+ });
run.scheduleOnce("afterRender", this, function() {
this._bindEvents();
@@ -86,20 +81,9 @@ export default Ember.Component.extend({
@on("didInsertElement")
_setup() {
- this.$picker = this.$(".emoji-picker");
- this.$modal = this.$(".emoji-picker-modal");
-
+ this.$picker = $(this.element.querySelector(".emoji-picker"));
+ this.$modal = $(this.element.querySelector(".emoji-picker-modal"));
this.appEvents.on("emoji-picker:close", this, "_closeEmojiPicker");
-
- if (!keyValueStore.getObject(EMOJI_USAGE)) {
- keyValueStore.setObject({ key: EMOJI_USAGE, value: [] });
- } else if (_.isPlainObject(keyValueStore.getObject(EMOJI_USAGE))) {
- // handle legacy format
- keyValueStore.setObject({
- key: EMOJI_USAGE,
- value: _.keys(keyValueStore.getObject(EMOJI_USAGE))
- });
- }
},
@on("didUpdateAttrs")
@@ -116,10 +100,7 @@ export default Ember.Component.extend({
@observes("selectedDiversity")
selectedDiversityChanged() {
- keyValueStore.setObject({
- key: EMOJI_SELECTED_DIVERSITY,
- value: this.selectedDiversity
- });
+ this.emojiStore.diversity = this.selectedDiversity;
$.each(
this.$list.find(".emoji[data-loaded='1'].diversity"),
@@ -228,8 +209,8 @@ export default Ember.Component.extend({
@on("willDestroyElement")
_unbindEvents() {
- this.$().off();
- this.$(window).off("resize");
+ $(this.element).off();
+ $(window).off("resize");
clearInterval(this._refreshInterval);
$("#reply-control").off("div-resizing");
$("html").off("mouseup.emoji-picker");
@@ -312,7 +293,7 @@ export default Ember.Component.extend({
},
_bindResizing() {
- this.$(window).on("resize", () => {
+ $(window).on("resize", () => {
run.throttle(this, this._positionPicker, 16);
});
@@ -326,7 +307,7 @@ export default Ember.Component.extend({
".section[data-section='recent'] .clear-recent"
);
$recent.on("click", () => {
- keyValueStore.setObject({ key: EMOJI_USAGE, value: [] });
+ this.emojiStore.favorites = [];
this.set("recentEmojis", []);
this._scrollTo(0);
return false;
@@ -461,14 +442,17 @@ export default Ember.Component.extend({
);
$diversityScales.on("click", event => {
const $selectedDiversity = $(event.currentTarget);
- this.set("selectedDiversity", parseInt($selectedDiversity.data("level")));
+ this.set(
+ "selectedDiversity",
+ parseInt($selectedDiversity.data("level"), 10)
+ );
return false;
});
},
_isReplyControlExpanded() {
const verticalSpace =
- this.$(window).height() -
+ $(window).height() -
$(".d-header").height() -
$("#reply-control").height();
@@ -480,7 +464,7 @@ export default Ember.Component.extend({
return;
}
- let windowWidth = this.$(window).width();
+ let windowWidth = $(window).width();
const desktopModalePositioning = options => {
let attributes = {
@@ -529,7 +513,7 @@ export default Ember.Component.extend({
this.$picker.css(_.merge(attributes, options));
};
- if (Ember.testing || !this.automaticPositioning) {
+ if (ENV.environment === "test" || !this.automaticPositioning) {
desktopPositioning();
return;
}
@@ -565,7 +549,7 @@ export default Ember.Component.extend({
} else {
const previewInputOffset = $(".d-editor-input").offset();
- const pickerHeight = $(".emoji-picker").height();
+ const pickerHeight = $(".d-editor .emoji-picker").height();
const editorHeight = $(".d-editor-input").height();
const windowBottom = $(window).scrollTop() + $(window).height();
@@ -608,12 +592,8 @@ export default Ember.Component.extend({
},
_trackEmojiUsage(code) {
- let recent = keyValueStore.getObject(EMOJI_USAGE) || [];
- recent = recent.filter(r => r !== code);
- recent.unshift(code);
- recent.length = Math.min(recent.length, PER_ROW);
- keyValueStore.setObject({ key: EMOJI_USAGE, value: recent });
- this.set("recentEmojis", recent);
+ this.emojiStore.track(code);
+ this.set("recentEmojis", this.emojiStore.favorites.slice(0, PER_ROW));
},
_scrollTo(y) {
diff --git a/app/assets/javascripts/discourse/components/emoji-uploader.js.es6 b/app/assets/javascripts/discourse/components/emoji-uploader.js.es6
index f3273d8322..77e2d5e9ff 100644
--- a/app/assets/javascripts/discourse/components/emoji-uploader.js.es6
+++ b/app/assets/javascripts/discourse/components/emoji-uploader.js.es6
@@ -1,11 +1,13 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import { notEmpty, not } from "@ember/object/computed";
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
import UploadMixin from "discourse/mixins/upload";
-export default Ember.Component.extend(UploadMixin, {
+export default Component.extend(UploadMixin, {
type: "emoji",
uploadUrl: "/admin/customize/emojis",
- hasName: Ember.computed.notEmpty("name"),
- addDisabled: Ember.computed.not("hasName"),
+ hasName: notEmpty("name"),
+ addDisabled: not("hasName"),
uploadOptions() {
return {
@@ -13,7 +15,7 @@ export default Ember.Component.extend(UploadMixin, {
};
},
- @computed("hasName", "name")
+ @discourseComputed("hasName", "name")
data(hasName, name) {
return hasName ? { name } : {};
},
diff --git a/app/assets/javascripts/discourse/components/expand-post.js.es6 b/app/assets/javascripts/discourse/components/expand-post.js.es6
index 4bd2b8d4a5..b4d1791966 100644
--- a/app/assets/javascripts/discourse/components/expand-post.js.es6
+++ b/app/assets/javascripts/discourse/components/expand-post.js.es6
@@ -1,6 +1,7 @@
+import Component from "@ember/component";
import { ajax } from "discourse/lib/ajax";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "",
expanded: null,
_loading: false,
@@ -15,19 +16,21 @@ export default Ember.Component.extend({
if (this.expanded) {
this.set("expanded", false);
item.set("expandedExcerpt", null);
- return;
+ return false;
}
const topicId = item.get("topic_id");
const postNumber = item.get("post_number");
this._loading = true;
- return ajax(`/posts/by_number/${topicId}/${postNumber}.json`)
+ ajax(`/posts/by_number/${topicId}/${postNumber}.json`)
.then(result => {
this.set("expanded", true);
item.set("expandedExcerpt", result.cooked);
})
.finally(() => (this._loading = false));
+
+ return false;
}
}
});
diff --git a/app/assets/javascripts/discourse/components/expanding-text-area.js.es6 b/app/assets/javascripts/discourse/components/expanding-text-area.js.es6
index 7af160b4fe..4c3432f277 100644
--- a/app/assets/javascripts/discourse/components/expanding-text-area.js.es6
+++ b/app/assets/javascripts/discourse/components/expanding-text-area.js.es6
@@ -1,17 +1,19 @@
-import { on, observes } from "ember-addons/ember-computed-decorators";
+import { scheduleOnce } from "@ember/runloop";
+import { on, observes } from "discourse-common/utils/decorators";
import autosize from "discourse/lib/autosize";
export default Ember.TextArea.extend({
@on("didInsertElement")
_startWatching() {
- Ember.run.scheduleOnce("afterRender", () => {
- this.$().focus();
+ scheduleOnce("afterRender", () => {
+ $(this.element).focus();
autosize(this.element);
});
},
@observes("value")
_updateAutosize() {
+ this.element.value = this.value;
const evt = document.createEvent("Event");
evt.initEvent("autosize:update", true, false);
this.element.dispatchEvent(evt);
@@ -19,6 +21,6 @@ export default Ember.TextArea.extend({
@on("willDestroyElement")
_disableAutosize() {
- autosize.destroy(this.$());
+ autosize.destroy($(this.element));
}
});
diff --git a/app/assets/javascripts/discourse/components/featured-topic.js.es6 b/app/assets/javascripts/discourse/components/featured-topic.js.es6
index eb0bde08ad..a975b893e9 100644
--- a/app/assets/javascripts/discourse/components/featured-topic.js.es6
+++ b/app/assets/javascripts/discourse/components/featured-topic.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
classNameBindings: [":featured-topic"],
click(e) {
diff --git a/app/assets/javascripts/discourse/components/flag-action-type.js.es6 b/app/assets/javascripts/discourse/components/flag-action-type.js.es6
index 6a7a713d4e..a88356c9a9 100644
--- a/app/assets/javascripts/discourse/components/flag-action-type.js.es6
+++ b/app/assets/javascripts/discourse/components/flag-action-type.js.es6
@@ -1,15 +1,22 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { and, not, equal } from "@ember/object/computed";
+import Component from "@ember/component";
import { MAX_MESSAGE_LENGTH } from "discourse/models/post-action-type";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Component.extend({
+export default Component.extend({
classNames: ["flag-action-type"],
- @computed("flag.name_key")
+ @discourseComputed("flag.name_key")
customPlaceholder(nameKey) {
return I18n.t("flagging.custom_placeholder_" + nameKey);
},
- @computed("flag.name", "flag.name_key", "flag.is_custom_flag", "username")
+ @discourseComputed(
+ "flag.name",
+ "flag.name_key",
+ "flag.is_custom_flag",
+ "username"
+ )
formattedName(name, nameKey, isCustomFlag, username) {
if (isCustomFlag) {
return name.replace("{{username}}", username);
@@ -18,21 +25,21 @@ export default Ember.Component.extend({
}
},
- @computed("flag", "selectedFlag")
+ @discourseComputed("flag", "selectedFlag")
selected(flag, selectedFlag) {
return flag === selectedFlag;
},
- showMessageInput: Ember.computed.and("flag.is_custom_flag", "selected"),
- showDescription: Ember.computed.not("showMessageInput"),
- isNotifyUser: Ember.computed.equal("flag.name_key", "notify_user"),
+ showMessageInput: and("flag.is_custom_flag", "selected"),
+ showDescription: not("showMessageInput"),
+ isNotifyUser: equal("flag.name_key", "notify_user"),
- @computed("flag.description", "flag.short_description")
+ @discourseComputed("flag.description", "flag.short_description")
description(long_description, short_description) {
return this.site.mobileView ? short_description : long_description;
},
- @computed("message.length")
+ @discourseComputed("message.length")
customMessageLengthClasses(messageLength) {
return messageLength <
Discourse.SiteSettings.min_personal_message_post_length
@@ -40,7 +47,7 @@ export default Ember.Component.extend({
: "ok";
},
- @computed("message.length")
+ @discourseComputed("message.length")
customMessageLength(messageLength) {
const len = messageLength || 0;
const minLen = Discourse.SiteSettings.min_personal_message_post_length;
diff --git a/app/assets/javascripts/discourse/components/flag-selection.js.es6 b/app/assets/javascripts/discourse/components/flag-selection.js.es6
index ca02fd03bd..39548b045f 100644
--- a/app/assets/javascripts/discourse/components/flag-selection.js.es6
+++ b/app/assets/javascripts/discourse/components/flag-selection.js.es6
@@ -1,20 +1,22 @@
-import { observes } from "ember-addons/ember-computed-decorators";
+import { next } from "@ember/runloop";
+import Component from "@ember/component";
+import { observes } from "discourse-common/utils/decorators";
// Mostly hacks because `flag.hbs` didn't use `radio-button`
-export default Ember.Component.extend({
+export default Component.extend({
_selectRadio() {
- this.$("input[type='radio']").prop("checked", false);
+ this.element.querySelector("input[type='radio']").checked = false;
const nameKey = this.nameKey;
if (!nameKey) {
return;
}
- this.$("#radio_" + nameKey).prop("checked", "true");
+ this.element.querySelector("#radio_" + nameKey).checked = "true";
},
@observes("nameKey")
selectedChanged() {
- Ember.run.next(this, this._selectRadio);
+ next(this, this._selectRadio);
}
});
diff --git a/app/assets/javascripts/discourse/components/flat-button.js.es6 b/app/assets/javascripts/discourse/components/flat-button.js.es6
index bcac9cc01f..f2f38fdc27 100644
--- a/app/assets/javascripts/discourse/components/flat-button.js.es6
+++ b/app/assets/javascripts/discourse/components/flat-button.js.es6
@@ -1,11 +1,12 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "button",
classNames: ["btn-flat"],
attributeBindings: ["disabled", "translatedTitle:title"],
- @computed("title")
+ @discourseComputed("title")
translatedTitle(title) {
if (title) return I18n.t(title);
},
diff --git a/app/assets/javascripts/discourse/components/footer-message.js.es6 b/app/assets/javascripts/discourse/components/footer-message.js.es6
index 22263eca31..e829a884c5 100644
--- a/app/assets/javascripts/discourse/components/footer-message.js.es6
+++ b/app/assets/javascripts/discourse/components/footer-message.js.es6
@@ -1,3 +1,4 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
classNames: ["footer-message"]
});
diff --git a/app/assets/javascripts/discourse/components/footer-nav.js.es6 b/app/assets/javascripts/discourse/components/footer-nav.js.es6
index 8e1a4e8a2f..c4d412346e 100644
--- a/app/assets/javascripts/discourse/components/footer-nav.js.es6
+++ b/app/assets/javascripts/discourse/components/footer-nav.js.es6
@@ -1,8 +1,8 @@
+import { throttle } from "@ember/runloop";
import MountWidget from "discourse/components/mount-widget";
import MobileScrollDirection from "discourse/mixins/mobile-scroll-direction";
import Scrolling from "discourse/mixins/scrolling";
-import { observes } from "ember-addons/ember-computed-decorators";
-import { isiPad } from "discourse/lib/utilities";
+import { observes } from "discourse-common/utils/decorators";
import { isAppWebview, postRNWebviewMessage } from "discourse/lib/utilities";
const MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE = 150;
@@ -37,7 +37,7 @@ const FooterNavComponent = MountWidget.extend(
this.appEvents.on("modal:body-dismissed", this, "_modalOff");
}
- if (isiPad()) {
+ if (this.capabilities.isIpadOS) {
$("body").addClass("footer-nav-ipad");
} else {
this.bindScrolling({ name: "footer-nav" });
@@ -56,7 +56,7 @@ const FooterNavComponent = MountWidget.extend(
this.appEvents.off("modal:body-removed", this, "_modalOff");
}
- if (isiPad()) {
+ if (this.capabilities.isIpadOS) {
$("body").removeClass("footer-nav-ipad");
} else {
this.unbindScrolling("footer-nav");
@@ -79,7 +79,7 @@ const FooterNavComponent = MountWidget.extend(
const offset = window.pageYOffset || $("html").scrollTop();
- Ember.run.throttle(
+ throttle(
this,
this.calculateDirection,
offset,
@@ -91,7 +91,7 @@ const FooterNavComponent = MountWidget.extend(
// in the header, otherwise, we hide it.
@observes("mobileScrollDirection")
toggleMobileFooter() {
- this.$().toggleClass(
+ $(this.element).toggleClass(
"visible",
this.mobileScrollDirection === null ? true : false
);
diff --git a/app/assets/javascripts/discourse/components/future-date-input.js.es6 b/app/assets/javascripts/discourse/components/future-date-input.js.es6
index 9386c1b3f7..d84ab3c334 100644
--- a/app/assets/javascripts/discourse/components/future-date-input.js.es6
+++ b/app/assets/javascripts/discourse/components/future-date-input.js.es6
@@ -1,21 +1,18 @@
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import { isEmpty } from "@ember/utils";
+import { equal, and, empty } from "@ember/object/computed";
+import Component from "@ember/component";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { FORMAT } from "select-kit/components/future-date-input-selector";
import { PUBLISH_TO_CATEGORY_STATUS_TYPE } from "discourse/controllers/edit-topic-timer";
-export default Ember.Component.extend({
+export default Component.extend({
selection: null,
date: null,
time: null,
includeDateTime: true,
- isCustom: Ember.computed.equal("selection", "pick_date_and_time"),
- isBasedOnLastPost: Ember.computed.equal(
- "selection",
- "set_based_on_last_post"
- ),
- displayDateAndTimePicker: Ember.computed.and("includeDateTime", "isCustom"),
+ isCustom: equal("selection", "pick_date_and_time"),
+ isBasedOnLastPost: equal("selection", "set_based_on_last_post"),
+ displayDateAndTimePicker: and("includeDateTime", "isCustom"),
displayLabel: null,
init() {
@@ -36,7 +33,7 @@ export default Ember.Component.extend({
}
},
- timeInputDisabled: Ember.computed.empty("date"),
+ timeInputDisabled: empty("date"),
@observes("date", "time")
_updateInput() {
@@ -48,9 +45,10 @@ export default Ember.Component.extend({
const dateTime = moment(`${this.date}${time}`);
if (dateTime.isValid()) {
- this.set("input", dateTime.format(FORMAT));
+ this.attrs.onChangeInput &&
+ this.attrs.onChangeInput(dateTime.format(FORMAT));
} else {
- this.set("input", null);
+ this.attrs.onChangeInput && this.attrs.onChangeInput(null);
}
},
@@ -59,7 +57,7 @@ export default Ember.Component.extend({
this.set("basedOnLastPost", this.isBasedOnLastPost);
},
- @computed("input", "isBasedOnLastPost")
+ @discourseComputed("input", "isBasedOnLastPost")
duration(input, isBasedOnLastPost) {
const now = moment();
@@ -70,7 +68,7 @@ export default Ember.Component.extend({
}
},
- @computed("input", "isBasedOnLastPost")
+ @discourseComputed("input", "isBasedOnLastPost")
executeAt(input, isBasedOnLastPost) {
if (isBasedOnLastPost) {
return moment()
@@ -87,7 +85,7 @@ export default Ember.Component.extend({
if (this.label) this.set("displayLabel", I18n.t(this.label));
},
- @computed(
+ @discourseComputed(
"statusType",
"input",
"isCustom",
@@ -107,21 +105,21 @@ export default Ember.Component.extend({
) {
if (!statusType || willCloseImmediately) return false;
- if (
- statusType === PUBLISH_TO_CATEGORY_STATUS_TYPE &&
- Ember.isEmpty(categoryId)
- ) {
+ if (statusType === PUBLISH_TO_CATEGORY_STATUS_TYPE && isEmpty(categoryId)) {
return false;
}
if (isCustom) {
- return date || time;
+ if (date) {
+ return moment(`${date}${time ? " " + time : ""}`).isAfter(moment());
+ }
+ return time;
} else {
return input;
}
},
- @computed("isBasedOnLastPost", "input", "lastPostedAt")
+ @discourseComputed("isBasedOnLastPost", "input", "lastPostedAt")
willCloseImmediately(isBasedOnLastPost, input, lastPostedAt) {
if (isBasedOnLastPost && input) {
let closeDate = moment(lastPostedAt);
@@ -130,7 +128,7 @@ export default Ember.Component.extend({
}
},
- @computed("isBasedOnLastPost", "lastPostedAt")
+ @discourseComputed("isBasedOnLastPost", "lastPostedAt")
willCloseI18n(isBasedOnLastPost, lastPostedAt) {
if (isBasedOnLastPost) {
const diff = Math.round(
diff --git a/app/assets/javascripts/discourse/components/generated-invite-link.js.es6 b/app/assets/javascripts/discourse/components/generated-invite-link.js.es6
index 35179719ec..8b475cbbdb 100644
--- a/app/assets/javascripts/discourse/components/generated-invite-link.js.es6
+++ b/app/assets/javascripts/discourse/components/generated-invite-link.js.es6
@@ -1,7 +1,8 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
didInsertElement() {
this._super(...arguments);
- this.$("input")
+ $(this.element.querySelector("input"))
.select()
.focus();
}
diff --git a/app/assets/javascripts/discourse/components/global-notice.js.es6 b/app/assets/javascripts/discourse/components/global-notice.js.es6
index 3d8c46679b..8ca725abaa 100644
--- a/app/assets/javascripts/discourse/components/global-notice.js.es6
+++ b/app/assets/javascripts/discourse/components/global-notice.js.es6
@@ -1,122 +1,234 @@
-import { on } from "ember-addons/ember-computed-decorators";
-import { iconHTML } from "discourse-common/lib/icon-library";
+import { bind, cancel } from "@ember/runloop";
+import Component from "@ember/component";
import LogsNotice from "discourse/services/logs-notice";
-import { bufferedRender } from "discourse-common/lib/buffered-render";
+import EmberObject from "@ember/object";
+import { computed } from "@ember/object";
-export default Ember.Component.extend(
- bufferedRender({
- rerenderTriggers: ["site.isReadOnly", "siteSettings.disable_emails"],
+const _pluginNotices = [];
- buildBuffer(buffer) {
+export function addGlobalNotice(text, id, options = {}) {
+ _pluginNotices.push(Notice.create({ text, id, options }));
+}
+
+const GLOBAL_NOTICE_DISMISSED_PROMPT_KEY = "dismissed-global-notice-v2";
+
+const Notice = EmberObject.extend({
+ text: null,
+ id: null,
+ options: null,
+
+ init() {
+ this._super(...arguments);
+
+ const defaults = {
+ // can this banner be hidden
+ dismissable: false,
+ // prepend html content
+ html: null,
+ // will define the style of the banner, follows alerts styling
+ level: "info",
+ // should the banner be permanently hidden?
+ persistentDismiss: true,
+ // callback function when dismissing a banner
+ onDismiss: null,
+ // show/hide banner function, will take precedence over everything
+ visibility: null,
+ // how long before banner should show again, eg: moment.duration(1, "week")
+ dismissDuration: null
+ };
+
+ this.options = this.set(
+ "options",
+ Object.assign(defaults, this.options || {})
+ );
+ }
+});
+
+export default Component.extend({
+ logNotice: null,
+
+ init() {
+ this._super(...arguments);
+
+ this._setupObservers();
+ },
+
+ willDestroyElement() {
+ this._super(...arguments);
+
+ this._tearDownObservers();
+ },
+
+ notices: computed(
+ "site.isReadOnly",
+ "siteSettings.disable_emails",
+ "logNotice.{id,text,hidden}",
+ function() {
let notices = [];
if ($.cookie("dosp") === "1") {
$.removeCookie("dosp", { path: "/" });
- notices.push([I18n.t("forced_anonymous"), "forced-anonymous"]);
+ notices.push(
+ Notice.create({
+ text: I18n.t("forced_anonymous"),
+ id: "forced-anonymous"
+ })
+ );
}
- if (this.session.get("safe_mode")) {
- notices.push([I18n.t("safe_mode.enabled"), "safe-mode"]);
+ if (this.session && this.session.safe_mode) {
+ notices.push(
+ Notice.create({ text: I18n.t("safe_mode.enabled"), id: "safe-mode" })
+ );
}
- if (this.site.get("isReadOnly")) {
- notices.push([I18n.t("read_only_mode.enabled"), "alert-read-only"]);
+ if (this.site.isReadOnly) {
+ notices.push(
+ Notice.create({
+ text: I18n.t("read_only_mode.enabled"),
+ id: "alert-read-only"
+ })
+ );
}
if (
this.siteSettings.disable_emails === "yes" ||
this.siteSettings.disable_emails === "non-staff"
) {
- notices.push([I18n.t("emails_are_disabled"), "alert-emails-disabled"]);
+ notices.push(
+ Notice.create({
+ text: I18n.t("emails_are_disabled"),
+ id: "alert-emails-disabled"
+ })
+ );
}
- if (this.site.get("wizard_required")) {
+ if (this.site.wizard_required) {
const requiredText = I18n.t("wizard_required", {
url: Discourse.getURL("/wizard")
});
- notices.push([requiredText, "alert-wizard"]);
+ notices.push(Notice.create({ text: requiredText, id: "alert-wizard" }));
}
if (
- this.currentUser &&
- this.currentUser.get("staff") &&
+ this.get("currentUser.staff") &&
this.siteSettings.bootstrap_mode_enabled
) {
if (this.siteSettings.bootstrap_mode_min_users > 0) {
- notices.push([
- I18n.t("bootstrap_mode_enabled", {
- min_users: this.siteSettings.bootstrap_mode_min_users
- }),
- "alert-bootstrap-mode"
- ]);
+ notices.push(
+ Notice.create({
+ text: I18n.t("bootstrap_mode_enabled", {
+ min_users: this.siteSettings.bootstrap_mode_min_users
+ }),
+ id: "alert-bootstrap-mode"
+ })
+ );
} else {
- notices.push([
- I18n.t("bootstrap_mode_disabled"),
- "alert-bootstrap-mode"
- ]);
+ notices.push(
+ Notice.create({
+ text: I18n.t("bootstrap_mode_disabled"),
+ id: "alert-bootstrap-mode"
+ })
+ );
}
}
- if (!_.isEmpty(this.siteSettings.global_notice)) {
- notices.push([this.siteSettings.global_notice, "alert-global-notice"]);
- }
-
- if (!LogsNotice.currentProp("hidden")) {
- notices.push([
- LogsNotice.currentProp("message"),
- "alert-logs-notice",
- `
${iconHTML("times")}
`
- ]);
- }
-
- if (notices.length > 0) {
- buffer.push(
- notices
- .map(n => {
- var html = `
`;
- if (n[2]) html += n[2];
- html += `${n[0]}
`;
- return html;
- })
- .join("")
+ if (
+ this.siteSettings.global_notice &&
+ this.siteSettings.global_notice.length
+ ) {
+ notices.push(
+ Notice.create({
+ text: this.siteSettings.global_notice,
+ id: "alert-global-notice"
+ })
);
}
- },
- @on("didInsertElement")
- _setupLogsNotice() {
- this._boundRerenderBuffer = Ember.run.bind(this, this.rerenderBuffer);
- LogsNotice.current().addObserver("hidden", this._boundRerenderBuffer);
-
- this._boundResetCurrentProp = Ember.run.bind(
- this,
- this._resetCurrentProp
- );
- $(this.element).on(
- "click.global-notice",
- ".alert-logs-notice .close",
- this._boundResetCurrentProp
- );
- },
-
- @on("willDestroyElement")
- _teardownLogsNotice() {
- if (this._boundResetCurrentProp) {
- $(this.element).off("click.global-notice", this._boundResetCurrentProp);
+ if (this.logNotice) {
+ notices.push(this.logNotice);
}
- if (this._boundRerenderBuffer) {
- LogsNotice.current().removeObserver(
- "hidden",
- this._boundRerenderBuffer
- );
- }
- },
+ return notices.concat(_pluginNotices).filter(notice => {
+ if (notice.options.visibility) {
+ return notice.options.visibility(notice);
+ } else {
+ const key = `${GLOBAL_NOTICE_DISMISSED_PROMPT_KEY}-${notice.id}`;
+ const value = this.keyValueStore.get(key);
- _resetCurrentProp() {
- LogsNotice.currentProp("text", "");
+ // banner has never been dismissed
+ if (!value) {
+ return true;
+ }
+
+ // banner has no persistent dismiss and should always show on load
+ if (!notice.options.persistentDismiss) {
+ return true;
+ }
+
+ if (notice.options.dismissDuration) {
+ const resetAt = moment(value).add(notice.options.dismissDuration);
+ return moment().isAfter(resetAt);
+ } else {
+ return false;
+ }
+ }
+ });
}
- })
-);
+ ),
+
+ actions: {
+ dismissNotice(notice) {
+ if (notice.options.onDismiss) {
+ notice.options.onDismiss(notice);
+ }
+
+ if (notice.options.persistentDismiss) {
+ this.keyValueStore.set({
+ key: `${GLOBAL_NOTICE_DISMISSED_PROMPT_KEY}-${notice.id}`,
+ value: moment().toISOString(true)
+ });
+ }
+
+ const alert = document.getElementById(`global-notice-${notice.id}`);
+ if (alert) alert.style.display = "none";
+ }
+ },
+
+ _setupObservers() {
+ this._boundLogsNoticeHandler = bind(this, this._handleLogsNoticeUpdate);
+ LogsNotice.current().addObserver("hidden", this._boundLogsNoticeHandler);
+ LogsNotice.current().addObserver("text", this._boundLogsNoticeHandler);
+ },
+
+ _tearDownObservers() {
+ if (this._boundLogsNoticeHandler) {
+ LogsNotice.current().removeObserver("text", this._boundLogsNoticeHandler);
+ LogsNotice.current().removeObserver(
+ "hidden",
+ this._boundLogsNoticeHandler
+ );
+ cancel(this._boundLogsNoticeHandler);
+ }
+ },
+
+ _handleLogsNoticeUpdate() {
+ const logNotice = Notice.create({
+ text: LogsNotice.currentProp("message"),
+ id: "alert-logs-notice",
+ options: {
+ dismissable: true,
+ persistentDismiss: false,
+ visibility() {
+ return !LogsNotice.currentProp("hidden");
+ },
+ onDismiss() {
+ LogsNotice.currentProp("hidden", true);
+ LogsNotice.currentProp("text", "");
+ }
+ }
+ });
+
+ this.set("logNotice", logNotice);
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/google-search.js.es6 b/app/assets/javascripts/discourse/components/google-search.js.es6
index e99e862f8e..43db648e81 100644
--- a/app/assets/javascripts/discourse/components/google-search.js.es6
+++ b/app/assets/javascripts/discourse/components/google-search.js.es6
@@ -1,12 +1,14 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { alias } from "@ember/object/computed";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
classNames: ["google-search-form"],
classNameBindings: ["hidden:hidden"],
- hidden: Ember.computed.alias("siteSettings.login_required"),
+ hidden: alias("siteSettings.login_required"),
- @computed
+ @discourseComputed
siteUrl() {
return `${location.protocol}//${location.host}${Discourse.getURL("/")}`;
}
diff --git a/app/assets/javascripts/discourse/components/group-activity-filter.js.es6 b/app/assets/javascripts/discourse/components/group-activity-filter.js.es6
index 145b770e51..790c675e49 100644
--- a/app/assets/javascripts/discourse/components/group-activity-filter.js.es6
+++ b/app/assets/javascripts/discourse/components/group-activity-filter.js.es6
@@ -1,3 +1,4 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
tagName: "li"
});
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 2c3df56b1f..71884696fb 100644
--- a/app/assets/javascripts/discourse/components/group-card-contents.js.es6
+++ b/app/assets/javascripts/discourse/components/group-card-contents.js.es6
@@ -1,15 +1,18 @@
+import { alias, match, gt, or } from "@ember/object/computed";
+import Component from "@ember/component";
import { setting } from "discourse/lib/computed";
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
import CardContentsBase from "discourse/mixins/card-contents-base";
import CleansUp from "discourse/mixins/cleans-up";
import { groupPath } from "discourse/lib/url";
+import { Promise } from "rsvp";
const maxMembersToDisplay = 10;
-export default Ember.Component.extend(CardContentsBase, CleansUp, {
+export default Component.extend(CardContentsBase, CleansUp, {
elementId: "group-card",
triggeringLinkClass: "mention-group",
- classNames: ["no-bg"],
+ classNames: ["no-bg", "group-card"],
classNameBindings: [
"visible:show",
"showBadges",
@@ -20,11 +23,11 @@ export default Ember.Component.extend(CardContentsBase, CleansUp, {
allowBackgrounds: setting("allow_profile_backgrounds"),
showBadges: setting("enable_badges"),
- postStream: Ember.computed.alias("topic.postStream"),
- viewingTopic: Ember.computed.match("currentPath", /^topic\./),
+ postStream: alias("topic.postStream"),
+ viewingTopic: match("currentPath", /^topic\./),
- showMoreMembers: Ember.computed.gt("moreMembersCount", 0),
- hasMembersOrIsMember: Ember.computed.or(
+ showMoreMembers: gt("moreMembersCount", 0),
+ hasMembersOrIsMember: or(
"group.members",
"group.is_group_owner_display",
"group.is_group_user"
@@ -32,14 +35,14 @@ export default Ember.Component.extend(CardContentsBase, CleansUp, {
group: null,
- @computed("group.user_count", "group.members.length")
+ @discourseComputed("group.user_count", "group.members.length")
moreMembersCount: (memberCount, maxMemberDisplay) =>
memberCount - maxMemberDisplay,
- @computed("group.name")
+ @discourseComputed("group.name")
groupClass: name => (name ? `group-card-${name}` : ""),
- @computed("group")
+ @discourseComputed("group")
groupPath(group) {
return groupPath(group.name);
},
@@ -53,8 +56,9 @@ export default Ember.Component.extend(CardContentsBase, CleansUp, {
if (!group.flair_url && !group.flair_bg_color) {
group.set("flair_url", "fa-users");
}
- group.set("limit", maxMembersToDisplay);
- return group.findMembers();
+ return group.members.length < maxMembersToDisplay
+ ? group.findMembers({ limit: maxMembersToDisplay }, true)
+ : Promise.resolve();
})
.catch(() => this._close())
.finally(() => this.set("loading", null));
diff --git a/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6 b/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6
index f16d48827e..81b17bcb8d 100644
--- a/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6
+++ b/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6
@@ -1,30 +1,33 @@
-import computed from "ember-addons/ember-computed-decorators";
-import { observes } from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { debounce } from "@ember/runloop";
+import Component from "@ember/component";
+import { observes } from "discourse-common/utils/decorators";
import { escapeExpression } from "discourse/lib/utilities";
import { convertIconClass } from "discourse-common/lib/icon-library";
import { ajax } from "discourse/lib/ajax";
+import { htmlSafe } from "@ember/template";
-export default Ember.Component.extend({
+export default Component.extend({
classNames: ["group-flair-inputs"],
- @computed
+ @discourseComputed
demoAvatarUrl() {
return Discourse.getURL("/images/avatar.png");
},
- @computed("model.flair_url")
+ @discourseComputed("model.flair_url")
flairPreviewIcon(flairURL) {
return flairURL && /fa(r|b?)-/.test(flairURL);
},
- @computed("model.flair_url", "flairPreviewIcon")
+ @discourseComputed("model.flair_url", "flairPreviewIcon")
flairPreviewIconUrl(flairURL, flairPreviewIcon) {
return flairPreviewIcon ? convertIconClass(flairURL) : "";
},
@observes("model.flair_url")
_loadSVGIcon() {
- Ember.run.debounce(this, this._loadIcon, 1000);
+ debounce(this, this._loadIcon, 1000);
},
_loadIcon() {
@@ -46,12 +49,12 @@ export default Ember.Component.extend({
}
},
- @computed("model.flair_url", "flairPreviewIcon")
+ @discourseComputed("model.flair_url", "flairPreviewIcon")
flairPreviewImage(flairURL, flairPreviewIcon) {
return flairURL && !flairPreviewIcon;
},
- @computed(
+ @discourseComputed(
"model.flair_url",
"flairPreviewImage",
"model.flairBackgroundHexColor",
@@ -75,15 +78,15 @@ export default Ember.Component.extend({
if (flairHexColor) style += `color: #${flairHexColor};`;
- return Ember.String.htmlSafe(style);
+ return htmlSafe(style);
},
- @computed("model.flairBackgroundHexColor")
+ @discourseComputed("model.flairBackgroundHexColor")
flairPreviewClasses(flairBackgroundHexColor) {
if (flairBackgroundHexColor) return "rounded";
},
- @computed("flairPreviewImage")
+ @discourseComputed("flairPreviewImage")
flairPreviewLabel(flairPreviewImage) {
const key = flairPreviewImage ? "image" : "icon";
return I18n.t(`groups.flair_preview_${key}`);
diff --git a/app/assets/javascripts/discourse/components/group-index-toggle.js.es6 b/app/assets/javascripts/discourse/components/group-index-toggle.js.es6
index 8a35118a89..f84b9b3d21 100644
--- a/app/assets/javascripts/discourse/components/group-index-toggle.js.es6
+++ b/app/assets/javascripts/discourse/components/group-index-toggle.js.es6
@@ -1,28 +1,30 @@
+import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
-import { bufferedRender } from "discourse-common/lib/buffered-render";
-export default Ember.Component.extend(
- bufferedRender({
- tagName: "th",
- classNames: ["sortable"],
- rerenderTriggers: ["order", "desc"],
-
- buildBuffer(buffer) {
- buffer.push("");
- },
-
- click() {
- if (this.order === this.field) {
- this.set("desc", this.desc ? null : true);
- } else {
- this.setProperties({ order: this.field, desc: null });
- }
+export default Component.extend({
+ tagName: "th",
+ classNames: ["sortable"],
+ chevronIcon: null,
+ toggleProperties() {
+ if (this.order === this.field) {
+ this.set("desc", this.desc ? null : true);
+ } else {
+ this.setProperties({ order: this.field, desc: null });
}
- })
-);
+ },
+ toggleChevron() {
+ if (this.order === this.field) {
+ let chevron = iconHTML(this.desc ? "chevron-down" : "chevron-up");
+ this.set("chevronIcon", `${chevron}`.htmlSafe());
+ } else {
+ this.set("chevronIcon", null);
+ }
+ },
+ click() {
+ this.toggleProperties();
+ },
+ didReceiveAttrs() {
+ this._super(...arguments);
+ this.toggleChevron();
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/group-manage-logs-filter.js.es6 b/app/assets/javascripts/discourse/components/group-manage-logs-filter.js.es6
index 0944c3ddf1..98e0c16cdd 100644
--- a/app/assets/javascripts/discourse/components/group-manage-logs-filter.js.es6
+++ b/app/assets/javascripts/discourse/components/group-manage-logs-filter.js.es6
@@ -1,14 +1,15 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "",
- @computed("type")
+ @discourseComputed("type")
label(type) {
return I18n.t(`groups.manage.logs.${type}`);
},
- @computed("value", "type")
+ @discourseComputed("value", "type")
filterText(value, type) {
return type === "action"
? I18n.t(`group_histories.actions.${value}`)
diff --git a/app/assets/javascripts/discourse/components/group-manage-logs-row.js.es6 b/app/assets/javascripts/discourse/components/group-manage-logs-row.js.es6
index b7765f9620..acba43f2b7 100644
--- a/app/assets/javascripts/discourse/components/group-manage-logs-row.js.es6
+++ b/app/assets/javascripts/discourse/components/group-manage-logs-row.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
tagName: "",
expandDetails: false,
diff --git a/app/assets/javascripts/discourse/components/group-manage-save-button.js.es6 b/app/assets/javascripts/discourse/components/group-manage-save-button.js.es6
index 1487151fde..9adcf9ea82 100644
--- a/app/assets/javascripts/discourse/components/group-manage-save-button.js.es6
+++ b/app/assets/javascripts/discourse/components/group-manage-save-button.js.es6
@@ -1,10 +1,11 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
import { popupAjaxError } from "discourse/lib/ajax-error";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Component.extend({
+export default Component.extend({
saving: null,
- @computed("saving")
+ @discourseComputed("saving")
savingText(saving) {
if (saving) return I18n.t("saving");
return saving ? I18n.t("saving") : I18n.t("save");
diff --git a/app/assets/javascripts/discourse/components/group-member-dropdown.js.es6 b/app/assets/javascripts/discourse/components/group-member-dropdown.js.es6
index 5cb7099058..1ee959ecb2 100644
--- a/app/assets/javascripts/discourse/components/group-member-dropdown.js.es6
+++ b/app/assets/javascripts/discourse/components/group-member-dropdown.js.es6
@@ -1,23 +1,16 @@
-import computed from "ember-addons/ember-computed-decorators";
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
+import { computed } from "@ember/object";
export default DropdownSelectBoxComponent.extend({
pluginApiIdentifiers: ["group-member-dropdown"],
- classNames: "group-member-dropdown",
- showFullTitle: false,
- allowInitialValueMutation: false,
- allowAutoSelectFirst: false,
+ classNames: ["group-member-dropdown"],
- init() {
- this._super(...arguments);
-
- this.headerIcon = ["wrench"];
+ selectKitOptions: {
+ icon: "wrench",
+ showFullTitle: false
},
- autoHighlight() {},
-
- @computed("member.owner")
- content(isOwner) {
+ content: computed("member.owner", function() {
const items = [
{
id: "removeMember",
@@ -29,8 +22,8 @@ export default DropdownSelectBoxComponent.extend({
}
];
- if (this.currentUser && this.currentUser.admin) {
- if (isOwner) {
+ if (this.get("currentUser.admin")) {
+ if (this.member.owner) {
items.push({
id: "removeOwner",
name: I18n.t("groups.members.remove_owner"),
@@ -52,19 +45,5 @@ export default DropdownSelectBoxComponent.extend({
}
return items;
- },
-
- mutateValue(id) {
- switch (id) {
- case "removeMember":
- this.removeMember(this.member);
- break;
- case "makeOwner":
- this.makeOwner(this.get("member.username"));
- break;
- case "removeOwner":
- this.removeOwner(this.member);
- break;
- }
- }
+ })
});
diff --git a/app/assets/javascripts/discourse/components/group-member.js.es6 b/app/assets/javascripts/discourse/components/group-member.js.es6
index be68373fa7..1c511cab15 100644
--- a/app/assets/javascripts/discourse/components/group-member.js.es6
+++ b/app/assets/javascripts/discourse/components/group-member.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
classNames: ["item"],
actions: {
diff --git a/app/assets/javascripts/discourse/components/group-members-input.js.es6 b/app/assets/javascripts/discourse/components/group-members-input.js.es6
deleted file mode 100644
index 8ef63b5a50..0000000000
--- a/app/assets/javascripts/discourse/components/group-members-input.js.es6
+++ /dev/null
@@ -1,88 +0,0 @@
-import computed from "ember-addons/ember-computed-decorators";
-import { popupAjaxError } from "discourse/lib/ajax-error";
-import { propertyEqual } from "discourse/lib/computed";
-
-export default Ember.Component.extend({
- classNames: ["group-members-input"],
- addButton: true,
-
- @computed("model.limit", "model.offset", "model.user_count")
- currentPage(limit, offset, userCount) {
- if (userCount === 0) {
- return 0;
- }
-
- return Math.floor(offset / limit) + 1;
- },
-
- @computed("model.limit", "model.user_count")
- totalPages(limit, userCount) {
- if (userCount === 0) {
- return 0;
- }
- return Math.ceil(userCount / limit);
- },
-
- @computed("model.usernames")
- disableAddButton(usernames) {
- return !usernames || !(usernames.length > 0);
- },
-
- showingFirst: Ember.computed.lte("currentPage", 1),
- showingLast: propertyEqual("currentPage", "totalPages"),
-
- actions: {
- next() {
- if (this.showingLast) {
- return;
- }
-
- const group = this.model;
- const offset = Math.min(
- group.get("offset") + group.get("limit"),
- group.get("user_count")
- );
- group.set("offset", offset);
-
- return group.findMembers();
- },
-
- previous() {
- if (this.showingFirst) {
- return;
- }
-
- const group = this.model;
- const offset = Math.max(group.get("offset") - group.get("limit"), 0);
- group.set("offset", offset);
-
- return group.findMembers();
- },
-
- addMembers() {
- if (Ember.isEmpty(this.get("model.usernames"))) {
- return;
- }
- this.model.addMembers(this.get("model.usernames")).catch(popupAjaxError);
- this.set("model.usernames", null);
- },
-
- removeMember(member) {
- const message = I18n.t("groups.manage.delete_member_confirm", {
- username: member.get("username"),
- group: this.get("model.name")
- });
-
- return bootbox.confirm(
- message,
- I18n.t("no_value"),
- I18n.t("yes_value"),
- confirm => {
- if (confirm) {
- this.model.removeMember(member);
- }
- }
- );
- }
- }
-});
diff --git a/app/assets/javascripts/discourse/components/group-membership-button.js.es6 b/app/assets/javascripts/discourse/components/group-membership-button.js.es6
index 31ac171734..7c808bc3eb 100644
--- a/app/assets/javascripts/discourse/components/group-membership-button.js.es6
+++ b/app/assets/javascripts/discourse/components/group-membership-button.js.es6
@@ -1,26 +1,27 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
-export default Ember.Component.extend({
+export default Component.extend({
classNames: ["group-membership-button"],
- @computed("model.public_admission", "userIsGroupUser")
+ @discourseComputed("model.public_admission", "userIsGroupUser")
canJoinGroup(publicAdmission, userIsGroupUser) {
return publicAdmission && !userIsGroupUser;
},
- @computed("model.public_exit", "userIsGroupUser")
+ @discourseComputed("model.public_exit", "userIsGroupUser")
canLeaveGroup(publicExit, userIsGroupUser) {
return publicExit && userIsGroupUser;
},
- @computed("model.allow_membership_requests", "userIsGroupUser")
+ @discourseComputed("model.allow_membership_requests", "userIsGroupUser")
canRequestMembership(allowMembershipRequests, userIsGroupUser) {
return allowMembershipRequests && !userIsGroupUser;
},
- @computed("model.is_group_user")
+ @discourseComputed("model.is_group_user")
userIsGroupUser(isGroupUser) {
return !!isGroupUser;
},
@@ -30,6 +31,14 @@ export default Ember.Component.extend({
$.cookie("destination_url", window.location.href);
},
+ removeFromGroup() {
+ this.model
+ .removeMember(this.currentUser)
+ .then(() => this.model.set("is_group_user", false))
+ .catch(popupAjaxError)
+ .finally(() => this.set("updatingMembership", false));
+ },
+
actions: {
joinGroup() {
if (this.currentUser) {
@@ -52,17 +61,21 @@ export default Ember.Component.extend({
leaveGroup() {
this.set("updatingMembership", true);
- const model = this.model;
- model
- .removeMember(this.currentUser)
- .then(() => {
- model.set("is_group_user", false);
- })
- .catch(popupAjaxError)
- .finally(() => {
- this.set("updatingMembership", false);
- });
+ if (this.model.public_admission) {
+ this.removeFromGroup();
+ } else {
+ return bootbox.confirm(
+ I18n.t("groups.confirm_leave"),
+ I18n.t("no_value"),
+ I18n.t("yes_value"),
+ result => {
+ result
+ ? this.removeFromGroup()
+ : this.set("updatingMembership", false);
+ }
+ );
+ }
},
showRequestMembershipForm() {
diff --git a/app/assets/javascripts/discourse/components/group-navigation.js.es6 b/app/assets/javascripts/discourse/components/group-navigation.js.es6
index 91ad923ffc..89720fbbe8 100644
--- a/app/assets/javascripts/discourse/components/group-navigation.js.es6
+++ b/app/assets/javascripts/discourse/components/group-navigation.js.es6
@@ -1,3 +1,4 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
tagName: ""
});
diff --git a/app/assets/javascripts/discourse/components/group-post.js.es6 b/app/assets/javascripts/discourse/components/group-post.js.es6
index f94f36dc8d..34c83529ef 100644
--- a/app/assets/javascripts/discourse/components/group-post.js.es6
+++ b/app/assets/javascripts/discourse/components/group-post.js.es6
@@ -1,6 +1,7 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
- @computed("post.url")
+export default Component.extend({
+ @discourseComputed("post.url")
postUrl: Discourse.getURL
});
diff --git a/app/assets/javascripts/discourse/components/group-selector.js.es6 b/app/assets/javascripts/discourse/components/group-selector.js.es6
index 9447f63875..74c1c4b16f 100644
--- a/app/assets/javascripts/discourse/components/group-selector.js.es6
+++ b/app/assets/javascripts/discourse/components/group-selector.js.es6
@@ -1,12 +1,13 @@
-import {
+import { isEmpty } from "@ember/utils";
+import Component from "@ember/component";
+import discourseComputed, {
on,
- observes,
- default as computed
-} from "ember-addons/ember-computed-decorators";
+ observes
+} from "discourse-common/utils/decorators";
import { findRawTemplate } from "discourse/lib/raw-templates";
-export default Ember.Component.extend({
- @computed("placeholderKey")
+export default Component.extend({
+ @discourseComputed("placeholderKey")
placeholder(placeholderKey) {
return placeholderKey ? I18n.t(placeholderKey) : "";
},
@@ -22,11 +23,11 @@ export default Ember.Component.extend({
let selectedGroups;
let groupNames = this.groupNames;
- this.$("input").autocomplete({
+ $(this.element.querySelector("input")).autocomplete({
allowAny: false,
items: _.isArray(groupNames)
? groupNames
- : Ember.isEmpty(groupNames)
+ : isEmpty(groupNames)
? []
: [groupNames],
single: this.single,
diff --git a/app/assets/javascripts/discourse/components/groups-form-interaction-fields.js.es6 b/app/assets/javascripts/discourse/components/groups-form-interaction-fields.js.es6
index 6ccc23df73..62a7d17ece 100644
--- a/app/assets/javascripts/discourse/components/groups-form-interaction-fields.js.es6
+++ b/app/assets/javascripts/discourse/components/groups-form-interaction-fields.js.es6
@@ -1,6 +1,7 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
-export default Ember.Component.extend({
+export default Component.extend({
init() {
this._super(...arguments);
@@ -13,19 +14,25 @@ export default Ember.Component.extend({
},
{
name: I18n.t(
- "admin.groups.manage.interaction.visibility_levels.members"
+ "admin.groups.manage.interaction.visibility_levels.logged_on_users"
),
value: 1
},
{
- name: I18n.t("admin.groups.manage.interaction.visibility_levels.staff"),
+ name: I18n.t(
+ "admin.groups.manage.interaction.visibility_levels.members"
+ ),
value: 2
},
+ {
+ name: I18n.t("admin.groups.manage.interaction.visibility_levels.staff"),
+ value: 3
+ },
{
name: I18n.t(
"admin.groups.manage.interaction.visibility_levels.owners"
),
- value: 3
+ value: 4
}
];
@@ -34,11 +41,16 @@ export default Ember.Component.extend({
{ name: I18n.t("groups.alias_levels.only_admins"), value: 1 },
{ name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 },
{ name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 },
+ { name: I18n.t("groups.alias_levels.owners_mods_and_admins"), value: 4 },
{ name: I18n.t("groups.alias_levels.everyone"), value: 99 }
];
},
- @computed("siteSettings.email_in", "model.automatic", "currentUser.admin")
+ @discourseComputed(
+ "siteSettings.email_in",
+ "model.automatic",
+ "currentUser.admin"
+ )
showEmailSettings(emailIn, automatic, isAdmin) {
return emailIn && isAdmin && !automatic;
}
diff --git a/app/assets/javascripts/discourse/components/groups-form-membership-fields.js.es6 b/app/assets/javascripts/discourse/components/groups-form-membership-fields.js.es6
index 306f9a0f30..78903d6c5a 100644
--- a/app/assets/javascripts/discourse/components/groups-form-membership-fields.js.es6
+++ b/app/assets/javascripts/discourse/components/groups-form-membership-fields.js.es6
@@ -1,6 +1,10 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
+import { computed } from "@ember/object";
+
+export default Component.extend({
+ tokenSeparator: "|",
-export default Ember.Component.extend({
init() {
this._super(...arguments);
@@ -16,15 +20,42 @@ export default Ember.Component.extend({
];
},
- @computed("model.visibility_level", "model.public_admission")
+ groupTrustLevel: computed(
+ "model.grant_trust_level",
+ "trustLevelOptions",
+ function() {
+ return (
+ this.model.get("grant_trust_level") ||
+ this.trustLevelOptions.firstObject.value
+ );
+ }
+ ),
+
+ @discourseComputed("model.visibility_level", "model.public_admission")
disableMembershipRequestSetting(visibility_level, publicAdmission) {
- visibility_level = parseInt(visibility_level);
- return visibility_level !== 0 || publicAdmission;
+ visibility_level = parseInt(visibility_level, 10);
+ return publicAdmission || visibility_level > 1;
},
- @computed("model.visibility_level", "model.allow_membership_requests")
+ @discourseComputed(
+ "model.visibility_level",
+ "model.allow_membership_requests"
+ )
disablePublicSetting(visibility_level, allowMembershipRequests) {
- visibility_level = parseInt(visibility_level);
- return visibility_level !== 0 || allowMembershipRequests;
+ visibility_level = parseInt(visibility_level, 10);
+ return allowMembershipRequests || visibility_level > 1;
+ },
+
+ emailDomains: computed("model.emailDomains", function() {
+ return this.model.emailDomains.split(this.tokenSeparator).filter(Boolean);
+ }),
+
+ actions: {
+ onChangeEmailDomainsSetting(value) {
+ this.set(
+ "model.automatic_membership_email_domains",
+ value.join(this.tokenSeparator)
+ );
+ }
}
});
diff --git a/app/assets/javascripts/discourse/components/groups-form-profile-fields.js.es6 b/app/assets/javascripts/discourse/components/groups-form-profile-fields.js.es6
index 15c2efbd96..988e4ac843 100644
--- a/app/assets/javascripts/discourse/components/groups-form-profile-fields.js.es6
+++ b/app/assets/javascripts/discourse/components/groups-form-profile-fields.js.es6
@@ -1,12 +1,13 @@
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import { isEmpty } from "@ember/utils";
+import { not } from "@ember/object/computed";
+import Component from "@ember/component";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Group from "discourse/models/group";
-import InputValidation from "discourse/models/input-validation";
-import debounce from "discourse/lib/debounce";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import discourseDebounce from "discourse/lib/debounce";
+import EmberObject from "@ember/object";
-export default Ember.Component.extend({
+export default Component.extend({
disableSave: null,
nameInput: null,
@@ -21,9 +22,9 @@ export default Ember.Component.extend({
}
},
- canEdit: Ember.computed.not("model.automatic"),
+ canEdit: not("model.automatic"),
- @computed("basicNameValidation", "uniqueNameValidation")
+ @discourseComputed("basicNameValidation", "uniqueNameValidation")
nameValidation(basicNameValidation, uniqueNameValidation) {
return uniqueNameValidation ? uniqueNameValidation : basicNameValidation;
},
@@ -61,36 +62,38 @@ export default Ember.Component.extend({
);
},
- checkGroupName: debounce(function() {
+ checkGroupName: discourseDebounce(function() {
name = this.nameInput;
- if (Ember.isEmpty(name)) return;
+ if (isEmpty(name)) return;
- Group.checkName(name).then(response => {
- const validationName = "uniqueNameValidation";
+ Group.checkName(name)
+ .then(response => {
+ const validationName = "uniqueNameValidation";
- if (response.available) {
- this.set(
- validationName,
- InputValidation.create({
- ok: true,
- reason: I18n.t("admin.groups.new.name.available")
- })
- );
+ if (response.available) {
+ this.set(
+ validationName,
+ EmberObject.create({
+ ok: true,
+ reason: I18n.t("admin.groups.new.name.available")
+ })
+ );
- this.set("disableSave", false);
- this.set("model.name", this.nameInput);
- } else {
- let reason;
-
- if (response.errors) {
- reason = response.errors.join(" ");
+ this.set("disableSave", false);
+ this.set("model.name", this.nameInput);
} else {
- reason = I18n.t("admin.groups.new.name.not_available");
- }
+ let reason;
- this.set(validationName, this._failedInputValidation(reason));
- }
- });
+ if (response.errors) {
+ reason = response.errors.join(" ");
+ } else {
+ reason = I18n.t("admin.groups.new.name.not_available");
+ }
+
+ this.set(validationName, this._failedInputValidation(reason));
+ }
+ })
+ .catch(popupAjaxError);
}, 500),
_failedInputValidation(reason) {
@@ -98,6 +101,6 @@ export default Ember.Component.extend({
const options = { failed: true };
if (reason) options.reason = reason;
- this.set("basicNameValidation", InputValidation.create(options));
+ this.set("basicNameValidation", EmberObject.create(options));
}
});
diff --git a/app/assets/javascripts/discourse/components/groups-info.js.es6 b/app/assets/javascripts/discourse/components/groups-info.js.es6
index a95c9da337..18f86c829d 100644
--- a/app/assets/javascripts/discourse/components/groups-info.js.es6
+++ b/app/assets/javascripts/discourse/components/groups-info.js.es6
@@ -1,11 +1,12 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "span",
classNames: ["group-info-details"],
- @computed("group.full_name", "group.title")
- showFullName(fullName, title) {
- return fullName && fullName.length && fullName !== title;
+ @discourseComputed("group.full_name")
+ showFullName(fullName) {
+ return fullName && fullName.length;
}
});
diff --git a/app/assets/javascripts/discourse/components/hide-modal-trigger.js.es6 b/app/assets/javascripts/discourse/components/hide-modal-trigger.js.es6
index e3951d5277..df632e2557 100644
--- a/app/assets/javascripts/discourse/components/hide-modal-trigger.js.es6
+++ b/app/assets/javascripts/discourse/components/hide-modal-trigger.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
didInsertElement() {
this._super(...arguments);
$(".d-modal.fixed-modal")
diff --git a/app/assets/javascripts/discourse/components/highlight-text.js.es6 b/app/assets/javascripts/discourse/components/highlight-text.js.es6
index 73c2b045bc..a98ffdb653 100644
--- a/app/assets/javascripts/discourse/components/highlight-text.js.es6
+++ b/app/assets/javascripts/discourse/components/highlight-text.js.es6
@@ -1,11 +1,12 @@
+import Component from "@ember/component";
import highlightText from "discourse/lib/highlight-text";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "span",
_highlightOnInsert: function() {
const term = this.highlight;
- highlightText(this.$(), term);
+ highlightText($(this.element), term);
}
.observes("highlight")
.on("didInsertElement")
diff --git a/app/assets/javascripts/discourse/components/honeypot-input.js.es6 b/app/assets/javascripts/discourse/components/honeypot-input.js.es6
new file mode 100644
index 0000000000..8231303da5
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/honeypot-input.js.es6
@@ -0,0 +1,16 @@
+import { on } from "discourse-common/utils/decorators";
+
+export default Ember.TextField.extend({
+ @on("init")
+ _init() {
+ // Chrome autocomplete is buggy per:
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=987293
+ // work around issue while leaving a semi useable honeypot for
+ // bots that are running full Chrome
+ if (navigator.userAgent.indexOf("Chrome") > -1) {
+ this.set("type", "text");
+ } else {
+ this.set("type", "password");
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/ignored-user-list-item.js.es6 b/app/assets/javascripts/discourse/components/ignored-user-list-item.js.es6
index 5bf27c13a1..6f26b5700b 100644
--- a/app/assets/javascripts/discourse/components/ignored-user-list-item.js.es6
+++ b/app/assets/javascripts/discourse/components/ignored-user-list-item.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
tagName: "div",
items: null,
actions: {
diff --git a/app/assets/javascripts/discourse/components/ignored-user-list.js.es6 b/app/assets/javascripts/discourse/components/ignored-user-list.js.es6
index 9c951dfeed..f73ac290e4 100644
--- a/app/assets/javascripts/discourse/components/ignored-user-list.js.es6
+++ b/app/assets/javascripts/discourse/components/ignored-user-list.js.es6
@@ -1,8 +1,9 @@
+import Component from "@ember/component";
import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
import User from "discourse/models/user";
-export default Ember.Component.extend({
+export default Component.extend({
item: null,
actions: {
removeIgnoredUser(item) {
@@ -20,6 +21,7 @@ export default Ember.Component.extend({
model: this.model
});
modal.setProperties({
+ ignoredUsername: null,
onUserIgnored: username => {
this.items.addObject(username);
}
diff --git a/app/assets/javascripts/discourse/components/image-uploader.js.es6 b/app/assets/javascripts/discourse/components/image-uploader.js.es6
index 6bdb84553a..6db47602ad 100644
--- a/app/assets/javascripts/discourse/components/image-uploader.js.es6
+++ b/app/assets/javascripts/discourse/components/image-uploader.js.es6
@@ -1,10 +1,13 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import { next } from "@ember/runloop";
+import Component from "@ember/component";
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, {
+export default Component.extend(UploadMixin, {
classNames: ["image-uploader"],
loadingLightbox: false,
@@ -21,36 +24,36 @@ export default Ember.Component.extend(UploadMixin, {
}
},
- @computed("imageUrl", "placeholderUrl")
+ @discourseComputed("imageUrl", "placeholderUrl")
showingPlaceholder(imageUrl, placeholderUrl) {
return !imageUrl && placeholderUrl;
},
- @computed("placeholderUrl")
+ @discourseComputed("placeholderUrl")
placeholderStyle(url) {
- if (Ember.isEmpty(url)) {
+ if (isEmpty(url)) {
return "".htmlSafe();
}
return `background-image: url(${url})`.htmlSafe();
},
- @computed("imageUrl")
+ @discourseComputed("imageUrl")
imageCDNURL(url) {
- if (Ember.isEmpty(url)) {
+ if (isEmpty(url)) {
return "".htmlSafe();
}
return Discourse.getURLWithCDN(url);
},
- @computed("imageCDNURL")
+ @discourseComputed("imageCDNURL")
backgroundStyle(url) {
return `background-image: url(${url})`.htmlSafe();
},
- @computed("imageUrl")
+ @discourseComputed("imageUrl")
imageBaseName(imageUrl) {
- if (Ember.isEmpty(imageUrl)) return;
+ if (isEmpty(imageUrl)) return;
return imageUrl.split("/").slice(-1)[0];
},
@@ -76,11 +79,13 @@ export default Ember.Component.extend(UploadMixin, {
},
_openLightbox() {
- Ember.run.next(() => this.$("a.lightbox").magnificPopup("open"));
+ next(() =>
+ $(this.element.querySelector("a.lightbox")).magnificPopup("open")
+ );
},
_applyLightbox() {
- if (this.imageUrl) Ember.run.next(() => lightbox(this.$()));
+ if (this.imageUrl) next(() => lightbox($(this.element)));
},
actions: {
diff --git a/app/assets/javascripts/discourse/components/images-uploader.js.es6 b/app/assets/javascripts/discourse/components/images-uploader.js.es6
index 5fd63d0d01..4e68324b0c 100644
--- a/app/assets/javascripts/discourse/components/images-uploader.js.es6
+++ b/app/assets/javascripts/discourse/components/images-uploader.js.es6
@@ -1,15 +1,14 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
import UploadMixin from "discourse/mixins/upload";
-export default Ember.Component.extend(UploadMixin, {
+export default Component.extend(UploadMixin, {
type: "avatar",
tagName: "span",
- @computed("uploading")
+ @discourseComputed("uploading")
uploadButtonText(uploading) {
- return uploading
- ? I18n.t("uploading")
- : I18n.t("user.change_avatar.upload_picture");
+ return uploading ? I18n.t("uploading") : I18n.t("upload");
},
validateUploadedFilesOptions() {
diff --git a/app/assets/javascripts/discourse/components/input-tip.js.es6 b/app/assets/javascripts/discourse/components/input-tip.js.es6
index aceafa7458..b48a5df8cf 100644
--- a/app/assets/javascripts/discourse/components/input-tip.js.es6
+++ b/app/assets/javascripts/discourse/components/input-tip.js.es6
@@ -1,19 +1,29 @@
-import { bufferedRender } from "discourse-common/lib/buffered-render";
+import { alias, not } from "@ember/object/computed";
+import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
-export default Ember.Component.extend(
- bufferedRender({
- classNameBindings: [":tip", "good", "bad"],
- rerenderTriggers: ["validation"],
+export default Component.extend({
+ classNameBindings: [":tip", "good", "bad"],
+ tipIcon: null,
+ tipReason: null,
- bad: Ember.computed.alias("validation.failed"),
- good: Ember.computed.not("bad"),
+ bad: alias("validation.failed"),
+ good: not("bad"),
- buildBuffer(buffer) {
- const reason = this.get("validation.reason");
- if (reason) {
- buffer.push(iconHTML(this.good ? "check" : "times") + " " + reason);
- }
+ tipIconHTML() {
+ let icon = iconHTML(this.good ? "check" : "times");
+ return `${icon}`.htmlSafe();
+ },
+
+ didReceiveAttrs() {
+ this._super(...arguments);
+ let reason = this.get("validation.reason");
+ if (reason) {
+ this.set("tipIcon", this.tipIconHTML());
+ this.set("tipReason", reason);
+ } else {
+ this.set("tipIcon", null);
+ this.set("tipReason", null);
}
- })
-);
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/invite-panel.js.es6 b/app/assets/javascripts/discourse/components/invite-panel.js.es6
index 432a0b7325..8622e97ac5 100644
--- a/app/assets/javascripts/discourse/components/invite-panel.js.es6
+++ b/app/assets/javascripts/discourse/components/invite-panel.js.es6
@@ -1,14 +1,20 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import { computed } from "@ember/object";
+import { alias, and, equal } from "@ember/object/computed";
+import EmberObject from "@ember/object";
+import Component from "@ember/component";
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";
+import { getNativeContact } from "discourse/lib/pwa-utils";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: null,
- inviteModel: Ember.computed.alias("panel.model.inviteModel"),
- userInvitedShow: Ember.computed.alias("panel.model.userInvitedShow"),
+ inviteModel: alias("panel.model.inviteModel"),
+ userInvitedShow: alias("panel.model.userInvitedShow"),
// If this isn't defined, it will proxy to the user topic on the preferences
// page which is wrong.
@@ -18,7 +24,7 @@ export default Ember.Component.extend({
inviteIcon: "envelope",
invitingExistingUserToTopic: false,
- isAdmin: Ember.computed.alias("currentUser.admin"),
+ isAdmin: alias("currentUser.admin"),
willDestroyElement() {
this._super(...arguments);
@@ -26,7 +32,7 @@ export default Ember.Component.extend({
this.reset();
},
- @computed(
+ @discourseComputed(
"isAdmin",
"emailOrUsername",
"invitingToTopic",
@@ -45,7 +51,7 @@ export default Ember.Component.extend({
can_invite_to
) {
if (saving) return true;
- if (Ember.isEmpty(emailOrUsername)) return true;
+ if (isEmpty(emailOrUsername)) return true;
const emailTrimmed = emailOrUsername.trim();
@@ -60,11 +66,7 @@ export default Ember.Component.extend({
}
// when inviting to private topic via email, group name must be specified
- if (
- isPrivateTopic &&
- Ember.isEmpty(groupNames) &&
- emailValid(emailTrimmed)
- ) {
+ if (isPrivateTopic && isEmpty(groupNames) && emailValid(emailTrimmed)) {
return true;
}
@@ -73,7 +75,7 @@ export default Ember.Component.extend({
return false;
},
- @computed(
+ @discourseComputed(
"isAdmin",
"emailOrUsername",
"inviteModel.saving",
@@ -91,7 +93,7 @@ export default Ember.Component.extend({
) {
if (hasCustomMessage) return true;
if (saving) return true;
- if (Ember.isEmpty(emailOrUsername)) return true;
+ if (isEmpty(emailOrUsername)) return true;
const email = emailOrUsername.trim();
@@ -106,49 +108,49 @@ export default Ember.Component.extend({
}
// when inviting to private topic via email, group name must be specified
- if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(email)) {
+ if (isPrivateTopic && isEmpty(groupNames) && emailValid(email)) {
return true;
}
return false;
},
- @computed("inviteModel.saving")
+ @discourseComputed("inviteModel.saving")
buttonTitle(saving) {
return saving ? "topic.inviting" : "topic.invite_reply.action";
},
// 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("inviteModel")
+ @discourseComputed("inviteModel")
invitingToTopic(inviteModel) {
return inviteModel !== this.currentUser;
},
- @computed("inviteModel", "inviteModel.details.can_invite_via_email")
+ @discourseComputed("inviteModel", "inviteModel.details.can_invite_via_email")
canInviteViaEmail(inviteModel, canInviteViaEmail) {
return this.inviteModel === this.currentUser ? true : canInviteViaEmail;
},
- @computed("isPM", "canInviteViaEmail")
+ @discourseComputed("isPM", "canInviteViaEmail")
showCopyInviteButton(isPM, canInviteViaEmail) {
return canInviteViaEmail && !isPM;
},
- topicId: Ember.computed.alias("inviteModel.id"),
+ topicId: alias("inviteModel.id"),
// eg: visible only to specific group members
- isPrivateTopic: Ember.computed.and(
+ isPrivateTopic: and(
"invitingToTopic",
"inviteModel.category.read_restricted"
),
- isPM: Ember.computed.equal("inviteModel.archetype", "private_message"),
+ isPM: equal("inviteModel.archetype", "private_message"),
// scope to allowed usernames
- allowExistingMembers: Ember.computed.alias("invitingToTopic"),
+ allowExistingMembers: alias("invitingToTopic"),
- @computed("isAdmin", "inviteModel.group_users")
+ @discourseComputed("isAdmin", "inviteModel.group_users")
isGroupOwnerOrAdmin(isAdmin, groupUsers) {
return (
isAdmin || (groupUsers && groupUsers.some(groupUser => groupUser.owner))
@@ -156,7 +158,7 @@ export default Ember.Component.extend({
},
// Show Groups? (add invited user to private group)
- @computed(
+ @discourseComputed(
"isGroupOwnerOrAdmin",
"emailOrUsername",
"isPrivateTopic",
@@ -180,13 +182,17 @@ export default Ember.Component.extend({
);
},
- @computed("emailOrUsername")
+ showContactPicker: computed(function() {
+ return this.capabilities.hasContactPicker;
+ }),
+
+ @discourseComputed("emailOrUsername")
showCustomMessage(emailOrUsername) {
return this.inviteModel === this.currentUser || emailValid(emailOrUsername);
},
// Instructional text for the modal.
- @computed(
+ @discourseComputed(
"isPM",
"invitingToTopic",
"emailOrUsername",
@@ -215,7 +221,7 @@ export default Ember.Component.extend({
return I18n.t("topic.invite_reply.to_username");
} else {
// when inviting to a topic, display instructions based on provided entity
- if (Ember.isEmpty(emailOrUsername)) {
+ if (isEmpty(emailOrUsername)) {
return I18n.t("topic.invite_reply.to_topic_blank");
} else if (emailValid(emailOrUsername)) {
this.set("inviteIcon", "envelope");
@@ -231,7 +237,7 @@ export default Ember.Component.extend({
}
},
- @computed("isPrivateTopic")
+ @discourseComputed("isPrivateTopic")
showGroupsClass(isPrivateTopic) {
return isPrivateTopic ? "required" : "optional";
},
@@ -240,7 +246,7 @@ export default Ember.Component.extend({
return Group.findAll({ term, ignore_automatic: true });
},
- @computed("isPM", "emailOrUsername", "invitingExistingUserToTopic")
+ @discourseComputed("isPM", "emailOrUsername", "invitingExistingUserToTopic")
successMessage(isPM, emailOrUsername, invitingExistingUserToTopic) {
if (this.hasGroups) {
return I18n.t("topic.invite_private.success_group");
@@ -257,14 +263,14 @@ export default Ember.Component.extend({
}
},
- @computed("isPM")
+ @discourseComputed("isPM")
errorMessage(isPM) {
return isPM
? I18n.t("topic.invite_private.error")
: I18n.t("topic.invite_reply.error");
},
- @computed("canInviteViaEmail")
+ @discourseComputed("canInviteViaEmail")
placeholderKey(canInviteViaEmail) {
return canInviteViaEmail
? "topic.invite_private.email_or_username_placeholder"
@@ -323,7 +329,7 @@ export default Ember.Component.extend({
.then(data => {
model.setProperties({ saving: false, finished: true });
this.get("inviteModel.details.allowed_groups").pushObject(
- Ember.Object.create(data.group)
+ EmberObject.create(data.group)
);
this.appEvents.trigger("post-stream:refresh");
})
@@ -349,7 +355,7 @@ export default Ember.Component.extend({
});
} else if (this.isPM && result && result.user) {
this.get("inviteModel.details.allowed_users").pushObject(
- Ember.Object.create(result.user)
+ EmberObject.create(result.user)
);
this.appEvents.trigger("post-stream:refresh");
} else if (
@@ -433,6 +439,12 @@ export default Ember.Component.extend({
} else {
this.set("customMessage", null);
}
+ },
+
+ searchContact() {
+ getNativeContact(["email"], false).then(result => {
+ this.set("emailOrUsername", result[0].email[0]);
+ });
}
}
});
diff --git a/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6
index 487ae8aa46..f0e3ee875c 100644
--- a/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6
+++ b/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6
@@ -1,9 +1,10 @@
+import Component from "@ember/component";
import {
showEntrance,
navigateToTopic
} from "discourse/components/topic-list-item";
-export default Ember.Component.extend({
+export default Component.extend({
attributeBindings: ["topic.id:data-topic-id"],
classNameBindings: [
":latest-topic-list-item",
diff --git a/app/assets/javascripts/discourse/components/link-to-input.js.es6 b/app/assets/javascripts/discourse/components/link-to-input.js.es6
index 34eaedc7d4..19015ec0e8 100644
--- a/app/assets/javascripts/discourse/components/link-to-input.js.es6
+++ b/app/assets/javascripts/discourse/components/link-to-input.js.es6
@@ -1,11 +1,13 @@
-export default Ember.Component.extend({
+import { schedule } from "@ember/runloop";
+import Component from "@ember/component";
+export default Component.extend({
showInput: false,
click() {
this.onClick();
- Ember.run.schedule("afterRender", () => {
- this.$()
+ schedule("afterRender", () => {
+ $(this.element)
.find("input")
.focus();
});
diff --git a/app/assets/javascripts/discourse/components/links-redirect.js.es6 b/app/assets/javascripts/discourse/components/links-redirect.js.es6
index 84d5f890f0..0ddd92b21b 100644
--- a/app/assets/javascripts/discourse/components/links-redirect.js.es6
+++ b/app/assets/javascripts/discourse/components/links-redirect.js.es6
@@ -1,6 +1,7 @@
+import Component from "@ember/component";
import ClickTrack from "discourse/lib/click-track";
-export default Ember.Component.extend({
+export default Component.extend({
didInsertElement() {
this._super(...arguments);
diff --git a/app/assets/javascripts/discourse/components/load-more.js.es6 b/app/assets/javascripts/discourse/components/load-more.js.es6
index f81b33b764..8668fa5742 100644
--- a/app/assets/javascripts/discourse/components/load-more.js.es6
+++ b/app/assets/javascripts/discourse/components/load-more.js.es6
@@ -1,6 +1,7 @@
+import Component from "@ember/component";
import LoadMore from "discourse/mixins/load-more";
-export default Ember.Component.extend(LoadMore, {
+export default Component.extend(LoadMore, {
init() {
this._super(...arguments);
diff --git a/app/assets/javascripts/discourse/components/login-buttons.js.es6 b/app/assets/javascripts/discourse/components/login-buttons.js.es6
index 3279bda5cc..1073dd3ec0 100644
--- a/app/assets/javascripts/discourse/components/login-buttons.js.es6
+++ b/app/assets/javascripts/discourse/components/login-buttons.js.es6
@@ -1,16 +1,17 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
import { findAll } from "discourse/models/login-method";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Component.extend({
+export default Component.extend({
elementId: "login-buttons",
classNameBindings: ["hidden"],
- @computed("buttons.length", "showLoginWithEmailLink")
+ @discourseComputed("buttons.length", "showLoginWithEmailLink")
hidden(buttonsCount, showLoginWithEmailLink) {
return buttonsCount === 0 && !showLoginWithEmailLink;
},
- @computed
+ @discourseComputed
buttons() {
return findAll();
},
diff --git a/app/assets/javascripts/discourse/components/login-modal.js.es6 b/app/assets/javascripts/discourse/components/login-modal.js.es6
index 59939542cb..2ee72f8722 100644
--- a/app/assets/javascripts/discourse/components/login-modal.js.es6
+++ b/app/assets/javascripts/discourse/components/login-modal.js.es6
@@ -1,4 +1,6 @@
-export default Ember.Component.extend({
+import { schedule } from "@ember/runloop";
+import Component from "@ember/component";
+export default Component.extend({
didInsertElement() {
this._super(...arguments);
@@ -13,7 +15,7 @@ export default Ember.Component.extend({
this.set("loginName", $.cookie("email"));
}
- Ember.run.schedule("afterRender", () => {
+ schedule("afterRender", () => {
$(
"#login-account-password, #login-account-name, #login-second-factor"
).keydown(e => {
diff --git a/app/assets/javascripts/discourse/components/mobile-category-topic.js.es6 b/app/assets/javascripts/discourse/components/mobile-category-topic.js.es6
index 4c47f0e126..fcd85640e6 100644
--- a/app/assets/javascripts/discourse/components/mobile-category-topic.js.es6
+++ b/app/assets/javascripts/discourse/components/mobile-category-topic.js.es6
@@ -1,6 +1,7 @@
+import Component from "@ember/component";
import { showEntrance } from "discourse/components/topic-list-item";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "tr",
classNameBindings: [
":category-topic-link",
diff --git a/app/assets/javascripts/discourse/components/mobile-nav.js.es6 b/app/assets/javascripts/discourse/components/mobile-nav.js.es6
index f63183b217..02d35f53d3 100644
--- a/app/assets/javascripts/discourse/components/mobile-nav.js.es6
+++ b/app/assets/javascripts/discourse/components/mobile-nav.js.es6
@@ -1,10 +1,12 @@
-import { on, observes } from "ember-addons/ember-computed-decorators";
+import { next } from "@ember/runloop";
+import Component from "@ember/component";
+import { on, observes } from "discourse-common/utils/decorators";
-export default Ember.Component.extend({
+export default Component.extend({
@on("init")
_init() {
if (!this.get("site.mobileView")) {
- var classes = this.desktopClass;
+ let classes = this.desktopClass;
if (classes) {
classes = classes.split(" ");
this.set("classNames", classes);
@@ -20,17 +22,19 @@ export default Ember.Component.extend({
@observes("currentPath")
currentPathChanged() {
this.set("expanded", false);
- Ember.run.next(() => this._updateSelectedHtml());
+ next(() => this._updateSelectedHtml());
},
_updateSelectedHtml() {
- const active = this.$(".active");
- if (active && active.html) {
- this.set("selectedHtml", active.html());
+ const active = this.element.querySelector(".active");
+ if (active && active.innerHTML) {
+ this.set("selectedHtml", active.innerHTML);
}
},
didInsertElement() {
+ this._super(...arguments);
+
this._updateSelectedHtml();
},
@@ -38,14 +42,17 @@ export default Ember.Component.extend({
toggleExpanded() {
this.toggleProperty("expanded");
- Ember.run.next(() => {
+ next(() => {
if (this.expanded) {
$(window)
.off("click.mobile-nav")
.on("click.mobile-nav", e => {
- let expander = this.$(".expander");
- expander = expander && expander[0];
- if ($(e.target)[0] !== expander) {
+ if (!this.element || this.isDestroying || this.isDestroyed) {
+ return;
+ }
+
+ const expander = this.element.querySelector(".expander");
+ if (expander && e.target !== expander) {
this.set("expanded", false);
$(window).off("click.mobile-nav");
}
diff --git a/app/assets/javascripts/discourse/components/modal-panel.js.es6 b/app/assets/javascripts/discourse/components/modal-panel.js.es6
index b441457a7d..254dd32b73 100644
--- a/app/assets/javascripts/discourse/components/modal-panel.js.es6
+++ b/app/assets/javascripts/discourse/components/modal-panel.js.es6
@@ -1,6 +1,7 @@
+import Component from "@ember/component";
import { fmt } from "discourse/lib/computed";
-export default Ember.Component.extend({
+export default Component.extend({
panel: null,
panelComponent: fmt("panel.id", "%@-panel"),
diff --git a/app/assets/javascripts/discourse/components/modal-tab.js.es6 b/app/assets/javascripts/discourse/components/modal-tab.js.es6
index c7a392507c..94ff00ef9d 100644
--- a/app/assets/javascripts/discourse/components/modal-tab.js.es6
+++ b/app/assets/javascripts/discourse/components/modal-tab.js.es6
@@ -1,14 +1,16 @@
+import { equal, alias } from "@ember/object/computed";
+import Component from "@ember/component";
import { propertyEqual } from "discourse/lib/computed";
-export default Ember.Component.extend({
+export default 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"),
+ singleTab: equal("panelsLength", 1),
+ title: alias("panel.title"),
isActive: propertyEqual("panel.id", "selectedPanel.id"),
click() {
diff --git a/app/assets/javascripts/discourse/components/mount-widget.js.es6 b/app/assets/javascripts/discourse/components/mount-widget.js.es6
index d8431f2820..e62b7acab3 100644
--- a/app/assets/javascripts/discourse/components/mount-widget.js.es6
+++ b/app/assets/javascripts/discourse/components/mount-widget.js.es6
@@ -1,8 +1,12 @@
+import { cancel } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import Component from "@ember/component";
import { diff, patch } from "virtual-dom";
import { WidgetClickHook } from "discourse/widgets/hooks";
import { queryRegistry } from "discourse/widgets/widget";
import { getRegister } from "discourse-common/lib/get-owner";
import DirtyKeys from "discourse/lib/dirty-keys";
+import { camelize } from "@ember/string";
let _cleanCallbacks = {};
export function addWidgetCleanCallback(widgetName, fn) {
@@ -14,7 +18,7 @@ export function resetWidgetCleanCallbacks() {
_cleanCallbacks = {};
}
-export default Ember.Component.extend({
+export default Component.extend({
_tree: null,
_rootNode: null,
_timeout: null,
@@ -49,7 +53,7 @@ export default Ember.Component.extend({
this._rootNode = document.createElement("div");
this.element.appendChild(this._rootNode);
- this._timeout = Ember.run.scheduleOnce("render", this, this.rerenderWidget);
+ this._timeout = scheduleOnce("render", this, this.rerenderWidget);
},
willClearRender() {
@@ -67,7 +71,7 @@ export default Ember.Component.extend({
const [eventName, caller] = evt;
this.appEvents.off(eventName, this, caller);
});
- Ember.run.cancel(this._timeout);
+ cancel(this._timeout);
},
afterRender() {},
@@ -77,7 +81,7 @@ export default Ember.Component.extend({
afterPatch() {},
eventDispatched(eventName, key, refreshArg) {
- const onRefresh = Ember.String.camelize(eventName.replace(/:/, "-"));
+ const onRefresh = camelize(eventName.replace(/:/, "-"));
this.dirtyKeys.keyDirty(key, { onRefresh, refreshArg });
this.queueRerender();
},
@@ -96,13 +100,13 @@ export default Ember.Component.extend({
this._renderCallback = callback;
}
- Ember.run.scheduleOnce("render", this, this.rerenderWidget);
+ scheduleOnce("render", this, this.rerenderWidget);
},
buildArgs() {},
rerenderWidget() {
- Ember.run.cancel(this._timeout);
+ cancel(this._timeout);
if (this._rootNode) {
if (!this._widgetClass) {
diff --git a/app/assets/javascripts/discourse/components/nav-item.js.es6 b/app/assets/javascripts/discourse/components/nav-item.js.es6
index 2cff295403..7358e2ded6 100644
--- a/app/assets/javascripts/discourse/components/nav-item.js.es6
+++ b/app/assets/javascripts/discourse/components/nav-item.js.es6
@@ -1,13 +1,15 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { inject as service } from "@ember/service";
+import Component from "@ember/component";
/* You might be looking for navigation-item. */
import { iconHTML } from "discourse-common/lib/icon-library";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "li",
classNameBindings: ["active"],
- router: Ember.inject.service(),
+ router: service(),
- @computed("label", "i18nLabel", "icon")
+ @discourseComputed("label", "i18nLabel", "icon")
contents(label, i18nLabel, icon) {
let text = i18nLabel || I18n.t(label);
if (icon) {
@@ -16,7 +18,7 @@ export default Ember.Component.extend({
return text;
},
- @computed("route", "router.currentRoute")
+ @discourseComputed("route", "router.currentRoute")
active(route, currentRoute) {
if (!route) {
return;
diff --git a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 b/app/assets/javascripts/discourse/components/navigation-bar.js.es6
index de4ba53a1d..3da0001ad9 100644
--- a/app/assets/javascripts/discourse/components/navigation-bar.js.es6
+++ b/app/assets/javascripts/discourse/components/navigation-bar.js.es6
@@ -1,11 +1,11 @@
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import { next } from "@ember/runloop";
+import Component from "@ember/component";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import DiscourseURL from "discourse/lib/url";
import { renderedConnectorsFor } from "discourse/lib/plugin-connectors";
+import FilterModeMixin from "discourse/mixins/filter-mode";
-export default Ember.Component.extend({
+export default Component.extend(FilterModeMixin, {
tagName: "ul",
classNameBindings: [":nav", ":nav-pills"],
elementId: "navigation-bar",
@@ -15,14 +15,12 @@ export default Ember.Component.extend({
this.set("connectors", renderedConnectorsFor("extra-nav-item", null, this));
},
- @computed("filterMode", "navItems")
- selectedNavItem(filterMode, navItems) {
- if (filterMode.indexOf("top/") === 0) {
- filterMode = "top";
- }
- var item = navItems.find(
- i => i.get("filterMode").indexOf(filterMode) === 0
- );
+ @discourseComputed("filterType", "navItems")
+ selectedNavItem(filterType, navItems) {
+ let item = navItems.find(i => i.active === true);
+
+ item = item || navItems.find(i => i.get("filterType") === filterType);
+
if (!item) {
let connectors = this.connectors;
let category = this.category;
@@ -34,7 +32,7 @@ export default Ember.Component.extend({
typeof (c.connectorClass.displayName === "function")
) {
let path = c.connectorClass.path(category);
- if (path.indexOf(filterMode) > 0) {
+ if (path.indexOf(filterType) > 0) {
item = {
displayName: c.connectorClass.displayName()
};
@@ -68,15 +66,15 @@ export default Ember.Component.extend({
if (this.expanded) {
DiscourseURL.appEvents.on("dom:clean", this, this.ensureDropClosed);
- Ember.run.next(() => {
+ next(() => {
if (!this.expanded) {
return;
}
- this.$(".drop a").on("click", () => {
- this.$(".drop").hide();
+ $(this.element.querySelector(".drop a")).on("click", () => {
+ this.element.querySelector(".drop").style.display = "none";
- Ember.run.next(() => {
+ next(() => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
diff --git a/app/assets/javascripts/discourse/components/navigation-item.js.es6 b/app/assets/javascripts/discourse/components/navigation-item.js.es6
index 5ef8f99d15..5a3f172fa7 100644
--- a/app/assets/javascripts/discourse/components/navigation-item.js.es6
+++ b/app/assets/javascripts/discourse/components/navigation-item.js.es6
@@ -1,61 +1,69 @@
-import computed from "ember-addons/ember-computed-decorators";
-import { bufferedRender } from "discourse-common/lib/buffered-render";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
+import FilterModeMixin from "discourse/mixins/filter-mode";
-export default Ember.Component.extend(
- bufferedRender({
- tagName: "li",
- classNameBindings: [
- "active",
- "content.hasIcon:has-icon",
- "content.classNames",
- "hidden"
- ],
- attributeBindings: ["content.title:title"],
- hidden: false,
- rerenderTriggers: ["content.count"],
+export default Component.extend(FilterModeMixin, {
+ tagName: "li",
+ classNameBindings: [
+ "active",
+ "content.hasIcon:has-icon",
+ "content.classNames",
+ "isHidden:hidden"
+ ],
+ attributeBindings: ["content.title:title"],
+ hidden: false,
+ rerenderTriggers: ["content.count"],
+ activeClass: "",
+ hrefLink: null,
- @computed("content.filterMode", "filterMode")
- active(contentFilterMode, filterMode) {
- return (
- contentFilterMode === filterMode ||
- filterMode.indexOf(contentFilterMode) === 0
- );
- },
-
- buildBuffer(buffer) {
- const content = this.content;
-
- let href = content.get("href");
-
- // Include the category id if the option is present
- if (content.get("includeCategoryId")) {
- let categoryId = this.get("category.id");
- if (categoryId) {
- href += `?category_id=${categoryId}`;
- }
- }
-
- if (
- !this.active &&
- this.currentUser &&
- this.currentUser.trust_level > 0 &&
- (content.get("name") === "new" || content.get("name") === "unread") &&
- content.get("count") < 1
- ) {
- this.set("hidden", true);
- } else {
- this.set("hidden", false);
- }
-
- buffer.push(
- `
`
- );
-
- if (content.get("hasIcon")) {
- buffer.push(" ");
- }
- buffer.push(this.get("content.displayName"));
- buffer.push(" ");
+ @discourseComputed("content.filterType", "filterType", "content.active")
+ active(contentFilterType, filterType, active) {
+ if (active !== undefined) {
+ return active;
}
- })
-);
+ return contentFilterType === filterType;
+ },
+
+ @discourseComputed("content.count")
+ isHidden(count) {
+ return (
+ !this.active &&
+ this.currentUser &&
+ this.currentUser.trust_level > 0 &&
+ (this.content.get("name") === "new" ||
+ this.content.get("name") === "unread") &&
+ count < 1
+ );
+ },
+
+ didReceiveAttrs() {
+ this._super(...arguments);
+ const content = this.content;
+
+ let href = content.get("href");
+ let queryParams = [];
+
+ // Include the category id if the option is present
+ if (content.get("includeCategoryId")) {
+ let categoryId = this.get("content.category.id");
+ if (categoryId) {
+ queryParams.push(`category_id=${categoryId}`);
+ }
+ }
+
+ // ensures we keep discovery query params added through plugin api
+ if (content.persistedQueryParams) {
+ Object.keys(content.persistedQueryParams).forEach(key => {
+ const value = content.persistedQueryParams[key];
+ queryParams.push(`${key}=${value}`);
+ });
+ }
+
+ if (queryParams.length) {
+ href += `?${queryParams.join("&")}`;
+ }
+ this.set("hrefLink", href);
+
+ this.set("activeClass", this.active ? "active" : "");
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/notification-consent-banner.js.es6 b/app/assets/javascripts/discourse/components/notification-consent-banner.js.es6
index 8d20705f04..97ee9b0aa7 100644
--- a/app/assets/javascripts/discourse/components/notification-consent-banner.js.es6
+++ b/app/assets/javascripts/discourse/components/notification-consent-banner.js.es6
@@ -1,13 +1,11 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
-
+import discourseComputed from "discourse-common/utils/decorators";
import { keyValueStore as pushNotificationKeyValueStore } from "discourse/lib/push-notifications";
-
-import { default as DesktopNotificationConfig } from "discourse/components/desktop-notification-config";
+import DesktopNotificationConfig from "discourse/components/desktop-notification-config";
const userDismissedPromptKey = "dismissed-prompt";
export default DesktopNotificationConfig.extend({
- @computed
+ @discourseComputed
bannerDismissed: {
set(value) {
pushNotificationKeyValueStore.setItem(userDismissedPromptKey, value);
@@ -18,7 +16,7 @@ export default DesktopNotificationConfig.extend({
}
},
- @computed(
+ @discourseComputed(
"isNotSupported",
"isEnabled",
"bannerDismissed",
diff --git a/app/assets/javascripts/discourse/components/number-field.js.es6 b/app/assets/javascripts/discourse/components/number-field.js.es6
index 1399de713e..ca2db4d124 100644
--- a/app/assets/javascripts/discourse/components/number-field.js.es6
+++ b/app/assets/javascripts/discourse/components/number-field.js.es6
@@ -1,15 +1,15 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
export default Ember.TextField.extend({
classNameBindings: ["invalid"],
- @computed("number")
+ @discourseComputed("number")
value: {
get(number) {
- return parseInt(number);
+ return parseInt(number, 10);
},
set(value) {
- const num = parseInt(value);
+ const num = parseInt(value, 10);
if (isNaN(num)) {
this.set("invalid", true);
return value;
@@ -21,7 +21,7 @@ export default Ember.TextField.extend({
}
},
- @computed("placeholderKey")
+ @discourseComputed("placeholderKey")
placeholder(key) {
return key ? I18n.t(key) : "";
}
diff --git a/app/assets/javascripts/discourse/components/plugin-connector.js.es6 b/app/assets/javascripts/discourse/components/plugin-connector.js.es6
index b5df63aa8f..b89e242763 100644
--- a/app/assets/javascripts/discourse/components/plugin-connector.js.es6
+++ b/app/assets/javascripts/discourse/components/plugin-connector.js.es6
@@ -1,6 +1,22 @@
-import { observes } from "ember-addons/ember-computed-decorators";
+import Component from "@ember/component";
+import { defineProperty, computed } from "@ember/object";
+import deprecated from "discourse-common/lib/deprecated";
+import { buildArgsWithDeprecations } from "discourse/lib/plugin-connectors";
+import { afterRender } from "discourse-common/utils/decorators";
-export default Ember.Component.extend({
+let _decorators = {};
+
+// Don't call this directly: use `plugin-api/decoratePluginOutlet`
+export function addPluginOutletDecorator(outletName, callback) {
+ _decorators[outletName] = _decorators[outletName] || [];
+ _decorators[outletName].push(callback);
+}
+
+export function resetDecorators() {
+ _decorators = {};
+}
+
+export default Component.extend({
init() {
this._super(...arguments);
@@ -8,18 +24,58 @@ export default Ember.Component.extend({
this.set("layoutName", connector.templateName);
const args = this.args || {};
- Object.keys(args).forEach(key => this.set(key, args[key]));
+ Object.keys(args).forEach(key => {
+ defineProperty(
+ this,
+ key,
+ computed("args", () => (this.args || {})[key])
+ );
+ });
+
+ const deprecatedArgs = this.deprecatedArgs || {};
+ Object.keys(deprecatedArgs).forEach(key => {
+ defineProperty(
+ this,
+ key,
+ computed("deprecatedArgs", () => {
+ deprecated(
+ `The ${key} property is deprecated, but is being used in ${this.layoutName}`
+ );
+
+ return (this.deprecatedArgs || {})[key];
+ })
+ );
+ });
const connectorClass = this.get("connector.connectorClass");
- connectorClass.setupComponent.call(this, args, this);
-
this.set("actions", connectorClass.actions);
+
+ for (const [name, action] of Object.entries(this.actions)) {
+ this.set(name, action);
+ }
+
+ const merged = buildArgsWithDeprecations(args, deprecatedArgs);
+ connectorClass.setupComponent.call(this, merged, this);
},
- @observes("args")
- _argsChanged() {
- const args = this.args || {};
- Object.keys(args).forEach(key => this.set(key, args[key]));
+ didReceiveAttrs() {
+ this._super(...arguments);
+
+ this._decoratePluginOutlets();
+ },
+
+ @afterRender
+ _decoratePluginOutlets() {
+ (_decorators[this.connector.outletName] || []).forEach(dec =>
+ dec(this.element, this.args)
+ );
+ },
+
+ willDestroyElement() {
+ this._super(...arguments);
+
+ const connectorClass = this.get("connector.connectorClass");
+ connectorClass.teardownComponent.call(this, this);
},
send(name, ...args) {
diff --git a/app/assets/javascripts/discourse/components/plugin-outlet.js.es6 b/app/assets/javascripts/discourse/components/plugin-outlet.js.es6
index 36b8929157..f5dc399b2f 100644
--- a/app/assets/javascripts/discourse/components/plugin-outlet.js.es6
+++ b/app/assets/javascripts/discourse/components/plugin-outlet.js.es6
@@ -1,3 +1,4 @@
+import Component from "@ember/component";
/**
A plugin outlet is an extension point for templates where other templates can
be inserted by plugins.
@@ -29,9 +30,12 @@
The list of disabled plugins is returned via the `Site` singleton.
**/
-import { renderedConnectorsFor } from "discourse/lib/plugin-connectors";
+import {
+ renderedConnectorsFor,
+ buildArgsWithDeprecations
+} from "discourse/lib/plugin-connectors";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "span",
connectors: null,
@@ -45,7 +49,10 @@ export default Ember.Component.extend({
this._super(...arguments);
const name = this.name;
if (name) {
- const args = this.args;
+ const args = buildArgsWithDeprecations(
+ this.args || {},
+ this.deprecatedArgs || {}
+ );
this.set("connectors", renderedConnectorsFor(name, args, this));
}
}
diff --git a/app/assets/javascripts/discourse/components/popup-input-tip.js.es6 b/app/assets/javascripts/discourse/components/popup-input-tip.js.es6
index 3088ca25a4..08f6cf39af 100644
--- a/app/assets/javascripts/discourse/components/popup-input-tip.js.es6
+++ b/app/assets/javascripts/discourse/components/popup-input-tip.js.es6
@@ -1,72 +1,68 @@
+import { alias, not } from "@ember/object/computed";
+import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
-import { bufferedRender } from "discourse-common/lib/buffered-render";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
-export default Ember.Component.extend(
- bufferedRender({
- classNameBindings: [":popup-tip", "good", "bad", "lastShownAt::hide"],
- animateAttribute: null,
- bouncePixels: 6,
- bounceDelay: 100,
- rerenderTriggers: ["validation.reason"],
+export default Component.extend({
+ classNameBindings: [":popup-tip", "good", "bad", "lastShownAt::hide"],
+ animateAttribute: null,
+ bouncePixels: 6,
+ bounceDelay: 100,
+ rerenderTriggers: ["validation.reason"],
+ closeIcon: `${iconHTML("times-circle")}`.htmlSafe(),
+ tipReason: null,
- click() {
- this.set("shownAt", null);
- this.set("validation.lastShownAt", null);
- },
+ click() {
+ this.set("shownAt", null);
+ this.set("validation.lastShownAt", null);
+ },
- bad: Ember.computed.alias("validation.failed"),
- good: Ember.computed.not("bad"),
+ bad: alias("validation.failed"),
+ good: not("bad"),
- @computed("shownAt", "validation.lastShownAt")
- lastShownAt(shownAt, lastShownAt) {
- return shownAt || lastShownAt;
- },
+ @discourseComputed("shownAt", "validation.lastShownAt")
+ lastShownAt(shownAt, lastShownAt) {
+ return shownAt || lastShownAt;
+ },
- @observes("lastShownAt")
- bounce() {
- if (this.lastShownAt) {
- var $elem = this.$();
- if (!this.animateAttribute) {
- this.animateAttribute =
- $elem.css("left") === "auto" ? "right" : "left";
- }
- if (this.animateAttribute === "left") {
- this.bounceLeft($elem);
- } else {
- this.bounceRight($elem);
- }
+ @observes("lastShownAt")
+ bounce() {
+ if (this.lastShownAt) {
+ var $elem = $(this.element);
+ if (!this.animateAttribute) {
+ this.animateAttribute = $elem.css("left") === "auto" ? "right" : "left";
}
- },
-
- buildBuffer(buffer) {
- const reason = this.get("validation.reason");
- if (!reason) {
- return;
- }
-
- buffer.push(
- `
${iconHTML("times-circle")} ${reason}`
- );
- },
-
- bounceLeft($elem) {
- for (var i = 0; i < 5; i++) {
- $elem
- .animate({ left: "+=" + this.bouncePixels }, this.bounceDelay)
- .animate({ left: "-=" + this.bouncePixels }, this.bounceDelay);
- }
- },
-
- bounceRight($elem) {
- for (var i = 0; i < 5; i++) {
- $elem
- .animate({ right: "-=" + this.bouncePixels }, this.bounceDelay)
- .animate({ right: "+=" + this.bouncePixels }, this.bounceDelay);
+ if (this.animateAttribute === "left") {
+ this.bounceLeft($elem);
+ } else {
+ this.bounceRight($elem);
}
}
- })
-);
+ },
+
+ didReceiveAttrs() {
+ this._super(...arguments);
+ let reason = this.get("validation.reason");
+ if (reason) {
+ this.set("tipReason", `${reason}`.htmlSafe());
+ } else {
+ this.set("tipReason", null);
+ }
+ },
+
+ bounceLeft($elem) {
+ for (var i = 0; i < 5; i++) {
+ $elem
+ .animate({ left: "+=" + this.bouncePixels }, this.bounceDelay)
+ .animate({ left: "-=" + this.bouncePixels }, this.bounceDelay);
+ }
+ },
+
+ bounceRight($elem) {
+ for (var i = 0; i < 5; i++) {
+ $elem
+ .animate({ right: "-=" + this.bouncePixels }, this.bounceDelay)
+ .animate({ right: "+=" + this.bouncePixels }, this.bounceDelay);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/preference-checkbox.js.es6 b/app/assets/javascripts/discourse/components/preference-checkbox.js.es6
index b0e84c117d..83ad27853f 100644
--- a/app/assets/javascripts/discourse/components/preference-checkbox.js.es6
+++ b/app/assets/javascripts/discourse/components/preference-checkbox.js.es6
@@ -1,9 +1,10 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
classNames: ["controls"],
- @computed("labelKey")
+ @discourseComputed("labelKey")
label(labelKey) {
return I18n.t(labelKey);
},
diff --git a/app/assets/javascripts/discourse/components/private-message-glyph.js.es6 b/app/assets/javascripts/discourse/components/private-message-glyph.js.es6
new file mode 100644
index 0000000000..26e636b05d
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/private-message-glyph.js.es6
@@ -0,0 +1,8 @@
+import Component from "@ember/component";
+
+export default Component.extend({
+ tagName: null,
+ href: null,
+ title: null,
+ ariaLabel: null
+});
diff --git a/app/assets/javascripts/discourse/components/pwa-install-banner.js.es6 b/app/assets/javascripts/discourse/components/pwa-install-banner.js.es6
new file mode 100644
index 0000000000..1dd4bef396
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/pwa-install-banner.js.es6
@@ -0,0 +1,63 @@
+import { bind } from "@ember/runloop";
+import Component from "@ember/component";
+import discourseComputed, { on } from "discourse-common/utils/decorators";
+
+const USER_DISMISSED_PROMPT_KEY = "dismissed-pwa-install-banner";
+
+export default Component.extend({
+ deferredInstallPromptEvent: null,
+
+ _handleInstallPromptEvent(event) {
+ // Prevent Chrome 76+ from automatically showing the prompt
+ event.preventDefault();
+ // Stash the event so it can be triggered later
+ this.set("deferredInstallPromptEvent", event);
+ },
+
+ @on("didInsertElement")
+ _registerListener() {
+ this._promptEventHandler = bind(this, this._handleInstallPromptEvent);
+ window.addEventListener("beforeinstallprompt", this._promptEventHandler);
+ },
+
+ @on("willDestroyElement")
+ _unregisterListener() {
+ window.removeEventListener("beforeinstallprompt", this._promptEventHandler);
+ },
+
+ @discourseComputed
+ bannerDismissed: {
+ set(value) {
+ this.keyValueStore.set({ key: USER_DISMISSED_PROMPT_KEY, value });
+ return this.keyValueStore.get(USER_DISMISSED_PROMPT_KEY);
+ },
+ get() {
+ return this.keyValueStore.get(USER_DISMISSED_PROMPT_KEY);
+ }
+ },
+
+ @discourseComputed("deferredInstallPromptEvent", "bannerDismissed")
+ showPWAInstallBanner() {
+ const launchedFromDiscourseHub =
+ window.location.search.indexOf("discourse_app=1") !== -1;
+
+ return (
+ this.capabilities.isAndroid &&
+ this.get("currentUser.trust_level") > 0 &&
+ this.deferredInstallPromptEvent && // Pass the browser engagement checks
+ !window.matchMedia("(display-mode: standalone)").matches && // Not be in the installed PWA already
+ !launchedFromDiscourseHub && // not launched via official app
+ !this.bannerDismissed // Have not a previously dismissed install banner
+ );
+ },
+
+ actions: {
+ turnOn() {
+ this.set("bannerDismissed", true);
+ this.deferredInstallPromptEvent.prompt();
+ },
+ dismiss() {
+ this.set("bannerDismissed", true);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/quote-button.js.es6 b/app/assets/javascripts/discourse/components/quote-button.js.es6
index 2636fc8e41..a703e5e3d7 100644
--- a/app/assets/javascripts/discourse/components/quote-button.js.es6
+++ b/app/assets/javascripts/discourse/components/quote-button.js.es6
@@ -1,7 +1,9 @@
-import debounce from "discourse/lib/debounce";
-import { selectedText } from "discourse/lib/utilities";
+import { scheduleOnce } from "@ember/runloop";
+import Component from "@ember/component";
+import discourseDebounce from "discourse/lib/debounce";
+import { selectedText, selectedElement } from "discourse/lib/utilities";
-export default Ember.Component.extend({
+export default Component.extend({
classNames: ["quote-button"],
classNameBindings: ["visible"],
visible: false,
@@ -46,8 +48,26 @@ export default Ember.Component.extend({
}
}
+ let opts = { raw: true };
+ for (
+ let element = selectedElement();
+ element && element.tagName !== "ARTICLE";
+ element = element.parentElement
+ ) {
+ if (element.tagName === "ASIDE" && element.classList.contains("quote")) {
+ opts.username =
+ element.dataset.username ||
+ element
+ .querySelector(".title")
+ .textContent.trim()
+ .replace(/:$/, "");
+ opts.post = element.dataset.post;
+ opts.topic = element.dataset.topic;
+ }
+ }
+
const _selectedText = selectedText();
- quoteState.selected(postId, _selectedText);
+ quoteState.selected(postId, _selectedText, opts);
this.set("visible", quoteState.buffer.length > 0);
// avoid hard loops in quote selection unconditionally
@@ -83,7 +103,7 @@ export default Ember.Component.extend({
const $markerElement = $(markerElement);
const markerOffset = $markerElement.offset();
const parentScrollLeft = $markerElement.parent().scrollLeft();
- const $quoteButton = this.$();
+ const $quoteButton = $(this.element);
// remove the marker
const parent = markerElement.parentNode;
@@ -102,15 +122,18 @@ export default Ember.Component.extend({
}
// change the position of the button
- Ember.run.scheduleOnce("afterRender", () => {
+ scheduleOnce("afterRender", () => {
let top = markerOffset.top;
let left = markerOffset.left + Math.max(0, parentScrollLeft);
if (showAtEnd) {
- top = top + 20;
+ const nearRightEdgeOfScreen =
+ $(window).width() - $quoteButton.outerWidth() < left + 10;
+
+ top = nearRightEdgeOfScreen ? top + 50 : top + 20;
left = Math.min(
left + 10,
- $(window).width() - $quoteButton.outerWidth()
+ $(window).width() - $quoteButton.outerWidth() - 10
);
} else {
top = top - $quoteButton.outerHeight() - 5;
@@ -123,7 +146,10 @@ export default Ember.Component.extend({
didInsertElement() {
const { isWinphone, isAndroid } = this.capabilities;
const wait = isWinphone || isAndroid ? 250 : 25;
- const onSelectionChanged = debounce(() => this._selectionChanged(), wait);
+ const onSelectionChanged = discourseDebounce(
+ () => this._selectionChanged(),
+ wait
+ );
$(document)
.on("mousedown.quote-button", e => {
@@ -157,8 +183,8 @@ export default Ember.Component.extend({
},
click() {
- const { postId, buffer } = this.quoteState;
- this.attrs.selectText(postId, buffer).then(() => this._hideButton());
+ const { postId, buffer, opts } = this.quoteState;
+ this.attrs.selectText(postId, buffer, opts).then(() => this._hideButton());
return false;
}
});
diff --git a/app/assets/javascripts/discourse/components/radio-button.js.es6 b/app/assets/javascripts/discourse/components/radio-button.js.es6
index 8cdf74ecb9..1094aca2c7 100644
--- a/app/assets/javascripts/discourse/components/radio-button.js.es6
+++ b/app/assets/javascripts/discourse/components/radio-button.js.es6
@@ -1,6 +1,7 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "input",
type: "radio",
attributeBindings: [
@@ -12,14 +13,19 @@ export default Ember.Component.extend({
],
click() {
- const value = this.$().val();
- if (this.selection === value) {
- this.set("selection", undefined);
+ const value = $(this.element).val();
+
+ if (this.onChange) {
+ this.onChange(value);
+ } else {
+ if (this.selection === value) {
+ this.set("selection", undefined);
+ }
+ this.set("selection", value);
}
- this.set("selection", value);
},
- @computed("value", "selection")
+ @discourseComputed("value", "selection")
checked(value, selection) {
return value === selection;
}
diff --git a/app/assets/javascripts/discourse/components/related-messages.js.es6 b/app/assets/javascripts/discourse/components/related-messages.js.es6
index 1094f68809..46732424f8 100644
--- a/app/assets/javascripts/discourse/components/related-messages.js.es6
+++ b/app/assets/javascripts/discourse/components/related-messages.js.es6
@@ -1,17 +1,36 @@
-import computed from "ember-addons/ember-computed-decorators";
-import { iconHTML } from "discourse-common/lib/icon-library";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
elementId: "related-messages",
classNames: ["suggested-topics"],
- @computed("topic")
- relatedTitle(topic) {
- const href = this.currentUser && this.currentUser.pmPath(topic);
- return href
- ? `
${iconHTML("envelope", {
- class: "private-message-glyph"
- })} ${I18n.t("related_messages.title")} `
- : I18n.t("related_messages.title");
+ @discourseComputed("topic")
+ targetUser(topic) {
+ if (!topic || !topic.isPrivateMessage) {
+ return;
+ }
+ const allowedUsers = topic.details.allowed_users;
+ if (
+ topic.relatedMessages &&
+ topic.relatedMessages.length >= 5 &&
+ allowedUsers.length === 2 &&
+ topic.details.allowed_groups.length === 0 &&
+ allowedUsers.find(u => u.username === this.currentUser.username)
+ ) {
+ return allowedUsers.find(u => u.username !== this.currentUser.username);
+ }
+ },
+
+ @discourseComputed
+ searchLink() {
+ return Discourse.getURL(
+ `/search?expanded=true&q=%40${this.targetUser.username}%20in%3Apersonal-direct`
+ );
+ },
+
+ @discourseComputed("topic")
+ relatedTitleLink(topic) {
+ return this.currentUser && this.currentUser.pmPath(topic);
}
});
diff --git a/app/assets/javascripts/discourse/components/reviewable-bundled-action.js.es6 b/app/assets/javascripts/discourse/components/reviewable-bundled-action.js.es6
index d42a2dd290..2b3cc0fdf4 100644
--- a/app/assets/javascripts/discourse/components/reviewable-bundled-action.js.es6
+++ b/app/assets/javascripts/discourse/components/reviewable-bundled-action.js.es6
@@ -1,8 +1,10 @@
-export default Ember.Component.extend({
+import { gt, alias } from "@ember/object/computed";
+import Component from "@ember/component";
+export default Component.extend({
tagName: "",
- multiple: Ember.computed.gt("bundle.actions.length", 1),
- first: Ember.computed.alias("bundle.actions.firstObject"),
+ multiple: gt("bundle.actions.length", 1),
+ first: alias("bundle.actions.firstObject"),
actions: {
performById(id) {
diff --git a/app/assets/javascripts/discourse/components/reviewable-claimed-topic.js.es6 b/app/assets/javascripts/discourse/components/reviewable-claimed-topic.js.es6
index d73a32d304..273b51afa7 100644
--- a/app/assets/javascripts/discourse/components/reviewable-claimed-topic.js.es6
+++ b/app/assets/javascripts/discourse/components/reviewable-claimed-topic.js.es6
@@ -1,11 +1,12 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { ajax } from "discourse/lib/ajax";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "",
- @computed
+ @discourseComputed
enabled() {
return this.siteSettings.reviewable_claiming !== "disabled";
},
diff --git a/app/assets/javascripts/discourse/components/reviewable-conversation-post.js.es6 b/app/assets/javascripts/discourse/components/reviewable-conversation-post.js.es6
index b57e6aa5de..9fc360d10c 100644
--- a/app/assets/javascripts/discourse/components/reviewable-conversation-post.js.es6
+++ b/app/assets/javascripts/discourse/components/reviewable-conversation-post.js.es6
@@ -1,3 +1,5 @@
-export default Ember.Component.extend({
- showUsername: Ember.computed.gte("index", 1)
+import { gte } from "@ember/object/computed";
+import Component from "@ember/component";
+export default Component.extend({
+ showUsername: gte("index", 1)
});
diff --git a/app/assets/javascripts/discourse/components/reviewable-field-tags.js.es6 b/app/assets/javascripts/discourse/components/reviewable-field-tags.js.es6
new file mode 100644
index 0000000000..9280d8ee4d
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/reviewable-field-tags.js.es6
@@ -0,0 +1,14 @@
+import Component from "@ember/component";
+
+export default Component.extend({
+ actions: {
+ onChange(tags) {
+ this.valueChanged &&
+ this.valueChanged({
+ target: {
+ value: tags
+ }
+ });
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/reviewable-flagged-post.js.es6 b/app/assets/javascripts/discourse/components/reviewable-flagged-post.js.es6
index 1916a96125..d432beece9 100644
--- a/app/assets/javascripts/discourse/components/reviewable-flagged-post.js.es6
+++ b/app/assets/javascripts/discourse/components/reviewable-flagged-post.js.es6
@@ -1,17 +1,19 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { gt } from "@ember/object/computed";
+import Component from "@ember/component";
import { longDate } from "discourse/lib/formatter";
import { historyHeat } from "discourse/widgets/post-edits-indicator";
import showModal from "discourse/lib/show-modal";
-export default Ember.Component.extend({
- hasEdits: Ember.computed.gt("reviewable.post_version", 1),
+export default Component.extend({
+ hasEdits: gt("reviewable.post_version", 1),
- @computed("reviewable.post_updated_at")
+ @discourseComputed("reviewable.post_updated_at")
historyClass(updatedAt) {
return historyHeat(this.siteSettings, new Date(updatedAt));
},
- @computed("reviewable.post_updated_at")
+ @discourseComputed("reviewable.post_updated_at")
editedDate(updatedAt) {
return longDate(updatedAt);
},
diff --git a/app/assets/javascripts/discourse/components/reviewable-histories.js.es6 b/app/assets/javascripts/discourse/components/reviewable-histories.js.es6
index a5f1a7932e..f52c1c4000 100644
--- a/app/assets/javascripts/discourse/components/reviewable-histories.js.es6
+++ b/app/assets/javascripts/discourse/components/reviewable-histories.js.es6
@@ -1,3 +1,5 @@
-export default Ember.Component.extend({
- filteredHistories: Ember.computed.filterBy("histories", "created", false)
+import { filterBy } from "@ember/object/computed";
+import Component from "@ember/component";
+export default Component.extend({
+ filteredHistories: filterBy("histories", "created", false)
});
diff --git a/app/assets/javascripts/discourse/components/reviewable-item.js.es6 b/app/assets/javascripts/discourse/components/reviewable-item.js.es6
index d94128f348..786f7d5440 100644
--- a/app/assets/javascripts/discourse/components/reviewable-item.js.es6
+++ b/app/assets/javascripts/discourse/components/reviewable-item.js.es6
@@ -1,29 +1,38 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
-import computed from "ember-addons/ember-computed-decorators";
import Category from "discourse/models/category";
import optionalService from "discourse/lib/optional-service";
+import showModal from "discourse/lib/show-modal";
+import { dasherize } from "@ember/string";
+import { set } from "@ember/object";
let _components = {};
-export default Ember.Component.extend({
+export default Component.extend({
adminTools: optionalService(),
tagName: "",
updating: null,
editing: false,
_updates: null,
- @computed("reviewable.type")
+ @discourseComputed("reviewable.type")
customClass(type) {
return type.dasherize();
},
- @computed("siteSettings.reviewable_claiming", "reviewable.topic")
- claimEnabled(claimMode, topic) {
- return claimMode !== "disabled" && !!topic;
+ @discourseComputed("reviewable.topic_id", "reviewable.removed_topic_id")
+ topicId(topicId, removedTopicId) {
+ return topicId || removedTopicId;
},
- @computed(
+ @discourseComputed("siteSettings.reviewable_claiming", "topicId")
+ claimEnabled(claimMode, topicId) {
+ return claimMode !== "disabled" && !!topicId;
+ },
+
+ @discourseComputed(
"claimEnabled",
"siteSettings.reviewable_claiming",
"reviewable.claimed_by"
@@ -40,7 +49,10 @@ export default Ember.Component.extend({
return claimMode !== "required";
},
- @computed("siteSettings.reviewable_claiming", "reviewable.claimed_by")
+ @discourseComputed(
+ "siteSettings.reviewable_claiming",
+ "reviewable.claimed_by"
+ )
claimHelp(claimMode, claimedBy) {
if (claimedBy) {
return claimedBy.id === this.currentUser.id
@@ -57,13 +69,13 @@ export default Ember.Component.extend({
// Find a component to render, if one exists. For example:
// `ReviewableUser` will return `reviewable-user`
- @computed("reviewable.type")
+ @discourseComputed("reviewable.type")
reviewableComponent(type) {
if (_components[type] !== undefined) {
return _components[type];
}
- let dasherized = Ember.String.dasherize(type);
+ let dasherized = dasherize(type);
let templatePath = `components/${dasherized}`;
let template =
Ember.TEMPLATES[`${templatePath}`] ||
@@ -140,6 +152,13 @@ export default Ember.Component.extend({
},
actions: {
+ explainReviewable(reviewable) {
+ showModal("explain-reviewable", {
+ title: "review.explain.title",
+ model: reviewable
+ });
+ },
+
edit() {
this.set("editing", true);
this._updates = { payload: {} };
@@ -168,15 +187,18 @@ export default Ember.Component.extend({
.finally(() => this.set("updating", false));
},
- categoryChanged(category) {
+ categoryChanged(categoryId) {
+ let category = Category.findById(categoryId);
+
if (!category) {
category = Category.findUncategorized();
}
+
this._updates.category_id = category.id;
},
valueChanged(fieldId, event) {
- Ember.set(this._updates, fieldId, event.target.value);
+ set(this._updates, fieldId, event.target.value);
},
perform(action) {
diff --git a/app/assets/javascripts/discourse/components/reviewable-queued-post.js.es6 b/app/assets/javascripts/discourse/components/reviewable-queued-post.js.es6
new file mode 100644
index 0000000000..2e17ce9423
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/reviewable-queued-post.js.es6
@@ -0,0 +1,10 @@
+import Component from "@ember/component";
+import showModal from "discourse/lib/show-modal";
+
+export default Component.extend({
+ actions: {
+ showRawEmail() {
+ showModal("raw-email").set("rawEmail", this.reviewable.payload.raw_email);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/reviewable-user.js.es6 b/app/assets/javascripts/discourse/components/reviewable-user.js.es6
index f071ba7b33..5feb5d84f2 100644
--- a/app/assets/javascripts/discourse/components/reviewable-user.js.es6
+++ b/app/assets/javascripts/discourse/components/reviewable-user.js.es6
@@ -1,7 +1,8 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
-export default Ember.Component.extend({
- @computed("reviewable.user_fields")
+export default Component.extend({
+ @discourseComputed("reviewable.user_fields")
userFields(fields) {
return this.site.collectUserFields(fields);
}
diff --git a/app/assets/javascripts/discourse/components/scroll-tracker.js.es6 b/app/assets/javascripts/discourse/components/scroll-tracker.js.es6
index caad7dc87a..7637845c1e 100644
--- a/app/assets/javascripts/discourse/components/scroll-tracker.js.es6
+++ b/app/assets/javascripts/discourse/components/scroll-tracker.js.es6
@@ -1,6 +1,8 @@
+import { next } from "@ember/runloop";
+import Component from "@ember/component";
import Scrolling from "discourse/mixins/scrolling";
-export default Ember.Component.extend(Scrolling, {
+export default Component.extend(Scrolling, {
didReceiveAttrs() {
this._super(...arguments);
@@ -18,7 +20,7 @@ export default Ember.Component.extend(Scrolling, {
const data = this.session.get(this.trackerName);
if (data && data.position >= 0 && data.tag === this.tag) {
- Ember.run.next(() => $(window).scrollTop(data.position + 1));
+ next(() => $(window).scrollTop(data.position + 1));
}
},
diff --git a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
index dd26007e10..04f0897eb6 100644
--- a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
+++ b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
@@ -1,3 +1,6 @@
+import { next } from "@ember/runloop";
+import { debounce } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
import DiscourseURL from "discourse/lib/url";
import MountWidget from "discourse/components/mount-widget";
import { cloak, uncloak } from "discourse/widgets/post-stream";
@@ -40,7 +43,8 @@ export default MountWidget.extend({
"gaps",
"selectedQuery",
"selectedPostsCount",
- "searchService"
+ "searchService",
+ "showReadIndicator"
);
},
@@ -89,7 +93,9 @@ export default MountWidget.extend({
const windowTop = $w.scrollTop();
const postsWrapperTop = $(".posts-wrapper").offset().top;
- const $posts = this.$(".onscreen-post, .cloaked-post");
+ const $posts = $(
+ this.element.querySelectorAll(".onscreen-post, .cloaked-post")
+ );
const viewportTop = windowTop - slack;
const topView = findTopView(
$posts,
@@ -201,7 +207,7 @@ export default MountWidget.extend({
// will cause the browser to scroll to the top of the document
// in Chrome. This makes sure the scroll works correctly if that
// happens.
- Ember.run.next(() => $("html, body").scrollTop(whereY));
+ next(() => $("html, body").scrollTop(whereY));
}
});
};
@@ -267,7 +273,7 @@ export default MountWidget.extend({
},
_scrollTriggered() {
- Ember.run.scheduleOnce("afterRender", this, this.scrolled);
+ scheduleOnce("afterRender", this, this.scrolled);
},
_posted(staged) {
@@ -289,6 +295,12 @@ export default MountWidget.extend({
onRefresh: "refreshLikes"
});
}
+
+ if (args.refreshReaders) {
+ this.dirtyKeys.keyDirty(`post-menu-${args.id}`, {
+ onRefresh: "refreshReaders"
+ });
+ }
} else if (args.force) {
this.dirtyKeys.forceAll();
}
@@ -297,13 +309,12 @@ export default MountWidget.extend({
},
_debouncedScroll() {
- Ember.run.debounce(this, this._scrollTriggered, 10);
+ debounce(this, this._scrollTriggered, 10);
},
didInsertElement() {
this._super(...arguments);
- const debouncedScroll = () =>
- Ember.run.debounce(this, this._scrollTriggered, 10);
+ const debouncedScroll = () => debounce(this, this._scrollTriggered, 10);
this._previouslyNearby = {};
@@ -314,12 +325,12 @@ export default MountWidget.extend({
this.appEvents.on("post-stream:posted", this, "_posted");
- this.$().on("mouseenter.post-stream", "button.widget-button", e => {
+ $(this.element).on("mouseenter.post-stream", "button.widget-button", e => {
$("button.widget-button").removeClass("d-hover");
$(e.target).addClass("d-hover");
});
- this.$().on("mouseleave.post-stream", "button.widget-button", () => {
+ $(this.element).on("mouseleave.post-stream", "button.widget-button", () => {
$("button.widget-button").removeClass("d-hover");
});
@@ -331,8 +342,8 @@ export default MountWidget.extend({
$(document).unbind("touchmove.post-stream");
$(window).unbind("scroll.post-stream");
this.appEvents.off("post-stream:refresh", this, "_debouncedScroll");
- this.$().off("mouseenter.post-stream");
- this.$().off("mouseleave.post-stream");
+ $(this.element).off("mouseenter.post-stream");
+ $(this.element).off("mouseleave.post-stream");
this.appEvents.off("post-stream:refresh", this, "_refresh");
this.appEvents.off("post-stream:posted", this, "_posted");
}
diff --git a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6
index 75b2e5460a..855fed4e28 100644
--- a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6
+++ b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6
@@ -1,7 +1,11 @@
-import { observes } from "ember-addons/ember-computed-decorators";
+import { debounce } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import Component from "@ember/component";
+import { observes } from "discourse-common/utils/decorators";
import { escapeExpression } from "discourse/lib/utilities";
import Group from "discourse/models/group";
import Badge from "discourse/models/badge";
+import Category from "discourse/models/category";
const REGEXP_BLOCKS = /(([^" \t\n\x0B\f\r]+)?(("[^"]+")?))/g;
@@ -16,10 +20,10 @@ const REGEXP_MIN_POST_COUNT_PREFIX = /^min_post_count:/gi;
const REGEXP_POST_TIME_PREFIX = /^(before|after):/gi;
const REGEXP_TAGS_REPLACE = /(^(tags?:|#(?=[a-z0-9\-]+::tag))|::tag\s?$)/gi;
-const REGEXP_IN_MATCH = /^(in|with):(posted|watching|tracking|bookmarks|first|pinned|unpinned|wiki|unseen|image)/gi;
+const REGEXP_IN_MATCH = /^(in|with):(posted|created|watching|tracking|bookmarks|first|pinned|unpinned|wiki|unseen|image)/gi;
const REGEXP_SPECIAL_IN_LIKES_MATCH = /^in:likes/gi;
const REGEXP_SPECIAL_IN_TITLE_MATCH = /^in:title/gi;
-const REGEXP_SPECIAL_IN_PRIVATE_MATCH = /^in:private/gi;
+const REGEXP_SPECIAL_IN_PERSONAL_MATCH = /^in:personal/gi;
const REGEXP_SPECIAL_IN_SEEN_MATCH = /^in:seen/gi;
const REGEXP_CATEGORY_SLUG = /^(\#[a-zA-Z0-9\-:]+)/gi;
@@ -28,7 +32,7 @@ const REGEXP_POST_TIME_WHEN = /^(before|after)/gi;
const IN_OPTIONS_MAPPING = { images: "with" };
-export default Ember.Component.extend({
+export default Component.extend({
classNames: ["search-advanced-options"],
init() {
@@ -37,6 +41,7 @@ export default Ember.Component.extend({
this.inOptionsForUsers = [
{ name: I18n.t("search.advanced.filters.unseen"), value: "unseen" },
{ name: I18n.t("search.advanced.filters.posted"), value: "posted" },
+ { name: I18n.t("search.advanced.filters.created"), value: "created" },
{ name: I18n.t("search.advanced.filters.watching"), value: "watching" },
{ name: I18n.t("search.advanced.filters.tracking"), value: "tracking" },
{ name: I18n.t("search.advanced.filters.bookmarks"), value: "bookmarks" }
@@ -53,6 +58,7 @@ export default Ember.Component.extend({
this.statusOptions = [
{ name: I18n.t("search.advanced.statuses.open"), value: "open" },
{ name: I18n.t("search.advanced.statuses.closed"), value: "closed" },
+ { name: I18n.t("search.advanced.statuses.public"), value: "public" },
{ name: I18n.t("search.advanced.statuses.archived"), value: "archived" },
{
name: I18n.t("search.advanced.statuses.noreplies"),
@@ -71,13 +77,13 @@ export default Ember.Component.extend({
this._init();
- Ember.run.scheduleOnce("afterRender", () => this._update());
+ scheduleOnce("afterRender", () => this._update());
},
@observes("searchTerm")
_updateOptions() {
this._update();
- Ember.run.debounce(this, this._update, 250);
+ debounce(this, this._update, 250);
},
_init() {
@@ -93,7 +99,7 @@ export default Ember.Component.extend({
in: {
title: false,
likes: false,
- private: false,
+ personal: false,
seen: false
},
all_tags: false
@@ -140,8 +146,8 @@ export default Ember.Component.extend({
);
this.setSearchedTermSpecialInValue(
- "searchedTerms.special.in.private",
- REGEXP_SPECIAL_IN_PRIVATE_MATCH
+ "searchedTerms.special.in.personal",
+ REGEXP_SPECIAL_IN_PERSONAL_MATCH
);
this.setSearchedTermSpecialInValue(
@@ -221,7 +227,7 @@ export default Ember.Component.extend({
.replace(REGEXP_CATEGORY_PREFIX, "")
.split(":");
if (subcategories.length > 1) {
- const userInput = Discourse.Category.findBySlug(
+ const userInput = Category.findBySlug(
subcategories[1],
subcategories[0]
);
@@ -231,14 +237,14 @@ export default Ember.Component.extend({
)
this.set("searchedTerms.category", userInput);
} else if (isNaN(subcategories)) {
- const userInput = Discourse.Category.findSingleBySlug(subcategories[0]);
+ const userInput = Category.findSingleBySlug(subcategories[0]);
if (
(!existingInput && userInput) ||
(existingInput && userInput && existingInput.id !== userInput.id)
)
this.set("searchedTerms.category", userInput);
} else {
- const userInput = Discourse.Category.findById(subcategories[0]);
+ const userInput = Category.findById(subcategories[0]);
if (
(!existingInput && userInput) ||
(existingInput && userInput && existingInput.id !== userInput.id)
@@ -512,9 +518,9 @@ export default Ember.Component.extend({
this.updateInRegex(REGEXP_SPECIAL_IN_LIKES_MATCH, "likes");
},
- @observes("searchedTerms.special.in.private")
- updateSearchTermForSpecialInPrivate() {
- this.updateInRegex(REGEXP_SPECIAL_IN_PRIVATE_MATCH, "private");
+ @observes("searchedTerms.special.in.personal")
+ updateSearchTermForSpecialInPersonal() {
+ this.updateInRegex(REGEXP_SPECIAL_IN_PERSONAL_MATCH, "personal");
},
@observes("searchedTerms.special.in.seen")
@@ -547,7 +553,6 @@ export default Ember.Component.extend({
}
},
- @observes("searchedTerms.time.when", "searchedTerms.time.days")
updateSearchTermForPostTime() {
const match = this.filterBlocks(REGEXP_POST_TIME_PREFIX);
const timeDaysFilter = this.get("searchedTerms.time.days");
@@ -597,5 +602,28 @@ export default Ember.Component.extend({
badgeFinder(term) {
return Badge.findAll({ search: term });
+ },
+
+ actions: {
+ onChangeWhenTime(time) {
+ if (time) {
+ this.set("searchedTerms.time.when", time);
+ this.updateSearchTermForPostTime();
+ }
+ },
+ onChangeWhenDate(date) {
+ if (date) {
+ this.set("searchedTerms.time.days", moment(date).format("YYYY-MM-DD"));
+ this.updateSearchTermForPostTime();
+ }
+ },
+
+ onChangeCategory(categoryId) {
+ if (categoryId) {
+ this.set("searchedTerms.category", Category.findById(categoryId));
+ } else {
+ this.set("searchedTerms.category", null);
+ }
+ }
}
});
diff --git a/app/assets/javascripts/discourse/components/search-text-field.js.es6 b/app/assets/javascripts/discourse/components/search-text-field.js.es6
index e2629bd812..fc6ca86c08 100644
--- a/app/assets/javascripts/discourse/components/search-text-field.js.es6
+++ b/app/assets/javascripts/discourse/components/search-text-field.js.es6
@@ -1,19 +1,19 @@
-import computed from "ember-addons/ember-computed-decorators";
-import { on } from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { on } from "discourse-common/utils/decorators";
import TextField from "discourse/components/text-field";
import { applySearchAutocomplete } from "discourse/lib/search";
export default TextField.extend({
autocomplete: "discourse",
- @computed("searchService.searchContextEnabled")
+ @discourseComputed("searchService.searchContextEnabled")
placeholder(searchContextEnabled) {
return searchContextEnabled ? "" : I18n.t("search.full_page_title");
},
@on("didInsertElement")
becomeFocused() {
- const $searchInput = this.$();
+ const $searchInput = $(this.element);
applySearchAutocomplete($searchInput, this.siteSettings);
if (!this.hasAutofocus) {
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 f990437ce5..47bd68a487 100644
--- a/app/assets/javascripts/discourse/components/second-factor-form.js.es6
+++ b/app/assets/javascripts/discourse/components/second-factor-form.js.es6
@@ -1,22 +1,33 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
-export default Ember.Component.extend({
- @computed("secondFactorMethod")
+export default Component.extend({
+ @discourseComputed("secondFactorMethod")
secondFactorTitle(secondFactorMethod) {
- return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP
- ? I18n.t("login.second_factor_title")
- : I18n.t("login.second_factor_backup_title");
+ switch (secondFactorMethod) {
+ case SECOND_FACTOR_METHODS.TOTP:
+ return I18n.t("login.second_factor_title");
+ case SECOND_FACTOR_METHODS.SECURITY_KEY:
+ return I18n.t("login.second_factor_title");
+ case SECOND_FACTOR_METHODS.BACKUP_CODE:
+ return I18n.t("login.second_factor_backup_title");
+ }
},
- @computed("secondFactorMethod")
+ @discourseComputed("secondFactorMethod")
secondFactorDescription(secondFactorMethod) {
- return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP
- ? I18n.t("login.second_factor_description")
- : I18n.t("login.second_factor_backup_description");
+ switch (secondFactorMethod) {
+ case SECOND_FACTOR_METHODS.TOTP:
+ return I18n.t("login.second_factor_description");
+ case SECOND_FACTOR_METHODS.SECURITY_KEY:
+ return I18n.t("login.security_key_description");
+ case SECOND_FACTOR_METHODS.BACKUP_CODE:
+ return I18n.t("login.second_factor_backup_description");
+ }
},
- @computed("secondFactorMethod", "isLogin")
+ @discourseComputed("secondFactorMethod", "isLogin")
linkText(secondFactorMethod, isLogin) {
if (isLogin) {
return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP
@@ -29,6 +40,13 @@ export default Ember.Component.extend({
}
},
+ @discourseComputed("backupEnabled", "secondFactorMethod")
+ showToggleMethodLink(backupEnabled, secondFactorMethod) {
+ return (
+ backupEnabled && secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY
+ );
+ },
+
actions: {
toggleSecondFactorMethod() {
const secondFactorMethod = this.secondFactorMethod;
diff --git a/app/assets/javascripts/discourse/components/second-factor-input.js.es6 b/app/assets/javascripts/discourse/components/second-factor-input.js.es6
index 00e27039a4..98ca98ac01 100644
--- a/app/assets/javascripts/discourse/components/second-factor-input.js.es6
+++ b/app/assets/javascripts/discourse/components/second-factor-input.js.es6
@@ -1,23 +1,24 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
-export default Ember.Component.extend({
- @computed("secondFactorMethod")
+export default Component.extend({
+ @discourseComputed("secondFactorMethod")
type(secondFactorMethod) {
if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) return "tel";
if (secondFactorMethod === SECOND_FACTOR_METHODS.BACKUP_CODE) return "text";
},
- @computed("secondFactorMethod")
+ @discourseComputed("secondFactorMethod")
pattern(secondFactorMethod) {
if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) return "[0-9]{6}";
if (secondFactorMethod === SECOND_FACTOR_METHODS.BACKUP_CODE)
return "[a-z0-9]{16}";
},
- @computed("secondFactorMethod")
+ @discourseComputed("secondFactorMethod")
maxlength(secondFactorMethod) {
if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) return "6";
- if (secondFactorMethod === SECOND_FACTOR_METHODS.BACKUP_CODE) return "16";
+ if (secondFactorMethod === SECOND_FACTOR_METHODS.BACKUP_CODE) return "32";
}
});
diff --git a/app/assets/javascripts/discourse/components/security-key-form.js.es6 b/app/assets/javascripts/discourse/components/security-key-form.js.es6
new file mode 100644
index 0000000000..d315bf6889
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/security-key-form.js.es6
@@ -0,0 +1,12 @@
+import Component from "@ember/component";
+import { SECOND_FACTOR_METHODS } from "discourse/models/user";
+
+export default Component.extend({
+ actions: {
+ useAnotherMethod() {
+ this.set("showSecurityKey", false);
+ this.set("showSecondFactor", true);
+ this.set("secondFactorMethod", SECOND_FACTOR_METHODS.TOTP);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/share-panel.js.es6 b/app/assets/javascripts/discourse/components/share-panel.js.es6
index 872c1771bf..78bc76db23 100644
--- a/app/assets/javascripts/discourse/components/share-panel.js.es6
+++ b/app/assets/javascripts/discourse/components/share-panel.js.es6
@@ -1,30 +1,34 @@
+import { isEmpty } from "@ember/utils";
+import { alias } from "@ember/object/computed";
+import { schedule } from "@ember/runloop";
+import Component from "@ember/component";
import { escapeExpression } from "discourse/lib/utilities";
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
import Sharing from "discourse/lib/sharing";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: null,
- type: Ember.computed.alias("panel.model.type"),
+ type: alias("panel.model.type"),
- topic: Ember.computed.alias("panel.model.topic"),
+ topic: alias("panel.model.topic"),
- @computed
+ @discourseComputed
sources() {
return Sharing.activeSources(this.siteSettings.share_links);
},
- @computed("type", "topic.title")
+ @discourseComputed("type", "topic.title")
shareTitle(type, topicTitle) {
topicTitle = escapeExpression(topicTitle);
return I18n.t("share.topic_html", { topicTitle });
},
- @computed("panel.model.shareUrl", "topic.shareUrl")
+ @discourseComputed("panel.model.shareUrl", "topic.shareUrl")
shareUrl(forcedShareUrl, shareUrl) {
shareUrl = forcedShareUrl || shareUrl;
- if (Ember.isEmpty(shareUrl)) {
+ if (isEmpty(shareUrl)) {
return;
}
@@ -41,10 +45,12 @@ export default Ember.Component.extend({
this._super(...arguments);
const shareUrl = this.shareUrl;
- const $linkInput = this.$(".topic-share-url");
- const $linkForTouch = this.$(".topic-share-url-for-touch a");
+ const $linkInput = $(this.element.querySelector(".topic-share-url"));
+ const $linkForTouch = $(
+ this.element.querySelector(".topic-share-url-for-touch a")
+ );
- Ember.run.schedule("afterRender", () => {
+ schedule("afterRender", () => {
if (!this.capabilities.touch) {
$linkForTouch.parent().remove();
diff --git a/app/assets/javascripts/discourse/components/share-popup.js.es6 b/app/assets/javascripts/discourse/components/share-popup.js.es6
index eb878763b7..ba32b26585 100644
--- a/app/assets/javascripts/discourse/components/share-popup.js.es6
+++ b/app/assets/javascripts/discourse/components/share-popup.js.es6
@@ -1,24 +1,25 @@
+import { isEmpty } from "@ember/utils";
+import { bind } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import Component from "@ember/component";
import { wantsNewWindow } from "discourse/lib/intercept-click";
import { longDateNoYear } from "discourse/lib/formatter";
-import {
- default as computed,
- on
-} from "ember-addons/ember-computed-decorators";
+import discourseComputed, { on } from "discourse-common/utils/decorators";
import Sharing from "discourse/lib/sharing";
import { nativeShare } from "discourse/lib/pwa-utils";
-export default Ember.Component.extend({
+export default Component.extend({
elementId: "share-link",
classNameBindings: ["visible"],
link: null,
visible: null,
- @computed
+ @discourseComputed
sources() {
return Sharing.activeSources(this.siteSettings.share_links);
},
- @computed("type", "postNumber")
+ @discourseComputed("type", "postNumber")
shareTitle(type, postNumber) {
if (type === "topic") {
return I18n.t("share.topic");
@@ -29,7 +30,7 @@ export default Ember.Component.extend({
return I18n.t("share.topic");
},
- @computed("date")
+ @discourseComputed("date")
displayDate(date) {
return longDateNoYear(new Date(date));
},
@@ -54,9 +55,9 @@ export default Ember.Component.extend({
_showUrl($target, url) {
const $currentTargetOffset = $target.offset();
- const $this = this.$();
+ const $this = $(this.element);
- if (Ember.isEmpty(url)) {
+ if (isEmpty(url)) {
return;
}
@@ -85,10 +86,10 @@ export default Ember.Component.extend({
if (!this.site.mobileView) {
$this.css({ left: "" + x + "px" });
}
- this.set("link", encodeURI(url));
+ this.set("link", url);
this.set("visible", true);
- Ember.run.scheduleOnce("afterRender", this, this._focusUrl);
+ scheduleOnce("afterRender", this, this._focusUrl);
},
_mouseDownHandler(event) {
@@ -153,9 +154,9 @@ export default Ember.Component.extend({
@on("init")
_setupHandlers() {
- this._boundMouseDownHandler = Ember.run.bind(this, this._mouseDownHandler);
- this._boundClickHandler = Ember.run.bind(this, this._clickHandler);
- this._boundKeydownHandler = Ember.run.bind(this, this._keydownHandler);
+ this._boundMouseDownHandler = bind(this, this._mouseDownHandler);
+ this._boundClickHandler = bind(this, this._clickHandler);
+ this._boundKeydownHandler = bind(this, this._keydownHandler);
},
didInsertElement() {
@@ -170,7 +171,7 @@ export default Ember.Component.extend({
)
.on("keydown.share-view", this._boundKeydownHandler);
- this.appEvents.on("share:url", this._shareUrlHandler);
+ this.appEvents.on("share:url", this, "_shareUrlHandler");
},
willDestroyElement() {
@@ -181,7 +182,7 @@ export default Ember.Component.extend({
.off("mousedown.outside-share-link", this._boundMouseDownHandler)
.off("keydown.share-view", this._boundKeydownHandler);
- this.appEvents.off("share:url", this._shareUrlHandler);
+ this.appEvents.off("share:url", this, "_shareUrlHandler");
},
actions: {
diff --git a/app/assets/javascripts/discourse/components/share-source.js.es6 b/app/assets/javascripts/discourse/components/share-source.js.es6
index 0eb7020886..1ce8f6e5da 100644
--- a/app/assets/javascripts/discourse/components/share-source.js.es6
+++ b/app/assets/javascripts/discourse/components/share-source.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
classNameBindings: [":social-link"],
actions: {
diff --git a/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 b/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6
index 002a13e7da..4374380e03 100644
--- a/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6
+++ b/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6
@@ -1,17 +1,18 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "",
publishing: false,
- @computed("topic.destination_category_id")
+ @discourseComputed("topic.destination_category_id")
validCategory(destCatId) {
return destCatId && destCatId !== this.site.shared_drafts_category_id;
},
actions: {
- updateDestinationCategory(category) {
- return this.topic.updateDestinationCategory(category.get("id"));
+ updateDestinationCategory(categoryId) {
+ return this.topic.updateDestinationCategory(categoryId);
},
publish() {
diff --git a/app/assets/javascripts/discourse/components/signup-cta.js.es6 b/app/assets/javascripts/discourse/components/signup-cta.js.es6
index dbbc7ad1d5..7d6f1623e4 100644
--- a/app/assets/javascripts/discourse/components/signup-cta.js.es6
+++ b/app/assets/javascripts/discourse/components/signup-cta.js.es6
@@ -1,4 +1,8 @@
-export default Ember.Component.extend({
+import { later } from "@ember/runloop";
+import Component from "@ember/component";
+import { on } from "@ember/object/evented";
+
+export default Component.extend({
action: "showCreateAccount",
actions: {
@@ -9,16 +13,13 @@ export default Ember.Component.extend({
hideForSession() {
this.session.set("hideSignupCta", true);
this.keyValueStore.setItem("anon-cta-hidden", new Date().getTime());
- Ember.run.later(
- () => this.session.set("showSignupCta", false),
- 20 * 1000
- );
+ later(() => this.session.set("showSignupCta", false), 20 * 1000);
}
},
- _turnOffIfHidden: function() {
+ _turnOffIfHidden: on("willDestroyElement", function() {
if (this.session.get("hideSignupCta")) {
this.session.set("showSignupCta", false);
}
- }.on("willDestroyElement")
+ })
});
diff --git a/app/assets/javascripts/discourse/components/site-header.js.es6 b/app/assets/javascripts/discourse/components/site-header.js.es6
index 98e8dbc6ed..38d42d9ea0 100644
--- a/app/assets/javascripts/discourse/components/site-header.js.es6
+++ b/app/assets/javascripts/discourse/components/site-header.js.es6
@@ -1,5 +1,8 @@
+import { cancel } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
+import { later } from "@ember/runloop";
import MountWidget from "discourse/components/mount-widget";
-import { observes } from "ember-addons/ember-computed-decorators";
+import { observes } from "discourse-common/utils/decorators";
import Docking from "discourse/mixins/docking";
import PanEvents, {
SWIPE_VELOCITY,
@@ -9,10 +12,6 @@ import PanEvents, {
const PANEL_BODY_MARGIN = 30;
-//android supports pulling in from the screen edges
-const SCREEN_EDGE_MARGIN = 20;
-const SCREEN_OFFSET = 300;
-
const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
widget: "header",
docAt: null,
@@ -42,7 +41,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
_animateClosing($panel, menuOrigin, windowWidth) {
$panel.css(menuOrigin, -windowWidth);
this._animate = true;
- Ember.run.schedule("afterRender", () => {
+ schedule("afterRender", () => {
this.eventDispatched("dom:clean", "header");
this._panMenuOffset = 0;
});
@@ -66,7 +65,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
_handlePanDone(offset, event) {
const $window = $(window);
- const windowWidth = parseInt($window.width());
+ const windowWidth = $window.width();
const $menuPanels = $(".menu-panel");
const menuOrigin = this._panMenuOrigin;
this._shouldMenuClose(event, menuOrigin)
@@ -112,8 +111,6 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
panStart(e) {
const center = e.center;
const $centeredElement = $(document.elementFromPoint(center.x, center.y));
- const $window = $(window);
- const windowWidth = parseInt($window.width());
if (
($centeredElement.hasClass("panel-body") ||
$centeredElement.hasClass("header-cloak") ||
@@ -122,30 +119,6 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
) {
e.originalEvent.preventDefault();
this._isPanning = true;
- } else if (
- center.x < SCREEN_EDGE_MARGIN &&
- !this.$(".menu-panel").length &&
- e.direction === "right"
- ) {
- this._animate = false;
- this._panMenuOrigin = "left";
- this._panMenuOffset = -SCREEN_OFFSET;
- this._isPanning = true;
- $("header.d-header").removeClass("scroll-down scroll-up");
- this.eventDispatched(this._leftMenuAction(), "header");
- window.requestAnimationFrame(() => this.panMove(e));
- } else if (
- windowWidth - center.x < SCREEN_EDGE_MARGIN &&
- !this.$(".menu-panel").length &&
- e.direction === "left"
- ) {
- this._animate = false;
- this._panMenuOrigin = "right";
- this._panMenuOffset = -SCREEN_OFFSET;
- this._isPanning = true;
- $("header.d-header").removeClass("scroll-down scroll-up");
- this.eventDispatched(this._rightMenuAction(), "header");
- window.requestAnimationFrame(() => this.panMove(e));
} else {
this._isPanning = false;
}
@@ -224,7 +197,6 @@ 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", this, "setTopic");
@@ -235,24 +207,17 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
this.dispatch("search-autocomplete:after-complete", "search-term");
this.appEvents.on("dom:clean", this, "_cleanDom");
-
- // 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"));
- }
},
_cleanDom() {
// For performance, only trigger a re-render if any menu panels are visible
- if (this.$(".menu-panel").length) {
+ if (this.element.querySelector(".menu-panel")) {
this.eventDispatched("dom:clean", "header");
}
},
willDestroyElement() {
this._super(...arguments);
- const { isAndroid } = this.capabilities;
$("body").off("keydown.header");
$(window).off("resize.discourse-menu-panel");
@@ -260,11 +225,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
this.appEvents.off("header:hide-topic", this, "setTopic");
this.appEvents.off("dom:clean", this, "_cleanDom");
- if (isAndroid) {
- this.removeTouchListeners($("body"));
- }
-
- Ember.run.cancel(this._scheduledRemoveAnimate);
+ cancel(this._scheduledRemoveAnimate);
window.cancelAnimationFrame(this._scheduledMovingAnimation);
},
@@ -285,16 +246,16 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
}
const $window = $(window);
- const windowWidth = parseInt($window.width());
+ const windowWidth = $window.width();
const headerWidth = $("#main-outlet .container").width() || 1100;
- const remaining = parseInt((windowWidth - headerWidth) / 2);
+ const remaining = (windowWidth - headerWidth) / 2;
const viewMode = remaining < 50 ? "slide-in" : "drop-down";
$menuPanels.each((idx, panel) => {
const $panel = $(panel);
const $headerCloak = $(".header-cloak");
- let width = parseInt($panel.attr("data-max-width") || 300);
+ let width = parseInt($panel.attr("data-max-width"), 10) || 300;
if (windowWidth - width < 50) {
width = windowWidth - 50;
}
@@ -319,8 +280,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
const $panelBody = $(".panel-body", $panel);
// 2 pixel fudge allows for firefox subpixel sizing stuff causing scrollbar
- let contentHeight =
- parseInt($(".panel-body-contents", $panel).height()) + 2;
+ let contentHeight = $(".panel-body-contents", $panel).height() + 2;
// We use a mutationObserver to check for style changes, so it's important
// we don't set it if it doesn't change. Same goes for the $panelBody!
@@ -339,7 +299,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
}
// adjust panel height
- const fullHeight = parseInt($window.height());
+ const fullHeight = $window.height();
const offsetTop = $panel.offset().top;
const scrollTop = $window.scrollTop();
@@ -388,7 +348,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
if (this._animate) {
$panel.addClass("animate");
$headerCloak.addClass("animate");
- this._scheduledRemoveAnimate = Ember.run.later(() => {
+ this._scheduledRemoveAnimate = later(() => {
$panel.removeClass("animate");
$headerCloak.removeClass("animate");
}, 200);
@@ -404,16 +364,20 @@ export default SiteHeaderComponent;
export function headerHeight() {
const $header = $("header.d-header");
+
+ // Header may not exist in tests (e.g. in the user menu component test).
+ if ($header.length === 0) {
+ return 0;
+ }
+
const headerOffset = $header.offset();
const headerOffsetTop = headerOffset ? headerOffset.top : 0;
- return parseInt(
- $header.outerHeight() + headerOffsetTop - $(window).scrollTop()
- );
+ return $header.outerHeight() + headerOffsetTop - $(window).scrollTop();
}
export function headerTop() {
const $header = $("header.d-header");
const headerOffset = $header.offset();
const headerOffsetTop = headerOffset ? headerOffset.top : 0;
- return parseInt(headerOffsetTop - $(window).scrollTop());
+ return headerOffsetTop - $(window).scrollTop();
}
diff --git a/app/assets/javascripts/discourse/components/suggested-topics.js.es6 b/app/assets/javascripts/discourse/components/suggested-topics.js.es6
index da1ae4f00a..53986a8303 100644
--- a/app/assets/javascripts/discourse/components/suggested-topics.js.es6
+++ b/app/assets/javascripts/discourse/components/suggested-topics.js.es6
@@ -1,22 +1,24 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { get } from "@ember/object";
+import Component from "@ember/component";
import { categoryBadgeHTML } from "discourse/helpers/category-link";
-import { iconHTML } from "discourse-common/lib/icon-library";
+import Site from "discourse/models/site";
+import { computed } from "@ember/object";
-export default Ember.Component.extend({
+export default Component.extend({
elementId: "suggested-topics",
classNames: ["suggested-topics"],
- @computed("topic")
- suggestedTitle(topic) {
- const href = this.currentUser && this.currentUser.pmPath(topic);
- return topic.get("isPrivateMessage") && href
- ? `
${iconHTML("envelope", {
- class: "private-message-glyph"
- })} ${I18n.t("suggested_topics.pm_title")} `
- : I18n.t("suggested_topics.title");
- },
+ suggestedTitleLabel: computed("topic", function() {
+ const href = this.currentUser && this.currentUser.pmPath(this.topic);
+ if (this.topic.get("isPrivateMessage") && href) {
+ return "suggested_topics.pm_title";
+ } else {
+ return "suggested_topics.title";
+ }
+ }),
- @computed("topic", "topicTrackingState.messageCount")
+ @discourseComputed("topic", "topicTrackingState.messageCount")
browseMoreMessage(topic) {
// TODO decide what to show for pms
if (topic.get("isPrivateMessage")) {
@@ -32,8 +34,7 @@ export default Ember.Component.extend({
if (
category &&
- Ember.get(category, "id") ===
- Discourse.Site.currentProp("uncategorized_category_id")
+ get(category, "id") === Site.currentProp("uncategorized_category_id")
) {
category = null;
}
diff --git a/app/assets/javascripts/discourse/components/tag-drop-link.js.es6 b/app/assets/javascripts/discourse/components/tag-drop-link.js.es6
index ab41561429..d26c68d0b3 100644
--- a/app/assets/javascripts/discourse/components/tag-drop-link.js.es6
+++ b/app/assets/javascripts/discourse/components/tag-drop-link.js.es6
@@ -1,7 +1,8 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
import DiscourseURL from "discourse/lib/url";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "a",
classNameBindings: [
":tag-badge-wrapper",
@@ -11,16 +12,16 @@ export default Ember.Component.extend({
],
attributeBindings: ["href"],
- @computed("tagId", "category")
+ @discourseComputed("tagId", "category")
href(tagId, category) {
- var url = "/tags";
if (category) {
- url += category.url;
+ return "/tags" + category.url + "/" + tagId;
+ } else {
+ return "/tag/" + tagId;
}
- return url + "/" + tagId;
},
- @computed("tagId")
+ @discourseComputed("tagId")
tagClass(tagId) {
return "tag-" + tagId;
},
diff --git a/app/assets/javascripts/discourse/components/tag-groups-form.js.es6 b/app/assets/javascripts/discourse/components/tag-groups-form.js.es6
new file mode 100644
index 0000000000..b77220cd08
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/tag-groups-form.js.es6
@@ -0,0 +1,70 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import Component from "@ember/component";
+import { bufferedProperty } from "discourse/mixins/buffered-content";
+import PermissionType from "discourse/models/permission-type";
+
+export default Component.extend(bufferedProperty("model"), {
+ tagName: "",
+
+ @discourseComputed("buffered.isSaving", "buffered.name", "buffered.tag_names")
+ savingDisabled(isSaving, name, tagNames) {
+ return isSaving || isEmpty(name) || isEmpty(tagNames);
+ },
+
+ actions: {
+ setPermissions(permissionName) {
+ if (permissionName === "private") {
+ this.buffered.set("permissions", {
+ staff: PermissionType.FULL
+ });
+ } else if (permissionName === "visible") {
+ this.buffered.set("permissions", {
+ staff: PermissionType.FULL,
+ everyone: PermissionType.READONLY
+ });
+ } else {
+ this.buffered.set("permissions", {
+ everyone: PermissionType.FULL
+ });
+ }
+ },
+
+ save() {
+ const attrs = this.buffered.getProperties(
+ "name",
+ "tag_names",
+ "parent_tag_name",
+ "one_per_topic",
+ "permissions"
+ );
+
+ this.model.save(attrs).then(() => {
+ this.commitBuffer();
+
+ if (this.onSave) {
+ this.onSave();
+ }
+ });
+ },
+
+ destroy() {
+ return bootbox.confirm(
+ I18n.t("tagging.groups.confirm_delete"),
+ I18n.t("no_value"),
+ I18n.t("yes_value"),
+ destroy => {
+ if (!destroy) {
+ return;
+ }
+
+ this.model.destroyRecord().then(() => {
+ if (this.onDestroy) {
+ this.onDestroy();
+ }
+ });
+ }
+ );
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/tag-info.js.es6 b/app/assets/javascripts/discourse/components/tag-info.js.es6
new file mode 100644
index 0000000000..5f2d0e7d6c
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/tag-info.js.es6
@@ -0,0 +1,134 @@
+import { ajax } from "discourse/lib/ajax";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import showModal from "discourse/lib/show-modal";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
+import { reads, and } from "@ember/object/computed";
+import { isEmpty } from "@ember/utils";
+
+export default Component.extend({
+ tagName: "",
+ loading: false,
+ tagInfo: null,
+ newSynonyms: null,
+ showEditControls: false,
+ canAdminTag: reads("currentUser.staff"),
+ editSynonymsMode: and("canAdminTag", "showEditControls"),
+
+ @discourseComputed("tagInfo.tag_group_names")
+ tagGroupsInfo(tagGroupNames) {
+ return I18n.t("tagging.tag_groups_info", {
+ count: tagGroupNames.length,
+ tag_groups: tagGroupNames.join(", ")
+ });
+ },
+
+ @discourseComputed("tagInfo.categories")
+ categoriesInfo(categories) {
+ return I18n.t("tagging.category_restrictions", {
+ count: categories.length
+ });
+ },
+
+ @discourseComputed(
+ "tagInfo.tag_group_names",
+ "tagInfo.categories",
+ "tagInfo.synonyms"
+ )
+ nothingToShow(tagGroupNames, categories, synonyms) {
+ return isEmpty(tagGroupNames) && isEmpty(categories) && isEmpty(synonyms);
+ },
+
+ didInsertElement() {
+ this._super(...arguments);
+ this.loadTagInfo();
+ },
+
+ loadTagInfo() {
+ if (this.loading) {
+ return;
+ }
+ this.set("loading", true);
+ return this.store
+ .find("tag-info", this.tag.id)
+ .then(result => {
+ this.set("tagInfo", result);
+ this.set(
+ "tagInfo.synonyms",
+ result.synonyms.map(s => this.store.createRecord("tag", s))
+ );
+ })
+ .finally(() => this.set("loading", false))
+ .catch(popupAjaxError);
+ },
+
+ actions: {
+ toggleEditControls() {
+ this.toggleProperty("showEditControls");
+ },
+
+ renameTag() {
+ showModal("rename-tag", { model: this.tag });
+ },
+
+ deleteTag() {
+ this.sendAction("deleteAction", this.tagInfo);
+ },
+
+ unlinkSynonym(tag) {
+ ajax(`/tag/${this.tagInfo.name}/synonyms/${tag.id}`, {
+ type: "DELETE"
+ })
+ .then(() => this.tagInfo.synonyms.removeObject(tag))
+ .catch(popupAjaxError);
+ },
+
+ deleteSynonym(tag) {
+ bootbox.confirm(
+ I18n.t("tagging.delete_synonym_confirm", { tag_name: tag.text }),
+ result => {
+ if (!result) return;
+
+ tag
+ .destroyRecord()
+ .then(() => this.tagInfo.synonyms.removeObject(tag))
+ .catch(popupAjaxError);
+ }
+ );
+ },
+
+ addSynonyms() {
+ bootbox.confirm(
+ I18n.t("tagging.add_synonyms_explanation", {
+ count: this.newSynonyms.length,
+ tag_name: this.tagInfo.name
+ }),
+ result => {
+ if (!result) return;
+
+ ajax(`/tag/${this.tagInfo.name}/synonyms`, {
+ type: "POST",
+ data: {
+ synonyms: this.newSynonyms
+ }
+ })
+ .then(response => {
+ if (response.success) {
+ this.set("newSynonyms", null);
+ this.loadTagInfo();
+ } else if (response.failed_tags) {
+ bootbox.alert(
+ I18n.t("tagging.add_synonyms_failed", {
+ tag_names: Object.keys(response.failed_tags).join(", ")
+ })
+ );
+ } else {
+ bootbox.alert(I18n.t("generic_error"));
+ }
+ })
+ .catch(popupAjaxError);
+ }
+ );
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/tag-list.js.es6 b/app/assets/javascripts/discourse/components/tag-list.js.es6
index a6578bb398..d02bd3b8ca 100644
--- a/app/assets/javascripts/discourse/components/tag-list.js.es6
+++ b/app/assets/javascripts/discourse/components/tag-list.js.es6
@@ -1,23 +1,42 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { sort } from "@ember/object/computed";
+import Component from "@ember/component";
+import Category from "discourse/models/category";
-export default Ember.Component.extend({
- classNameBindings: [":tag-list", "categoryClass"],
+export default Component.extend({
+ classNameBindings: [
+ ":tags-list",
+ ":tag-list",
+ "categoryClass",
+ "tagGroupNameClass"
+ ],
isPrivateMessage: false,
- sortedTags: Ember.computed.sort("tags", "sortProperties"),
+ sortedTags: sort("tags", "sortProperties"),
- @computed("titleKey")
+ @discourseComputed("titleKey")
title(titleKey) {
return titleKey && I18n.t(titleKey);
},
- @computed("categoryId")
+ @discourseComputed("categoryId")
category(categoryId) {
- return categoryId && Discourse.Category.findById(categoryId);
+ return categoryId && Category.findById(categoryId);
},
- @computed("category.fullSlug")
+ @discourseComputed("category.fullSlug")
categoryClass(slug) {
return slug && `tag-list-${slug}`;
+ },
+
+ @discourseComputed("tagGroupName")
+ tagGroupNameClass(groupName) {
+ if (groupName) {
+ groupName = groupName
+ .replace(/\s+/g, "-")
+ .replace(/[!\"#$%&'\(\)\*\+,\.\/:;<=>\?\@\[\\\]\^`\{\|\}~]/g, "")
+ .toLowerCase();
+ return groupName && `tag-group-${groupName}`;
+ }
}
});
diff --git a/app/assets/javascripts/discourse/components/tags-admin-dropdown.js.es6 b/app/assets/javascripts/discourse/components/tags-admin-dropdown.js.es6
index 85e8caf025..58f28b3796 100644
--- a/app/assets/javascripts/discourse/components/tags-admin-dropdown.js.es6
+++ b/app/assets/javascripts/discourse/components/tags-admin-dropdown.js.es6
@@ -1,50 +1,41 @@
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
+import { computed } from "@ember/object";
export default DropdownSelectBoxComponent.extend({
pluginApiIdentifiers: ["tags-admin-dropdown"],
- classNames: "tags-admin-dropdown",
- showFullTitle: false,
- allowInitialValueMutation: false,
+ classNames: ["tags-admin-dropdown"],
actionsMapping: null,
- init() {
- this._super(...arguments);
-
- this.headerIcon = ["bars", "caret-down"];
+ selectKitOptions: {
+ icons: ["bars", "caret-down"],
+ showFullTitle: false
},
- autoHighlight() {},
-
- computeContent() {
- const items = [
+ content: computed(function() {
+ return [
{
id: "manageGroups",
name: I18n.t("tagging.manage_groups"),
description: I18n.t("tagging.manage_groups_description"),
- icon: "wrench",
- __sk_row_type: "noopRow"
+ icon: "wrench"
},
{
id: "uploadTags",
name: I18n.t("tagging.upload"),
description: I18n.t("tagging.upload_description"),
- icon: "upload",
- __sk_row_type: "noopRow"
+ icon: "upload"
},
{
id: "deleteUnusedTags",
name: I18n.t("tagging.delete_unused"),
description: I18n.t("tagging.delete_unused_description"),
- icon: "trash",
- __sk_row_type: "noopRow"
+ icon: "trash-alt"
}
];
-
- return items;
- },
+ }),
actions: {
- onSelect(id) {
+ onChange(id) {
const action = this.actionsMapping[id];
if (action) {
diff --git a/app/assets/javascripts/discourse/components/tap-tile-grid.js.es6 b/app/assets/javascripts/discourse/components/tap-tile-grid.js.es6
new file mode 100644
index 0000000000..37d5fda33a
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/tap-tile-grid.js.es6
@@ -0,0 +1,6 @@
+import Component from "@ember/component";
+
+export default Component.extend({
+ classNames: ["tap-tile-grid"],
+ activeTile: null
+});
diff --git a/app/assets/javascripts/discourse/components/tap-tile.js.es6 b/app/assets/javascripts/discourse/components/tap-tile.js.es6
new file mode 100644
index 0000000000..a7dfbe267c
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/tap-tile.js.es6
@@ -0,0 +1,12 @@
+import Component from "@ember/component";
+import { propertyEqual } from "discourse/lib/computed";
+
+export default Component.extend({
+ classNames: ["tap-tile"],
+ classNameBindings: ["active"],
+ click() {
+ this.onSelect(this.tileId);
+ },
+
+ active: propertyEqual("activeTile", "tileId")
+});
diff --git a/app/assets/javascripts/discourse/components/text-field.js.es6 b/app/assets/javascripts/discourse/components/text-field.js.es6
index eca66af1d3..1978cd0ab2 100644
--- a/app/assets/javascripts/discourse/components/text-field.js.es6
+++ b/app/assets/javascripts/discourse/components/text-field.js.es6
@@ -1,4 +1,4 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
import { siteDir, isRTL, isLTR } from "discourse/lib/text-direction";
export default Ember.TextField.extend({
@@ -10,7 +10,7 @@ export default Ember.TextField.extend({
"dir"
],
- @computed
+ @discourseComputed
dir() {
if (this.siteSettings.support_mixed_text_direction) {
let val = this.value;
@@ -37,7 +37,7 @@ export default Ember.TextField.extend({
}
},
- @computed("placeholderKey")
+ @discourseComputed("placeholderKey")
placeholder: {
get() {
if (this._placeholder) return this._placeholder;
diff --git a/app/assets/javascripts/discourse/components/text-overflow.js.es6 b/app/assets/javascripts/discourse/components/text-overflow.js.es6
index 402b3a9cf1..90a9cc21a2 100644
--- a/app/assets/javascripts/discourse/components/text-overflow.js.es6
+++ b/app/assets/javascripts/discourse/components/text-overflow.js.es6
@@ -1,10 +1,13 @@
-export default Ember.Component.extend({
+import { next } from "@ember/runloop";
+import Component from "@ember/component";
+export default Component.extend({
didInsertElement() {
this._super(...arguments);
- Ember.run.next(null, () => {
- const $this = this.$();
+ next(null, () => {
+ const $this = $(this.element);
if ($this) {
+ $this.find("br").replaceWith(" ");
$this.find("hr").remove();
$this.ellipsis();
}
diff --git a/app/assets/javascripts/discourse/components/time-input.js.es6 b/app/assets/javascripts/discourse/components/time-input.js.es6
new file mode 100644
index 0000000000..e636380a77
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/time-input.js.es6
@@ -0,0 +1,76 @@
+import { oneWay, or } from "@ember/object/computed";
+import { schedule } from "@ember/runloop";
+import Component from "@ember/component";
+import { isNumeric } from "discourse/lib/utilities";
+
+export default Component.extend({
+ classNames: ["d-time-input"],
+ hours: null,
+ minutes: null,
+ _hours: oneWay("hours"),
+ _minutes: oneWay("minutes"),
+ isSafari: oneWay("capabilities.isSafari"),
+ isMobile: oneWay("site.mobileView"),
+ nativePicker: or("isSafari", "isMobile"),
+
+ actions: {
+ onInput(options, event) {
+ event.preventDefault();
+
+ if (this.onChange) {
+ let value = event.target.value;
+
+ if (!isNumeric(value)) {
+ value = 0;
+ } else {
+ value = parseInt(value, 10);
+ }
+
+ if (options.prop === "hours") {
+ value = Math.max(0, Math.min(value, 23))
+ .toString()
+ .padStart(2, "0");
+ this._processHoursChange(value);
+ } else {
+ value = Math.max(0, Math.min(value, 59))
+ .toString()
+ .padStart(2, "0");
+ this._processMinutesChange(value);
+ }
+
+ schedule("afterRender", () => (event.target.value = value));
+ }
+ },
+
+ onFocusIn(value, event) {
+ if (value && event.target) {
+ event.target.select();
+ }
+ },
+
+ onChangeTime(event) {
+ const time = event.target.value;
+
+ if (time && this.onChange) {
+ this.onChange({
+ hours: time.split(":")[0],
+ minutes: time.split(":")[1]
+ });
+ }
+ }
+ },
+
+ _processHoursChange(hours) {
+ this.onChange({
+ hours,
+ minutes: this._minutes || "00"
+ });
+ },
+
+ _processMinutesChange(minutes) {
+ this.onChange({
+ hours: this._hours || "00",
+ minutes
+ });
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/top-period-buttons.js.es6 b/app/assets/javascripts/discourse/components/top-period-buttons.js.es6
index 059b1a1468..0eaf69a992 100644
--- a/app/assets/javascripts/discourse/components/top-period-buttons.js.es6
+++ b/app/assets/javascripts/discourse/components/top-period-buttons.js.es6
@@ -1,9 +1,10 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
classNames: ["top-title-buttons"],
- @computed("period")
+ @discourseComputed("period")
periods(period) {
return this.site.get("periods").filter(p => p !== period);
},
diff --git a/app/assets/javascripts/discourse/components/topic-category.js.es6 b/app/assets/javascripts/discourse/components/topic-category.js.es6
index 97fcde13cd..3f003c8d77 100644
--- a/app/assets/javascripts/discourse/components/topic-category.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-category.js.es6
@@ -1,2 +1,3 @@
+import Component from "@ember/component";
// Injections don't occur without a class
-export default Ember.Component.extend();
+export default Component.extend();
diff --git a/app/assets/javascripts/discourse/components/topic-entrance.js.es6 b/app/assets/javascripts/discourse/components/topic-entrance.js.es6
index e90ccd5857..b27a7fe15d 100644
--- a/app/assets/javascripts/discourse/components/topic-entrance.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-entrance.js.es6
@@ -1,6 +1,8 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { scheduleOnce } from "@ember/runloop";
+import Component from "@ember/component";
import DiscourseURL from "discourse/lib/url";
import CleansUp from "discourse/mixins/cleans-up";
-import computed from "ember-addons/ember-computed-decorators";
function entranceDate(dt, showTime) {
const today = new Date();
@@ -25,30 +27,30 @@ function entranceDate(dt, showTime) {
);
}
-export default Ember.Component.extend(CleansUp, {
+export default Component.extend(CleansUp, {
elementId: "topic-entrance",
classNameBindings: ["visible::hidden"],
_position: null,
topic: null,
visible: null,
- @computed("topic.created_at")
+ @discourseComputed("topic.created_at")
createdDate: createdAt => new Date(createdAt),
- @computed("topic.bumped_at")
+ @discourseComputed("topic.bumped_at")
bumpedDate: bumpedAt => new Date(bumpedAt),
- @computed("createdDate", "bumpedDate")
+ @discourseComputed("createdDate", "bumpedDate")
showTime(createdDate, bumpedDate) {
return (
bumpedDate.getTime() - createdDate.getTime() < 1000 * 60 * 60 * 24 * 2
);
},
- @computed("createdDate", "showTime")
+ @discourseComputed("createdDate", "showTime")
topDate: (createdDate, showTime) => entranceDate(createdDate, showTime),
- @computed("bumpedDate", "showTime")
+ @discourseComputed("bumpedDate", "showTime")
bottomDate: (bumpedDate, showTime) => entranceDate(bumpedDate, showTime),
didInsertElement() {
@@ -61,8 +63,8 @@ export default Ember.Component.extend(CleansUp, {
const $self = $(this.element);
const width = $self.width();
const height = $self.height();
- pos.left = parseInt(pos.left) - width / 2;
- pos.top = parseInt(pos.top) - height / 2;
+ pos.left = parseInt(pos.left, 10) - width / 2;
+ pos.top = parseInt(pos.top, 10) - height / 2;
const windowWidth = $(window).width();
if (pos.left + width > windowWidth) {
@@ -76,7 +78,7 @@ export default Ember.Component.extend(CleansUp, {
this.setProperties({ topic: data.topic, visible: true });
- Ember.run.scheduleOnce("afterRender", this, this._setCSS);
+ scheduleOnce("afterRender", this, this._setCSS);
$("html")
.off("mousedown.topic-entrance")
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 0c487e7353..d37ff5c28f 100644
--- a/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6
@@ -1,67 +1,53 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { alias, or, and } from "@ember/object/computed";
+import Component from "@ember/component";
import { getTopicFooterButtons } from "discourse/lib/register-topic-footer-button";
-export default Ember.Component.extend({
+export default Component.extend({
elementId: "topic-footer-buttons",
// Allow us to extend it
layoutName: "components/topic-footer-buttons",
- @computed("topic.isPrivateMessage")
+ @discourseComputed("topic.isPrivateMessage")
canArchive(isPM) {
return this.siteSettings.enable_personal_messages && isPM;
},
buttons: getTopicFooterButtons(),
- @computed("buttons.[]")
+ @discourseComputed("buttons.[]")
inlineButtons(buttons) {
return buttons.filter(button => !button.dropdown);
},
// topic.assigned_to_user is for backward plugin support
- @computed("buttons.[]", "topic.assigned_to_user")
+ @discourseComputed("buttons.[]", "topic.assigned_to_user")
dropdownButtons(buttons) {
return buttons.filter(button => button.dropdown);
},
- @computed("topic.isPrivateMessage")
+ @discourseComputed("topic.isPrivateMessage")
showNotificationsButton(isPM) {
return !isPM || this.siteSettings.enable_personal_messages;
},
- canInviteTo: Ember.computed.alias("topic.details.can_invite_to"),
+ canInviteTo: alias("topic.details.can_invite_to"),
- canDefer: Ember.computed.alias("currentUser.enable_defer"),
+ canDefer: alias("currentUser.enable_defer"),
- inviteDisabled: Ember.computed.or(
- "topic.archived",
- "topic.closed",
- "topic.deleted"
- ),
+ inviteDisabled: or("topic.archived", "topic.closed", "topic.deleted"),
- @computed
- showAdminButton() {
- return (
- !this.site.mobileView &&
- this.currentUser &&
- this.currentUser.get("canManageTopic")
- );
- },
+ showEditOnFooter: and("topic.isPrivateMessage", "site.can_tag_pms"),
- showEditOnFooter: Ember.computed.and(
- "topic.isPrivateMessage",
- "site.can_tag_pms"
- ),
-
- @computed("topic.message_archived")
+ @discourseComputed("topic.message_archived")
archiveIcon: archived => (archived ? "envelope" : "folder"),
- @computed("topic.message_archived")
+ @discourseComputed("topic.message_archived")
archiveTitle: archived =>
archived ? "topic.move_to_inbox.help" : "topic.archive_message.help",
- @computed("topic.message_archived")
+ @discourseComputed("topic.message_archived")
archiveLabel: archived =>
archived ? "topic.move_to_inbox.title" : "topic.archive_message.title"
});
diff --git a/app/assets/javascripts/discourse/components/topic-join-group-notice.js.es6 b/app/assets/javascripts/discourse/components/topic-join-group-notice.js.es6
new file mode 100644
index 0000000000..07db8b0fa8
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/topic-join-group-notice.js.es6
@@ -0,0 +1,18 @@
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
+
+export default Component.extend({
+ classNames: ["topic-notice"],
+
+ @discourseComputed("model.group.{full_name,name,allow_membership_requests}")
+ accessViaGroupText(group) {
+ const name = group.full_name || group.name;
+ const suffix = group.allow_membership_requests ? "request" : "join";
+ return I18n.t(`topic.group_${suffix}`, { name });
+ },
+
+ @discourseComputed("model.group.allow_membership_requests")
+ accessViaGroupButtonText(allowRequest) {
+ return `groups.${allowRequest ? "request" : "join"}`;
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/topic-list-item.js.es6
index cc0c07d801..7bc2d64603 100644
--- a/app/assets/javascripts/discourse/components/topic-list-item.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-list-item.js.es6
@@ -1,8 +1,11 @@
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
+import { alias } from "@ember/object/computed";
+import Component from "@ember/component";
+import { schedule } from "@ember/runloop";
import DiscourseURL from "discourse/lib/url";
-import computed from "ember-addons/ember-computed-decorators";
-import { bufferedRender } from "discourse-common/lib/buffered-render";
import { findRawTemplate } from "discourse/lib/raw-templates";
import { wantsNewWindow } from "discourse/lib/intercept-click";
+import { on } from "@ember/object/evented";
export function showEntrance(e) {
let target = $(e.target);
@@ -29,20 +32,74 @@ export function navigateToTopic(topic, href) {
return false;
}
-export const ListItemDefaults = {
+export default Component.extend({
tagName: "tr",
classNameBindings: [":topic-list-item", "unboundClassNames", "topic.visited"],
attributeBindings: ["data-topic-id"],
- "data-topic-id": Ember.computed.alias("topic.id"),
+ "data-topic-id": alias("topic.id"),
- @computed
+ didReceiveAttrs() {
+ this._super(...arguments);
+ this.renderTopicListItem();
+ },
+
+ @observes("topic.pinned")
+ renderTopicListItem() {
+ const template = findRawTemplate("list/topic-list-item");
+ if (template) {
+ this.set("topicListItemContents", template(this).htmlSafe());
+ }
+ },
+
+ didInsertElement() {
+ this._super(...arguments);
+
+ if (this.includeUnreadIndicator) {
+ this.messageBus.subscribe(this.unreadIndicatorChannel, data => {
+ const nodeClassList = document.querySelector(
+ `.indicator-topic-${data.topic_id}`
+ ).classList;
+
+ if (data.show_indicator) {
+ nodeClassList.remove("read");
+ } else {
+ nodeClassList.add("read");
+ }
+ });
+ }
+ },
+
+ willDestroyElement() {
+ this._super(...arguments);
+
+ if (this.includeUnreadIndicator) {
+ this.messageBus.unsubscribe(this.unreadIndicatorChannel);
+ }
+ },
+
+ @discourseComputed("topic.id")
+ unreadIndicatorChannel(topicId) {
+ return `/private-messages/unread-indicator/${topicId}`;
+ },
+
+ @discourseComputed("topic.unread_by_group_member")
+ unreadClass(unreadByGroupMember) {
+ return unreadByGroupMember ? "" : "read";
+ },
+
+ @discourseComputed("topic.unread_by_group_member")
+ includeUnreadIndicator(unreadByGroupMember) {
+ return typeof unreadByGroupMember !== "undefined";
+ },
+
+ @discourseComputed
newDotText() {
return this.currentUser && this.currentUser.trust_level > 0
? ""
: I18n.t("filters.new.lower_title");
},
- @computed("topic", "lastVisitedTopic")
+ @discourseComputed("topic", "lastVisitedTopic")
unboundClassNames(topic, lastVisitedTopic) {
let classes = [];
@@ -87,7 +144,7 @@ export const ListItemDefaults = {
return this.get("topic.op_like_count") > 0;
},
- @computed
+ @discourseComputed
expandPinned: function() {
const pinned = this.get("topic.pinned");
if (!pinned) {
@@ -150,18 +207,32 @@ export const ListItemDefaults = {
return this.unhandledRowClick(e, topic);
},
+ actions: {
+ toggleBookmark() {
+ this.topic.toggleBookmark().finally(() => this.renderTopicListItem());
+ }
+ },
+
+ unhandledRowClick() {},
+
navigateToTopic,
highlight(opts = { isLastViewedTopic: false }) {
- const $topic = this.$();
- $topic
- .addClass("highlighted")
- .attr("data-islastviewedtopic", opts.isLastViewedTopic);
+ schedule("afterRender", () => {
+ if (!this.element || this.isDestroying || this.isDestroyed) {
+ return;
+ }
- $topic.on("animationend", () => $topic.removeClass("highlighted"));
+ const $topic = $(this.element);
+ $topic
+ .addClass("highlighted")
+ .attr("data-islastviewedtopic", opts.isLastViewedTopic);
+
+ $topic.on("animationend", () => $topic.removeClass("highlighted"));
+ });
},
- _highlightIfNeeded: function() {
+ _highlightIfNeeded: on("didInsertElement", function() {
// highlight the last topic viewed
if (this.session.get("lastTopicIdViewed") === this.get("topic.id")) {
this.session.set("lastTopicIdViewed", null);
@@ -171,28 +242,5 @@ export const ListItemDefaults = {
this.set("topic.highlight", false);
this.highlight();
}
- }.on("didInsertElement")
-};
-
-export default Ember.Component.extend(
- ListItemDefaults,
- bufferedRender({
- rerenderTriggers: ["bulkSelectEnabled", "topic.pinned"],
-
- actions: {
- toggleBookmark() {
- this.topic.toggleBookmark().finally(() => this.rerenderBuffer());
- }
- },
-
- buildBuffer(buffer) {
- const template = findRawTemplate("list/topic-list-item");
- if (template) {
- buffer.push(template(this));
- }
- },
-
- // Can be overwritten by plugins to handle clicks on other parts of the row
- unhandledRowClick() {}
})
-);
+});
diff --git a/app/assets/javascripts/discourse/components/topic-list.js.es6 b/app/assets/javascripts/discourse/components/topic-list.js.es6
index 44846d5b0d..b4661eb68d 100644
--- a/app/assets/javascripts/discourse/components/topic-list.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-list.js.es6
@@ -1,42 +1,44 @@
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import { alias, reads } from "@ember/object/computed";
+import { schedule } from "@ember/runloop";
+import Component from "@ember/component";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
+import LoadMore from "discourse/mixins/load-more";
+import { on } from "@ember/object/evented";
-export default Ember.Component.extend({
+export default Component.extend(LoadMore, {
tagName: "table",
classNames: ["topic-list"],
showTopicPostBadges: true,
listTitle: "topic.title",
// Overwrite this to perform client side filtering of topics, if desired
- filteredTopics: Ember.computed.alias("topics"),
+ filteredTopics: alias("topics"),
- _init: function() {
+ _init: on("init", function() {
this.addObserver("hideCategory", this.rerender);
this.addObserver("order", this.rerender);
this.addObserver("ascending", this.rerender);
this.refreshLastVisited();
- }.on("init"),
+ }),
- @computed("bulkSelectEnabled")
+ @discourseComputed("bulkSelectEnabled")
toggleInTitle(bulkSelectEnabled) {
return !bulkSelectEnabled && this.canBulkSelect;
},
- @computed
+ @discourseComputed
sortable() {
return !!this.changeSort;
},
- skipHeader: Ember.computed.reads("site.mobileView"),
+ skipHeader: reads("site.mobileView"),
- @computed("order")
+ @discourseComputed("order")
showLikes(order) {
return order === "likes";
},
- @computed("order")
+ @discourseComputed("order")
showOpLikes(order) {
return order === "op_likes";
},
@@ -54,6 +56,28 @@ export default Ember.Component.extend({
this.refreshLastVisited();
},
+ scrolled() {
+ this._super(...arguments);
+ let onScroll = this.onScroll;
+ if (!onScroll) return;
+
+ onScroll.call(this);
+ },
+
+ scrollToLastPosition() {
+ if (!this.scrollOnLoad) return;
+
+ let scrollTo = this.session.get("topicListScrollPosition");
+ if (scrollTo && scrollTo >= 0) {
+ schedule("afterRender", () => $(window).scrollTop(scrollTo + 1));
+ }
+ },
+
+ didInsertElement() {
+ this._super(...arguments);
+ this.scrollToLastPosition();
+ },
+
_updateLastVisitedTopic(topics, order, ascending, top) {
this.set("lastVisitedTopic", null);
diff --git a/app/assets/javascripts/discourse/components/topic-navigation.js.es6 b/app/assets/javascripts/discourse/components/topic-navigation.js.es6
index 28cf8b4ae7..bff5f73a2e 100644
--- a/app/assets/javascripts/discourse/components/topic-navigation.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-navigation.js.es6
@@ -1,4 +1,7 @@
-import { observes } from "ember-addons/ember-computed-decorators";
+import EmberObject from "@ember/object";
+import { debounce, later } from "@ember/runloop";
+import Component from "@ember/component";
+import { observes } from "discourse-common/utils/decorators";
import showModal from "discourse/lib/show-modal";
import PanEvents, {
SWIPE_VELOCITY,
@@ -6,14 +9,16 @@ import PanEvents, {
SWIPE_VELOCITY_THRESHOLD
} from "discourse/mixins/pan-events";
-export default Ember.Component.extend(PanEvents, {
+const MIN_WIDTH_TIMELINE = 924;
+
+export default Component.extend(PanEvents, {
composerOpen: null,
info: null,
isPanning: false,
init() {
this._super(...arguments);
- this.set("info", Ember.Object.create());
+ this.set("info", EmberObject.create());
},
_performCheckSize() {
@@ -32,14 +37,18 @@ export default Ember.Component.extend(PanEvents, {
let renderTimeline = !this.site.mobileView;
if (renderTimeline) {
- const width = $(window).width();
- let height = $(window).height();
+ const width = window.innerWidth,
+ composer = document.getElementById("reply-control"),
+ timelineContainer = document.querySelector(".timeline-container"),
+ headerContainer = document.querySelector(".d-header"),
+ headerHeight = (headerContainer && headerContainer.offsetHeight) || 0;
- if (this.composerOpen) {
- height -= $("#reply-control").height();
+ if (timelineContainer && composer) {
+ renderTimeline =
+ width > MIN_WIDTH_TIMELINE &&
+ window.innerHeight - composer.offsetHeight - headerHeight >
+ timelineContainer.offsetHeight;
}
-
- renderTimeline = width > 924 && height > 520;
}
info.setProperties({
@@ -50,7 +59,7 @@ export default Ember.Component.extend(PanEvents, {
},
_checkSize() {
- Ember.run.scheduleOnce("afterRender", this, this._performCheckSize);
+ debounce(this, this._performCheckSize, 300, true);
},
// we need to store this so topic progress has something to init with
@@ -85,8 +94,7 @@ export default Ember.Component.extend(PanEvents, {
composerOpened() {
this.set("composerOpen", true);
- // we need to do the check after animation is done
- Ember.run.later(() => this._checkSize(), 500);
+ this._checkSize();
},
composerClosed() {
@@ -97,7 +105,7 @@ export default Ember.Component.extend(PanEvents, {
_collapseFullscreen() {
if (this.get("info.topicProgressExpanded")) {
$(".timeline-fullscreen").removeClass("show");
- Ember.run.later(() => {
+ later(() => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
@@ -135,7 +143,7 @@ export default Ember.Component.extend(PanEvents, {
} else if (offset <= 0) {
$timelineContainer.css("bottom", "");
} else {
- Ember.run.later(() => this._handlePanDone(offset, event), 20);
+ later(() => this._handlePanDone(offset, event), 20);
}
},
@@ -187,8 +195,9 @@ export default Ember.Component.extend(PanEvents, {
$(window).on("resize.discourse-topic-navigation", () =>
this._checkSize()
);
- this.appEvents.on("composer:will-open", this, this.composerOpened);
- this.appEvents.on("composer:will-close", this, this.composerClosed);
+ this.appEvents.on("composer:opened", this, this.composerOpened);
+ this.appEvents.on("composer:resized", this, this.composerOpened);
+ this.appEvents.on("composer:closed", this, this.composerClosed);
$("#reply-control").on("div-resized.discourse-topic-navigation", () =>
this._checkSize()
);
@@ -209,8 +218,9 @@ export default Ember.Component.extend(PanEvents, {
if (!this.site.mobileView) {
$(window).off("resize.discourse-topic-navigation");
- this.appEvents.off("composer:will-open", this, this.composerOpened);
- this.appEvents.off("composer:will-close", this, this.composerClosed);
+ this.appEvents.off("composer:opened", this, this.composerOpened);
+ this.appEvents.off("composer:resized", this, this.composerOpened);
+ this.appEvents.off("composer:closed", this, this.composerClosed);
$("#reply-control").off("div-resized.discourse-topic-navigation");
}
}
diff --git a/app/assets/javascripts/discourse/components/topic-post-badges.js.es6 b/app/assets/javascripts/discourse/components/topic-post-badges.js.es6
index 69235be0ca..9d3cded17f 100644
--- a/app/assets/javascripts/discourse/components/topic-post-badges.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-post-badges.js.es6
@@ -1,32 +1,17 @@
-import { bufferedRender } from "discourse-common/lib/buffered-render";
+import Component from "@ember/component";
-// Creates a link
-function link(buffer, prop, url, cssClass, i18nKey, text) {
- if (!prop) {
- return;
+export default Component.extend({
+ tagName: "span",
+ classNameBindings: [":topic-post-badges"],
+ rerenderTriggers: ["url", "unread", "newPosts", "unseen"],
+ newDotText: null,
+ init() {
+ this._super(...arguments);
+ this.set(
+ "newDotText",
+ this.currentUser && this.currentUser.trust_level > 0
+ ? " "
+ : I18n.t("filters.new.lower_title")
+ );
}
- const title = I18n.t("topic." + i18nKey, { count: prop });
- buffer.push(
- `
${text ||
- prop} \n`
- );
-}
-
-export default Ember.Component.extend(
- bufferedRender({
- tagName: "span",
- classNameBindings: [":topic-post-badges"],
- rerenderTriggers: ["url", "unread", "newPosts", "unseen"],
-
- buildBuffer(buffer) {
- const newDotText =
- this.currentUser && this.currentUser.trust_level > 0
- ? " "
- : I18n.t("filters.new.lower_title");
- const url = this.url;
- link(buffer, this.unread, url, "unread", "unread_posts");
- link(buffer, this.newPosts, url, "new-posts", "new_posts");
- link(buffer, this.unseen, url, "new-topic", "new", newDotText);
- }
- })
-);
+});
diff --git a/app/assets/javascripts/discourse/components/topic-progress.js.es6 b/app/assets/javascripts/discourse/components/topic-progress.js.es6
index 599de61ab5..61c4484be4 100644
--- a/app/assets/javascripts/discourse/components/topic-progress.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-progress.js.es6
@@ -1,22 +1,22 @@
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import { alias } from "@ember/object/computed";
+import { scheduleOnce } from "@ember/runloop";
+import Component from "@ember/component";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
-export default Ember.Component.extend({
+export default Component.extend({
elementId: "topic-progress-wrapper",
classNameBindings: ["docked"],
docked: false,
progressPosition: null,
- postStream: Ember.computed.alias("topic.postStream"),
+ postStream: alias("topic.postStream"),
_streamPercentage: null,
- @computed("progressPosition")
+ @discourseComputed("progressPosition")
jumpTopDisabled(progressPosition) {
return progressPosition <= 3;
},
- @computed(
+ @discourseComputed(
"postStream.filteredPostsCount",
"topic.highest_post_number",
"progressPosition"
@@ -28,7 +28,7 @@ export default Ember.Component.extend({
);
},
- @computed(
+ @discourseComputed(
"postStream.loaded",
"topic.currentPost",
"postStream.filteredPostsCount"
@@ -41,14 +41,14 @@ export default Ember.Component.extend({
);
},
- @computed("postStream.filteredPostsCount")
+ @discourseComputed("postStream.filteredPostsCount")
hugeNumberOfPosts(filteredPostsCount) {
return (
filteredPostsCount >= this.siteSettings.short_progress_text_threshold
);
},
- @computed("hugeNumberOfPosts", "topic.highest_post_number")
+ @discourseComputed("hugeNumberOfPosts", "topic.highest_post_number")
jumpToBottomTitle(hugeNumberOfPosts, highestPostNumber) {
if (hugeNumberOfPosts) {
return I18n.t("topic.progress.jump_bottom_with_number", {
@@ -59,7 +59,7 @@ export default Ember.Component.extend({
}
},
- @computed("progressPosition", "topic.last_read_post_id")
+ @discourseComputed("progressPosition", "topic.last_read_post_id")
showBackButton(position, lastReadId) {
if (!lastReadId) {
return;
@@ -72,7 +72,7 @@ export default Ember.Component.extend({
@observes("postStream.stream.[]")
_updateBar() {
- Ember.run.scheduleOnce("afterRender", this, this._updateProgressBar);
+ scheduleOnce("afterRender", this, this._updateProgressBar);
},
_topicScrolled(event) {
@@ -99,16 +99,11 @@ export default Ember.Component.extend({
const prevEvent = this.prevEvent;
if (prevEvent) {
- Ember.run.scheduleOnce(
- "afterRender",
- this,
- this._topicScrolled,
- prevEvent
- );
+ scheduleOnce("afterRender", this, this._topicScrolled, prevEvent);
} else {
- Ember.run.scheduleOnce("afterRender", this, this._updateProgressBar);
+ scheduleOnce("afterRender", this, this._updateProgressBar);
}
- Ember.run.scheduleOnce("afterRender", this, this._dock);
+ scheduleOnce("afterRender", this, this._dock);
},
willDestroyElement() {
@@ -126,7 +121,7 @@ export default Ember.Component.extend({
return;
}
- const $topicProgress = this.$("#topic-progress");
+ const $topicProgress = $(this.element.querySelector("#topic-progress"));
// speeds up stuff, bypass jquery slowness and extra checks
if (!this._totalWidth) {
this._totalWidth = $topicProgress[0].offsetWidth;
@@ -151,22 +146,34 @@ export default Ember.Component.extend({
},
_dock() {
- const $wrapper = this.$();
+ const $wrapper = $(this.element);
if (!$wrapper || $wrapper.length === 0) return;
const $html = $("html");
const offset = window.pageYOffset || $html.scrollTop();
const progressHeight = this.site.mobileView
? 0
- : $("#topic-progress").height();
+ : $("#topic-progress").outerHeight();
const maximumOffset = $("#topic-bottom").offset().top + progressHeight;
const windowHeight = $(window).height();
- const composerHeight = $("#reply-control").height() || 0;
+ let composerHeight = $("#reply-control").height() || 0;
const isDocked = offset >= maximumOffset - windowHeight + composerHeight;
- const bottom = $("body").height() - maximumOffset;
+ let bottom = $("body").height() - maximumOffset;
+
+ const $iPadFooterNav = $(".footer-nav-ipad .footer-nav");
+ if ($iPadFooterNav && $iPadFooterNav.length > 0) {
+ bottom += $iPadFooterNav.outerHeight();
+ }
const wrapperDir = $html.hasClass("rtl") ? "left" : "right";
+ const draftComposerHeight = 40;
if (composerHeight > 0) {
+ const $iPhoneFooterNav = $(".footer-nav-visible .footer-nav");
+ const $replyDraft = $("#reply-control.draft");
+ if ($iPhoneFooterNav.outerHeight() && $replyDraft.outerHeight()) {
+ composerHeight =
+ $replyDraft.outerHeight() + $iPhoneFooterNav.outerHeight();
+ }
$wrapper.css("bottom", isDocked ? bottom : composerHeight);
} else {
$wrapper.css("bottom", isDocked ? bottom : "");
@@ -175,11 +182,16 @@ export default Ember.Component.extend({
this.set("docked", isDocked);
const $replyArea = $("#reply-control .reply-area");
- if ($replyArea && $replyArea.length > 0) {
+ if ($replyArea && $replyArea.length > 0 && wrapperDir === "left") {
$wrapper.css(wrapperDir, `${$replyArea.offset().left}px`);
} else {
$wrapper.css(wrapperDir, "1em");
}
+
+ $wrapper.css(
+ "margin-bottom",
+ !isDocked && composerHeight > draftComposerHeight ? "0px" : ""
+ );
},
click(e) {
diff --git a/app/assets/javascripts/discourse/components/topic-status.js.es6 b/app/assets/javascripts/discourse/components/topic-status.js.es6
index a8dbe464a0..ecd6f448de 100644
--- a/app/assets/javascripts/discourse/components/topic-status.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-status.js.es6
@@ -1,57 +1,79 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
-import { bufferedRender } from "discourse-common/lib/buffered-render";
-import { escapeExpression } from "discourse/lib/utilities";
-import TopicStatusIcons from "discourse/helpers/topic-status-icons";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Component.extend(
- bufferedRender({
- classNames: ["topic-statuses"],
+export default Component.extend({
+ classNames: ["topic-statuses"],
- rerenderTriggers: [
- "topic.archived",
- "topic.closed",
- "topic.pinned",
- "topic.visible",
- "topic.unpinned",
- "topic.is_warning"
- ],
-
- click(e) {
- // only pin unpin for now
- if (this.canAct && $(e.target).hasClass("d-icon-thumbtack")) {
- const topic = this.topic;
- topic.get("pinned") ? topic.clearPin() : topic.rePin();
- }
-
- return false;
- },
-
- @computed("disableActions")
- canAct(disableActions) {
- return Discourse.User.current() && !disableActions;
- },
-
- buildBuffer(buffer) {
- const canAct = this.canAct;
+ click(e) {
+ // only pin unpin for now
+ if (this.canAct && $(e.target).hasClass("d-icon-thumbtack")) {
const topic = this.topic;
-
- if (!topic) {
- return;
- }
-
- TopicStatusIcons.render(topic, function(name, key) {
- const actionable = ["pinned", "unpinned"].includes(key) && canAct;
- const title = escapeExpression(I18n.t(`topic_statuses.${key}.help`)),
- startTag = actionable ? "a href" : "span",
- endTag = actionable ? "a" : "span",
- iconArgs = key === "unpinned" ? { class: "unpinned" } : null,
- icon = iconHTML(name, iconArgs);
-
- buffer.push(
- `<${startTag} title='${title}' class='topic-status'>${icon}${endTag}>`
- );
- });
+ topic.get("pinned") ? topic.clearPin() : topic.rePin();
}
- })
-);
+
+ return false;
+ },
+
+ @discourseComputed("disableActions")
+ canAct(disableActions) {
+ return this.currentUser && !disableActions;
+ },
+
+ @discourseComputed("topic.closed", "topic.archived")
+ topicClosedArchived(closed, archived) {
+ if (closed && archived) {
+ this._set("closedArchived", "lock", "locked_and_archived");
+ this._reset("closed");
+ this._reset("archived");
+ return true;
+ } else {
+ this._reset("closedArchived");
+ closed ? this._set("closed", "lock", "locked") : this._reset("closed");
+ archived
+ ? this._set("archived", "lock", "archived")
+ : this._reset("archived");
+ return false;
+ }
+ },
+
+ @discourseComputed("topic.is_warning")
+ topicWarning(warning) {
+ return warning
+ ? this._set("warning", "envelope", "warning")
+ : this._reset("warning");
+ },
+
+ @discourseComputed("topic.pinned")
+ topicPinned(pinned) {
+ return pinned
+ ? this._set("pinned", "thumbtack", "pinned")
+ : this._reset("pinned");
+ },
+
+ @discourseComputed("topic.unpinned")
+ topicUnpinned(unpinned) {
+ return unpinned
+ ? this._set("unpinned", "thumbtack", "unpinned", { class: "unpinned" })
+ : this._reset("unpinned");
+ },
+
+ @discourseComputed("topic.invisible")
+ topicInvisible(invisible) {
+ return invisible
+ ? this._set("invisible", "far-eye-slash", "unlisted")
+ : this._reset("invisible");
+ },
+
+ _set(name, icon, key, iconArgs = null) {
+ this.set(`${name}Icon`, iconHTML(`${icon}`, iconArgs).htmlSafe());
+ this.set(`${name}Title`, I18n.t(`topic_statuses.${key}.help`));
+ return true;
+ },
+
+ _reset(name) {
+ this.set(`${name}Icon`, null);
+ this.set(`${name}Title`, null);
+ return false;
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 b/app/assets/javascripts/discourse/components/topic-timeline.js.es6
index 1b0d92ca51..945524d41c 100644
--- a/app/assets/javascripts/discourse/components/topic-timeline.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-timeline.js.es6
@@ -1,9 +1,17 @@
+import { next } from "@ember/runloop";
import MountWidget from "discourse/components/mount-widget";
import Docking from "discourse/mixins/docking";
-import { observes } from "ember-addons/ember-computed-decorators";
+import { observes } from "discourse-common/utils/decorators";
import optionalService from "discourse/lib/optional-service";
-const headerPadding = () => parseInt($("#main-outlet").css("padding-top")) + 3;
+const headerPadding = () => {
+ let topPadding = parseInt($("#main-outlet").css("padding-top"), 10) + 3;
+ const iPadNavHeight = $(".footer-nav-ipad .footer-nav").height();
+ if (iPadNavHeight) {
+ topPadding += iPadNavHeight;
+ }
+ return topPadding;
+};
export default MountWidget.extend(Docking, {
adminTools: optionalService(),
@@ -51,9 +59,10 @@ export default MountWidget.extend(Docking, {
const mainOffset = $("#main").offset();
const offsetTop = mainOffset ? mainOffset.top : 0;
const topicTop = $(".container.posts").offset().top - offsetTop;
- const topicBottom = $("#topic-bottom").offset().top;
- const $timeline = this.$(".timeline-container");
- const timelineHeight = $timeline.height() || 400;
+ const topicBottom =
+ $("#topic-bottom").offset().top - $("#main-outlet").offset().top;
+ const timeline = this.element.querySelector(".timeline-container");
+ const timelineHeight = (timeline && timeline.offsetHeight) || 400;
const footerHeight = $(".timeline-footer-controls").outerHeight(true) || 0;
const prev = this.dockAt;
@@ -86,12 +95,28 @@ export default MountWidget.extend(Docking, {
this._super(...arguments);
if (this.fullscreen && !this.addShowClass) {
- Ember.run.next(() => {
+ next(() => {
this.set("addShowClass", true);
this.queueRerender();
});
}
this.dispatch("topic:current-post-scrolled", "timeline-scrollarea");
+ this.dispatch("topic:toggle-actions", "topic-admin-menu-button");
+ if (!this.site.mobileView) {
+ this.appEvents.on("composer:opened", this, this.queueRerender);
+ this.appEvents.on("composer:resized", this, this.queueRerender);
+ this.appEvents.on("composer:closed", this, this.queueRerender);
+ }
+ },
+
+ willDestroyElement() {
+ this._super(...arguments);
+
+ if (!this.site.mobileView) {
+ this.appEvents.off("composer:opened", this, this.queueRerender);
+ this.appEvents.off("composer:resized", this, this.queueRerender);
+ this.appEvents.off("composer:closed", this, this.queueRerender);
+ }
}
});
diff --git a/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 b/app/assets/javascripts/discourse/components/topic-timer-info.js.es6
index 4b3110bc29..4b758ed6a2 100644
--- a/app/assets/javascripts/discourse/components/topic-timer-info.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-timer-info.js.es6
@@ -1,98 +1,126 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { cancel } from "@ember/runloop";
+import { later } from "@ember/runloop";
+import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
-import { bufferedRender } from "discourse-common/lib/buffered-render";
import Category from "discourse/models/category";
+import { REMINDER_TYPE } from "discourse/controllers/edit-topic-timer";
+import ENV from "discourse-common/config/environment";
-export default Ember.Component.extend(
- bufferedRender({
- classNames: ["topic-status-info"],
- _delayedRerender: null,
+export default Component.extend({
+ classNames: ["topic-status-info"],
+ _delayedRerender: null,
+ clockIcon: `${iconHTML("far-clock")}`.htmlSafe(),
+ trashCanIcon: `${iconHTML("trash-alt")}`.htmlSafe(),
+ trashCanTitle: I18n.t("post.controls.remove_timer"),
+ title: null,
+ notice: null,
+ showTopicTimer: null,
- rerenderTriggers: [
- "topicClosed",
- "statusType",
- "executeAt",
- "basedOnLastPost",
- "duration",
- "categoryId"
- ],
+ @discourseComputed("statusType")
+ canRemoveTimer(type) {
+ if (type === REMINDER_TYPE) return true;
+ return this.currentUser && this.currentUser.get("canManageTopic");
+ },
- buildBuffer(buffer) {
- if (!this.executeAt) return;
+ @discourseComputed("canRemoveTimer", "removeTopicTimer")
+ showTrashCan(canRemoveTimer, removeTopicTimer) {
+ return canRemoveTimer && removeTopicTimer;
+ },
- const topicStatus = this.topicClosed ? "close" : "open";
- const topicStatusKnown = this.topicClosed !== undefined;
- if (topicStatusKnown && topicStatus === this.statusType) return;
-
- const statusUpdateAt = moment(this.executeAt);
- const duration = moment.duration(statusUpdateAt - moment());
- const minutesLeft = duration.asMinutes();
-
- if (minutesLeft > 0) {
- let rerenderDelay = 1000;
- if (minutesLeft > 2160) {
- rerenderDelay = 12 * 60 * 60000;
- } else if (minutesLeft > 1410) {
- rerenderDelay = 60 * 60000;
- } else if (minutesLeft > 90) {
- rerenderDelay = 30 * 60000;
- } else if (minutesLeft > 2) {
- rerenderDelay = 60000;
- }
- let autoCloseHours = this.duration || 0;
-
- buffer.push(`
${iconHTML("far-clock")} `);
-
- let options = {
- timeLeft: duration.humanize(true),
- duration: moment.duration(autoCloseHours, "hours").humanize()
- };
-
- const categoryId = this.categoryId;
- if (categoryId) {
- const category = Category.findById(categoryId);
-
- options = Object.assign(
- {
- categoryName: category.get("slug"),
- categoryUrl: category.get("url")
- },
- options
- );
- }
-
- buffer.push(
- `${I18n.t(
- this._noticeKey(),
- options
- )} `
- );
- buffer.push(" ");
-
- // TODO Sam: concerned this can cause a heavy rerender loop
- if (!Ember.testing) {
- this._delayedRerender = Ember.run.later(
- this,
- this.rerender,
- rerenderDelay
- );
- }
- }
- },
-
- willDestroyElement() {
- if (this._delayedRerender) {
- Ember.run.cancel(this._delayedRerender);
- }
- },
-
- _noticeKey() {
- const statusType = this.statusType;
-
- if (this.basedOnLastPost) {
- return `topic.status_update_notice.auto_${statusType}_based_on_last_post`;
- } else {
- return `topic.status_update_notice.auto_${statusType}`;
- }
+ renderTopicTimer() {
+ if (!this.executeAt || this.executeAt < moment()) {
+ this.set("showTopicTimer", null);
+ return;
}
- })
-);
+
+ const topicStatus = this.topicClosed ? "close" : "open";
+ const topicStatusKnown = this.topicClosed !== undefined;
+ if (topicStatusKnown && topicStatus === this.statusType) return;
+
+ const statusUpdateAt = moment(this.executeAt);
+ const duration = moment.duration(statusUpdateAt - moment());
+ const minutesLeft = duration.asMinutes();
+ if (minutesLeft > 0) {
+ let rerenderDelay = 1000;
+ if (minutesLeft > 2160) {
+ rerenderDelay = 12 * 60 * 60000;
+ } else if (minutesLeft > 1410) {
+ rerenderDelay = 60 * 60000;
+ } else if (minutesLeft > 90) {
+ rerenderDelay = 30 * 60000;
+ } else if (minutesLeft > 2) {
+ rerenderDelay = 60000;
+ }
+ let autoCloseHours = this.duration || 0;
+
+ let options = {
+ timeLeft: duration.humanize(true),
+ duration: moment.duration(autoCloseHours, "hours").humanize()
+ };
+
+ const categoryId = this.categoryId;
+ if (categoryId) {
+ const category = Category.findById(categoryId);
+
+ options = Object.assign(
+ {
+ categoryName: category.get("slug"),
+ categoryUrl: category.get("url")
+ },
+ options
+ );
+ }
+
+ this.setProperties({
+ title: `${moment(this.executeAt).format("LLLL")}`.htmlSafe(),
+ notice: `${I18n.t(this._noticeKey(), options)}`.htmlSafe(),
+ showTopicTimer: true
+ });
+
+ // TODO Sam: concerned this can cause a heavy rerender loop
+ if (ENV.environment !== "test") {
+ this._delayedRerender = later(() => {
+ this.renderTopicTimer();
+ }, rerenderDelay);
+ }
+ } else {
+ this.set("showTopicTimer", null);
+ }
+ },
+
+ didReceiveAttrs() {
+ this._super(...arguments);
+ this.renderTopicTimer();
+ },
+
+ didInsertElement() {
+ this._super(...arguments);
+
+ if (this.removeTopicTimer) {
+ $(this.element).on(
+ "click.topic-timer-remove",
+ "button",
+ this.removeTopicTimer
+ );
+ }
+ },
+
+ willDestroyElement() {
+ $(this.element).off("click.topic-timer-remove", this.removeTopicTimer);
+
+ if (this._delayedRerender) {
+ cancel(this._delayedRerender);
+ }
+ },
+
+ _noticeKey() {
+ const statusType = this.statusType;
+
+ if (this.basedOnLastPost) {
+ return `topic.status_update_notice.auto_${statusType}_based_on_last_post`;
+ } else {
+ return `topic.status_update_notice.auto_${statusType}`;
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/topic-title.js.es6 b/app/assets/javascripts/discourse/components/topic-title.js.es6
index 17f99b48ee..7bbc478cb3 100644
--- a/app/assets/javascripts/discourse/components/topic-title.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-title.js.es6
@@ -1,5 +1,6 @@
+import Component from "@ember/component";
import KeyEnterEscape from "discourse/mixins/key-enter-escape";
-export default Ember.Component.extend(KeyEnterEscape, {
+export default Component.extend(KeyEnterEscape, {
elementId: "topic-title"
});
diff --git a/app/assets/javascripts/discourse/components/track-selected.js.es6 b/app/assets/javascripts/discourse/components/track-selected.js.es6
index bf720378b7..e8d0ec3428 100644
--- a/app/assets/javascripts/discourse/components/track-selected.js.es6
+++ b/app/assets/javascripts/discourse/components/track-selected.js.es6
@@ -1,5 +1,10 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+import { observes } from "discourse-common/utils/decorators";
+
+export default Component.extend({
tagName: "span",
+
+ @observes("selected")
selectionChanged: function() {
const selected = this.selected;
const list = this.selectedList;
@@ -10,5 +15,5 @@ export default Ember.Component.extend({
} else {
list.removeObject(id);
}
- }.observes("selected")
+ }
});
diff --git a/app/assets/javascripts/discourse/components/user-badge.js.es6 b/app/assets/javascripts/discourse/components/user-badge.js.es6
index bca90d4435..6f9c4850b5 100644
--- a/app/assets/javascripts/discourse/components/user-badge.js.es6
+++ b/app/assets/javascripts/discourse/components/user-badge.js.es6
@@ -1,14 +1,15 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "span",
- @computed("count")
+ @discourseComputed("count")
showGrantCount(count) {
return count && count > 1;
},
- @computed("badge", "user")
+ @discourseComputed("badge", "user")
badgeUrl() {
// NOTE: I tried using a link-to helper here but the queryParams mean it fails
var username = this.get("user.username_lower") || "";
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 7fdb614e2a..cf2502c552 100644
--- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6
+++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6
@@ -1,217 +1,215 @@
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import { isEmpty } from "@ember/utils";
+import { alias, gte, and, gt, not, or } from "@ember/object/computed";
+import EmberObject from "@ember/object";
+import Component from "@ember/component";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import User from "discourse/models/user";
import { propertyNotEqual, setting } from "discourse/lib/computed";
import { durationTiny } from "discourse/lib/formatter";
import CanCheckEmails from "discourse/mixins/can-check-emails";
import CardContentsBase from "discourse/mixins/card-contents-base";
import CleansUp from "discourse/mixins/cleans-up";
+import { prioritizeNameInUx } from "discourse/lib/settings";
+import { set } from "@ember/object";
+import { getOwner } from "@ember/application";
-export default Ember.Component.extend(
- CardContentsBase,
- CanCheckEmails,
- CleansUp,
- {
- elementId: "user-card",
- triggeringLinkClass: "mention",
- classNameBindings: [
- "visible:show",
- "showBadges",
- "user.card_background::no-bg",
- "isFixed:fixed",
- "usernameClass"
- ],
- allowBackgrounds: setting("allow_profile_backgrounds"),
- showBadges: setting("enable_badges"),
+export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
+ elementId: "user-card",
+ classNames: "user-card",
+ triggeringLinkClass: "mention",
+ classNameBindings: [
+ "visible:show",
+ "showBadges",
+ "user.card_background::no-bg",
+ "isFixed:fixed",
+ "usernameClass"
+ ],
+ allowBackgrounds: setting("allow_profile_backgrounds"),
+ showBadges: setting("enable_badges"),
- postStream: Ember.computed.alias("topic.postStream"),
- enoughPostsForFiltering: Ember.computed.gte("topicPostCount", 2),
- showFilter: Ember.computed.and(
- "viewingTopic",
- "postStream.hasNoFilters",
- "enoughPostsForFiltering"
- ),
- showName: propertyNotEqual("user.name", "user.username"),
- hasUserFilters: Ember.computed.gt("postStream.userFilters.length", 0),
- showMoreBadges: Ember.computed.gt("moreBadgesCount", 0),
- showDelete: Ember.computed.and(
- "viewingAdmin",
- "showName",
- "user.canBeDeleted"
- ),
- linkWebsite: Ember.computed.not("user.isBasic"),
- hasLocationOrWebsite: Ember.computed.or(
- "user.location",
- "user.website_name"
- ),
- isSuspendedOrHasBio: Ember.computed.or(
- "user.suspend_reason",
- "user.bio_cooked"
- ),
- showCheckEmail: Ember.computed.and("user.staged", "canCheckEmails"),
+ postStream: alias("topic.postStream"),
+ enoughPostsForFiltering: gte("topicPostCount", 2),
+ showFilter: and(
+ "viewingTopic",
+ "postStream.hasNoFilters",
+ "enoughPostsForFiltering"
+ ),
+ showName: propertyNotEqual("user.name", "user.username"),
+ hasUserFilters: gt("postStream.userFilters.length", 0),
+ showMoreBadges: gt("moreBadgesCount", 0),
+ showDelete: and("viewingAdmin", "showName", "user.canBeDeleted"),
+ linkWebsite: not("user.isBasic"),
+ hasLocationOrWebsite: or("user.location", "user.website_name"),
+ isSuspendedOrHasBio: or("user.suspend_reason", "user.bio_cooked"),
+ showCheckEmail: and("user.staged", "canCheckEmails"),
- user: null,
+ user: null,
- // If inside a topic
- topicPostCount: null,
+ // If inside a topic
+ topicPostCount: null,
- @computed("user.staff")
- staff: isStaff => (isStaff ? "staff" : ""),
+ showFeaturedTopic: and(
+ "user.featured_topic",
+ "siteSettings.allow_featured_topic_on_user_profiles"
+ ),
- @computed("user.trust_level")
- newUser: trustLevel => (trustLevel === 0 ? "new-user" : ""),
+ @discourseComputed("user.staff")
+ staff: isStaff => (isStaff ? "staff" : ""),
- @computed("user.name")
- nameFirst(name) {
- return (
- !this.siteSettings.prioritize_username_in_ux &&
- name &&
- name.trim().length > 0
- );
- },
+ @discourseComputed("user.trust_level")
+ newUser: trustLevel => (trustLevel === 0 ? "new-user" : ""),
- @computed("username")
- usernameClass: username => (username ? `user-card-${username}` : ""),
+ @discourseComputed("user.name")
+ nameFirst(name) {
+ return prioritizeNameInUx(name, this.siteSettings);
+ },
- @computed("username", "topicPostCount")
- togglePostsLabel(username, count) {
- return I18n.t("topic.filter_to", { username, count });
- },
+ @discourseComputed("username")
+ usernameClass: username => (username ? `user-card-${username}` : ""),
- @computed("user.user_fields.@each.value")
- publicUserFields() {
- const siteUserFields = this.site.get("user_fields");
- if (!Ember.isEmpty(siteUserFields)) {
- const userFields = this.get("user.user_fields");
- return siteUserFields
- .filterBy("show_on_user_card", true)
- .sortBy("position")
- .map(field => {
- Ember.set(field, "dasherized_name", field.get("name").dasherize());
- const value = userFields ? userFields[field.get("id")] : null;
- return Ember.isEmpty(value)
- ? null
- : Ember.Object.create({ value, field });
- })
- .compact();
- }
- },
+ @discourseComputed("username", "topicPostCount")
+ togglePostsLabel(username, count) {
+ return I18n.t("topic.filter_to", { username, count });
+ },
- @computed("user.trust_level")
- removeNoFollow(trustLevel) {
- return trustLevel > 2 && !this.siteSettings.tl3_links_no_follow;
- },
-
- @computed("user.badge_count", "user.featured_user_badges.length")
- moreBadgesCount: (badgeCount, badgeLength) => badgeCount - badgeLength,
-
- @computed("user.time_read", "user.recent_time_read")
- showRecentTimeRead(timeRead, recentTimeRead) {
- return timeRead !== recentTimeRead && recentTimeRead !== 0;
- },
-
- @computed("user.recent_time_read")
- recentTimeRead(recentTimeReadSeconds) {
- return durationTiny(recentTimeReadSeconds);
- },
-
- @computed("showRecentTimeRead", "user.time_read", "recentTimeRead")
- timeReadTooltip(showRecent, timeRead, recentTimeRead) {
- if (showRecent) {
- return I18n.t("time_read_recently_tooltip", {
- time_read: durationTiny(timeRead),
- recent_time_read: recentTimeRead
- });
- } else {
- return I18n.t("time_read_tooltip", {
- time_read: durationTiny(timeRead)
- });
- }
- },
-
- @observes("user.card_background_upload_url")
- addBackground() {
- if (!this.allowBackgrounds) {
- return;
- }
-
- const $this = this.$();
- if (!$this) {
- return;
- }
-
- const url = this.get("user.card_background_upload_url");
- const bg = Ember.isEmpty(url)
- ? ""
- : `url(${Discourse.getURLWithCDN(url)})`;
- $this.css("background-image", bg);
- },
-
- _showCallback(username, $target) {
- this._positionCard($target);
- this.setProperties({ visible: true, loading: true });
-
- const args = { stats: false };
- args.include_post_count_for = this.get("topic.id");
- User.findByUsername(username, args)
- .then(user => {
- if (user.topic_post_count) {
- this.set(
- "topicPostCount",
- user.topic_post_count[args.include_post_count_for]
- );
- }
- this.setProperties({ user });
+ @discourseComputed("user.user_fields.@each.value")
+ publicUserFields() {
+ const siteUserFields = this.site.get("user_fields");
+ if (!isEmpty(siteUserFields)) {
+ const userFields = this.get("user.user_fields");
+ return siteUserFields
+ .filterBy("show_on_user_card", true)
+ .sortBy("position")
+ .map(field => {
+ set(field, "dasherized_name", field.get("name").dasherize());
+ const value = userFields ? userFields[field.get("id")] : null;
+ return isEmpty(value) ? null : EmberObject.create({ value, field });
})
- .catch(() => this._close())
- .finally(() => this.set("loading", null));
- },
+ .compact();
+ }
+ },
- _close() {
- this._super(...arguments);
+ @discourseComputed("user.trust_level")
+ removeNoFollow(trustLevel) {
+ return trustLevel > 2 && !this.siteSettings.tl3_links_no_follow;
+ },
- this.setProperties({
- user: null,
- topicPostCount: null
+ @discourseComputed("user.badge_count", "user.featured_user_badges.length")
+ moreBadgesCount: (badgeCount, badgeLength) => badgeCount - badgeLength,
+
+ @discourseComputed("user.time_read", "user.recent_time_read")
+ showRecentTimeRead(timeRead, recentTimeRead) {
+ return timeRead !== recentTimeRead && recentTimeRead !== 0;
+ },
+
+ @discourseComputed("user.recent_time_read")
+ recentTimeRead(recentTimeReadSeconds) {
+ return durationTiny(recentTimeReadSeconds);
+ },
+
+ @discourseComputed("showRecentTimeRead", "user.time_read", "recentTimeRead")
+ timeReadTooltip(showRecent, timeRead, recentTimeRead) {
+ if (showRecent) {
+ return I18n.t("time_read_recently_tooltip", {
+ time_read: durationTiny(timeRead),
+ recent_time_read: recentTimeRead
});
- },
+ } else {
+ return I18n.t("time_read_tooltip", {
+ time_read: durationTiny(timeRead)
+ });
+ }
+ },
- cleanUp() {
+ @observes("user.card_background_upload_url")
+ addBackground() {
+ if (!this.allowBackgrounds) {
+ return;
+ }
+
+ const thisElem = this.element;
+ if (!thisElem) {
+ return;
+ }
+
+ const url = this.get("user.card_background_upload_url");
+ const bg = isEmpty(url) ? "" : `url(${Discourse.getURLWithCDN(url)})`;
+ thisElem.style.backgroundImage = bg;
+ },
+
+ _showCallback(username, $target) {
+ this._positionCard($target);
+ this.setProperties({ visible: true, loading: true });
+
+ const args = {
+ forCard: this.siteSettings.enable_new_user_card_route,
+ include_post_count_for: this.get("topic.id")
+ };
+
+ User.findByUsername(username, args)
+ .then(user => {
+ if (user.topic_post_count) {
+ this.set(
+ "topicPostCount",
+ user.topic_post_count[args.include_post_count_for]
+ );
+ }
+ this.setProperties({ user });
+ })
+ .catch(() => this._close())
+ .finally(() => this.set("loading", null));
+ },
+
+ _close() {
+ this._super(...arguments);
+
+ this.setProperties({
+ user: null,
+ topicPostCount: null
+ });
+ },
+
+ cleanUp() {
+ this._close();
+ },
+
+ actions: {
+ close() {
this._close();
},
- actions: {
- close() {
- this._close();
- },
+ composePM(user, post) {
+ this._close();
- cancelFilter() {
- const postStream = this.postStream;
- postStream.cancelFilter();
- postStream.refresh();
- this._close();
- },
+ getOwner(this)
+ .lookup("router:main")
+ .send("composePrivateMessage", user, post);
+ },
- togglePosts() {
- this.togglePosts(this.user);
- this._close();
- },
+ cancelFilter() {
+ const postStream = this.postStream;
+ postStream.cancelFilter();
+ postStream.refresh();
+ this._close();
+ },
- deleteUser() {
- this.user.delete();
- this._close();
- },
+ togglePosts() {
+ this.togglePosts(this.user);
+ this._close();
+ },
- showUser(username) {
- this.showUser(username);
- this._close();
- },
+ deleteUser() {
+ this.user.delete();
+ this._close();
+ },
- checkEmail(user) {
- user.checkEmail();
- }
+ showUser(username) {
+ this.showUser(username);
+ this._close();
+ },
+
+ checkEmail(user) {
+ user.checkEmail();
}
}
-);
+});
diff --git a/app/assets/javascripts/discourse/components/user-field.js.es6 b/app/assets/javascripts/discourse/components/user-field.js.es6
index 1585e9a3a5..f8e2554ab0 100644
--- a/app/assets/javascripts/discourse/components/user-field.js.es6
+++ b/app/assets/javascripts/discourse/components/user-field.js.es6
@@ -1,12 +1,24 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
import { fmt } from "discourse/lib/computed";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Component.extend({
- classNameBindings: [":user-field", "field.field_type"],
+export default Component.extend({
+ classNameBindings: [":user-field", "field.field_type", "customFieldClass"],
layoutName: fmt("field.field_type", "components/user-fields/%@"),
- @computed
+ @discourseComputed
noneLabel() {
return "user_fields.none";
+ },
+
+ @discourseComputed("field.name")
+ customFieldClass(fieldName) {
+ if (fieldName) {
+ fieldName = fieldName
+ .replace(/\s+/g, "-")
+ .replace(/[!\"#$%&'\(\)\*\+,\.\/:;<=>\?\@\[\\\]\^`\{\|\}~]/g, "")
+ .toLowerCase();
+ return fieldName && `user-field-${fieldName}`;
+ }
}
});
diff --git a/app/assets/javascripts/discourse/components/user-flag-percentage.js.es6 b/app/assets/javascripts/discourse/components/user-flag-percentage.js.es6
index 874f19c7bb..60e3c4ccef 100644
--- a/app/assets/javascripts/discourse/components/user-flag-percentage.js.es6
+++ b/app/assets/javascripts/discourse/components/user-flag-percentage.js.es6
@@ -1,15 +1,16 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "",
- @computed("percentage")
+ @discourseComputed("percentage")
showPercentage(percentage) {
return percentage.total >= 3;
},
// We do a little logic to choose which icon to display and which text
- @computed("agreed", "disagreed", "ignored")
+ @discourseComputed("agreed", "disagreed", "ignored")
percentage(agreed, disagreed, ignored) {
let total = agreed + disagreed + ignored;
let result = { total };
diff --git a/app/assets/javascripts/discourse/components/user-info.js.es6 b/app/assets/javascripts/discourse/components/user-info.js.es6
index e507b30570..2de553d13d 100644
--- a/app/assets/javascripts/discourse/components/user-info.js.es6
+++ b/app/assets/javascripts/discourse/components/user-info.js.es6
@@ -1,26 +1,28 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { alias } from "@ember/object/computed";
+import Component from "@ember/component";
import { userPath } from "discourse/lib/url";
-function normalize(name) {
+export function normalize(name) {
return name.replace(/[\-\_ \.]/g, "").toLowerCase();
}
-export default Ember.Component.extend({
+export default Component.extend({
classNameBindings: [":user-info", "size"],
attributeBindings: ["data-username"],
size: "small",
- @computed("user.username")
+ @discourseComputed("user.username")
userPath(username) {
return userPath(username);
},
- "data-username": Ember.computed.alias("user.username"),
+ "data-username": alias("user.username"),
// TODO: In later ember releases `hasBlock` works without this
- hasBlock: Ember.computed.alias("template"),
+ hasBlock: alias("template"),
- @computed("user.name", "user.username")
+ @discourseComputed("user.name", "user.username")
name(name, username) {
if (name && normalize(username) !== normalize(name)) {
return name;
diff --git a/app/assets/javascripts/discourse/components/user-link.js.es6 b/app/assets/javascripts/discourse/components/user-link.js.es6
index 7b86bed4fd..6ea395e5f0 100644
--- a/app/assets/javascripts/discourse/components/user-link.js.es6
+++ b/app/assets/javascripts/discourse/components/user-link.js.es6
@@ -1,6 +1,8 @@
-export default Ember.Component.extend({
+import { alias } from "@ember/object/computed";
+import Component from "@ember/component";
+export default Component.extend({
tagName: "a",
attributeBindings: ["href", "data-user-card"],
- href: Ember.computed.alias("user.path"),
- "data-user-card": Ember.computed.alias("user.username")
+ href: alias("user.path"),
+ "data-user-card": alias("user.username")
});
diff --git a/app/assets/javascripts/discourse/components/user-notifications-large.js.es6 b/app/assets/javascripts/discourse/components/user-notifications-large.js.es6
index de804fb75e..3d2a2112b5 100644
--- a/app/assets/javascripts/discourse/components/user-notifications-large.js.es6
+++ b/app/assets/javascripts/discourse/components/user-notifications-large.js.es6
@@ -1,5 +1,5 @@
import MountWidget from "discourse/components/mount-widget";
-import { observes } from "ember-addons/ember-computed-decorators";
+import { observes } from "discourse-common/utils/decorators";
export default MountWidget.extend({
widget: "user-notifications-large",
diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6
index 2cc95441ed..3d796d03d6 100644
--- a/app/assets/javascripts/discourse/components/user-selector.js.es6
+++ b/app/assets/javascripts/discourse/components/user-selector.js.es6
@@ -1,4 +1,5 @@
-import { on, observes } from "ember-addons/ember-computed-decorators";
+import { isEmpty } from "@ember/utils";
+import { on, observes } from "discourse-common/utils/decorators";
import TextField from "discourse/components/text-field";
import userSearch from "discourse/lib/user-search";
import { findRawTemplate } from "discourse/lib/raw-templates";
@@ -7,10 +8,34 @@ export default TextField.extend({
autocorrect: false,
autocapitalize: false,
name: "user-selector",
+ canReceiveUpdates: false,
+ single: false,
+ fullWidthWrap: false,
- @observes("usernames")
- _update() {
- if (this.canReceiveUpdates === "true") {
+ init() {
+ this._super(...arguments);
+
+ this._paste = e => {
+ let pastedText = "";
+ if (window.clipboardData && window.clipboardData.getData) {
+ // IE
+ pastedText = window.clipboardData.getData("Text");
+ } else if (e.clipboardData && e.clipboardData.getData) {
+ pastedText = e.clipboardData.getData("text/plain");
+ }
+
+ if (pastedText.length > 0) {
+ this.importText(pastedText);
+ e.preventDefault();
+ return false;
+ }
+ };
+ },
+
+ didUpdateAttrs() {
+ this._super(...arguments);
+
+ if (this.canReceiveUpdates) {
this._createAutocompleteInstance({ updateData: true });
}
},
@@ -18,6 +43,7 @@ export default TextField.extend({
@on("willDestroyElement")
_destroyAutocompleteInstance() {
$(this.element).autocomplete("destroy");
+ this.element.addEventListener("paste", this._paste);
},
@on("didInsertElement")
@@ -41,16 +67,19 @@ export default TextField.extend({
allowEmails = bool("allowEmails"),
fullWidthWrap = bool("fullWidthWrap");
- const excludedUsernames = () => {
+ const allExcludedUsernames = () => {
// hack works around some issues with allowAny eventing
- const usernames = single ? [] : selected;
+ let usernames = single ? [] : selected;
if (currentUser && excludeCurrentUser) {
- return usernames.concat([currentUser.username]);
+ usernames.concat([currentUser.username]);
}
- return usernames;
+
+ return usernames.concat(this.excludedUsernames || []);
};
+ this.element.addEventListener("paste", this._paste);
+
const userSelectorComponent = this;
$(this.element)
@@ -67,7 +96,7 @@ export default TextField.extend({
return userSearch({
term,
topicId: userSelectorComponent.topicId,
- exclude: excludedUsernames(),
+ exclude: allExcludedUsernames(),
includeGroups,
allowedUsers,
includeMentionableGroups,
@@ -84,7 +113,7 @@ export default TextField.extend({
}
return v.username || v.name;
} else {
- const excludes = excludedUsernames();
+ const excludes = allExcludedUsernames();
return v.usernames.filter(item => excludes.indexOf(item) === -1);
}
},
@@ -127,10 +156,32 @@ export default TextField.extend({
});
},
+ importText(text) {
+ let usernames = [];
+ if ((this.usernames || "").length > 0) {
+ usernames = this.usernames.split(",");
+ }
+
+ (text || "").split(/[, \n]+/).forEach(val => {
+ val = val.replace(/^@+/, "").trim();
+ if (
+ val.length > 0 &&
+ (!this.excludedUsernames || !this.excludedUsernames.includes(val))
+ ) {
+ usernames.push(val);
+ }
+ });
+ this.set("usernames", usernames.uniq().join(","));
+
+ if (!this.canReceiveUpdates) {
+ this._createAutocompleteInstance({ updateData: true });
+ }
+ },
+
// THIS IS A HUGE HACK TO SUPPORT CLEARING THE INPUT
@observes("usernames")
_clearInput() {
- if (arguments.length > 1 && Ember.isEmpty(this.usernames)) {
+ if (arguments.length > 1 && isEmpty(this.usernames)) {
$(this.element)
.parent()
.find("a")
diff --git a/app/assets/javascripts/discourse/components/user-stat.js.es6 b/app/assets/javascripts/discourse/components/user-stat.js.es6
index 879198734c..1f3fe86644 100644
--- a/app/assets/javascripts/discourse/components/user-stat.js.es6
+++ b/app/assets/javascripts/discourse/components/user-stat.js.es6
@@ -1,6 +1,8 @@
-export default Ember.Component.extend({
+import { equal } from "@ember/object/computed";
+import Component from "@ember/component";
+export default Component.extend({
classNames: ["user-stat"],
type: "number",
- isNumber: Ember.computed.equal("type", "number"),
- isDuration: Ember.computed.equal("type", "duration")
+ isNumber: equal("type", "number"),
+ isDuration: equal("type", "duration")
});
diff --git a/app/assets/javascripts/discourse/components/user-stream-item.js.es6 b/app/assets/javascripts/discourse/components/user-stream-item.js.es6
index 332e16520d..f0c5ca36e5 100644
--- a/app/assets/javascripts/discourse/components/user-stream-item.js.es6
+++ b/app/assets/javascripts/discourse/components/user-stream-item.js.es6
@@ -1,7 +1,8 @@
+import Component from "@ember/component";
import { propertyEqual } from "discourse/lib/computed";
import { actionDescription } from "discourse/widgets/post-small-action";
-export default Ember.Component.extend({
+export default Component.extend({
classNameBindings: [
":user-stream-item",
":item", // DEPRECATED: 'item' class
diff --git a/app/assets/javascripts/discourse/components/user-stream.js.es6 b/app/assets/javascripts/discourse/components/user-stream.js.es6
index ca24119a52..62d9fe7f0d 100644
--- a/app/assets/javascripts/discourse/components/user-stream.js.es6
+++ b/app/assets/javascripts/discourse/components/user-stream.js.es6
@@ -1,3 +1,5 @@
+import { schedule } from "@ember/runloop";
+import Component from "@ember/component";
import LoadMore from "discourse/mixins/load-more";
import ClickTrack from "discourse/lib/click-track";
import Post from "discourse/models/post";
@@ -5,9 +7,11 @@ import DiscourseURL from "discourse/lib/url";
import Draft from "discourse/models/draft";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { getOwner } from "discourse-common/lib/get-owner";
+import { observes } from "discourse-common/utils/decorators";
+import { on } from "@ember/object/evented";
-export default Ember.Component.extend(LoadMore, {
- _initialize: function() {
+export default Component.extend(LoadMore, {
+ _initialize: on("init", function() {
const filter = this.get("stream.filter");
if (filter) {
this.set("classNames", [
@@ -15,36 +19,41 @@ export default Ember.Component.extend(LoadMore, {
"filter-" + filter.toString().replace(",", "-")
]);
}
- }.on("init"),
+ }),
loading: false,
eyelineSelector: ".user-stream .item",
classNames: ["user-stream"],
+ @observes("stream.user.id")
_scrollTopOnModelChange: function() {
- Ember.run.schedule("afterRender", () => $(document).scrollTop(0));
- }.observes("stream.user.id"),
+ schedule("afterRender", () => $(document).scrollTop(0));
+ },
- _inserted: function() {
+ _inserted: on("didInsertElement", function() {
this.bindScrolling({ name: "user-stream-view" });
$(window).on("resize.discourse-on-scroll", () => this.scrolled());
- this.$().on("click.details-disabled", "details.disabled", () => false);
- this.$().on("click.discourse-redirect", ".excerpt a", function(e) {
+ $(this.element).on(
+ "click.details-disabled",
+ "details.disabled",
+ () => false
+ );
+ $(this.element).on("click.discourse-redirect", ".excerpt a", function(e) {
return ClickTrack.trackClick(e);
});
- }.on("didInsertElement"),
+ }),
// This view is being removed. Shut down operations
- _destroyed: function() {
+ _destroyed: on("willDestroyElement", function() {
this.unbindScrolling("user-stream-view");
$(window).unbind("resize.discourse-on-scroll");
- this.$().off("click.details-disabled", "details.disabled");
+ $(this.element).off("click.details-disabled", "details.disabled");
// Unbind link tracking
- this.$().off("click.discourse-redirect", ".excerpt a");
- }.on("willDestroyElement"),
+ $(this.element).off("click.discourse-redirect", ".excerpt a");
+ }),
actions: {
removeBookmark(userAction) {
@@ -66,13 +75,16 @@ export default Ember.Component.extend(LoadMore, {
} else {
Draft.get(item.draft_key)
.then(d => {
- if (d.draft) {
- composer.open({
- draft: d.draft,
- draftKey: item.draft_key,
- draftSequence: d.draft_sequence
- });
+ const draft = d.draft || item.data;
+ if (!draft) {
+ return;
}
+
+ composer.open({
+ draft,
+ draftKey: item.draft_key,
+ draftSequence: d.draft_sequence
+ });
})
.catch(error => {
popupAjaxError(error);
diff --git a/app/assets/javascripts/discourse/components/user-summary-category-search.js.es6 b/app/assets/javascripts/discourse/components/user-summary-category-search.js.es6
index 86def06ee1..202ee1eb58 100644
--- a/app/assets/javascripts/discourse/components/user-summary-category-search.js.es6
+++ b/app/assets/javascripts/discourse/components/user-summary-category-search.js.es6
@@ -1,9 +1,10 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "",
- @computed("user", "category")
+ @discourseComputed("user", "category")
searchParams() {
return `@${this.get("user.username")} #${this.get("category.slug")}`;
}
diff --git a/app/assets/javascripts/discourse/components/user-summary-section.js.es6 b/app/assets/javascripts/discourse/components/user-summary-section.js.es6
index b5deb64dc4..d566ee18ab 100644
--- a/app/assets/javascripts/discourse/components/user-summary-section.js.es6
+++ b/app/assets/javascripts/discourse/components/user-summary-section.js.es6
@@ -1,3 +1,4 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
classNames: ["top-sub-section"]
});
diff --git a/app/assets/javascripts/discourse/components/user-summary-topic.js.es6 b/app/assets/javascripts/discourse/components/user-summary-topic.js.es6
index 145b770e51..790c675e49 100644
--- a/app/assets/javascripts/discourse/components/user-summary-topic.js.es6
+++ b/app/assets/javascripts/discourse/components/user-summary-topic.js.es6
@@ -1,3 +1,4 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
tagName: "li"
});
diff --git a/app/assets/javascripts/discourse/components/user-summary-topics-list.js.es6 b/app/assets/javascripts/discourse/components/user-summary-topics-list.js.es6
index 5035daa2a7..5cee1e00e9 100644
--- a/app/assets/javascripts/discourse/components/user-summary-topics-list.js.es6
+++ b/app/assets/javascripts/discourse/components/user-summary-topics-list.js.es6
@@ -1,12 +1,13 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
// should be kept in sync with 'UserSummary::MAX_SUMMARY_RESULTS'
const MAX_SUMMARY_RESULTS = 6;
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "",
- @computed("items.length")
+ @discourseComputed("items.length")
hasMore(length) {
return length >= MAX_SUMMARY_RESULTS;
}
diff --git a/app/assets/javascripts/discourse/components/user-summary-user.js.es6 b/app/assets/javascripts/discourse/components/user-summary-user.js.es6
index 145b770e51..790c675e49 100644
--- a/app/assets/javascripts/discourse/components/user-summary-user.js.es6
+++ b/app/assets/javascripts/discourse/components/user-summary-user.js.es6
@@ -1,3 +1,4 @@
-export default Ember.Component.extend({
+import Component from "@ember/component";
+export default Component.extend({
tagName: "li"
});
diff --git a/app/assets/javascripts/discourse/components/watch-read.js.es6 b/app/assets/javascripts/discourse/components/watch-read.js.es6
index 68b1d1f480..16cc583f4f 100644
--- a/app/assets/javascripts/discourse/components/watch-read.js.es6
+++ b/app/assets/javascripts/discourse/components/watch-read.js.es6
@@ -1,6 +1,7 @@
+import Component from "@ember/component";
import isElementInViewport from "discourse/lib/is-element-in-viewport";
-export default Ember.Component.extend({
+export default Component.extend({
didInsertElement() {
this._super(...arguments);
const currentUser = this.currentUser;
diff --git a/app/assets/javascripts/discourse/controllers/about.js.es6 b/app/assets/javascripts/discourse/controllers/about.js.es6
index f17aac609b..9225421995 100644
--- a/app/assets/javascripts/discourse/controllers/about.js.es6
+++ b/app/assets/javascripts/discourse/controllers/about.js.es6
@@ -1,9 +1,11 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { gt } from "@ember/object/computed";
+import Controller from "@ember/controller";
-export default Ember.Controller.extend({
- faqOverriden: Ember.computed.gt("siteSettings.faq_url.length", 0),
+export default Controller.extend({
+ faqOverriden: gt("siteSettings.faq_url.length", 0),
- @computed
+ @discourseComputed
contactInfo() {
if (this.siteSettings.contact_url) {
return I18n.t("about.contact_info", {
diff --git a/app/assets/javascripts/discourse/controllers/account-created-edit-email.js.es6 b/app/assets/javascripts/discourse/controllers/account-created-edit-email.js.es6
index b22fd6de9a..dd3314110b 100644
--- a/app/assets/javascripts/discourse/controllers/account-created-edit-email.js.es6
+++ b/app/assets/javascripts/discourse/controllers/account-created-edit-email.js.es6
@@ -1,12 +1,13 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Controller from "@ember/controller";
import { changeEmail } from "discourse/lib/user-activation";
-import computed from "ember-addons/ember-computed-decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
-export default Ember.Controller.extend({
+export default Controller.extend({
accountCreated: null,
newEmail: null,
- @computed("newEmail", "accountCreated.email")
+ @discourseComputed("newEmail", "accountCreated.email")
submitDisabled(newEmail, currentEmail) {
return newEmail === currentEmail;
},
diff --git a/app/assets/javascripts/discourse/controllers/account-created-index.js.es6 b/app/assets/javascripts/discourse/controllers/account-created-index.js.es6
index ed822d3d3b..00d381a080 100644
--- a/app/assets/javascripts/discourse/controllers/account-created-index.js.es6
+++ b/app/assets/javascripts/discourse/controllers/account-created-index.js.es6
@@ -1,6 +1,7 @@
+import Controller from "@ember/controller";
import { resendActivationEmail } from "discourse/lib/user-activation";
-export default Ember.Controller.extend({
+export default Controller.extend({
actions: {
sendActivationEmail() {
resendActivationEmail(this.get("accountCreated.username")).then(() => {
diff --git a/app/assets/javascripts/discourse/controllers/activation-edit.js.es6 b/app/assets/javascripts/discourse/controllers/activation-edit.js.es6
index ebe73984d0..010878f720 100644
--- a/app/assets/javascripts/discourse/controllers/activation-edit.js.es6
+++ b/app/assets/javascripts/discourse/controllers/activation-edit.js.es6
@@ -1,16 +1,18 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { extractError } from "discourse/lib/ajax-error";
import { changeEmail } from "discourse/lib/user-activation";
-export default Ember.Controller.extend(ModalFunctionality, {
- login: Ember.inject.controller(),
+export default Controller.extend(ModalFunctionality, {
+ login: inject(),
currentEmail: null,
newEmail: null,
password: null,
- @computed("newEmail", "currentEmail")
+ @discourseComputed("newEmail", "currentEmail")
submitDisabled(newEmail, currentEmail) {
return newEmail === currentEmail;
},
diff --git a/app/assets/javascripts/discourse/controllers/add-post-notice.js.es6 b/app/assets/javascripts/discourse/controllers/add-post-notice.js.es6
index d5c40b9a04..b0c92b3a81 100644
--- a/app/assets/javascripts/discourse/controllers/add-post-notice.js.es6
+++ b/app/assets/javascripts/discourse/controllers/add-post-notice.js.es6
@@ -1,7 +1,10 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-import computed from "ember-addons/ember-computed-decorators";
+import { cookAsync } from "discourse/lib/text";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
post: null,
resolve: null,
reject: null,
@@ -9,9 +12,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
notice: null,
saving: false,
- @computed("saving", "notice")
+ @discourseComputed("saving", "notice")
disabled(saving, notice) {
- return saving || Ember.isEmpty(notice);
+ return saving || isEmpty(notice);
},
onShow() {
@@ -42,10 +45,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
post
.updatePostField("notice", notice)
- .then(() => {
+ .then(() => cookAsync(notice, { features: { onebox: false } }))
+ .then(cookedNotice => {
post.setProperties({
notice_type: "custom",
- notice_args: notice
+ notice_args: cookedNotice.string
});
resolve();
this.send("closeModal");
diff --git a/app/assets/javascripts/discourse/controllers/application.js.es6 b/app/assets/javascripts/discourse/controllers/application.js.es6
index ce23c502fa..c96c10c6e0 100644
--- a/app/assets/javascripts/discourse/controllers/application.js.es6
+++ b/app/assets/javascripts/discourse/controllers/application.js.es6
@@ -1,11 +1,14 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { inject as service } from "@ember/service";
+import Controller from "@ember/controller";
import { isAppWebview, isiOSPWA } from "discourse/lib/utilities";
-export default Ember.Controller.extend({
+export default Controller.extend({
showTop: true,
showFooter: false,
+ router: service(),
- @computed
+ @discourseComputed
canSignUp() {
return (
!Discourse.SiteSettings.invite_only &&
@@ -14,12 +17,12 @@ export default Ember.Controller.extend({
);
},
- @computed
+ @discourseComputed
loginRequired() {
- return Discourse.SiteSettings.login_required && !Discourse.User.current();
+ return Discourse.SiteSettings.login_required && !this.currentUser;
},
- @computed
+ @discourseComputed
showFooterNav() {
return isAppWebview() || isiOSPWA();
}
diff --git a/app/assets/javascripts/discourse/controllers/associate-account-confirm.js.es6 b/app/assets/javascripts/discourse/controllers/associate-account-confirm.js.es6
index e9685e0cf9..e138dc71db 100644
--- a/app/assets/javascripts/discourse/controllers/associate-account-confirm.js.es6
+++ b/app/assets/javascripts/discourse/controllers/associate-account-confirm.js.es6
@@ -1,8 +1,9 @@
+import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
actions: {
finishConnect() {
ajax({
diff --git a/app/assets/javascripts/discourse/controllers/auth-token.js.es6 b/app/assets/javascripts/discourse/controllers/auth-token.js.es6
index 1d933fb191..a24f6a53b1 100644
--- a/app/assets/javascripts/discourse/controllers/auth-token.js.es6
+++ b/app/assets/javascripts/discourse/controllers/auth-token.js.es6
@@ -1,8 +1,10 @@
+import { next } from "@ember/runloop";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import { userPath } from "discourse/lib/url";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
expanded: false,
onShow() {
@@ -23,7 +25,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
highlightSecure() {
this.send("closeModal");
- Ember.run.next(() => {
+ next(() => {
const $prefPasswordDiv = $(".pref-password");
$prefPasswordDiv.addClass("highlighted");
diff --git a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6
index 624eb3ab22..959b296cec 100644
--- a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6
+++ b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6
@@ -1,11 +1,12 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
-import { allowsImages } from "discourse/lib/utilities";
+import { allowsImages } from "discourse/lib/uploads";
import { popupAjaxError } from "discourse/lib/ajax-error";
-export default Ember.Controller.extend(ModalFunctionality, {
- @computed(
+export default Controller.extend(ModalFunctionality, {
+ @discourseComputed(
"selected",
"user.system_avatar_upload_id",
"user.gravatar_avatar_upload_id",
@@ -22,7 +23,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
}
},
- @computed(
+ @discourseComputed(
"selected",
"user.system_avatar_template",
"user.gravatar_avatar_template",
@@ -39,9 +40,12 @@ export default Ember.Controller.extend(ModalFunctionality, {
}
},
- @computed()
+ @discourseComputed()
allowAvatarUpload() {
- return this.siteSettings.allow_uploaded_avatars && allowsImages();
+ return (
+ this.siteSettings.allow_uploaded_avatars &&
+ allowsImages(this.currentUser.staff)
+ );
},
actions: {
diff --git a/app/assets/javascripts/discourse/controllers/badges/index.js.es6 b/app/assets/javascripts/discourse/controllers/badges/index.js.es6
index eb36dbe521..e056401703 100644
--- a/app/assets/javascripts/discourse/controllers/badges/index.js.es6
+++ b/app/assets/javascripts/discourse/controllers/badges/index.js.es6
@@ -1,7 +1,8 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Controller from "@ember/controller";
-export default Ember.Controller.extend({
- @computed("model")
+export default Controller.extend({
+ @discourseComputed("model")
badgeGroups(model) {
var sorted = _.sortBy(model, function(badge) {
var pos = badge.get("badge_grouping.position");
diff --git a/app/assets/javascripts/discourse/controllers/badges/show.js.es6 b/app/assets/javascripts/discourse/controllers/badges/show.js.es6
index d3411ae6d3..e9d70aa80f 100644
--- a/app/assets/javascripts/discourse/controllers/badges/show.js.es6
+++ b/app/assets/javascripts/discourse/controllers/badges/show.js.es6
@@ -1,40 +1,50 @@
+import { inject } from "@ember/controller";
+import EmberObject from "@ember/object";
+import Controller from "@ember/controller";
+import Badge from "discourse/models/badge";
import UserBadge from "discourse/models/user-badge";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
-import BadgeSelectController from "discourse/mixins/badge-select-controller";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
-export default Ember.Controller.extend(BadgeSelectController, {
+export default Controller.extend({
queryParams: ["username"],
noMoreBadges: false,
userBadges: null,
- application: Ember.inject.controller(),
+ application: inject(),
hiddenSetTitle: true,
- @computed("userBadgesAll")
+ @discourseComputed("userBadgesAll")
filteredList(userBadgesAll) {
return userBadgesAll.filterBy("badge.allow_title", true);
},
- @computed("username")
+ @discourseComputed("filteredList")
+ selectableUserBadges(filteredList) {
+ return [
+ EmberObject.create({
+ badge: Badge.create({ name: I18n.t("badges.none") })
+ }),
+ ...filteredList.uniqBy("badge.name")
+ ];
+ },
+
+ @discourseComputed("username")
user(username) {
if (username) {
return this.userBadges[0].get("user");
}
},
- @computed("username", "model.grant_count", "userBadges.grant_count")
+ @discourseComputed("username", "model.grant_count", "userBadges.grant_count")
grantCount(username, modelCount, userCount) {
return username ? userCount : modelCount;
},
- @computed("model.grant_count", "userBadges.grant_count")
+ @discourseComputed("model.grant_count", "userBadges.grant_count")
othersCount(modelCount, userCount) {
return modelCount - userCount;
},
- @computed("model.allow_title", "model.has_badge", "model")
+ @discourseComputed("model.allow_title", "model.has_badge", "model")
canSelectTitle(hasTitleBadges, hasBadge) {
return this.siteSettings.enable_badges && hasTitleBadges && hasBadge;
},
@@ -68,7 +78,7 @@ export default Ember.Controller.extend(BadgeSelectController, {
}
},
- @computed("noMoreBadges", "grantCount", "userBadges.length")
+ @discourseComputed("noMoreBadges", "grantCount", "userBadges.length")
canLoadMore(noMoreBadges, grantCount, userBadgeLength) {
if (noMoreBadges) {
return false;
@@ -76,7 +86,7 @@ export default Ember.Controller.extend(BadgeSelectController, {
return grantCount > (userBadgeLength || 0);
},
- @computed("user", "model.grant_count")
+ @discourseComputed("user", "model.grant_count")
canShowOthers(user, grantCount) {
return !!user && grantCount > 1;
},
diff --git a/app/assets/javascripts/discourse/controllers/basic-modal-body.js.es6 b/app/assets/javascripts/discourse/controllers/basic-modal-body.js.es6
index 1ee99a7549..03d95d5cb2 100644
--- a/app/assets/javascripts/discourse/controllers/basic-modal-body.js.es6
+++ b/app/assets/javascripts/discourse/controllers/basic-modal-body.js.es6
@@ -1,5 +1,6 @@
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
modal: null
});
diff --git a/app/assets/javascripts/discourse/controllers/bookmark.js.es6 b/app/assets/javascripts/discourse/controllers/bookmark.js.es6
new file mode 100644
index 0000000000..3be0e107af
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/bookmark.js.es6
@@ -0,0 +1,223 @@
+import Controller from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+import discourseComputed from "discourse-common/utils/decorators";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import { htmlSafe } from "@ember/template";
+import { ajax } from "discourse/lib/ajax";
+import { reads } from "@ember/object/computed";
+
+const START_OF_DAY_HOUR = 8;
+const REMINDER_TYPES = {
+ AT_DESKTOP: "at_desktop",
+ LATER_TODAY: "later_today",
+ NEXT_BUSINESS_DAY: "next_business_day",
+ TOMORROW: "tomorrow",
+ NEXT_WEEK: "next_week",
+ NEXT_MONTH: "next_month",
+ CUSTOM: "custom"
+};
+
+export default Controller.extend(ModalFunctionality, {
+ loading: false,
+ errorMessage: null,
+ name: null,
+ selectedReminderType: null,
+ closeWithoutSaving: false,
+ isSavingBookmarkManually: false,
+ onCloseWithoutSaving: null,
+
+ onShow() {
+ this.setProperties({
+ errorMessage: null,
+ name: null,
+ selectedReminderType: null,
+ closeWithoutSaving: false,
+ isSavingBookmarkManually: false
+ });
+ },
+
+ // we always want to save the bookmark unless the user specifically
+ // clicks the save or cancel button to mimic browser behaviour
+ onClose() {
+ if (!this.closeWithoutSaving && !this.isSavingBookmarkManually) {
+ this.saveBookmark();
+ }
+ if (this.onCloseWithoutSaving && this.closeWithoutSaving) {
+ this.onCloseWithoutSaving();
+ }
+ },
+
+ usingMobileDevice: reads("site.mobileView"),
+ showBookmarkReminderControls: false,
+
+ @discourseComputed()
+ reminderTypes: () => {
+ return REMINDER_TYPES;
+ },
+
+ @discourseComputed()
+ showLaterToday() {
+ return !this.laterToday().isSame(this.tomorrow(), "date");
+ },
+
+ @discourseComputed()
+ laterTodayFormatted() {
+ return htmlSafe(
+ I18n.t("bookmarks.reminders.later_today", {
+ date: this.laterToday().format(I18n.t("dates.time"))
+ })
+ );
+ },
+
+ @discourseComputed()
+ tomorrowFormatted() {
+ return htmlSafe(
+ I18n.t("bookmarks.reminders.tomorrow", {
+ date: this.tomorrow().format(I18n.t("dates.time_short_day"))
+ })
+ );
+ },
+
+ @discourseComputed()
+ nextBusinessDayFormatted() {
+ return htmlSafe(
+ I18n.t("bookmarks.reminders.next_business_day", {
+ date: this.nextBusinessDay().format(I18n.t("dates.time_short_day"))
+ })
+ );
+ },
+
+ @discourseComputed()
+ nextWeekFormatted() {
+ return htmlSafe(
+ I18n.t("bookmarks.reminders.next_week", {
+ date: this.nextWeek().format(I18n.t("dates.long_no_year"))
+ })
+ );
+ },
+
+ @discourseComputed()
+ nextMonthFormatted() {
+ return htmlSafe(
+ I18n.t("bookmarks.reminders.next_month", {
+ date: this.nextMonth().format(I18n.t("dates.long_no_year"))
+ })
+ );
+ },
+
+ @discourseComputed()
+ userHasTimezoneSet() {
+ return !_.isEmpty(this.userTimezone());
+ },
+
+ saveBookmark() {
+ const reminderAt = this.reminderAt();
+ const reminderAtISO = reminderAt ? reminderAt.toISOString() : null;
+ const data = {
+ reminder_type: this.selectedReminderType,
+ reminder_at: reminderAtISO,
+ name: this.name,
+ post_id: this.model.postId
+ };
+
+ return ajax("/bookmarks", { type: "POST", data }).then(() => {
+ if (this.afterSave) {
+ this.afterSave(reminderAtISO);
+ }
+ });
+ },
+
+ reminderAt() {
+ if (!this.selectedReminderType) {
+ return;
+ }
+
+ switch (this.selectedReminderType) {
+ case REMINDER_TYPES.AT_DESKTOP:
+ // TODO: Implement at desktop bookmark reminder functionality
+ return "";
+ case REMINDER_TYPES.LATER_TODAY:
+ return this.laterToday();
+ case REMINDER_TYPES.NEXT_BUSINESS_DAY:
+ return this.nextBusinessDay();
+ case REMINDER_TYPES.TOMORROW:
+ return this.tomorrow();
+ case REMINDER_TYPES.NEXT_WEEK:
+ return this.nextWeek();
+ case REMINDER_TYPES.NEXT_MONTH:
+ return this.nextMonth();
+ case REMINDER_TYPES.CUSTOM:
+ // TODO: Implement custom bookmark reminder times
+ return "";
+ }
+ },
+
+ nextWeek() {
+ return this.startOfDay(this.now().add(7, "days"));
+ },
+
+ nextMonth() {
+ return this.startOfDay(this.now().add(1, "month"));
+ },
+
+ nextBusinessDay() {
+ const currentDay = this.now().isoWeekday(); // 1=Mon, 7=Sun
+ let next = null;
+
+ // friday
+ if (currentDay === 5) {
+ next = this.now().add(3, "days");
+ // saturday
+ } else if (currentDay === 6) {
+ next = this.now().add(2, "days");
+ } else {
+ next = this.now().add(1, "day");
+ }
+
+ return this.startOfDay(next);
+ },
+
+ tomorrow() {
+ return this.startOfDay(this.now().add(1, "day"));
+ },
+
+ startOfDay(momentDate) {
+ return momentDate.hour(START_OF_DAY_HOUR).startOf("hour");
+ },
+
+ userTimezone() {
+ return this.currentUser.timezone;
+ },
+
+ now() {
+ return moment.tz(this.userTimezone());
+ },
+
+ laterToday() {
+ let later = this.now().add(3, "hours");
+ return later.minutes() < 30
+ ? later.minutes(30)
+ : later.add(30, "minutes").startOf("hour");
+ },
+
+ actions: {
+ saveAndClose() {
+ this.isSavingBookmarkManually = true;
+ this.saveBookmark()
+ .then(() => this.send("closeModal"))
+ .catch(e => {
+ this.isSavingBookmarkManually = false;
+ popupAjaxError(e);
+ });
+ },
+
+ closeWithoutSavingBookmark() {
+ this.closeWithoutSaving = true;
+ this.send("closeModal");
+ },
+
+ selectReminderType(type) {
+ this.set("selectedReminderType", type);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/bulk-notification-level.js.es6 b/app/assets/javascripts/discourse/controllers/bulk-notification-level.js.es6
index f34195bf8a..d9627ee47f 100644
--- a/app/assets/javascripts/discourse/controllers/bulk-notification-level.js.es6
+++ b/app/assets/javascripts/discourse/controllers/bulk-notification-level.js.es6
@@ -1,12 +1,15 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { empty } from "@ember/object/computed";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import { topicLevels } from "discourse/lib/notification-levels";
// Support for changing the notification level of various topics
-export default Ember.Controller.extend({
- topicBulkActions: Ember.inject.controller(),
+export default Controller.extend({
+ topicBulkActions: inject(),
notificationLevelId: null,
- @computed
+ @discourseComputed
notificationLevels() {
return topicLevels.map(level => {
return {
@@ -17,7 +20,7 @@ export default Ember.Controller.extend({
});
},
- disabled: Ember.computed.empty("notificationLevelId"),
+ disabled: empty("notificationLevelId"),
actions: {
changeNotificationLevel() {
diff --git a/app/assets/javascripts/discourse/controllers/change-owner.js.es6 b/app/assets/javascripts/discourse/controllers/change-owner.js.es6
index 0738db8740..0380faf86c 100644
--- a/app/assets/javascripts/discourse/controllers/change-owner.js.es6
+++ b/app/assets/javascripts/discourse/controllers/change-owner.js.es6
@@ -1,28 +1,25 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import { alias } from "@ember/object/computed";
+import { next } from "@ember/runloop";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import DiscourseURL from "discourse/lib/url";
-import computed from "ember-addons/ember-computed-decorators";
+import Topic from "discourse/models/topic";
-export default Ember.Controller.extend(ModalFunctionality, {
- topicController: Ember.inject.controller("topic"),
+export default Controller.extend(ModalFunctionality, {
+ topicController: inject("topic"),
saving: false,
new_user: null,
- selectedPostsCount: Ember.computed.alias(
- "topicController.selectedPostsCount"
- ),
- selectedPostsUsername: Ember.computed.alias(
- "topicController.selectedPostsUsername"
- ),
+ selectedPostsCount: alias("topicController.selectedPostsCount"),
+ selectedPostsUsername: alias("topicController.selectedPostsUsername"),
- @computed("saving", "new_user")
+ @discourseComputed("saving", "new_user")
buttonDisabled(saving, newUser) {
- return saving || Ember.isEmpty(newUser);
- },
-
- @computed("saving")
- buttonTitle(saving) {
- return saving ? I18n.t("saving") : I18n.t("topic.change_owner.action");
+ return saving || isEmpty(newUser);
},
onShow() {
@@ -41,17 +38,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
username: this.new_user
};
- Discourse.Topic.changeOwners(
- this.get("topicController.model.id"),
- options
- ).then(
+ Topic.changeOwners(this.get("topicController.model.id"), options).then(
() => {
this.send("closeModal");
this.topicController.send("deselectAll");
if (this.get("topicController.multiSelect")) {
this.topicController.send("toggleMultiSelect");
}
- Ember.run.next(() =>
+ next(() =>
DiscourseURL.routeTo(this.get("topicController.model.url"))
);
},
diff --git a/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6 b/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6
index 6d64eb32e8..5cb6bdba53 100644
--- a/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6
+++ b/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6
@@ -1,34 +1,38 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import { next } from "@ember/runloop";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-import computed from "ember-addons/ember-computed-decorators";
import DiscourseURL from "discourse/lib/url";
import Topic from "discourse/models/topic";
// Modal related to changing the timestamp of posts
-export default Ember.Controller.extend(ModalFunctionality, {
- topicController: Ember.inject.controller("topic"),
+export default Controller.extend(ModalFunctionality, {
+ topicController: inject("topic"),
saving: false,
date: "",
time: "",
- @computed("saving")
+ @discourseComputed("saving")
buttonTitle(saving) {
return saving ? I18n.t("saving") : I18n.t("topic.change_timestamp.action");
},
- @computed("date", "time")
+ @discourseComputed("date", "time")
createdAt(date, time) {
return moment(`${date} ${time}`, "YYYY-MM-DD HH:mm:ss");
},
- @computed("createdAt")
+ @discourseComputed("createdAt")
validTimestamp(createdAt) {
return moment().diff(createdAt, "minutes") < 0;
},
- @computed("saving", "date", "validTimestamp")
+ @discourseComputed("saving", "date", "validTimestamp")
buttonDisabled(saving, date, validTimestamp) {
if (saving || validTimestamp) return true;
- return Ember.isEmpty(date);
+ return isEmpty(date);
},
onShow() {
@@ -45,7 +49,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
.then(() => {
this.send("closeModal");
this.setProperties({ date: "", time: "", saving: false });
- Ember.run.next(() => DiscourseURL.routeTo(topic.url));
+ next(() => DiscourseURL.routeTo(topic.url));
})
.catch(() =>
this.flash(I18n.t("topic.change_timestamp.error"), "alert-error")
diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6
index c7ed4c474d..e7d02fbd3c 100644
--- a/app/assets/javascripts/discourse/controllers/composer.js.es6
+++ b/app/assets/javascripts/discourse/controllers/composer.js.es6
@@ -1,23 +1,30 @@
+import { isEmpty } from "@ember/utils";
+import { and, or, alias, reads } from "@ember/object/computed";
+import { debounce } from "@ember/runloop";
+import { inject as service } from "@ember/service";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import DiscourseURL from "discourse/lib/url";
import Quote from "discourse/lib/quote";
import Draft from "discourse/models/draft";
import Composer from "discourse/models/composer";
-import {
- default as computed,
+import discourseComputed, {
observes,
on
-} from "ember-addons/ember-computed-decorators";
-import InputValidation from "discourse/models/input-validation";
+} from "discourse-common/utils/decorators";
import { getOwner } from "discourse-common/lib/get-owner";
+import { escapeExpression, safariHacksDisabled } from "discourse/lib/utilities";
import {
- escapeExpression,
- uploadIcon,
authorizesOneOrMoreExtensions,
- safariHacksDisabled
-} from "discourse/lib/utilities";
+ uploadIcon
+} from "discourse/lib/uploads";
import { emojiUnescape } from "discourse/lib/text";
import { shortDate } from "discourse/lib/formatter";
import { SAVE_LABELS, SAVE_ICONS } from "discourse/models/composer";
+import { Promise } from "rsvp";
+import ENV from "discourse-common/config/environment";
+import EmberObject, { computed } from "@ember/object";
+import deprecated from "discourse-common/lib/deprecated";
function loadDraft(store, opts) {
opts = opts || {};
@@ -39,33 +46,28 @@ function loadDraft(store, opts) {
((draft.title && draft.title !== "") || (draft.reply && draft.reply !== ""))
) {
const composer = store.createRecord("composer");
+ const serializedFields = Composer.serializedFieldsForDraft();
- composer.open({
+ let attrs = {
draftKey,
draftSequence,
- action: draft.action,
- title: draft.title,
- categoryId: draft.categoryId || opts.categoryId,
- postId: draft.postId,
- archetypeId: draft.archetypeId,
- reply: draft.reply,
- metaData: draft.metaData,
- usernames: draft.usernames,
draft: true,
- composerState: Composer.DRAFT,
- composerTime: draft.composerTime,
- typingTime: draft.typingTime,
- whisper: draft.whisper,
- tags: draft.tags,
- noBump: draft.noBump
+ composerState: Composer.DRAFT
+ };
+
+ serializedFields.forEach(f => {
+ attrs[f] = draft[f] || opts[f];
});
+
+ composer.open(attrs);
+
return composer;
}
}
const _popupMenuOptionsCallbacks = [];
-let _checkDraftPopup = !Ember.testing;
+let _checkDraftPopup = !ENV.environment === "test";
export function toggleCheckDraftPopup(enabled) {
_checkDraftPopup = enabled;
@@ -79,18 +81,10 @@ export function addPopupMenuOptionsCallback(callback) {
_popupMenuOptionsCallbacks.push(callback);
}
-export default Ember.Controller.extend({
- topicController: Ember.inject.controller("topic"),
- application: Ember.inject.controller(),
+export default Controller.extend({
+ topicController: inject("topic"),
+ router: service(),
- replyAsNewTopicDraft: Ember.computed.equal(
- "model.draftKey",
- Composer.REPLY_AS_NEW_TOPIC_KEY
- ),
- replyAsNewPrivateMessageDraft: Ember.computed.equal(
- "model.draftKey",
- Composer.REPLY_AS_NEW_PRIVATE_MESSAGE_KEY
- ),
checkedMessages: false,
messageCount: null,
showEditReason: false,
@@ -103,9 +97,9 @@ export default Ember.Controller.extend({
topic: null,
linkLookup: null,
showPreview: true,
- forcePreview: Ember.computed.and("site.mobileView", "showPreview"),
- whisperOrUnlistTopic: Ember.computed.or("isWhispering", "model.unlistTopic"),
- categories: Ember.computed.alias("site.categoriesList"),
+ forcePreview: and("site.mobileView", "showPreview"),
+ whisperOrUnlistTopic: or("isWhispering", "model.unlistTopic"),
+ categories: alias("site.categoriesList"),
@on("init")
_setupPreview() {
@@ -115,7 +109,7 @@ export default Ember.Controller.extend({
this.set("showPreview", val === "true");
},
- @computed("showPreview")
+ @discourseComputed("showPreview")
toggleText(showPreview) {
return showPreview
? I18n.t("composer.hide_preview")
@@ -132,10 +126,10 @@ export default Ember.Controller.extend({
}
},
- @computed(
+ @discourseComputed(
"model.replyingToTopic",
"model.creatingPrivateMessage",
- "model.targetUsernames",
+ "model.targetRecipients",
"model.composeState"
)
focusTarget(replyingToTopic, creatingPM, usernames, composeState) {
@@ -163,7 +157,7 @@ export default Ember.Controller.extend({
return "title";
},
- showToolbar: Ember.computed({
+ showToolbar: computed({
get() {
const keyValueStore = getOwner(this).lookup("key-value-store:main");
const storedVal = keyValueStore.get("toolbar-enabled");
@@ -187,9 +181,9 @@ export default Ember.Controller.extend({
}
}),
- topicModel: Ember.computed.alias("topicController.model"),
+ topicModel: alias("topicController.model"),
- @computed("model.canEditTitle", "model.creatingPrivateMessage")
+ @discourseComputed("model.canEditTitle", "model.creatingPrivateMessage")
canEditTags(canEditTitle, creatingPrivateMessage) {
return (
this.site.can_tag_topics &&
@@ -199,36 +193,42 @@ export default Ember.Controller.extend({
);
},
- @computed
- isStaffUser() {
- const currentUser = this.currentUser;
- return currentUser && currentUser.get("staff");
+ @discourseComputed("model.editingPost", "model.topic.details.can_edit")
+ disableCategoryChooser(editingPost, canEditTopic) {
+ return editingPost && !canEditTopic;
},
- canUnlistTopic: Ember.computed.and("model.creatingTopic", "isStaffUser"),
+ @discourseComputed("model.editingPost", "model.topic.canEditTags")
+ disableTagsChooser(editingPost, canEditTags) {
+ return editingPost && !canEditTags;
+ },
- @computed("canWhisper", "replyingToWhisper")
+ isStaffUser: reads("currentUser.staff"),
+
+ canUnlistTopic: and("model.creatingTopic", "isStaffUser"),
+
+ @discourseComputed("canWhisper", "replyingToWhisper")
showWhisperToggle(canWhisper, replyingToWhisper) {
return canWhisper && !replyingToWhisper;
},
- @computed("model.post")
+ @discourseComputed("model.post")
replyingToWhisper(repliedToPost) {
return (
repliedToPost && repliedToPost.post_type === this.site.post_types.whisper
);
},
- isWhispering: Ember.computed.or("replyingToWhisper", "model.whisper"),
+ isWhispering: or("replyingToWhisper", "model.whisper"),
- @computed("model.action", "isWhispering")
+ @discourseComputed("model.action", "isWhispering")
saveIcon(action, isWhispering) {
if (isWhispering) return "far-eye-slash";
return SAVE_ICONS[action];
},
- @computed("model.action", "isWhispering", "model.editConflict")
+ @discourseComputed("model.action", "isWhispering", "model.editConflict")
saveLabel(action, isWhispering, editConflict) {
if (editConflict) return "composer.overwrite_edit";
else if (isWhispering) return "composer.create_whisper";
@@ -236,7 +236,7 @@ export default Ember.Controller.extend({
return SAVE_LABELS[action];
},
- @computed("isStaffUser", "model.action")
+ @discourseComputed("isStaffUser", "model.action")
canWhisper(isStaffUser, action) {
return (
this.siteSettings.enable_whispers &&
@@ -246,7 +246,10 @@ export default Ember.Controller.extend({
},
_setupPopupMenuOption(callback) {
- let option = callback();
+ let option = callback(this);
+ if (typeof option === "undefined") {
+ return null;
+ }
if (typeof option.condition === "undefined") {
option.condition = true;
@@ -259,7 +262,7 @@ export default Ember.Controller.extend({
return option;
},
- @computed("model.composeState", "model.creatingTopic", "model.post")
+ @discourseComputed("model.composeState", "model.creatingTopic", "model.post")
popupMenuOptions(composeState) {
if (composeState === "open" || composeState === "fullscreen") {
const options = [];
@@ -287,16 +290,16 @@ export default Ember.Controller.extend({
);
return options.concat(
- _popupMenuOptionsCallbacks.map(callback =>
- this._setupPopupMenuOption(callback)
- )
+ _popupMenuOptionsCallbacks
+ .map(callback => this._setupPopupMenuOption(callback))
+ .filter(o => o)
);
}
},
- @computed("model.creatingPrivateMessage", "model.targetUsernames")
+ @discourseComputed("model.creatingPrivateMessage", "model.targetRecipients")
showWarning(creatingPrivateMessage, usernames) {
- if (!Discourse.User.currentProp("staff")) {
+ if (!this.get("currentUser.staff")) {
return false;
}
@@ -304,7 +307,7 @@ export default Ember.Controller.extend({
// We need exactly one user to issue a warning
if (
- Ember.isEmpty(usernames) ||
+ isEmpty(usernames) ||
usernames.split(",").length !== 1 ||
hasTargetGroups
) {
@@ -314,18 +317,20 @@ export default Ember.Controller.extend({
return creatingPrivateMessage;
},
- @computed("model.topic.title")
+ @discourseComputed("model.topic.title")
draftTitle(topicTitle) {
return emojiUnescape(escapeExpression(topicTitle));
},
- @computed
+ @discourseComputed
allowUpload() {
- return authorizesOneOrMoreExtensions();
+ return authorizesOneOrMoreExtensions(this.currentUser.staff);
},
- @computed()
- uploadIcon: () => uploadIcon(),
+ @discourseComputed()
+ uploadIcon() {
+ return uploadIcon(this.currentUser.staff);
+ },
actions: {
togglePreview() {
@@ -443,8 +448,8 @@ export default Ember.Controller.extend({
this.closeAutocomplete();
if (
- Ember.isEmpty(this.get("model.reply")) &&
- Ember.isEmpty(this.get("model.title"))
+ isEmpty(this.get("model.reply")) &&
+ isEmpty(this.get("model.title"))
) {
this.close();
} else {
@@ -507,7 +512,9 @@ export default Ember.Controller.extend({
},
cancel() {
- this.cancelComposer();
+ const differentDraftContext =
+ this.get("topic.id") !== this.get("model.topic.id");
+ this.cancelComposer(differentDraftContext);
},
save() {
@@ -557,7 +564,7 @@ export default Ember.Controller.extend({
max: group.max_mentions,
group_link: groupLink
});
- } else {
+ } else if (group.user_count > 0) {
body = I18n.t("composer.group_mentioned", {
group: `@${group.name}`,
count: group.user_count,
@@ -565,11 +572,13 @@ export default Ember.Controller.extend({
});
}
- this.appEvents.trigger("composer-messages:create", {
- extraClass: "custom-body",
- templateName: "custom-body",
- body
- });
+ if (body) {
+ this.appEvents.trigger("composer-messages:create", {
+ extraClass: "custom-body",
+ templateName: "custom-body",
+ body
+ });
+ }
});
}
},
@@ -591,7 +600,7 @@ export default Ember.Controller.extend({
}
},
- disableSubmit: Ember.computed.or("model.loading", "isUploading"),
+ disableSubmit: or("model.loading", "isUploading"),
save(force) {
if (this.disableSubmit) return;
@@ -684,23 +693,16 @@ export default Ember.Controller.extend({
}
}
- this.destroyDraft();
- this.close();
- this.appEvents.trigger("post-stream:refresh");
- return result;
+ return this.destroyDraft().then(() => {
+ this.close();
+ this.appEvents.trigger("post-stream:refresh");
+ return result;
+ });
}
- // If user "created a new topic/post" or "replied as a new topic" successfully, remove the draft.
- if (
- result.responseJson.action === "create_post" ||
- this.replyAsNewTopicDraft ||
- this.replyAsNewPrivateMessageDraft
- ) {
- this.destroyDraft();
- }
if (this.get("model.editingPost")) {
this.appEvents.trigger("post-stream:refresh", {
- id: parseInt(result.responseJson.id)
+ id: parseInt(result.responseJson.id, 10)
});
if (result.responseJson.post.post_number === 1) {
this.appEvents.trigger("header:update-topic", composer.topic);
@@ -712,6 +714,17 @@ export default Ember.Controller.extend({
if (result.responseJson.action === "create_post") {
this.appEvents.trigger("post:highlight", result.payload.post_number);
}
+
+ if (result.responseJson.route_to) {
+ this.destroyDraft();
+ if (result.responseJson.message) {
+ return bootbox.alert(result.responseJson.message, () => {
+ DiscourseURL.routeTo(result.responseJson.route_to);
+ });
+ }
+ return DiscourseURL.routeTo(result.responseJson.route_to);
+ }
+
this.close();
const currentUser = this.currentUser;
@@ -723,16 +736,18 @@ export default Ember.Controller.extend({
const post = result.target;
if (post && !staged) {
- DiscourseURL.routeTo(post.url);
+ DiscourseURL.routeTo(post.url, { skipIfOnScreen: true });
}
})
.catch(error => {
composer.set("disableDrafts", false);
- this.appEvents.one("composer:will-open", () => bootbox.alert(error));
+ if (error) {
+ this.appEvents.one("composer:will-open", () => bootbox.alert(error));
+ }
});
if (
- this.get("application.currentRouteName").split(".")[0] === "topic" &&
+ this.router.currentRouteName.split(".")[0] === "topic" &&
composer.get("topic.id") === this.get("topicModel.id")
) {
staged = composer.get("stagedPost");
@@ -749,21 +764,21 @@ export default Ember.Controller.extend({
// Notify the composer messages controller that a reply has been typed. Some
// messages only appear after typing.
checkReplyLength() {
- if (!Ember.isEmpty("model.reply")) {
+ if (!isEmpty("model.reply")) {
this.appEvents.trigger("composer:typed-reply");
}
},
/**
- Open the composer view
+ Open the composer view
- @method open
- @param {Object} opts Options for creating a post
- @param {String} opts.action The action we're performing: edit, reply or createTopic
- @param {Discourse.Post} [opts.post] The post we're replying to
- @param {Discourse.Topic} [opts.topic] The topic we're replying to
- @param {String} [opts.quote] If we're opening a reply from a quote, the quote we're making
- **/
+ @method open
+ @param {Object} opts Options for creating a post
+ @param {String} opts.action The action we're performing: edit, reply or createTopic
+ @param {Post} [opts.post] The post we're replying to
+ @param {Topic} [opts.topic] The topic we're replying to
+ @param {String} [opts.quote] If we're opening a reply from a quote, the quote we're making
+ **/
open(opts) {
opts = opts || {};
@@ -788,11 +803,7 @@ 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" &&
- !opts.disableScopedCategory
- ) {
+ if (opts.categoryId && !opts.disableScopedCategory) {
const category = this.site.categories.findBy("id", opts.categoryId);
if (category) {
this.set("scopedCategoryId", opts.categoryId);
@@ -809,7 +820,7 @@ export default Ember.Controller.extend({
composerModel = null;
}
- return new Ember.RSVP.Promise((resolve, reject) => {
+ return new Promise((resolve, reject) => {
if (composerModel && composerModel.replyDirty) {
// If we're already open, we don't have to do anything
if (
@@ -830,7 +841,12 @@ export default Ember.Controller.extend({
}
// If it's a different draft, cancel it and try opening again.
- return this.cancelComposer()
+ const differentDraftContext =
+ opts.post && composerModel.topic
+ ? composerModel.topic.id !== opts.post.topic_id
+ : true;
+
+ return this.cancelComposer(differentDraftContext)
.then(() => this.open(opts))
.then(resolve, reject);
}
@@ -847,7 +863,6 @@ export default Ember.Controller.extend({
data.draft = undefined;
return data;
}
-
return this.confirmDraftAbandon(data);
})
.then(data => {
@@ -862,7 +877,9 @@ export default Ember.Controller.extend({
// otherwise, do the draft check async
else if (!opts.draft && !opts.skipDraftCheck) {
Draft.get(opts.draftKey)
- .then(data => this.confirmDraftAbandon(data))
+ .then(data => {
+ return this.confirmDraftAbandon(data);
+ })
.then(data => {
if (data.draft) {
opts.draft = data.draft;
@@ -897,19 +914,24 @@ export default Ember.Controller.extend({
isWarning: false
});
- if (opts.usernames && !this.get("model.targetUsernames")) {
- this.set("model.targetUsernames", opts.usernames);
+ if (!this.model.targetRecipients) {
+ if (opts.usernames) {
+ deprecated("`usernames` is deprecated, use `recipients` instead.");
+ this.model.set("targetRecipients", opts.usernames);
+ } else if (opts.recipients) {
+ this.model.set("targetRecipients", opts.recipients);
+ }
}
if (
opts.topicTitle &&
opts.topicTitle.length <= this.siteSettings.max_topic_title_length
) {
- this.set("model.title", opts.topicTitle);
+ this.model.set("title", opts.topicTitle);
}
if (opts.topicCategoryId) {
- this.set("model.categoryId", opts.topicCategoryId);
+ this.model.set("categoryId", opts.topicCategoryId);
}
if (opts.topicTags && !this.site.mobileView && this.site.can_tag_topics) {
@@ -922,11 +944,11 @@ export default Ember.Controller.extend({
(array[index] = tag.substring(0, this.siteSettings.max_tag_length))
);
- this.set("model.tags", tags);
+ this.model.set("tags", tags);
}
if (opts.topicBody) {
- this.set("model.reply", opts.topicBody);
+ this.model.set("reply", opts.topicBody);
}
},
@@ -943,9 +965,11 @@ export default Ember.Controller.extend({
this.send("clearTopicDraft");
}
- Draft.clear(key, this.get("model.draftSequence")).then(() =>
+ return Draft.clear(key, this.get("model.draftSequence")).then(() =>
this.appEvents.trigger("draft:destroyed", key)
);
+ } else {
+ return Promise.resolve();
}
},
@@ -962,7 +986,7 @@ export default Ember.Controller.extend({
}
if (_checkDraftPopup) {
- return new Ember.RSVP.Promise(resolve => {
+ return new Promise(resolve => {
bootbox.dialog(I18n.t("drafts.abandon.confirm"), [
{
label: I18n.t("drafts.abandon.no_value"),
@@ -984,30 +1008,47 @@ export default Ember.Controller.extend({
}
},
- cancelComposer() {
- return new Ember.RSVP.Promise(resolve => {
+ cancelComposer(differentDraft = false) {
+ const keyPrefix =
+ this.model.action === "edit" ? "post.abandon_edit" : "post.abandon";
+
+ return new Promise(resolve => {
if (this.get("model.hasMetaData") || this.get("model.replyDirty")) {
- bootbox.dialog(I18n.t("post.abandon.confirm"), [
- { label: I18n.t("post.abandon.no_value") },
+ bootbox.dialog(I18n.t(keyPrefix + ".confirm"), [
{
- label: I18n.t("post.abandon.yes_value"),
- class: "btn-danger",
- callback: result => {
- if (result) {
- this.destroyDraft();
+ label: differentDraft
+ ? I18n.t(keyPrefix + ".no_save_draft")
+ : I18n.t(keyPrefix + ".no_value"),
+ callback: () => {
+ // cancel composer without destroying draft on new draft context
+ if (differentDraft) {
this.model.clearState();
this.close();
resolve();
}
}
+ },
+ {
+ label: I18n.t(keyPrefix + ".yes_value"),
+ class: "btn-danger",
+ callback: result => {
+ if (result) {
+ this.destroyDraft().then(() => {
+ this.model.clearState();
+ this.close();
+ resolve();
+ });
+ }
+ }
}
]);
} else {
// it is possible there is some sort of crazy draft with no body ... just give up on it
- this.destroyDraft();
- this.model.clearState();
- this.close();
- resolve();
+ this.destroyDraft().then(() => {
+ this.model.clearState();
+ this.close();
+ resolve();
+ });
}
});
},
@@ -1032,13 +1073,13 @@ export default Ember.Controller.extend({
@observes("model.reply", "model.title")
_shouldSaveDraft() {
- Ember.run.debounce(this, this._saveDraft, 2000);
+ debounce(this, this._saveDraft, 2000);
},
- @computed("model.categoryId", "lastValidatedAt")
+ @discourseComputed("model.categoryId", "lastValidatedAt")
categoryValidation(categoryId, lastValidatedAt) {
if (!this.siteSettings.allow_uncategorized_topics && !categoryId) {
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason: I18n.t("composer.error.category_missing"),
lastShownAt: lastValidatedAt
@@ -1046,7 +1087,7 @@ export default Ember.Controller.extend({
}
},
- @computed("model.category", "model.tags", "lastValidatedAt")
+ @discourseComputed("model.category", "model.tags", "lastValidatedAt")
tagValidation(category, tags, lastValidatedAt) {
const tagsArray = tags || [];
if (
@@ -1054,7 +1095,7 @@ export default Ember.Controller.extend({
category &&
category.minimum_required_tags > tagsArray.length
) {
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason: I18n.t("composer.error.tags_missing", {
count: category.minimum_required_tags
@@ -1093,12 +1134,12 @@ export default Ember.Controller.extend({
$(".d-editor-input").autocomplete({ cancel: true });
},
- @computed("model.action")
+ @discourseComputed("model.action")
canEdit(action) {
return action === "edit" && this.currentUser.can_edit;
},
- @computed("model.composeState")
+ @discourseComputed("model.composeState")
visible(state) {
return state && state !== "closed";
}
diff --git a/app/assets/javascripts/discourse/controllers/convert-to-public-topic.js.es6 b/app/assets/javascripts/discourse/controllers/convert-to-public-topic.js.es6
new file mode 100644
index 0000000000..2c08ff7884
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/convert-to-public-topic.js.es6
@@ -0,0 +1,27 @@
+import Controller from "@ember/controller";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+
+export default Controller.extend(ModalFunctionality, {
+ publicCategoryId: null,
+ saving: true,
+
+ onShow() {
+ this.setProperties({ publicCategoryId: null, saving: false });
+ },
+
+ actions: {
+ makePublic() {
+ let topic = this.model;
+ topic
+ .convertTopic("public", { categoryId: this.publicCategoryId })
+ .then(() => {
+ topic.set("archetype", "regular");
+ topic.set("category_id", this.publicCategoryId);
+ this.appEvents.trigger("header:show-topic", topic);
+ this.send("closeModal");
+ })
+ .catch(popupAjaxError);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6
index 1c8ecb254a..9e7c03d121 100644
--- a/app/assets/javascripts/discourse/controllers/create-account.js.es6
+++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6
@@ -1,40 +1,45 @@
+import { isEmpty } from "@ember/utils";
+import { notEmpty, or, not } from "@ember/object/computed";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { setting } from "discourse/lib/computed";
-import {
- default as computed,
+import discourseComputed, {
+ observes,
on
-} from "ember-addons/ember-computed-decorators";
+} from "discourse-common/utils/decorators";
import { emailValid } from "discourse/lib/utilities";
-import InputValidation from "discourse/models/input-validation";
import PasswordValidation from "discourse/mixins/password-validation";
import UsernameValidation from "discourse/mixins/username-validation";
import NameValidation from "discourse/mixins/name-validation";
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
import { userPath } from "discourse/lib/url";
import { findAll } from "discourse/models/login-method";
+import EmberObject from "@ember/object";
+import User from "discourse/models/user";
-export default Ember.Controller.extend(
+export default Controller.extend(
ModalFunctionality,
PasswordValidation,
UsernameValidation,
NameValidation,
UserFieldsValidation,
{
- login: Ember.inject.controller(),
+ login: inject(),
complete: false,
- accountPasswordConfirm: 0,
accountChallenge: 0,
+ accountHoneypot: 0,
formSubmitted: false,
rejectedEmails: Ember.A([]),
prefilledUsername: null,
userFields: null,
isDeveloper: false,
- hasAuthOptions: Ember.computed.notEmpty("authOptions"),
+ hasAuthOptions: notEmpty("authOptions"),
canCreateLocal: setting("enable_local_logins"),
- showCreateForm: Ember.computed.or("hasAuthOptions", "canCreateLocal"),
+ showCreateForm: or("hasAuthOptions", "canCreateLocal"),
resetForm() {
// We wrap the fields in a structure so we can assign a value
@@ -54,7 +59,7 @@ export default Ember.Controller.extend(
this._createUserFields();
},
- @computed(
+ @discourseComputed(
"passwordRequired",
"nameValidation.failed",
"emailValidation.failed",
@@ -64,21 +69,21 @@ export default Ember.Controller.extend(
"formSubmitted"
)
submitDisabled() {
- if (!this.get("emailValidation.failed") && !this.passwordRequired)
- return false; // 3rd party auth
if (this.formSubmitted) return true;
if (this.get("nameValidation.failed")) return true;
if (this.get("emailValidation.failed")) return true;
- if (this.get("usernameValidation.failed")) return true;
- if (this.get("passwordValidation.failed")) return true;
+ if (this.get("usernameValidation.failed") && this.usernameRequired)
+ return true;
+ if (this.get("passwordValidation.failed") && this.passwordRequired)
+ return true;
if (this.get("userFieldsValidation.failed")) return true;
return false;
},
- usernameRequired: Ember.computed.not("authOptions.omit_username"),
+ usernameRequired: not("authOptions.omit_username"),
- @computed
+ @discourseComputed
fullnameRequired() {
return (
this.get("siteSettings.full_name_required") ||
@@ -86,12 +91,12 @@ export default Ember.Controller.extend(
);
},
- @computed("authOptions.auth_provider")
+ @discourseComputed("authOptions.auth_provider")
passwordRequired(authProvider) {
- return Ember.isEmpty(authProvider);
+ return isEmpty(authProvider);
},
- @computed
+ @discourseComputed
disclaimerHtml() {
return I18n.t("create_account.disclaimer", {
tos_link: this.get("siteSettings.tos_url") || Discourse.getURL("/tos"),
@@ -102,17 +107,17 @@ export default Ember.Controller.extend(
},
// Check the email address
- @computed("accountEmail", "rejectedEmails.[]")
+ @discourseComputed("accountEmail", "rejectedEmails.[]")
emailValidation(email, rejectedEmails) {
// If blank, fail without a reason
- if (Ember.isEmpty(email)) {
- return InputValidation.create({
+ if (isEmpty(email)) {
+ return EmberObject.create({
failed: true
});
}
if (rejectedEmails.includes(email)) {
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason: I18n.t("user.email.invalid")
});
@@ -122,7 +127,7 @@ export default Ember.Controller.extend(
this.get("authOptions.email") === email &&
this.get("authOptions.email_valid")
) {
- return InputValidation.create({
+ return EmberObject.create({
ok: true,
reason: I18n.t("user.email.authenticated", {
provider: this.authProviderDisplayName(
@@ -133,19 +138,23 @@ export default Ember.Controller.extend(
}
if (emailValid(email)) {
- return InputValidation.create({
+ return EmberObject.create({
ok: true,
reason: I18n.t("user.email.ok")
});
}
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason: I18n.t("user.email.invalid")
});
},
- @computed("accountEmail", "authOptions.email", "authOptions.email_valid")
+ @discourseComputed(
+ "accountEmail",
+ "authOptions.email",
+ "authOptions.email_valid"
+ )
emailValidated() {
return (
this.get("authOptions.email") === this.accountEmail &&
@@ -162,6 +171,7 @@ export default Ember.Controller.extend(
: providerName;
},
+ @observes("emailValidation", "accountEmail")
prefillUsername: function() {
if (this.prefilledUsername) {
// If username field has been filled automatically, and email field just changed,
@@ -173,17 +183,17 @@ export default Ember.Controller.extend(
}
if (
this.get("emailValidation.ok") &&
- (Ember.isEmpty(this.accountUsername) || this.get("authOptions.email"))
+ (isEmpty(this.accountUsername) || this.get("authOptions.email"))
) {
// If email is valid and username has not been entered yet,
// or email and username were filled automatically by 3rd parth auth,
// then look for a registered username that matches the email.
this.fetchExistingUsername();
}
- }.observes("emailValidation", "accountEmail"),
+ },
// Determines whether at least one login button is enabled
- @computed
+ @discourseComputed
hasAtLeastOneLoginButton() {
return findAll().length > 0;
},
@@ -191,8 +201,16 @@ export default Ember.Controller.extend(
@on("init")
fetchConfirmationValue() {
return ajax(userPath("hp.json")).then(json => {
+ this._challengeDate = new Date();
+ // remove 30 seconds for jitter, make sure this works for at least
+ // 30 seconds so we don't have hard loops
+ this._challengeExpiry = parseInt(json.expires_in, 10) - 30;
+ if (this._challengeExpiry < 30) {
+ this._challengeExpiry = 30;
+ }
+
this.setProperties({
- accountPasswordConfirm: json.value,
+ accountHoneypot: json.value,
accountChallenge: json.challenge
.split("")
.reverse()
@@ -201,85 +219,100 @@ export default Ember.Controller.extend(
});
},
+ performAccountCreation() {
+ const attrs = this.getProperties(
+ "accountName",
+ "accountEmail",
+ "accountPassword",
+ "accountUsername",
+ "accountChallenge"
+ );
+
+ attrs["accountPasswordConfirm"] = this.accountHoneypot;
+
+ const userFields = this.userFields;
+ const destinationUrl = this.get("authOptions.destination_url");
+
+ if (!isEmpty(destinationUrl)) {
+ $.cookie("destination_url", destinationUrl, { path: "/" });
+ }
+
+ // Add the userfields to the data
+ if (!isEmpty(userFields)) {
+ attrs.userFields = {};
+ userFields.forEach(
+ f => (attrs.userFields[f.get("field.id")] = f.get("value"))
+ );
+ }
+
+ this.set("formSubmitted", true);
+ return User.createAccount(attrs).then(
+ result => {
+ this.set("isDeveloper", false);
+ if (result.success) {
+ // invalidate honeypot
+ this._challengeExpiry = 1;
+
+ // Trigger the browser's password manager using the hidden static login form:
+ const $hidden_login_form = $("#hidden-login-form");
+ $hidden_login_form
+ .find("input[name=username]")
+ .val(attrs.accountUsername);
+ $hidden_login_form
+ .find("input[name=password]")
+ .val(attrs.accountPassword);
+ $hidden_login_form
+ .find("input[name=redirect]")
+ .val(userPath("account-created"));
+ $hidden_login_form.submit();
+ } else {
+ this.flash(
+ result.message || I18n.t("create_account.failed"),
+ "error"
+ );
+ if (result.is_developer) {
+ this.set("isDeveloper", true);
+ }
+ if (
+ result.errors &&
+ result.errors.email &&
+ result.errors.email.length > 0 &&
+ result.values
+ ) {
+ this.rejectedEmails.pushObject(result.values.email);
+ }
+ if (
+ result.errors &&
+ result.errors.password &&
+ result.errors.password.length > 0
+ ) {
+ this.rejectedPasswords.pushObject(attrs.accountPassword);
+ }
+ this.set("formSubmitted", false);
+ $.removeCookie("destination_url");
+ }
+ },
+ () => {
+ this.set("formSubmitted", false);
+ $.removeCookie("destination_url");
+ return this.flash(I18n.t("create_account.failed"), "error");
+ }
+ );
+ },
+
actions: {
externalLogin(provider) {
this.login.send("externalLogin", provider);
},
createAccount() {
- const attrs = this.getProperties(
- "accountName",
- "accountEmail",
- "accountPassword",
- "accountUsername",
- "accountPasswordConfirm",
- "accountChallenge"
- );
- const userFields = this.userFields;
- const destinationUrl = this.get("authOptions.destination_url");
-
- if (!Ember.isEmpty(destinationUrl)) {
- $.cookie("destination_url", destinationUrl, { path: "/" });
- }
-
- // Add the userfields to the data
- if (!Ember.isEmpty(userFields)) {
- attrs.userFields = {};
- userFields.forEach(
- f => (attrs.userFields[f.get("field.id")] = f.get("value"))
+ if (new Date() - this._challengeDate > 1000 * this._challengeExpiry) {
+ this.fetchConfirmationValue().then(() =>
+ this.performAccountCreation()
);
+ } else {
+ this.performAccountCreation();
}
-
- this.set("formSubmitted", true);
- return Discourse.User.createAccount(attrs).then(
- result => {
- this.set("isDeveloper", false);
- if (result.success) {
- // Trigger the browser's password manager using the hidden static login form:
- const $hidden_login_form = $("#hidden-login-form");
- $hidden_login_form
- .find("input[name=username]")
- .val(attrs.accountUsername);
- $hidden_login_form
- .find("input[name=password]")
- .val(attrs.accountPassword);
- $hidden_login_form
- .find("input[name=redirect]")
- .val(userPath("account-created"));
- $hidden_login_form.submit();
- } else {
- this.flash(
- result.message || I18n.t("create_account.failed"),
- "error"
- );
- if (result.is_developer) {
- this.set("isDeveloper", true);
- }
- if (
- result.errors &&
- result.errors.email &&
- result.errors.email.length > 0 &&
- result.values
- ) {
- this.rejectedEmails.pushObject(result.values.email);
- }
- if (
- result.errors &&
- result.errors.password &&
- result.errors.password.length > 0
- ) {
- this.rejectedPasswords.pushObject(attrs.accountPassword);
- }
- this.set("formSubmitted", false);
- $.removeCookie("destination_url");
- }
- },
- () => {
- this.set("formSubmitted", false);
- $.removeCookie("destination_url");
- return this.flash(I18n.t("create_account.failed"), "error");
- }
- );
}
}
}
diff --git a/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6 b/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6
index b1fc3b3509..d286a37dab 100644
--- a/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6
+++ b/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6
@@ -1,3 +1,8 @@
+import { alias } from "@ember/object/computed";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
+import DiscourseNavigation from "discourse/components/d-navigation";
+
// Just add query params here to have them automatically passed to topic list filters.
export const queryParams = {
order: { replace: true, refreshModel: true },
@@ -14,23 +19,29 @@ export const queryParams = {
// Basic controller options
const controllerOpts = {
- discoveryTopics: Ember.inject.controller("discovery/topics"),
+ discoveryTopics: inject("discovery/topics"),
queryParams: Object.keys(queryParams)
};
// Aliases for the values
controllerOpts.queryParams.forEach(
- p => (controllerOpts[p] = Ember.computed.alias(`discoveryTopics.${p}`))
+ p => (controllerOpts[p] = alias(`discoveryTopics.${p}`))
);
-const Controller = Ember.Controller.extend(controllerOpts);
+const SortableController = Controller.extend(controllerOpts);
export const addDiscoveryQueryParam = function(p, opts) {
queryParams[p] = opts;
const cOpts = {};
- cOpts[p] = Ember.computed.alias(`discoveryTopics.${p}`);
+ cOpts[p] = alias(`discoveryTopics.${p}`);
cOpts["queryParams"] = Object.keys(queryParams);
- Controller.reopen(cOpts);
+ SortableController.reopen(cOpts);
+
+ if (opts && opts.persisted) {
+ DiscourseNavigation.reopen({
+ persistedQueryParams: queryParams
+ });
+ }
};
-export default Controller;
+export default SortableController;
diff --git a/app/assets/javascripts/discourse/controllers/discovery.js.es6 b/app/assets/javascripts/discourse/controllers/discovery.js.es6
index 858058e4b0..fa374b4372 100644
--- a/app/assets/javascripts/discourse/controllers/discovery.js.es6
+++ b/app/assets/javascripts/discourse/controllers/discovery.js.es6
@@ -1,31 +1,37 @@
+import { alias, not } from "@ember/object/computed";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import DiscourseURL from "discourse/lib/url";
+import Category from "discourse/models/category";
+import { observes } from "discourse-common/utils/decorators";
-export default Ember.Controller.extend({
- discoveryTopics: Ember.inject.controller("discovery/topics"),
- navigationCategory: Ember.inject.controller("navigation/category"),
- application: Ember.inject.controller(),
+export default Controller.extend({
+ discoveryTopics: inject("discovery/topics"),
+ navigationCategory: inject("navigation/category"),
+ application: inject(),
loading: false,
- category: Ember.computed.alias("navigationCategory.category"),
- noSubcategories: Ember.computed.alias("navigationCategory.noSubcategories"),
+ category: alias("navigationCategory.category"),
+ noSubcategories: alias("navigationCategory.noSubcategories"),
- loadedAllItems: Ember.computed.not("discoveryTopics.model.canLoadMore"),
+ loadedAllItems: not("discoveryTopics.model.canLoadMore"),
+ @observes("loadedAllItems")
_showFooter: function() {
this.set("application.showFooter", this.loadedAllItems);
- }.observes("loadedAllItems"),
+ },
showMoreUrl(period) {
let url = "",
category = this.category;
+
if (category) {
- url =
- "/c/" +
- Discourse.Category.slugFor(category) +
- (this.noSubcategories ? "/none" : "") +
- "/l";
+ url = `/c/${Category.slugFor(category)}/${category.id}${
+ this.noSubcategories ? "/none" : ""
+ }/l`;
}
+
url += "/top/" + period;
return url;
},
diff --git a/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6
index dedcef9811..69e59db2a9 100644
--- a/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6
+++ b/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6
@@ -1,5 +1,8 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { reads } from "@ember/object/computed";
+import { inject } from "@ember/controller";
import DiscoveryController from "discourse/controllers/discovery";
+import { dasherize } from "@ember/string";
const subcategoryStyleComponentNames = {
rows: "categories_only",
@@ -9,17 +12,14 @@ const subcategoryStyleComponentNames = {
};
export default DiscoveryController.extend({
- discovery: Ember.inject.controller(),
+ discovery: inject(),
// this makes sure the composer isn't scoping to a specific category
category: null,
- @computed
- canEdit() {
- return Discourse.User.currentProp("staff");
- },
+ canEdit: reads("currentUser.staff"),
- @computed("model.categories.[].featuredTopics.length")
+ @discourseComputed("model.categories.[].featuredTopics.length")
latestTopicOnly() {
return (
this.get("model.categories").find(
@@ -28,7 +28,7 @@ export default DiscoveryController.extend({
);
},
- @computed("model.parentCategory")
+ @discourseComputed("model.parentCategory")
categoryPageStyle(parentCategory) {
let style = this.site.mobileView
? "categories_with_featured_topics"
@@ -45,7 +45,7 @@ export default DiscoveryController.extend({
parentCategory && style === "categories_and_latest_topics"
? "categories_only"
: style;
- return Ember.String.dasherize(componentName);
+ return dasherize(componentName);
},
actions: {
refresh() {
diff --git a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6
index c6443bd37e..047960ba8a 100644
--- a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6
+++ b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6
@@ -1,3 +1,6 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { alias, not, gt, empty, notEmpty, equal } from "@ember/object/computed";
+import { inject } from "@ember/controller";
import DiscoveryController from "discourse/controllers/discovery";
import { queryParams } from "discourse/controllers/discovery-sortable";
import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
@@ -5,19 +8,17 @@ import { endWith } from "discourse/lib/computed";
import showModal from "discourse/lib/show-modal";
import { userPath } from "discourse/lib/url";
import TopicList from "discourse/models/topic-list";
-import computed from "ember-addons/ember-computed-decorators";
+import Topic from "discourse/models/topic";
const controllerOpts = {
- discovery: Ember.inject.controller(),
- discoveryTopics: Ember.inject.controller("discovery/topics"),
+ discovery: inject(),
+ discoveryTopics: inject("discovery/topics"),
period: null,
- canStar: Ember.computed.alias("currentUser.id"),
- showTopicPostBadges: Ember.computed.not("discoveryTopics.new"),
- redirectedReason: Ember.computed.alias(
- "currentUser.redirected_to_top.reason"
- ),
+ canStar: alias("currentUser.id"),
+ showTopicPostBadges: not("discoveryTopics.new"),
+ redirectedReason: alias("currentUser.redirected_to_top.reason"),
order: "default",
ascending: false,
@@ -82,8 +83,9 @@ const controllerOpts = {
},
resetNew() {
- this.topicTrackingState.resetNew();
- Discourse.Topic.resetNew().then(() => this.send("refresh"));
+ Topic.resetNew(this.category, !this.noSubcategories).then(() =>
+ this.send("refresh")
+ );
},
dismissReadPosts() {
@@ -98,17 +100,17 @@ const controllerOpts = {
return filter.match(new RegExp(filterType + "$", "gi")) ? true : false;
},
- @computed("model.filter", "model.topics.length")
+ @discourseComputed("model.filter", "model.topics.length")
showDismissRead(filter, topicsLength) {
return this.isFilterPage(filter, "unread") && topicsLength > 0;
},
- @computed("model.filter", "model.topics.length")
+ @discourseComputed("model.filter", "model.topics.length")
showResetNew(filter, topicsLength) {
- return filter === "new" && topicsLength > 0;
+ return this.isFilterPage(filter, "new") && topicsLength > 0;
},
- @computed("model.filter", "model.topics.length")
+ @discourseComputed("model.filter", "model.topics.length")
showDismissAtTop(filter, topicsLength) {
return (
(this.isFilterPage(filter, "new") ||
@@ -117,18 +119,18 @@ const controllerOpts = {
);
},
- hasTopics: Ember.computed.gt("model.topics.length", 0),
- allLoaded: Ember.computed.empty("model.more_topics_url"),
+ hasTopics: gt("model.topics.length", 0),
+ allLoaded: empty("model.more_topics_url"),
latest: endWith("model.filter", "latest"),
new: endWith("model.filter", "new"),
- top: Ember.computed.notEmpty("period"),
- yearly: Ember.computed.equal("period", "yearly"),
- quarterly: Ember.computed.equal("period", "quarterly"),
- monthly: Ember.computed.equal("period", "monthly"),
- weekly: Ember.computed.equal("period", "weekly"),
- daily: Ember.computed.equal("period", "daily"),
+ top: notEmpty("period"),
+ yearly: equal("period", "yearly"),
+ quarterly: equal("period", "quarterly"),
+ monthly: equal("period", "monthly"),
+ weekly: equal("period", "weekly"),
+ daily: equal("period", "daily"),
- @computed("allLoaded", "model.topics.length")
+ @discourseComputed("allLoaded", "model.topics.length")
footerMessage(allLoaded, topicsLength) {
if (!allLoaded) return;
@@ -151,7 +153,7 @@ const controllerOpts = {
}
},
- @computed("allLoaded", "model.topics.length")
+ @discourseComputed("allLoaded", "model.topics.length")
footerEducation(allLoaded, topicsLength) {
if (!allLoaded || topicsLength > 0 || !this.currentUser) {
return;
diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6
index 0e18b632b5..e014cd1ea2 100644
--- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6
+++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6
@@ -1,13 +1,15 @@
+import { isEmpty } from "@ember/utils";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import DiscourseURL from "discourse/lib/url";
import { extractError } from "discourse/lib/ajax-error";
-import {
- default as computed,
+import discourseComputed, {
on,
observes
-} from "ember-addons/ember-computed-decorators";
+} from "discourse-common/utils/decorators";
+import Category from "discourse/models/category";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
selectedTab: null,
saving: false,
deleting: false,
@@ -16,7 +18,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
@on("init")
_initPanels() {
- this.set("panels", []);
+ this.setProperties({
+ panels: [],
+ validators: []
+ });
},
onShow() {
@@ -27,14 +32,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
@observes("model.description")
changeSize() {
- if (!Ember.isEmpty(this.get("model.description"))) {
+ if (!isEmpty(this.get("model.description"))) {
this.set("modal.modalClass", "edit-category-modal full");
} else {
this.set("modal.modalClass", "edit-category-modal small");
}
},
- @computed("model.{id,name}")
+ @discourseComputed("model.{id,name}")
title(model) {
if (model.id) {
return I18n.t("category.edit_dialog_title", {
@@ -49,7 +54,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
this.set("modal.title", this.title);
},
- @computed("saving", "model.name", "model.color", "deleting")
+ @discourseComputed("saving", "model.name", "model.color", "deleting")
disabled(saving, name, color, deleting) {
if (saving || deleting) return true;
if (!name) return true;
@@ -57,25 +62,32 @@ export default Ember.Controller.extend(ModalFunctionality, {
return false;
},
- @computed("saving", "deleting")
+ @discourseComputed("saving", "deleting")
deleteDisabled(saving, deleting) {
return deleting || saving || false;
},
- @computed("name")
+ @discourseComputed("name")
categoryName(name) {
name = name || "";
return name.trim().length > 0 ? name : I18n.t("preview");
},
- @computed("saving", "model.id")
+ @discourseComputed("saving", "model.id")
saveLabel(saving, id) {
if (saving) return "saving";
return id ? "category.save" : "category.create";
},
actions: {
+ registerValidator(validator) {
+ this.validators.push(validator);
+ },
+
saveCategory() {
+ if (this.validators.some(validator => validator())) {
+ return;
+ }
const model = this.model;
const parentCategory = this.site.categories.findBy(
"id",
@@ -94,7 +106,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
slug: result.category.slug,
id: result.category.id
});
- DiscourseURL.redirectTo("/c/" + Discourse.Category.slugFor(model));
+ DiscourseURL.redirectTo(`/c/${Category.slugFor(model)}/${model.id}`);
})
.catch(error => {
this.flash(extractError(error), "error");
diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6
index d90ca5cbd4..a410f1fc90 100644
--- a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6
+++ b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6
@@ -1,7 +1,10 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import EmberObject from "@ember/object";
+import Controller from "@ember/controller";
+import discourseComputed from "discourse-common/utils/decorators";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import TopicTimer from "discourse/models/topic-timer";
import { popupAjaxError } from "discourse/lib/ajax-error";
+import { setProperties } from "@ember/object";
export const CLOSE_STATUS_TYPE = "close";
export const OPEN_STATUS_TYPE = "open";
@@ -10,11 +13,11 @@ export const DELETE_STATUS_TYPE = "delete";
export const REMINDER_TYPE = "reminder";
export const BUMP_TYPE = "bump";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
loading: false,
isPublic: "true",
- @computed("model.closed")
+ @discourseComputed("model.closed")
publicTimerTypes(closed) {
let types = [
{
@@ -47,17 +50,21 @@ export default Ember.Controller.extend(ModalFunctionality, {
return types;
},
- @computed()
+ @discourseComputed()
privateTimerTypes() {
return [{ id: REMINDER_TYPE, name: I18n.t("topic.reminder.title") }];
},
- @computed("isPublic", "publicTimerTypes", "privateTimerTypes")
+ @discourseComputed("isPublic", "publicTimerTypes", "privateTimerTypes")
selections(isPublic, publicTimerTypes, privateTimerTypes) {
return "true" === isPublic ? publicTimerTypes : privateTimerTypes;
},
- @computed("isPublic", "model.topic_timer", "model.private_topic_timer")
+ @discourseComputed(
+ "isPublic",
+ "model.topic_timer",
+ "model.private_topic_timer"
+ )
topicTimer(isPublic, publicTopicTimer, privateTopicTimer) {
return "true" === isPublic ? publicTopicTimer : privateTopicTimer;
},
@@ -76,7 +83,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (time) {
this.send("closeModal");
- Ember.setProperties(this.topicTimer, {
+ setProperties(this.topicTimer, {
execute_at: result.execute_at,
duration: result.duration,
category_id: result.category_id
@@ -86,20 +93,26 @@ export default Ember.Controller.extend(ModalFunctionality, {
} else {
const topicTimer =
this.isPublic === "true" ? "topic_timer" : "private_topic_timer";
- this.set(`model.${topicTimer}`, Ember.Object.create({}));
+ this.set(`model.${topicTimer}`, EmberObject.create({}));
this.setProperties({
selection: null
});
}
})
- .catch(error => {
- popupAjaxError(error);
- })
+ .catch(popupAjaxError)
.finally(() => this.set("loading", false));
},
actions: {
+ onChangeStatusType(value) {
+ this.set("topicTimer.status_type", value);
+ },
+
+ onChangeUpdateTime(value) {
+ this.set("topicTimer.updateTime", value);
+ },
+
saveTimer() {
if (!this.get("topicTimer.updateTime")) {
this.flash(
diff --git a/app/assets/javascripts/discourse/controllers/email-login.js.es6 b/app/assets/javascripts/discourse/controllers/email-login.js.es6
index 5a025ce7a6..4eab7fc1ba 100644
--- a/app/assets/javascripts/discourse/controllers/email-login.js.es6
+++ b/app/assets/javascripts/discourse/controllers/email-login.js.es6
@@ -1,20 +1,39 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Controller from "@ember/controller";
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
import { ajax } from "discourse/lib/ajax";
import DiscourseURL from "discourse/lib/url";
import { popupAjaxError } from "discourse/lib/ajax-error";
+import { getWebauthnCredential } from "discourse/lib/webauthn";
-export default Ember.Controller.extend({
- secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
+export default Controller.extend({
lockImageUrl: Discourse.getURL("/images/lock.svg"),
+
+ @discourseComputed("model")
+ secondFactorRequired(model) {
+ return model.security_key_required || model.second_factor_required;
+ },
+
+ @discourseComputed("model")
+ secondFactorMethod(model) {
+ return model.security_key_required
+ ? SECOND_FACTOR_METHODS.SECURITY_KEY
+ : SECOND_FACTOR_METHODS.TOTP;
+ },
+
actions: {
finishLogin() {
+ let data = { second_factor_method: this.secondFactorMethod };
+ if (this.securityKeyCredential) {
+ data.second_factor_token = this.securityKeyCredential;
+ } else {
+ data.second_factor_token = this.secondFactorToken;
+ }
+
ajax({
url: `/session/email-login/${this.model.token}`,
type: "POST",
- data: {
- second_factor_token: this.secondFactorToken,
- second_factor_method: this.secondFactorMethod
- }
+ data: data
})
.then(result => {
if (result.success) {
@@ -24,6 +43,19 @@ export default Ember.Controller.extend({
}
})
.catch(popupAjaxError);
+ },
+ authenticateSecurityKey() {
+ getWebauthnCredential(
+ this.model.challenge,
+ this.model.allowed_credential_ids,
+ credentialData => {
+ this.set("securityKeyCredential", credentialData);
+ this.send("finishLogin");
+ },
+ errorMessage => {
+ this.set("model.error", errorMessage);
+ }
+ );
}
}
});
diff --git a/app/assets/javascripts/discourse/controllers/exception.js.es6 b/app/assets/javascripts/discourse/controllers/exception.js.es6
index 1f4c454da4..e9a5f10d16 100644
--- a/app/assets/javascripts/discourse/controllers/exception.js.es6
+++ b/app/assets/javascripts/discourse/controllers/exception.js.es6
@@ -1,7 +1,7 @@
-import {
- on,
- default as computed
-} from "ember-addons/ember-computed-decorators";
+import { equal, gte, none, alias } from "@ember/object/computed";
+import { schedule } from "@ember/runloop";
+import Controller from "@ember/controller";
+import discourseComputed, { on } from "discourse-common/utils/decorators";
const ButtonBackBright = {
classes: "btn-primary",
@@ -17,7 +17,7 @@ const ButtonBackBright = {
classes: "btn-primary",
action: "tryLoading",
key: "errors.buttons.again",
- icon: "refresh"
+ icon: "sync"
},
ButtonLoadPage = {
classes: "btn-primary",
@@ -26,11 +26,11 @@ const ButtonBackBright = {
};
// The controller for the nice error page
-export default Ember.Controller.extend({
+export default Controller.extend({
thrown: null,
lastTransition: null,
- @computed
+ @discourseComputed
isNetwork() {
// never made it on the wire
if (this.get("thrown.readyState") === 0) return true;
@@ -41,10 +41,10 @@ export default Ember.Controller.extend({
return false;
},
- isNotFound: Ember.computed.equal("thrown.status", 404),
- isForbidden: Ember.computed.equal("thrown.status", 403),
- isServer: Ember.computed.gte("thrown.status", 500),
- isUnknown: Ember.computed.none("isNetwork", "isServer"),
+ isNotFound: equal("thrown.status", 404),
+ isForbidden: equal("thrown.status", 403),
+ isServer: gte("thrown.status", 500),
+ isUnknown: none("isNetwork", "isServer"),
// TODO
// make ajax requests to /srv/status with exponential backoff
@@ -57,7 +57,7 @@ export default Ember.Controller.extend({
this.set("loading", false);
},
- @computed("isNetwork", "isServer", "isUnknown")
+ @discourseComputed("isNetwork", "isServer", "isUnknown")
reason() {
if (this.isNetwork) {
return I18n.t("errors.reasons.network");
@@ -73,9 +73,9 @@ export default Ember.Controller.extend({
}
},
- requestUrl: Ember.computed.alias("thrown.requestedUrl"),
+ requestUrl: alias("thrown.requestedUrl"),
- @computed("networkFixed", "isNetwork", "isServer", "isUnknown")
+ @discourseComputed("networkFixed", "isNetwork", "isServer", "isUnknown")
desc() {
if (this.networkFixed) {
return I18n.t("errors.desc.network_fixed");
@@ -93,7 +93,7 @@ export default Ember.Controller.extend({
}
},
- @computed("networkFixed", "isNetwork", "isServer", "isUnknown")
+ @discourseComputed("networkFixed", "isNetwork", "isServer", "isUnknown")
enabledButtons() {
if (this.networkFixed) {
return [ButtonLoadPage];
@@ -112,7 +112,7 @@ export default Ember.Controller.extend({
tryLoading() {
this.set("loading", true);
- Ember.run.schedule("afterRender", () => {
+ schedule("afterRender", () => {
this.lastTransition.retry();
this.set("loading", false);
});
diff --git a/app/assets/javascripts/discourse/controllers/explain-reviewable.js.es6 b/app/assets/javascripts/discourse/controllers/explain-reviewable.js.es6
new file mode 100644
index 0000000000..268958e105
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/explain-reviewable.js.es6
@@ -0,0 +1,16 @@
+import Controller from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+
+export default Controller.extend(ModalFunctionality, {
+ loading: null,
+ reviewableExplanation: null,
+
+ onShow() {
+ this.setProperties({ loading: true, reviewableExplanation: null });
+
+ this.store
+ .find("reviewable-explanation", this.model.id)
+ .then(result => this.set("reviewableExplanation", result))
+ .finally(() => this.set("loading", false));
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/feature-topic-on-profile.js.es6 b/app/assets/javascripts/discourse/controllers/feature-topic-on-profile.js.es6
new file mode 100644
index 0000000000..39c89b5f79
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/feature-topic-on-profile.js.es6
@@ -0,0 +1,37 @@
+import Controller from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+import { ajax } from "discourse/lib/ajax";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import { none } from "@ember/object/computed";
+
+export default Controller.extend(ModalFunctionality, {
+ newFeaturedTopic: null,
+ saving: false,
+ noTopicSelected: none("newFeaturedTopic"),
+
+ onClose() {
+ this.set("newFeaturedTopic", null);
+ },
+
+ onShow() {
+ this.set("modal.modalClass", "choose-topic-modal");
+ },
+
+ actions: {
+ save() {
+ return ajax(`/u/${this.model.username}/feature-topic`, {
+ type: "PUT",
+ data: { topic_id: this.newFeaturedTopic.id }
+ })
+ .then(() => {
+ this.model.set("featured_topic", this.newFeaturedTopic);
+ this.send("closeModal");
+ })
+ .catch(popupAjaxError);
+ },
+
+ newTopicSelected(topic) {
+ this.set("newFeaturedTopic", topic);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6
index 9709b21c48..8fb459a145 100644
--- a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6
@@ -1,11 +1,13 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { categoryLinkHTML } from "discourse/helpers/category-link";
-import computed from "ember-addons/ember-computed-decorators";
-import InputValidation from "discourse/models/input-validation";
+import EmberObject from "@ember/object";
-export default Ember.Controller.extend(ModalFunctionality, {
- topicController: Ember.inject.controller("topic"),
+export default Controller.extend(ModalFunctionality, {
+ topicController: inject("topic"),
loading: true,
pinnedInCategoryCount: 0,
@@ -21,12 +23,16 @@ export default Ember.Controller.extend(ModalFunctionality, {
});
},
- @computed("model.category")
+ @discourseComputed("model.category")
categoryLink(category) {
return categoryLinkHTML(category, { allowUncategorized: true });
},
- @computed("categoryLink", "model.pinned_globally", "model.pinned_until")
+ @discourseComputed(
+ "categoryLink",
+ "model.pinned_globally",
+ "model.pinned_until"
+ )
unPinMessage(categoryLink, pinnedGlobally, pinnedUntil) {
let name = "topic.feature_topic.unpin";
if (pinnedGlobally) name += "_globally";
@@ -36,12 +42,12 @@ export default Ember.Controller.extend(ModalFunctionality, {
return I18n.t(name, { categoryLink, until });
},
- @computed("categoryLink")
+ @discourseComputed("categoryLink")
pinMessage(categoryLink) {
return I18n.t("topic.feature_topic.pin", { categoryLink });
},
- @computed("categoryLink", "pinnedInCategoryCount")
+ @discourseComputed("categoryLink", "pinnedInCategoryCount")
alreadyPinnedMessage(categoryLink, count) {
const key =
count === 0
@@ -50,40 +56,40 @@ export default Ember.Controller.extend(ModalFunctionality, {
return I18n.t(key, { categoryLink, count });
},
- @computed("parsedPinnedInCategoryUntil")
+ @discourseComputed("parsedPinnedInCategoryUntil")
pinDisabled(parsedPinnedInCategoryUntil) {
return !this._isDateValid(parsedPinnedInCategoryUntil);
},
- @computed("parsedPinnedGloballyUntil")
+ @discourseComputed("parsedPinnedGloballyUntil")
pinGloballyDisabled(parsedPinnedGloballyUntil) {
return !this._isDateValid(parsedPinnedGloballyUntil);
},
- @computed("model.pinnedInCategoryUntil")
+ @discourseComputed("model.pinnedInCategoryUntil")
parsedPinnedInCategoryUntil(pinnedInCategoryUntil) {
return this._parseDate(pinnedInCategoryUntil);
},
- @computed("model.pinnedGloballyUntil")
+ @discourseComputed("model.pinnedGloballyUntil")
parsedPinnedGloballyUntil(pinnedGloballyUntil) {
return this._parseDate(pinnedGloballyUntil);
},
- @computed("pinDisabled")
+ @discourseComputed("pinDisabled")
pinInCategoryValidation(pinDisabled) {
if (pinDisabled) {
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason: I18n.t("topic.feature_topic.pin_validation")
});
}
},
- @computed("pinGloballyDisabled")
+ @discourseComputed("pinGloballyDisabled")
pinGloballyValidation(pinGloballyDisabled) {
if (pinGloballyDisabled) {
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason: I18n.t("topic.feature_topic.pin_validation")
});
diff --git a/app/assets/javascripts/discourse/controllers/flag.js.es6 b/app/assets/javascripts/discourse/controllers/flag.js.es6
index bcfbb5e325..9eb3ae711d 100644
--- a/app/assets/javascripts/discourse/controllers/flag.js.es6
+++ b/app/assets/javascripts/discourse/controllers/flag.js.es6
@@ -1,11 +1,14 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { not } from "@ember/object/computed";
+import EmberObject from "@ember/object";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import ActionSummary from "discourse/models/action-summary";
import { MAX_MESSAGE_LENGTH } from "discourse/models/post-action-type";
-import computed from "ember-addons/ember-computed-decorators";
import optionalService from "discourse/lib/optional-service";
import { popupAjaxError } from "discourse/lib/ajax-error";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
adminTools: optionalService(),
userDetails: null,
selected: null,
@@ -29,17 +32,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
}
},
- @computed("spammerDetails.canDelete", "selected.name_key")
+ @discourseComputed("spammerDetails.canDelete", "selected.name_key")
showDeleteSpammer(canDeleteSpammer, nameKey) {
return canDeleteSpammer && nameKey === "spam";
},
- @computed("flagTopic")
+ @discourseComputed("flagTopic")
title(flagTopic) {
return flagTopic ? "flagging_topic.title" : "flagging.title";
},
- @computed("post", "flagTopic", "model.actions_summary.@each.can_act")
+ @discourseComputed("post", "flagTopic", "model.actions_summary.@each.can_act")
flagsAvailable() {
if (!this.flagTopic) {
// flagging post
@@ -57,7 +60,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
return flagsAvailable;
} else {
// flagging topic
- let lookup = Ember.Object.create();
+ let lookup = EmberObject.create();
let model = this.model;
model.get("actions_summary").forEach(a => {
a.flagTopic = model;
@@ -74,7 +77,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
}
},
- @computed("post", "flagTopic", "model.actions_summary.@each.can_act")
+ @discourseComputed("post", "flagTopic", "model.actions_summary.@each.can_act")
staffFlagsAvailable() {
return (
this.get("model.flagsAvailable") &&
@@ -82,7 +85,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
);
},
- @computed("selected.is_custom_flag", "message.length")
+ @discourseComputed("selected.is_custom_flag", "message.length")
submitEnabled() {
const selected = this.selected;
if (!selected) return false;
@@ -97,20 +100,20 @@ export default Ember.Controller.extend(ModalFunctionality, {
return true;
},
- submitDisabled: Ember.computed.not("submitEnabled"),
+ submitDisabled: not("submitEnabled"),
// Staff accounts can "take action"
- @computed("flagTopic", "selected.is_custom_flag")
+ @discourseComputed("flagTopic", "selected.is_custom_flag")
canTakeAction(flagTopic, isCustomFlag) {
return !flagTopic && !isCustomFlag && this.currentUser.get("staff");
},
- @computed("selected.is_custom_flag")
+ @discourseComputed("selected.is_custom_flag")
submitIcon(isCustomFlag) {
return isCustomFlag ? "envelope" : "flag";
},
- @computed("selected.is_custom_flag", "flagTopic")
+ @discourseComputed("selected.is_custom_flag", "flagTopic")
submitLabel(isCustomFlag, flagTopic) {
if (isCustomFlag) {
return flagTopic
@@ -154,6 +157,13 @@ export default Ember.Controller.extend(ModalFunctionality, {
params = $.extend(params, opts);
}
+ this.appEvents.trigger(
+ this.flagTopic ? "topic:flag-created" : "post:flag-created",
+ this.model,
+ postAction,
+ params
+ );
+
this.send("hideModal");
postAction
@@ -183,7 +193,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
}
},
- @computed("flagTopic", "selected.name_key")
+ @discourseComputed("flagTopic", "selected.name_key")
canSendWarning(flagTopic, nameKey) {
return (
!flagTopic && this.currentUser.get("staff") && nameKey === "notify_user"
diff --git a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6
index 288ffa8ba8..0f667683ad 100644
--- a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6
+++ b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6
@@ -1,16 +1,18 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { escapeExpression } from "discourse/lib/utilities";
import { extractError } from "discourse/lib/ajax-error";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
offerHelp: null,
helpSeen: false,
- @computed("accountEmailOrUsername", "disabled")
+ @discourseComputed("accountEmailOrUsername", "disabled")
submitDisabled(accountEmailOrUsername, disabled) {
- return Ember.isEmpty((accountEmailOrUsername || "").trim()) || disabled;
+ return isEmpty((accountEmailOrUsername || "").trim()) || disabled;
},
onShow() {
diff --git a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6
index af832d4fe2..a143203720 100644
--- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6
+++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6
@@ -1,3 +1,7 @@
+import { isEmpty } from "@ember/utils";
+import { or } from "@ember/object/computed";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import {
translateResults,
@@ -5,15 +9,12 @@ import {
getSearchKey,
isValidSearchTerm
} from "discourse/lib/search";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Category from "discourse/models/category";
import { escapeExpression } from "discourse/lib/utilities";
import { setTransient } from "discourse/lib/page-tracker";
-import { iconHTML } from "discourse-common/lib/icon-library";
import Composer from "discourse/models/composer";
+import { scrollTop } from "discourse/mixins/scroll-top";
const SortOrders = [
{ name: I18n.t("search.relevance"), id: 0 },
@@ -24,9 +25,9 @@ const SortOrders = [
];
const PAGE_LIMIT = 10;
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
- composer: Ember.inject.controller(),
+export default Controller.extend({
+ application: inject(),
+ composer: inject(),
bulkSelectEnabled: null,
loading: false,
@@ -43,17 +44,17 @@ export default Ember.Controller.extend({
page: 1,
resultCount: null,
- @computed("resultCount")
+ @discourseComputed("resultCount")
hasResults(resultCount) {
return (resultCount || 0) > 0;
},
- @computed("q")
+ @discourseComputed("q")
hasAutofocus(q) {
- return Ember.isEmpty(q);
+ return isEmpty(q);
},
- @computed("q")
+ @discourseComputed("q")
highlightQuery(q) {
if (!q) {
return;
@@ -62,7 +63,7 @@ export default Ember.Controller.extend({
return _.reject(q.split(/\s+/), t => t === "l").join(" ");
},
- @computed("skip_context", "context")
+ @discourseComputed("skip_context", "context")
searchContextEnabled: {
get(skip, context) {
return (!skip && context) || skip === "false";
@@ -72,7 +73,7 @@ export default Ember.Controller.extend({
}
},
- @computed("context", "context_id")
+ @discourseComputed("context", "context_id")
searchContextDescription(context, id) {
var name = id;
if (context === "category") {
@@ -86,18 +87,18 @@ export default Ember.Controller.extend({
return searchContextDescription(context, name);
},
- @computed("q")
+ @discourseComputed("q")
searchActive(q) {
return isValidSearchTerm(q);
},
- @computed("q")
+ @discourseComputed("q")
noSortQ(q) {
q = this.cleanTerm(q);
return escapeExpression(q);
},
- @computed("canCreateTopic", "siteSettings.login_required")
+ @discourseComputed("canCreateTopic", "siteSettings.login_required")
showSuggestion(canCreateTopic, loginRequired) {
return canCreateTopic || !loginRequired;
},
@@ -142,7 +143,7 @@ export default Ember.Controller.extend({
}
},
- @computed("q")
+ @discourseComputed("q")
showLikeCount(q) {
return q && q.indexOf("order:likes") > -1;
},
@@ -156,14 +157,14 @@ export default Ember.Controller.extend({
}
},
- @computed("q")
+ @discourseComputed("q")
isPrivateMessage(q) {
return (
q &&
this.currentUser &&
- (q.indexOf("in:private") > -1 ||
+ (q.indexOf("in:personal") > -1 ||
q.indexOf(
- `private_messages:${this.currentUser.get("username_lower")}`
+ `personal_messages:${this.currentUser.get("username_lower")}`
) > -1)
);
},
@@ -173,7 +174,7 @@ export default Ember.Controller.extend({
this.set("application.showFooter", !this.loading);
},
- @computed("resultCount", "noSortQ")
+ @discourseComputed("resultCount", "noSortQ")
resultCountLabel(count, term) {
const plus = count % 50 === 0 ? "+" : "";
return I18n.t("search.result_count", { count, plus, term });
@@ -184,27 +185,22 @@ export default Ember.Controller.extend({
this.set("resultCount", this.get("model.posts.length"));
},
- @computed("hasResults")
+ @discourseComputed("hasResults")
canBulkSelect(hasResults) {
return this.currentUser && this.currentUser.staff && hasResults;
},
- @computed("model.grouped_search_result.can_create_topic")
+ @discourseComputed("model.grouped_search_result.can_create_topic")
canCreateTopic(userCanCreateTopic) {
return this.currentUser && userCanCreateTopic;
},
- @computed("expanded")
- searchAdvancedIcon(expanded) {
- return iconHTML(expanded ? "caret-down" : "caret-right");
- },
-
- @computed("page")
+ @discourseComputed("page")
isLastPage(page) {
return page === PAGE_LIMIT;
},
- searchButtonDisabled: Ember.computed.or("searching", "loading"),
+ searchButtonDisabled: or("searching", "loading"),
_search() {
if (this.searching) {
@@ -224,6 +220,7 @@ export default Ember.Controller.extend({
this.set("bulkSelectEnabled", false);
this.selected.clear();
this.set("searching", true);
+ scrollTop();
} else {
this.set("loading", true);
}
@@ -285,7 +282,7 @@ export default Ember.Controller.extend({
}
this.composer.open({
action: Composer.CREATE_TOPIC,
- draftKey: Composer.CREATE_TOPIC,
+ draftKey: Composer.NEW_TOPIC_KEY,
topicCategory
});
},
diff --git a/app/assets/javascripts/discourse/controllers/grant-badge.js.es6 b/app/assets/javascripts/discourse/controllers/grant-badge.js.es6
index 13757e678e..a3cd4e11e8 100644
--- a/app/assets/javascripts/discourse/controllers/grant-badge.js.es6
+++ b/app/assets/javascripts/discourse/controllers/grant-badge.js.es6
@@ -1,86 +1,85 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import { extractError } from "discourse/lib/ajax-error";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import GrantBadgeController from "discourse/mixins/grant-badge-controller";
import Badge from "discourse/models/badge";
import UserBadge from "discourse/models/user-badge";
+import { all } from "rsvp";
-export default Ember.Controller.extend(
- ModalFunctionality,
- GrantBadgeController,
- {
- topicController: Ember.inject.controller("topic"),
- loading: true,
- saving: false,
- selectedBadgeId: null,
+export default Controller.extend(ModalFunctionality, GrantBadgeController, {
+ topicController: inject("topic"),
+ loading: true,
+ saving: false,
+ selectedBadgeId: null,
- init() {
- this._super(...arguments);
+ init() {
+ this._super(...arguments);
- this.allBadges = [];
- this.userBadges = [];
- },
+ this.allBadges = [];
+ this.userBadges = [];
+ },
- @computed("topicController.selectedPosts")
- post() {
- return this.get("topicController.selectedPosts")[0];
- },
+ @discourseComputed("topicController.selectedPosts")
+ post() {
+ return this.get("topicController.selectedPosts")[0];
+ },
- @computed("post")
- badgeReason(post) {
- const url = post.get("url");
- const protocolAndHost =
- window.location.protocol + "//" + window.location.host;
+ @discourseComputed("post")
+ badgeReason(post) {
+ const url = post.get("url");
+ const protocolAndHost =
+ window.location.protocol + "//" + window.location.host;
- return url.indexOf("/") === 0 ? protocolAndHost + url : url;
- },
+ return url.indexOf("/") === 0 ? protocolAndHost + url : url;
+ },
- @computed("saving", "selectedBadgeGrantable")
- buttonDisabled(saving, selectedBadgeGrantable) {
- return saving || !selectedBadgeGrantable;
- },
+ @discourseComputed("saving", "selectedBadgeGrantable")
+ buttonDisabled(saving, selectedBadgeGrantable) {
+ return saving || !selectedBadgeGrantable;
+ },
- onShow() {
- this.set("loading", true);
+ onShow() {
+ this.set("loading", true);
- Ember.RSVP.all([
- Badge.findAll(),
- UserBadge.findByUsername(this.get("post.username"))
- ]).then(([allBadges, userBadges]) => {
- this.setProperties({
- allBadges: allBadges,
- userBadges: userBadges,
- loading: false
- });
+ all([
+ Badge.findAll(),
+ UserBadge.findByUsername(this.get("post.username"))
+ ]).then(([allBadges, userBadges]) => {
+ this.setProperties({
+ allBadges: allBadges,
+ userBadges: userBadges,
+ loading: false
});
- },
+ });
+ },
- actions: {
- grantBadge() {
- this.set("saving", true);
+ actions: {
+ grantBadge() {
+ this.set("saving", true);
- this.grantBadge(
- this.selectedBadgeId,
- this.get("post.username"),
- this.badgeReason
+ this.grantBadge(
+ this.selectedBadgeId,
+ this.get("post.username"),
+ this.badgeReason
+ )
+ .then(
+ newBadge => {
+ this.set("selectedBadgeId", null);
+ this.flash(
+ I18n.t("badges.successfully_granted", {
+ username: this.get("post.username"),
+ badge: newBadge.get("badge.name")
+ }),
+ "success"
+ );
+ },
+ error => {
+ this.flash(extractError(error), "error");
+ }
)
- .then(
- newBadge => {
- this.set("selectedBadgeId", null);
- this.flash(
- I18n.t("badges.successfully_granted", {
- username: this.get("post.username"),
- badge: newBadge.get("badge.name")
- }),
- "success"
- );
- },
- error => {
- this.flash(extractError(error), "error");
- }
- )
- .finally(() => this.set("saving", false));
- }
+ .finally(() => this.set("saving", false));
}
}
-);
+});
diff --git a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6
index 3e4454ce0e..d73bf08d2f 100644
--- a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6
@@ -1,10 +1,12 @@
-import { observes } from "ember-addons/ember-computed-decorators";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
+import { observes } from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
-export default Ember.Controller.extend({
- group: Ember.inject.controller(),
- groupActivity: Ember.inject.controller(),
- application: Ember.inject.controller(),
+export default Controller.extend({
+ group: inject(),
+ groupActivity: inject(),
+ application: inject(),
canLoadMore: true,
loading: false,
emptyText: fmt("type", "groups.empty.%@"),
diff --git a/app/assets/javascripts/discourse/controllers/group-activity-topics.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity-topics.js.es6
index 2a559a196c..6d922b754e 100644
--- a/app/assets/javascripts/discourse/controllers/group-activity-topics.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-activity-topics.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Controller.extend({
+import Controller from "@ember/controller";
+export default Controller.extend({
actions: {
loadMore() {
this.model.loadMore();
diff --git a/app/assets/javascripts/discourse/controllers/group-activity.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity.js.es6
index 86356eeaac..5c48daee43 100644
--- a/app/assets/javascripts/discourse/controllers/group-activity.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-activity.js.es6
@@ -1,4 +1,6 @@
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+import { inject as service } from "@ember/service";
+import Controller from "@ember/controller";
+export default Controller.extend({
+ router: service(),
queryParams: ["category_id"]
});
diff --git a/app/assets/javascripts/discourse/controllers/group-add-members.js.es6 b/app/assets/javascripts/discourse/controllers/group-add-members.js.es6
index 6e478fe3d8..9b1f888113 100644
--- a/app/assets/javascripts/discourse/controllers/group-add-members.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-add-members.js.es6
@@ -1,12 +1,14 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import Controller from "@ember/controller";
import { extractError } from "discourse/lib/ajax-error";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
loading: false,
setAsOwner: false,
- @computed("model.usernames", "loading")
+ @discourseComputed("model.usernames", "loading")
disableAddButton(usernames, loading) {
return loading || !usernames || !(usernames.length > 0);
},
@@ -17,7 +19,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
const model = this.model;
const usernames = model.get("usernames");
- if (Ember.isEmpty(usernames)) {
+ if (isEmpty(usernames)) {
return;
}
let promise;
diff --git a/app/assets/javascripts/discourse/controllers/group-bulk-add.js.es6 b/app/assets/javascripts/discourse/controllers/group-bulk-add.js.es6
index 716347d4c6..0310d4e309 100644
--- a/app/assets/javascripts/discourse/controllers/group-bulk-add.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-bulk-add.js.es6
@@ -1,14 +1,16 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import Controller from "@ember/controller";
import { extractError } from "discourse/lib/ajax-error";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
loading: false,
- @computed("input", "loading", "result")
+ @discourseComputed("input", "loading", "result")
disableAddButton(input, loading, result) {
- return loading || Ember.isEmpty(input) || input.length <= 0 || result;
+ return loading || isEmpty(input) || input.length <= 0 || result;
},
actions: {
diff --git a/app/assets/javascripts/discourse/controllers/group-index.js.es6 b/app/assets/javascripts/discourse/controllers/group-index.js.es6
index 0018563b0d..6916164dd6 100644
--- a/app/assets/javascripts/discourse/controllers/group-index.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-index.js.es6
@@ -1,61 +1,74 @@
+import Controller, { inject } from "@ember/controller";
+import { alias } from "@ember/object/computed";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
-import Group from "discourse/models/group";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
-import debounce from "discourse/lib/debounce";
+import discourseDebounce from "discourse/lib/debounce";
+
+export default Controller.extend({
+ application: inject(),
-export default Ember.Controller.extend({
queryParams: ["order", "desc", "filter"],
+
order: "",
desc: null,
- loading: false,
- limit: null,
- offset: null,
- isOwner: Ember.computed.alias("model.is_group_owner"),
- showActions: false,
filter: null,
filterInput: null,
- application: Ember.inject.controller(),
+
+ loading: false,
+ isOwner: alias("model.is_group_owner"),
+ showActions: false,
@observes("filterInput")
- _setFilter: debounce(function() {
+ _setFilter: discourseDebounce(function() {
this.set("filter", this.filterInput);
}, 500),
@observes("order", "desc", "filter")
- refreshMembers() {
- this.set("loading", true);
- const model = this.model;
-
- if (model) {
- model.findMembers(this.memberParams).finally(() => {
- this.set(
- "application.showFooter",
- model.members.length >= model.user_count
- );
- this.set("loading", false);
- });
- }
+ _filtersChanged() {
+ this.findMembers(true);
},
- @computed("order", "desc", "filter")
+ findMembers(refresh) {
+ if (this.loading) {
+ return;
+ }
+
+ const model = this.model;
+ if (!model) {
+ return;
+ }
+
+ if (!refresh && model.members.length >= model.user_count) {
+ this.set("application.showFooter", true);
+ return;
+ }
+
+ this.set("loading", true);
+ model.findMembers(this.memberParams, refresh).finally(() => {
+ this.set(
+ "application.showFooter",
+ model.members.length >= model.user_count
+ );
+ this.set("loading", false);
+ });
+ },
+
+ @discourseComputed("order", "desc", "filter")
memberParams(order, desc, filter) {
return { order, desc, filter };
},
- @computed("model.members")
+ @discourseComputed("model.members.[]")
hasMembers(members) {
return members && members.length > 0;
},
- @computed("model")
+ @discourseComputed("model")
canManageGroup(model) {
return this.currentUser && this.currentUser.canManageGroup(model);
},
- @computed
+ @discourseComputed
filterPlaceholder() {
if (this.currentUser && this.currentUser.admin) {
return "groups.members.filter_placeholder_admin";
@@ -65,10 +78,28 @@ export default Ember.Controller.extend({
},
actions: {
+ loadMore() {
+ this.findMembers();
+ },
+
toggleActions() {
this.toggleProperty("showActions");
},
+ actOnGroup(member, actionId) {
+ switch (actionId) {
+ case "removeMember":
+ this.send("removeMember", member);
+ break;
+ case "makeOwner":
+ this.send("makeOwner", member.username);
+ break;
+ case "removeOwner":
+ this.send("removeOwner", member);
+ break;
+ }
+ },
+
removeMember(user) {
this.model.removeMember(user, this.memberParams);
},
@@ -89,38 +120,6 @@ export default Ember.Controller.extend({
.then(() => this.set("usernames", []))
.catch(popupAjaxError);
}
- },
-
- loadMore() {
- if (this.loading) {
- return;
- }
- if (this.get("model.members.length") >= this.get("model.user_count")) {
- this.set("application.showFooter", true);
- return;
- }
-
- this.set("loading", true);
-
- Group.loadMembers(
- this.get("model.name"),
- this.get("model.members.length"),
- this.limit,
- { order: this.order, desc: this.desc }
- ).then(result => {
- this.get("model.members").addObjects(
- result.members.map(member => Discourse.User.create(member))
- );
- this.setProperties({
- loading: false,
- user_count: result.meta.total,
- limit: result.meta.limit,
- offset: Math.min(
- result.meta.offset + result.meta.limit,
- result.meta.total
- )
- });
- });
}
}
});
diff --git a/app/assets/javascripts/discourse/controllers/group-manage-logs.js.es6 b/app/assets/javascripts/discourse/controllers/group-manage-logs.js.es6
index 22ee01cff5..41a12c2c98 100644
--- a/app/assets/javascripts/discourse/controllers/group-manage-logs.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-manage-logs.js.es6
@@ -1,20 +1,20 @@
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import { inject } from "@ember/controller";
+import EmberObject from "@ember/object";
+import Controller from "@ember/controller";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
-export default Ember.Controller.extend({
- group: Ember.inject.controller(),
+export default Controller.extend({
+ group: inject(),
loading: false,
offset: 0,
- application: Ember.inject.controller(),
+ application: inject(),
init() {
this._super(...arguments);
- this.set("filters", Ember.Object.create());
+ this.set("filters", EmberObject.create());
},
- @computed(
+ @discourseComputed(
"filters.action",
"filters.acting_user",
"filters.target_user",
@@ -51,7 +51,7 @@ export default Ember.Controller.extend({
reset() {
this.setProperties({
offset: 0,
- filters: Ember.Object.create()
+ filters: EmberObject.create()
});
},
diff --git a/app/assets/javascripts/discourse/controllers/group-manage-profile.js.es6 b/app/assets/javascripts/discourse/controllers/group-manage-profile.js.es6
index e316e97fad..719745dfa3 100644
--- a/app/assets/javascripts/discourse/controllers/group-manage-profile.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-manage-profile.js.es6
@@ -1,3 +1,4 @@
-export default Ember.Controller.extend({
+import Controller from "@ember/controller";
+export default Controller.extend({
saving: null
});
diff --git a/app/assets/javascripts/discourse/controllers/group-manage.js.es6 b/app/assets/javascripts/discourse/controllers/group-manage.js.es6
index 93e17ca0fe..7a9e7859eb 100644
--- a/app/assets/javascripts/discourse/controllers/group-manage.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-manage.js.es6
@@ -1,9 +1,11 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import { inject as service } from "@ember/service";
+import Controller from "@ember/controller";
+import discourseComputed from "discourse-common/utils/decorators";
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+export default Controller.extend({
+ router: service(),
- @computed("model.automatic")
+ @discourseComputed("model.automatic")
tabs(automatic) {
const defaultTabs = [
{ route: "group.manage.profile", title: "groups.manage.profile.title" },
diff --git a/app/assets/javascripts/discourse/controllers/group-messages.js.es6 b/app/assets/javascripts/discourse/controllers/group-messages.js.es6
index d4180e8b6b..a422a1f33c 100644
--- a/app/assets/javascripts/discourse/controllers/group-messages.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-messages.js.es6
@@ -1,3 +1,5 @@
-export default Ember.Controller.extend({
- application: Ember.inject.controller()
+import { inject as service } from "@ember/service";
+import Controller from "@ember/controller";
+export default Controller.extend({
+ router: service()
});
diff --git a/app/assets/javascripts/discourse/controllers/group-requests.js.es6 b/app/assets/javascripts/discourse/controllers/group-requests.js.es6
index 178db75758..c1664f0c32 100644
--- a/app/assets/javascripts/discourse/controllers/group-requests.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-requests.js.es6
@@ -1,79 +1,67 @@
+import Controller, { inject } from "@ember/controller";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
-import Group from "discourse/models/group";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
-import debounce from "discourse/lib/debounce";
+import discourseDebounce from "discourse/lib/debounce";
+
+export default Controller.extend({
+ application: inject(),
-export default Ember.Controller.extend({
queryParams: ["order", "desc", "filter"],
+
order: "",
desc: null,
- loading: false,
- limit: null,
- offset: null,
filter: null,
filterInput: null,
- application: Ember.inject.controller(),
+
+ loading: false,
@observes("filterInput")
- _setFilter: debounce(function() {
+ _setFilter: discourseDebounce(function() {
this.set("filter", this.filterInput);
}, 500),
@observes("order", "desc", "filter")
- refreshRequesters(force) {
- if (this.loading || !this.model) {
+ _filtersChanged() {
+ this.findRequesters(true);
+ },
+
+ findRequesters(refresh) {
+ if (this.loading) {
return;
}
- if (
- !force &&
- this.count &&
- this.get("model.requesters.length") >= this.count
- ) {
+ const model = this.model;
+ if (!model) {
+ return;
+ }
+
+ if (!refresh && model.members.length >= model.user_count) {
this.set("application.showFooter", true);
return;
}
this.set("loading", true);
- this.set("application.showFooter", false);
-
- Group.loadMembers(
- this.get("model.name"),
- force ? 0 : this.get("model.requesters.length"),
- this.limit,
- {
- order: this.order,
- desc: this.desc,
- filter: this.filter,
- requesters: true
- }
- ).then(result => {
- const requesters = (!force && this.get("model.requesters")) || [];
- requesters.addObjects(result.members.map(m => Discourse.User.create(m)));
- this.set("model.requesters", requesters);
-
- this.setProperties({
- loading: false,
- count: result.meta.total,
- limit: result.meta.limit,
- offset: Math.min(
- result.meta.offset + result.meta.limit,
- result.meta.total
- )
- });
+ model.findRequesters(this.memberParams, refresh).finally(() => {
+ this.set(
+ "application.showFooter",
+ model.requesters.length >= model.user_count
+ );
+ this.set("loading", false);
});
},
- @computed("model.requesters")
+ @discourseComputed("order", "desc", "filter")
+ memberParams(order, desc, filter) {
+ return { order, desc, filter };
+ },
+
+ @discourseComputed("model.requesters.[]")
hasRequesters(requesters) {
return requesters && requesters.length > 0;
},
- @computed
+ @discourseComputed
filterPlaceholder() {
if (this.currentUser && this.currentUser.admin) {
return "groups.members.filter_placeholder_admin";
@@ -91,7 +79,7 @@ export default Ember.Controller.extend({
actions: {
loadMore() {
- this.refreshRequesters();
+ this.findRequesters();
},
acceptRequest(user) {
diff --git a/app/assets/javascripts/discourse/controllers/group.js.es6 b/app/assets/javascripts/discourse/controllers/group.js.es6
index f6a44552aa..71354f58d3 100644
--- a/app/assets/javascripts/discourse/controllers/group.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group.js.es6
@@ -1,6 +1,11 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import EmberObject from "@ember/object";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
+import discourseComputed from "discourse-common/utils/decorators";
+import { inject as service } from "@ember/service";
+import { readOnly } from "@ember/object/computed";
-const Tab = Ember.Object.extend({
+const Tab = EmberObject.extend({
init() {
this._super(...arguments);
let name = this.name;
@@ -9,19 +14,28 @@ const Tab = Ember.Object.extend({
}
});
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+export default Controller.extend({
+ application: inject(),
counts: null,
showing: "members",
destroying: null,
+ router: service(),
+ currentPath: readOnly("router._router.currentPath"),
- @computed(
+ @discourseComputed(
"showMessages",
"model.user_count",
+ "model.request_count",
"canManageGroup",
"model.allow_membership_requests"
)
- tabs(showMessages, userCount, canManageGroup, allowMembershipRequests) {
+ tabs(
+ showMessages,
+ userCount,
+ requestCount,
+ canManageGroup,
+ allowMembershipRequests
+ ) {
const membersTab = Tab.create({
name: "members",
route: "group.index",
@@ -38,7 +52,8 @@ export default Ember.Controller.extend({
Tab.create({
name: "requests",
i18nKey: "requests.title",
- icon: "user-plus"
+ icon: "user-plus",
+ count: requestCount
})
);
}
@@ -65,7 +80,7 @@ export default Ember.Controller.extend({
return defaultTabs;
},
- @computed("model.is_group_user")
+ @discourseComputed("model.is_group_user")
showMessages(isGroupUser) {
if (!this.siteSettings.enable_personal_messages) {
return false;
@@ -74,17 +89,17 @@ export default Ember.Controller.extend({
return isGroupUser || (this.currentUser && this.currentUser.admin);
},
- @computed("model.is_group_owner", "model.automatic")
+ @discourseComputed("model.is_group_owner", "model.automatic")
canEditGroup(isGroupOwner, automatic) {
return !automatic && isGroupOwner;
},
- @computed("model.displayName", "model.full_name")
+ @discourseComputed("model.displayName", "model.full_name")
groupName(displayName, fullName) {
return (fullName || displayName).capitalize();
},
- @computed(
+ @discourseComputed(
"model.name",
"model.flair_url",
"model.flair_bg_color",
@@ -99,12 +114,12 @@ export default Ember.Controller.extend({
};
},
- @computed("model.messageable")
+ @discourseComputed("model.messageable")
displayGroupMessageButton(messageable) {
return this.currentUser && messageable;
},
- @computed("model", "model.automatic")
+ @discourseComputed("model", "model.automatic")
canManageGroup(model, automatic) {
return (
this.currentUser &&
diff --git a/app/assets/javascripts/discourse/controllers/groups-index.js.es6 b/app/assets/javascripts/discourse/controllers/groups-index.js.es6
index 1c6a824ded..00276d0215 100644
--- a/app/assets/javascripts/discourse/controllers/groups-index.js.es6
+++ b/app/assets/javascripts/discourse/controllers/groups-index.js.es6
@@ -1,18 +1,17 @@
-import debounce from "discourse/lib/debounce";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
+import discourseDebounce from "discourse/lib/debounce";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+export default Controller.extend({
+ application: inject(),
queryParams: ["order", "asc", "filter", "type"],
order: null,
asc: null,
filter: "",
type: null,
- @computed("model.extras.type_filters")
+ @discourseComputed("model.extras.type_filters")
types(typeFilters) {
const types = [];
@@ -26,7 +25,7 @@ export default Ember.Controller.extend({
},
@observes("filterInput")
- _setFilter: debounce(function() {
+ _setFilter: discourseDebounce(function() {
this.set("filter", this.filterInput);
}, 500),
diff --git a/app/assets/javascripts/discourse/controllers/groups-new.js.es6 b/app/assets/javascripts/discourse/controllers/groups-new.js.es6
index 293beffd53..f93c08959d 100644
--- a/app/assets/javascripts/discourse/controllers/groups-new.js.es6
+++ b/app/assets/javascripts/discourse/controllers/groups-new.js.es6
@@ -1,6 +1,7 @@
+import Controller from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
-export default Ember.Controller.extend({
+export default Controller.extend({
saving: null,
actions: {
diff --git a/app/assets/javascripts/discourse/controllers/history.js.es6 b/app/assets/javascripts/discourse/controllers/history.js.es6
index df2833e187..5a34f292cf 100644
--- a/app/assets/javascripts/discourse/controllers/history.js.es6
+++ b/app/assets/javascripts/discourse/controllers/history.js.es6
@@ -1,13 +1,18 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { alias, gt, not, or, equal } from "@ember/object/computed";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { categoryBadgeHTML } from "discourse/helpers/category-link";
-import computed from "ember-addons/ember-computed-decorators";
import { propertyGreaterThan, propertyLessThan } from "discourse/lib/computed";
-import { on, observes } from "ember-addons/ember-computed-decorators";
+import { on, observes } from "discourse-common/utils/decorators";
import { sanitizeAsync } from "discourse/lib/text";
import { iconHTML } from "discourse-common/lib/icon-library";
+import Post from "discourse/models/post";
+import Category from "discourse/models/category";
+import { computed } from "@ember/object";
function customTagArray(fieldName) {
- return function() {
+ return computed(fieldName, function() {
var val = this.get(fieldName);
if (!val) {
return val;
@@ -16,11 +21,11 @@ function customTagArray(fieldName) {
val = [val];
}
return val;
- }.property(fieldName);
+ });
}
// This controller handles displaying of history
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
loading: true,
viewMode: "side_by_side",
@@ -31,17 +36,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
}
},
- previousFeaturedLink: Ember.computed.alias(
- "model.featured_link_changes.previous"
- ),
- currentFeaturedLink: Ember.computed.alias(
- "model.featured_link_changes.current"
- ),
+ previousFeaturedLink: alias("model.featured_link_changes.previous"),
+ currentFeaturedLink: alias("model.featured_link_changes.current"),
previousTagChanges: customTagArray("model.tags_changes.previous"),
currentTagChanges: customTagArray("model.tags_changes.current"),
- @computed("previousVersion", "model.current_version", "model.version_count")
+ @discourseComputed(
+ "previousVersion",
+ "model.current_version",
+ "model.version_count"
+ )
revisionsText(previous, current, total) {
return I18n.t(
"post.revisions.controls.comparing_previous_to_current_out_of_total",
@@ -57,19 +62,19 @@ export default Ember.Controller.extend(ModalFunctionality, {
refresh(postId, postVersion) {
this.set("loading", true);
- Discourse.Post.loadRevision(postId, postVersion).then(result => {
+ Post.loadRevision(postId, postVersion).then(result => {
this.setProperties({ loading: false, model: result });
});
},
hide(postId, postVersion) {
- Discourse.Post.hideRevision(postId, postVersion).then(() =>
+ Post.hideRevision(postId, postVersion).then(() =>
this.refresh(postId, postVersion)
);
},
show(postId, postVersion) {
- Discourse.Post.showRevision(postId, postVersion).then(() =>
+ Post.showRevision(postId, postVersion).then(() =>
this.refresh(postId, postVersion)
);
},
@@ -85,10 +90,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
post.set("topic.fancy_title", result.topic.fancy_title);
}
if (result.category_id) {
- post.set(
- "topic.category",
- Discourse.Category.findById(result.category_id)
- );
+ post.set("topic.category", Category.findById(result.category_id));
}
this.send("closeModal");
})
@@ -103,22 +105,22 @@ export default Ember.Controller.extend(ModalFunctionality, {
});
},
- @computed("model.created_at")
+ @discourseComputed("model.created_at")
createdAtDate(createdAt) {
return moment(createdAt).format("LLLL");
},
- @computed("model.current_version")
+ @discourseComputed("model.current_version")
previousVersion(current) {
return current - 1;
},
- @computed("model.current_revision", "model.previous_revision")
+ @discourseComputed("model.current_revision", "model.previous_revision")
displayGoToPrevious(current, prev) {
return prev && current > prev;
},
- displayRevisions: Ember.computed.gt("model.version_count", 2),
+ displayRevisions: gt("model.version_count", 2),
displayGoToFirst: propertyGreaterThan(
"model.current_revision",
"model.first_revision"
@@ -132,27 +134,27 @@ export default Ember.Controller.extend(ModalFunctionality, {
"model.next_revision"
),
- hideGoToFirst: Ember.computed.not("displayGoToFirst"),
- hideGoToPrevious: Ember.computed.not("displayGoToPrevious"),
- hideGoToNext: Ember.computed.not("displayGoToNext"),
- hideGoToLast: Ember.computed.not("displayGoToLast"),
+ hideGoToFirst: not("displayGoToFirst"),
+ hideGoToPrevious: not("displayGoToPrevious"),
+ hideGoToNext: not("displayGoToNext"),
+ hideGoToLast: not("displayGoToLast"),
- loadFirstDisabled: Ember.computed.or("loading", "hideGoToFirst"),
- loadPreviousDisabled: Ember.computed.or("loading", "hideGoToPrevious"),
- loadNextDisabled: Ember.computed.or("loading", "hideGoToNext"),
- loadLastDisabled: Ember.computed.or("loading", "hideGoToLast"),
+ loadFirstDisabled: or("loading", "hideGoToFirst"),
+ loadPreviousDisabled: or("loading", "hideGoToPrevious"),
+ loadNextDisabled: or("loading", "hideGoToNext"),
+ loadLastDisabled: or("loading", "hideGoToLast"),
- @computed("model.previous_hidden")
+ @discourseComputed("model.previous_hidden")
displayShow(prevHidden) {
return prevHidden && this.currentUser && this.currentUser.get("staff");
},
- @computed("model.previous_hidden")
+ @discourseComputed("model.previous_hidden")
displayHide(prevHidden) {
return !prevHidden && this.currentUser && this.currentUser.get("staff");
},
- @computed(
+ @discourseComputed(
"model.last_revision",
"model.current_revision",
"model.can_edit",
@@ -162,22 +164,23 @@ export default Ember.Controller.extend(ModalFunctionality, {
return !!(canEdit && topicController && lastRevision === currentRevision);
},
- @computed("model.wiki")
+ @discourseComputed("model.wiki")
editButtonLabel(wiki) {
return `post.revisions.controls.${wiki ? "edit_wiki" : "edit_post"}`;
},
- @computed()
+ @discourseComputed()
displayRevert() {
return this.currentUser && this.currentUser.get("staff");
},
- isEitherRevisionHidden: Ember.computed.or(
- "model.previous_hidden",
- "model.current_hidden"
- ),
+ isEitherRevisionHidden: or("model.previous_hidden", "model.current_hidden"),
- @computed("model.previous_hidden", "model.current_hidden", "displayingInline")
+ @discourseComputed(
+ "model.previous_hidden",
+ "model.current_hidden",
+ "displayingInline"
+ )
hiddenClasses(prevHidden, currentHidden, displayingInline) {
if (displayingInline) {
return this.isEitherRevisionHidden ? "hidden-revision-either" : null;
@@ -193,50 +196,47 @@ export default Ember.Controller.extend(ModalFunctionality, {
}
},
- displayingInline: Ember.computed.equal("viewMode", "inline"),
- displayingSideBySide: Ember.computed.equal("viewMode", "side_by_side"),
- displayingSideBySideMarkdown: Ember.computed.equal(
- "viewMode",
- "side_by_side_markdown"
- ),
+ displayingInline: equal("viewMode", "inline"),
+ displayingSideBySide: equal("viewMode", "side_by_side"),
+ displayingSideBySideMarkdown: equal("viewMode", "side_by_side_markdown"),
- @computed("displayingInline")
+ @discourseComputed("displayingInline")
inlineClass(displayingInline) {
return displayingInline ? "btn-danger" : "btn-flat";
},
- @computed("displayingSideBySide")
+ @discourseComputed("displayingSideBySide")
sideBySideClass(displayingSideBySide) {
return displayingSideBySide ? "btn-danger" : "btn-flat";
},
- @computed("displayingSideBySideMarkdown")
+ @discourseComputed("displayingSideBySideMarkdown")
sideBySideMarkdownClass(displayingSideBySideMarkdown) {
return displayingSideBySideMarkdown ? "btn-danger" : "btn-flat";
},
- @computed("model.category_id_changes")
+ @discourseComputed("model.category_id_changes")
previousCategory(changes) {
if (changes) {
- var category = Discourse.Category.findById(changes["previous"]);
+ var category = Category.findById(changes["previous"]);
return categoryBadgeHTML(category, { allowUncategorized: true });
}
},
- @computed("model.category_id_changes")
+ @discourseComputed("model.category_id_changes")
currentCategory(changes) {
if (changes) {
- var category = Discourse.Category.findById(changes["current"]);
+ var category = Category.findById(changes["current"]);
return categoryBadgeHTML(category, { allowUncategorized: true });
}
},
- @computed("model.wiki_changes")
+ @discourseComputed("model.wiki_changes")
wikiDisabled(changes) {
return changes && !changes["current"];
},
- @computed("model.post_type_changes")
+ @discourseComputed("model.post_type_changes")
postTypeDisabled(changes) {
return (
changes &&
@@ -244,7 +244,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
);
},
- @computed("viewMode", "model.title_changes")
+ @discourseComputed("viewMode", "model.title_changes")
titleDiff(viewMode) {
if (viewMode === "side_by_side_markdown") {
viewMode = "side_by_side";
diff --git a/app/assets/javascripts/discourse/controllers/ignore-duration-with-username.js.es6 b/app/assets/javascripts/discourse/controllers/ignore-duration-with-username.js.es6
index 6ba026a1b8..491c496df7 100644
--- a/app/assets/javascripts/discourse/controllers/ignore-duration-with-username.js.es6
+++ b/app/assets/javascripts/discourse/controllers/ignore-duration-with-username.js.es6
@@ -1,8 +1,9 @@
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { popupAjaxError } from "discourse/lib/ajax-error";
import User from "discourse/models/user";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
loading: false,
ignoredUntil: null,
ignoredUsername: null,
diff --git a/app/assets/javascripts/discourse/controllers/ignore-duration.js.es6 b/app/assets/javascripts/discourse/controllers/ignore-duration.js.es6
index 574f69e373..c78b73cbb3 100644
--- a/app/assets/javascripts/discourse/controllers/ignore-duration.js.es6
+++ b/app/assets/javascripts/discourse/controllers/ignore-duration.js.es6
@@ -1,7 +1,8 @@
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { popupAjaxError } from "discourse/lib/ajax-error";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
loading: false,
ignoredUntil: null,
actions: {
diff --git a/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6 b/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6
new file mode 100644
index 0000000000..21ea0603f8
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6
@@ -0,0 +1,184 @@
+import { isEmpty } from "@ember/utils";
+import { debounce } from "@ember/runloop";
+import { cancel } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import Controller from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+import { searchForTerm } from "discourse/lib/search";
+
+export default Controller.extend(ModalFunctionality, {
+ _debounced: null,
+ _activeSearch: null,
+
+ onShow() {
+ this.setProperties({
+ linkUrl: "",
+ linkText: "",
+ searchResults: [],
+ searchLoading: false,
+ selectedRow: -1
+ });
+
+ scheduleOnce("afterRender", () => {
+ const element = document.querySelector(".insert-link");
+
+ element.addEventListener("keydown", e => this.keyDown(e));
+
+ element
+ .closest(".modal-inner-container")
+ .addEventListener("mousedown", e => this.mouseDown(e));
+
+ document.querySelector("input.link-url").focus();
+ });
+ },
+
+ keyDown(e) {
+ switch (e.which) {
+ case 40:
+ this.highlightRow(e, "down");
+ break;
+ case 38:
+ this.highlightRow(e, "up");
+ break;
+ case 13:
+ // override Enter behaviour when a row is selected
+ if (this.selectedRow > -1) {
+ const selected = document.querySelectorAll(
+ ".internal-link-results .search-link"
+ )[this.selectedRow];
+ this.selectLink(selected);
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ break;
+ case 27:
+ // Esc should cancel dropdown first
+ if (this.searchResults.length) {
+ this.set("searchResults", []);
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ break;
+ }
+ },
+
+ mouseDown(e) {
+ if (!e.target.closest(".inputs")) {
+ this.set("searchResults", []);
+ }
+ },
+
+ highlightRow(e, direction) {
+ const index =
+ direction === "down" ? this.selectedRow + 1 : this.selectedRow - 1;
+
+ if (index > -1 && index < this.searchResults.length) {
+ document
+ .querySelectorAll(".internal-link-results .search-link")
+ [index].focus();
+ this.set("selectedRow", index);
+ } else {
+ this.set("selectedRow", -1);
+ document.querySelector("input.link-url").focus();
+ }
+
+ e.preventDefault();
+ },
+
+ selectLink(el) {
+ this.setProperties({
+ linkUrl: el.href,
+ searchResults: [],
+ selectedRow: -1
+ });
+
+ if (!this.linkText && el.dataset.title) {
+ this.set("linkText", el.dataset.title);
+ }
+
+ document.querySelector("input.link-text").focus();
+ },
+
+ triggerSearch() {
+ if (this.linkUrl.length > 3 && this.linkUrl.indexOf("http") === -1) {
+ this.set("searchLoading", true);
+ this._activeSearch = searchForTerm(this.linkUrl, {
+ typeFilter: "topic"
+ });
+ this._activeSearch
+ .then(results => {
+ if (results && results.topics && results.topics.length > 0) {
+ this.set("searchResults", results.topics);
+ } else {
+ this.set("searchResults", []);
+ }
+ })
+ .finally(() => {
+ this.set("searchLoading", false);
+ this._activeSearch = null;
+ });
+ } else {
+ this.abortSearch();
+ }
+ },
+
+ abortSearch() {
+ if (this._activeSearch) {
+ this._activeSearch.abort();
+ }
+ this.setProperties({
+ searchResults: [],
+ searchLoading: false
+ });
+ },
+
+ onClose() {
+ const element = document.querySelector(".insert-link");
+ element.removeEventListener("keydown", this.keyDown);
+ element
+ .closest(".modal-inner-container")
+ .removeEventListener("mousedown", this.mouseDown);
+
+ cancel(this._debounced);
+ },
+
+ actions: {
+ ok() {
+ const origLink = this.linkUrl;
+ const linkUrl =
+ origLink.indexOf("://") === -1 ? `http://${origLink}` : origLink;
+ const sel = this.toolbarEvent.selected;
+
+ if (isEmpty(linkUrl)) {
+ return;
+ }
+
+ const linkText = this.linkText || "";
+
+ if (linkText.length) {
+ this.toolbarEvent.addText(`[${linkText}](${linkUrl})`);
+ } else {
+ if (sel.value) {
+ this.toolbarEvent.addText(`[${sel.value}](${linkUrl})`);
+ } else {
+ this.toolbarEvent.addText(`[${origLink}](${linkUrl})`);
+ this.toolbarEvent.selectText(sel.start + 1, origLink.length);
+ }
+ }
+ this.send("closeModal");
+ },
+ cancel() {
+ this.send("closeModal");
+ },
+ linkClick(e) {
+ if (!e.metaKey && !e.ctrlKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.selectLink(e.target.closest(".search-link"));
+ }
+ },
+ search() {
+ this._debounced = debounce(this, this.triggerSearch, 400);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/invites-show.js.es6 b/app/assets/javascripts/discourse/controllers/invites-show.js.es6
index 2664a95bd7..58b7ca3685 100644
--- a/app/assets/javascripts/discourse/controllers/invites-show.js.es6
+++ b/app/assets/javascripts/discourse/controllers/invites-show.js.es6
@@ -1,4 +1,7 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import { isEmpty } from "@ember/utils";
+import { alias, notEmpty } from "@ember/object/computed";
+import Controller from "@ember/controller";
+import discourseComputed from "discourse-common/utils/decorators";
import getUrl from "discourse-common/lib/get-url";
import DiscourseURL from "discourse/lib/url";
import { ajax } from "discourse/lib/ajax";
@@ -8,39 +11,39 @@ import NameValidation from "discourse/mixins/name-validation";
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
import { findAll as findLoginMethods } from "discourse/models/login-method";
-export default Ember.Controller.extend(
+export default Controller.extend(
PasswordValidation,
UsernameValidation,
NameValidation,
UserFieldsValidation,
{
- invitedBy: Ember.computed.alias("model.invited_by"),
- email: Ember.computed.alias("model.email"),
- accountUsername: Ember.computed.alias("model.username"),
- passwordRequired: Ember.computed.notEmpty("accountPassword"),
+ invitedBy: alias("model.invited_by"),
+ email: alias("model.email"),
+ accountUsername: alias("model.username"),
+ passwordRequired: notEmpty("accountPassword"),
successMessage: null,
errorMessage: null,
userFields: null,
inviteImageUrl: getUrl("/images/envelope.svg"),
- @computed
+ @discourseComputed
welcomeTitle() {
return I18n.t("invites.welcome_to", {
site_name: this.siteSettings.title
});
},
- @computed("email")
+ @discourseComputed("email")
yourEmailMessage(email) {
return I18n.t("invites.your_email", { email: email });
},
- @computed
+ @discourseComputed
externalAuthsEnabled() {
return findLoginMethods().length > 0;
},
- @computed(
+ @discourseComputed(
"usernameValidation.failed",
"passwordValidation.failed",
"nameValidation.failed",
@@ -55,7 +58,7 @@ export default Ember.Controller.extend(
return usernameFailed || passwordFailed || nameFailed || userFieldsFailed;
},
- @computed
+ @discourseComputed
fullnameRequired() {
return (
this.siteSettings.full_name_required || this.siteSettings.enable_names
@@ -66,7 +69,7 @@ export default Ember.Controller.extend(
submit() {
const userFields = this.userFields;
let userCustomFields = {};
- if (!Ember.isEmpty(userFields)) {
+ if (!isEmpty(userFields)) {
userFields.forEach(function(f) {
userCustomFields[f.get("field.id")] = f.get("value");
});
@@ -79,7 +82,8 @@ export default Ember.Controller.extend(
username: this.accountUsername,
name: this.accountName,
password: this.accountPassword,
- user_custom_fields: userCustomFields
+ user_custom_fields: userCustomFields,
+ timezone: moment.tz.guess()
}
})
.then(result => {
diff --git a/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6 b/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6
index 385db0406f..cd3b95936f 100644
--- a/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6
+++ b/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6
@@ -1,15 +1,16 @@
+import { alias } from "@ember/object/computed";
+import { next } from "@ember/runloop";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
model: null,
postNumber: null,
postDate: null,
- filteredPostsCount: Ember.computed.alias(
- "topic.postStream.filteredPostsCount"
- ),
+ filteredPostsCount: alias("topic.postStream.filteredPostsCount"),
onShow() {
- Ember.run.next(() => $("#post-jump").focus());
+ next(() => $("#post-jump").focus());
},
actions: {
@@ -23,7 +24,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
},
_jumpToIndex(postsCounts, postNumber) {
- const where = Math.min(postsCounts, Math.max(1, parseInt(postNumber)));
+ const where = Math.min(postsCounts, Math.max(1, parseInt(postNumber, 10)));
this.jumpToIndex(where);
this._close();
},
diff --git a/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js.es6 b/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js.es6
index 4e5c4846e0..7b3fbdcb5d 100644
--- a/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js.es6
+++ b/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js.es6
@@ -1,3 +1,4 @@
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
const KEY = "keyboard_shortcuts_help";
@@ -46,7 +47,7 @@ function buildShortcut(
return I18n.t(`${KEY}.${key}`, context);
}
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
onShow() {
this.set("modal.modalClass", "keyboard-shortcuts-modal");
},
@@ -77,6 +78,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
keys2: [SHIFT, "k"],
keysDelimiter: PLUS,
shortcutsDelimiter: "slash"
+ }),
+ go_to_unread_post: buildShortcut("navigation.go_to_unread_post", {
+ keys1: [SHIFT, "l"],
+ keysDelimiter: PLUS
})
},
application: {
@@ -158,6 +163,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
print: buildShortcut("actions.print", {
keys1: [CTRL, "p"],
keysDelimiter: PLUS
+ }),
+ defer: buildShortcut("actions.defer", {
+ keys1: [SHIFT, "u"],
+ keysDelimiter: PLUS
+ }),
+ topic_admin_actions: buildShortcut("actions.topic_admin_actions", {
+ keys1: [SHIFT, "a"],
+ keysDelimiter: PLUS
})
}
}
diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6
index 19da455a26..fe57a856ec 100644
--- a/app/assets/javascripts/discourse/controllers/login.js.es6
+++ b/app/assets/javascripts/discourse/controllers/login.js.es6
@@ -1,3 +1,11 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import { alias, or, readOnly } from "@ember/object/computed";
+import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import showModal from "discourse/lib/show-modal";
@@ -6,8 +14,8 @@ import { findAll } from "discourse/models/login-method";
import { escape } from "pretty-text/sanitizer";
import { escapeExpression, areCookiesEnabled } from "discourse/lib/utilities";
import { extractError } from "discourse/lib/ajax-error";
-import computed from "ember-addons/ember-computed-decorators";
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
+import { getWebauthnCredential } from "discourse/lib/webauthn";
// This is happening outside of the app via popup
const AuthErrors = [
@@ -18,12 +26,11 @@ const AuthErrors = [
"not_allowed_from_ip_address"
];
-export default Ember.Controller.extend(ModalFunctionality, {
- createAccount: Ember.inject.controller(),
- forgotPassword: Ember.inject.controller(),
- application: Ember.inject.controller(),
+export default Controller.extend(ModalFunctionality, {
+ createAccount: inject(),
+ forgotPassword: inject(),
+ application: inject(),
- authenticate: null,
loggingIn: false,
loggedIn: false,
processingEmailLink: false,
@@ -33,32 +40,32 @@ export default Ember.Controller.extend(ModalFunctionality, {
canLoginLocal: setting("enable_local_logins"),
canLoginLocalWithEmail: setting("enable_local_logins_via_email"),
- loginRequired: Ember.computed.alias("application.loginRequired"),
+ loginRequired: alias("application.loginRequired"),
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
resetForm() {
this.setProperties({
- authenticate: null,
loggingIn: false,
loggedIn: false,
secondFactorRequired: false,
showSecondFactor: false,
+ showSecurityKey: false,
showLoginButtons: true,
awaitingApproval: false
});
},
- @computed("showSecondFactor")
- credentialsClass(showSecondFactor) {
- return showSecondFactor ? "hidden" : "";
+ @discourseComputed("showSecondFactor", "showSecurityKey")
+ credentialsClass(showSecondFactor, showSecurityKey) {
+ return showSecondFactor || showSecurityKey ? "hidden" : "";
},
- @computed("showSecondFactor")
- secondFactorClass(showSecondFactor) {
- return showSecondFactor ? "" : "hidden";
+ @discourseComputed("showSecondFactor", "showSecurityKey")
+ secondFactorClass(showSecondFactor, showSecurityKey) {
+ return showSecondFactor || showSecurityKey ? "" : "hidden";
},
- @computed("awaitingApproval", "hasAtLeastOneLoginButton")
+ @discourseComputed("awaitingApproval", "hasAtLeastOneLoginButton")
modalBodyClasses(awaitingApproval, hasAtLeastOneLoginButton) {
const classes = ["login-modal"];
if (awaitingApproval) classes.push("awaiting-approval");
@@ -66,26 +73,31 @@ export default Ember.Controller.extend(ModalFunctionality, {
return classes.join(" ");
},
- @computed("canLoginLocalWithEmail")
+ @discourseComputed("showSecondFactor", "showSecurityKey")
+ disableLoginFields(showSecondFactor, showSecurityKey) {
+ return showSecondFactor || showSecurityKey;
+ },
+
+ @discourseComputed("canLoginLocalWithEmail")
hasAtLeastOneLoginButton(canLoginLocalWithEmail) {
return findAll().length > 0 || canLoginLocalWithEmail;
},
- @computed("loggingIn")
+ @discourseComputed("loggingIn")
loginButtonLabel(loggingIn) {
return loggingIn ? "login.logging_in" : "login.title";
},
- loginDisabled: Ember.computed.or("loggingIn", "loggedIn"),
+ loginDisabled: or("loggingIn", "loggedIn"),
- @computed("loggingIn", "authenticate", "application.canSignUp")
- showSignupLink(loggingIn, authenticate, canSignUp) {
- return canSignUp && !loggingIn && Ember.isEmpty(authenticate);
+ @discourseComputed("loggingIn", "application.canSignUp")
+ showSignupLink(loggingIn, canSignUp) {
+ return canSignUp && !loggingIn;
},
- showSpinner: Ember.computed.or("loggingIn", "authenticate"),
+ showSpinner: readOnly("loggingIn"),
- @computed("canLoginLocalWithEmail", "processingEmailLink")
+ @discourseComputed("canLoginLocalWithEmail", "processingEmailLink")
showLoginWithEmailLink(canLoginLocalWithEmail, processingEmailLink) {
return canLoginLocalWithEmail && !processingEmailLink;
},
@@ -96,7 +108,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
return;
}
- if (Ember.isEmpty(this.loginName) || Ember.isEmpty(this.loginPassword)) {
+ if (isEmpty(this.loginName) || isEmpty(this.loginPassword)) {
this.flash(I18n.t("login.blank_username_or_password"), "error");
return;
}
@@ -108,33 +120,46 @@ export default Ember.Controller.extend(ModalFunctionality, {
data: {
login: this.loginName,
password: this.loginPassword,
- second_factor_token: this.secondFactorToken,
- second_factor_method: this.secondFactorMethod
+ second_factor_token:
+ this.securityKeyCredential || this.secondFactorToken,
+ second_factor_method: this.secondFactorMethod,
+ timezone: moment.tz.guess()
}
}).then(
result => {
// Successful login
if (result && result.error) {
this.set("loggingIn", false);
+
if (
- result.reason === "invalid_second_factor" &&
+ (result.security_key_enabled || result.totp_enabled) &&
!this.secondFactorRequired
) {
document.getElementById("modal-alert").style.display = "none";
this.setProperties({
+ otherMethodAllowed: result.multiple_second_factor_methods,
secondFactorRequired: true,
showLoginButtons: false,
backupEnabled: result.backup_enabled,
- showSecondFactor: true
+ showSecondFactor: result.totp_enabled,
+ showSecurityKey: result.security_key_enabled,
+ secondFactorMethod: result.security_key_enabled
+ ? SECOND_FACTOR_METHODS.SECURITY_KEY
+ : SECOND_FACTOR_METHODS.TOTP,
+ securityKeyChallenge: result.challenge,
+ securityKeyAllowedCredentialIds: result.allowed_credential_ids
});
- Ember.run.schedule("afterRender", () =>
- document
- .getElementById("second-factor")
- .querySelector("input")
- .focus()
- );
+ // only need to focus the 2FA input for TOTP
+ if (!this.showSecurityKey) {
+ scheduleOnce("afterRender", () =>
+ document
+ .getElementById("second-factor")
+ .querySelector("input")
+ .focus()
+ );
+ }
return;
} else if (result.reason === "not_activated") {
@@ -212,17 +237,13 @@ export default Ember.Controller.extend(ModalFunctionality, {
return false;
},
- externalLogin(loginMethod, { fullScreenLogin = false } = {}) {
- const capabilities = this.capabilities;
- // On Mobile, Android or iOS always go with full screen
- if (
- this.isMobileDevice ||
- (capabilities && (capabilities.isIOS || capabilities.isAndroid))
- ) {
- fullScreenLogin = true;
+ externalLogin(loginMethod) {
+ if (this.loginDisabled) {
+ return;
}
- loginMethod.doLogin({ fullScreenLogin });
+ this.set("loggingIn", true);
+ loginMethod.doLogin().catch(() => this.set("loggingIn", false));
},
createAccount() {
@@ -252,7 +273,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
return;
}
- if (Ember.isEmpty(this.loginName)) {
+ if (isEmpty(this.loginName)) {
this.flash(I18n.t("login.blank_username"), "error");
return;
}
@@ -283,16 +304,20 @@ export default Ember.Controller.extend(ModalFunctionality, {
})
.catch(e => this.flash(extractError(e), "error"))
.finally(() => this.set("processingEmailLink", false));
- }
- },
+ },
- @computed("authenticate")
- authMessage(authenticate) {
- if (Ember.isEmpty(authenticate)) return "";
-
- const method = findAll().findBy("name", authenticate);
- if (method) {
- return method.message;
+ authenticateSecurityKey() {
+ getWebauthnCredential(
+ this.securityKeyChallenge,
+ this.securityKeyAllowedCredentialIds,
+ credentialData => {
+ this.set("securityKeyCredential", credentialData);
+ this.send("login");
+ },
+ errorMessage => {
+ this.flash(errorMessage, "error");
+ }
+ );
}
},
@@ -300,10 +325,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
const loginError = (errorMsg, className, callback) => {
showModal("login");
- Ember.run.next(() => {
+ next(() => {
if (callback) callback();
this.flash(errorMsg, className || "success");
- this.set("authenticate", null);
});
};
@@ -346,7 +370,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
$.removeCookie("destination_url");
window.location.href = destinationUrl;
} else if (window.location.pathname === Discourse.getURL("/login")) {
- window.location.pathname = Discourse.getURL("/");
+ window.location = Discourse.getURL("/");
} else {
window.location.reload();
}
@@ -358,7 +382,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
accountEmail: options.email,
accountUsername: options.username,
accountName: options.name,
- authOptions: Ember.Object.create(options)
+ authOptions: EmberObject.create(options)
});
showModal("createAccount");
diff --git a/app/assets/javascripts/discourse/controllers/modal.js.es6 b/app/assets/javascripts/discourse/controllers/modal.js.es6
index 77c79b724a..cf6c4e3aa2 100644
--- a/app/assets/javascripts/discourse/controllers/modal.js.es6
+++ b/app/assets/javascripts/discourse/controllers/modal.js.es6
@@ -1 +1,2 @@
-export default Ember.Controller.extend();
+import Controller from "@ember/controller";
+export default Controller.extend();
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 fa41654818..b2b7dd7f10 100644
--- a/app/assets/javascripts/discourse/controllers/move-to-topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/move-to-topic.js.es6
@@ -1,21 +1,26 @@
+import { isEmpty } from "@ember/utils";
+import { alias, equal } from "@ember/object/computed";
+import { next } from "@ember/runloop";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { movePosts, mergeTopic } from "discourse/models/topic";
import DiscourseURL from "discourse/lib/url";
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
topicName: null,
saving: false,
categoryId: null,
tags: null,
- canAddTags: Ember.computed.alias("site.can_create_tag"),
- canTagMessages: Ember.computed.alias("site.can_tag_pms"),
+ canAddTags: alias("site.can_create_tag"),
+ canTagMessages: alias("site.can_tag_pms"),
selectedTopicId: null,
- newTopic: Ember.computed.equal("selection", "new_topic"),
- existingTopic: Ember.computed.equal("selection", "existing_topic"),
- newMessage: Ember.computed.equal("selection", "new_message"),
- existingMessage: Ember.computed.equal("selection", "existing_message"),
+ newTopic: equal("selection", "new_topic"),
+ existingTopic: equal("selection", "existing_topic"),
+ newMessage: equal("selection", "new_message"),
+ existingMessage: equal("selection", "existing_message"),
participants: null,
init() {
@@ -36,21 +41,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
];
},
- topicController: Ember.inject.controller("topic"),
- selectedPostsCount: Ember.computed.alias(
- "topicController.selectedPostsCount"
- ),
- selectedAllPosts: Ember.computed.alias("topicController.selectedAllPosts"),
- selectedPosts: Ember.computed.alias("topicController.selectedPosts"),
+ topicController: inject("topic"),
+ selectedPostsCount: alias("topicController.selectedPostsCount"),
+ selectedAllPosts: alias("topicController.selectedAllPosts"),
+ selectedPosts: alias("topicController.selectedPosts"),
- @computed("saving", "selectedTopicId", "topicName")
+ @discourseComputed("saving", "selectedTopicId", "topicName")
buttonDisabled(saving, selectedTopicId, topicName) {
- return (
- saving || (Ember.isEmpty(selectedTopicId) && Ember.isEmpty(topicName))
- );
+ return saving || (isEmpty(selectedTopicId) && isEmpty(topicName));
},
- @computed(
+ @discourseComputed(
"saving",
"newTopic",
"existingTopic",
@@ -73,7 +74,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
onShow() {
this.setProperties({
- "modal.modalClass": "move-to-modal",
+ "modal.modalClass": "choose-topic-modal",
saving: false,
selection: "new_topic",
categoryId: null,
@@ -90,11 +91,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
);
} else if (!this.canSplitTopic) {
this.set("selection", "existing_topic");
- Ember.run.next(() => $("#choose-topic-title").focus());
+ next(() => $("#choose-topic-title").focus());
}
},
- @computed("selectedAllPosts", "selectedPosts", "selectedPosts.[]")
+ @discourseComputed("selectedAllPosts", "selectedPosts", "selectedPosts.[]")
canSplitTopic(selectedAllPosts, selectedPosts) {
return (
!selectedAllPosts &&
@@ -104,9 +105,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
);
},
- @computed("canSplitTopic")
+ @discourseComputed("canSplitTopic")
canSplitToPM(canSplitTopic) {
- return canSplitTopic && (this.currentUser && this.currentUser.admin);
+ return canSplitTopic && this.currentUser && this.currentUser.admin;
},
actions: {
diff --git a/app/assets/javascripts/discourse/controllers/navigation/categories.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/categories.js.es6
index ee6066f186..db7ac2bae9 100644
--- a/app/assets/javascripts/discourse/controllers/navigation/categories.js.es6
+++ b/app/assets/javascripts/discourse/controllers/navigation/categories.js.es6
@@ -1,10 +1,14 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { inject } from "@ember/controller";
import NavigationDefaultController from "discourse/controllers/navigation/default";
-import computed from "ember-addons/ember-computed-decorators";
export default NavigationDefaultController.extend({
- discoveryCategories: Ember.inject.controller("discovery/categories"),
+ discoveryCategories: inject("discovery/categories"),
- @computed("discoveryCategories.model", "discoveryCategories.model.draft")
+ @discourseComputed(
+ "discoveryCategories.model",
+ "discoveryCategories.model.draft"
+ )
draft() {
return this.get("discoveryCategories.model.draft");
}
diff --git a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6
index a54af35e2a..23da3c88df 100644
--- a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6
+++ b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6
@@ -1,8 +1,10 @@
+import { none, and } from "@ember/object/computed";
import NavigationDefaultController from "discourse/controllers/navigation/default";
+import FilterModeMixin from "discourse/mixins/filter-mode";
-export default NavigationDefaultController.extend({
- showingParentCategory: Ember.computed.none("category.parentCategory"),
- showingSubcategoryList: Ember.computed.and(
+export default NavigationDefaultController.extend(FilterModeMixin, {
+ showingParentCategory: none("category.parentCategory"),
+ showingSubcategoryList: and(
"category.show_subcategory_list",
"showingParentCategory"
)
diff --git a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6
index 0ad1878622..02dd26acde 100644
--- a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6
+++ b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6
@@ -1,10 +1,13 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
+import FilterModeMixin from "discourse/mixins/filter-mode";
-export default Ember.Controller.extend({
- discovery: Ember.inject.controller(),
- discoveryTopics: Ember.inject.controller("discovery/topics"),
+export default Controller.extend(FilterModeMixin, {
+ discovery: inject(),
+ discoveryTopics: inject("discovery/topics"),
- @computed("discoveryTopics.model", "discoveryTopics.model.draft")
+ @discourseComputed("discoveryTopics.model", "discoveryTopics.model.draft")
draft: function() {
return this.get("discoveryTopics.model.draft");
}
diff --git a/app/assets/javascripts/discourse/controllers/not-activated.js.es6 b/app/assets/javascripts/discourse/controllers/not-activated.js.es6
index 71824b3e15..ba520eb6f2 100644
--- a/app/assets/javascripts/discourse/controllers/not-activated.js.es6
+++ b/app/assets/javascripts/discourse/controllers/not-activated.js.es6
@@ -1,7 +1,8 @@
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { resendActivationEmail } from "discourse/lib/user-activation";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
actions: {
sendActivationEmail() {
resendActivationEmail(this.username).then(() => {
diff --git a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 b/app/assets/javascripts/discourse/controllers/password-reset.js.es6
index 2d48d74e2e..dcb1931ac0 100644
--- a/app/assets/javascripts/discourse/controllers/password-reset.js.es6
+++ b/app/assets/javascripts/discourse/controllers/password-reset.js.es6
@@ -1,30 +1,44 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import { alias, or, readOnly } from "@ember/object/computed";
+import Controller from "@ember/controller";
+import discourseComputed from "discourse-common/utils/decorators";
import DiscourseURL from "discourse/lib/url";
import { ajax } from "discourse/lib/ajax";
import PasswordValidation from "discourse/mixins/password-validation";
import { userPath } from "discourse/lib/url";
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
+import { getWebauthnCredential } from "discourse/lib/webauthn";
-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.backup_enabled"),
- secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
+export default Controller.extend(PasswordValidation, {
+ isDeveloper: alias("model.is_developer"),
+ admin: alias("model.admin"),
+ secondFactorRequired: alias("model.second_factor_required"),
+ securityKeyRequired: alias("model.security_key_required"),
+ backupEnabled: alias("model.backup_enabled"),
+ securityKeyOrSecondFactorRequired: or(
+ "model.second_factor_required",
+ "model.security_key_required"
+ ),
+ otherMethodAllowed: readOnly("model.multiple_second_factor_methods"),
+ @discourseComputed("model.security_key_required")
+ secondFactorMethod(security_key_required) {
+ return security_key_required
+ ? SECOND_FACTOR_METHODS.SECURITY_KEY
+ : SECOND_FACTOR_METHODS.TOTP;
+ },
passwordRequired: true,
errorMessage: null,
successMessage: null,
requiresApproval: false,
redirected: false,
- @computed()
+ @discourseComputed()
continueButtonText() {
return I18n.t("password_reset.continue", {
site_name: this.siteSettings.title
});
},
- @computed("redirectTo")
+ @discourseComputed("redirectTo")
redirectHref(redirectTo) {
return Discourse.getURL(redirectTo || "/");
},
@@ -38,7 +52,8 @@ export default Ember.Controller.extend(PasswordValidation, {
type: "PUT",
data: {
password: this.accountPassword,
- second_factor_token: this.secondFactorToken,
+ second_factor_token:
+ this.securityKeyCredential || this.secondFactorToken,
second_factor_method: this.secondFactorMethod
}
})
@@ -53,15 +68,17 @@ export default Ember.Controller.extend(PasswordValidation, {
DiscourseURL.redirectTo(result.redirect_to || "/");
}
} else {
- if (result.errors && result.errors.user_second_factors) {
+ if (result.errors && !result.errors.password) {
this.setProperties({
- secondFactorRequired: true,
+ secondFactorRequired: this.secondFactorRequired,
+ securityKeyRequired: this.securityKeyRequired,
password: null,
errorMessage: result.message
});
- } else if (this.secondFactorRequired) {
+ } else if (this.secondFactorRequired || this.securityKeyRequired) {
this.setProperties({
secondFactorRequired: false,
+ securityKeyRequired: false,
errorMessage: null
});
} else if (
@@ -90,6 +107,24 @@ export default Ember.Controller.extend(PasswordValidation, {
});
},
+ authenticateSecurityKey() {
+ getWebauthnCredential(
+ this.model.challenge,
+ this.model.allowed_credential_ids,
+ credentialData => {
+ this.set("securityKeyCredential", credentialData);
+ this.send("submit");
+ },
+ errorMessage => {
+ this.setProperties({
+ securityKeyRequired: true,
+ password: null,
+ errorMessage: errorMessage
+ });
+ }
+ );
+ },
+
done() {
this.set("redirected", true);
DiscourseURL.redirectTo(this.redirectTo || "/");
diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6
index d4180e8b6b..a422a1f33c 100644
--- a/app/assets/javascripts/discourse/controllers/preferences.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6
@@ -1,3 +1,5 @@
-export default Ember.Controller.extend({
- application: Ember.inject.controller()
+import { inject as service } from "@ember/service";
+import Controller from "@ember/controller";
+export default Controller.extend({
+ router: service()
});
diff --git a/app/assets/javascripts/discourse/controllers/preferences/about.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/about.js.es6
deleted file mode 100644
index 37e7247a8a..0000000000
--- a/app/assets/javascripts/discourse/controllers/preferences/about.js.es6
+++ /dev/null
@@ -1,11 +0,0 @@
-import computed from "ember-addons/ember-computed-decorators";
-
-export default Ember.Controller.extend({
- saving: false,
- newBio: null,
-
- @computed("saving")
- saveButtonText(saving) {
- return saving ? I18n.t("saving") : I18n.t("user.change");
- }
-});
diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
index 952788273f..6cac06c1aa 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
@@ -1,6 +1,8 @@
+import { not, or, gt } from "@ember/object/computed";
+import Controller from "@ember/controller";
import { iconHTML } from "discourse-common/lib/icon-library";
import CanCheckEmails from "discourse/mixins/can-check-emails";
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
import { propertyNotEqual, setting } from "discourse/lib/computed";
import { popupAjaxError } from "discourse/lib/ajax-error";
@@ -8,239 +10,247 @@ import showModal from "discourse/lib/show-modal";
import { findAll } from "discourse/models/login-method";
import { ajax } from "discourse/lib/ajax";
import { userPath } from "discourse/lib/url";
+import logout from "discourse/lib/logout";
// Number of tokens shown by default.
const DEFAULT_AUTH_TOKENS_COUNT = 2;
-export default Ember.Controller.extend(
- CanCheckEmails,
- PreferencesTabController,
- {
- init() {
- this._super(...arguments);
+export default Controller.extend(CanCheckEmails, PreferencesTabController, {
+ init() {
+ this._super(...arguments);
- this.saveAttrNames = ["name", "title"];
- this.set("revoking", {});
- },
+ this.saveAttrNames = ["name", "title", "primary_group_id"];
+ this.set("revoking", {});
+ },
- canEditName: setting("enable_names"),
- canSaveUser: true,
+ canEditName: setting("enable_names"),
+ canSaveUser: true,
- newNameInput: null,
- newTitleInput: null,
+ newNameInput: null,
+ newTitleInput: null,
+ newPrimaryGroupInput: null,
- passwordProgress: null,
+ passwordProgress: null,
- showAllAuthTokens: false,
+ showAllAuthTokens: false,
- revoking: null,
+ revoking: null,
- cannotDeleteAccount: Ember.computed.not("currentUser.can_delete_account"),
- deleteDisabled: Ember.computed.or(
- "model.isSaving",
- "deleting",
- "cannotDeleteAccount"
- ),
+ cannotDeleteAccount: not("currentUser.can_delete_account"),
+ deleteDisabled: or("model.isSaving", "deleting", "cannotDeleteAccount"),
- reset() {
- this.set("passwordProgress", null);
- },
+ reset() {
+ this.set("passwordProgress", null);
+ },
- @computed()
- nameInstructions() {
- return I18n.t(
- this.siteSettings.full_name_required
- ? "user.name.instructions_required"
- : "user.name.instructions"
+ @discourseComputed()
+ nameInstructions() {
+ return I18n.t(
+ this.siteSettings.full_name_required
+ ? "user.name.instructions_required"
+ : "user.name.instructions"
+ );
+ },
+
+ canSelectTitle: gt("model.availableTitles.length", 0),
+
+ @discourseComputed("model.filteredGroups")
+ canSelectPrimaryGroup(primaryGroupOptions) {
+ return (
+ primaryGroupOptions.length > 0 &&
+ this.siteSettings.user_selected_primary_groups
+ );
+ },
+
+ @discourseComputed("model.is_anonymous")
+ canChangePassword(isAnonymous) {
+ if (isAnonymous) {
+ return false;
+ } else {
+ return (
+ !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins
);
- },
+ }
+ },
- canSelectTitle: Ember.computed.gt("model.availableTitles.length", 0),
+ @discourseComputed("model.associated_accounts")
+ associatedAccountsLoaded(associatedAccounts) {
+ return typeof associatedAccounts !== "undefined";
+ },
- @computed("model.is_anonymous")
- canChangePassword(isAnonymous) {
- if (isAnonymous) {
- return false;
+ @discourseComputed("model.associated_accounts.[]")
+ authProviders(accounts) {
+ const allMethods = findAll();
+
+ const result = allMethods.map(method => {
+ return {
+ method,
+ account: accounts.find(account => account.name === method.name) // Will be undefined if no account
+ };
+ });
+
+ return result.filter(value => value.account || value.method.can_connect);
+ },
+
+ disableConnectButtons: propertyNotEqual("model.id", "currentUser.id"),
+
+ @discourseComputed(
+ "model.second_factor_enabled",
+ "canCheckEmails",
+ "model.is_anonymous"
+ )
+ canUpdateAssociatedAccounts(
+ secondFactorEnabled,
+ canCheckEmails,
+ isAnonymous
+ ) {
+ if (secondFactorEnabled || !canCheckEmails || isAnonymous) {
+ return false;
+ }
+ return findAll().length > 0;
+ },
+
+ @discourseComputed("showAllAuthTokens", "model.user_auth_tokens")
+ authTokens(showAllAuthTokens, tokens) {
+ tokens.sort((a, b) => {
+ if (a.is_active) {
+ return -1;
+ } else if (b.is_active) {
+ return 1;
} else {
- return (
- !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins
+ return b.seen_at.localeCompare(a.seen_at);
+ }
+ });
+
+ return showAllAuthTokens
+ ? tokens
+ : tokens.slice(0, DEFAULT_AUTH_TOKENS_COUNT);
+ },
+
+ canShowAllAuthTokens: gt(
+ "model.user_auth_tokens.length",
+ DEFAULT_AUTH_TOKENS_COUNT
+ ),
+
+ actions: {
+ save() {
+ this.set("saved", false);
+
+ this.model.setProperties({
+ name: this.newNameInput,
+ title: this.newTitleInput,
+ primary_group_id: this.newPrimaryGroupInput
+ });
+
+ return this.model
+ .save(this.saveAttrNames)
+ .then(() => this.set("saved", true))
+ .catch(popupAjaxError);
+ },
+
+ changePassword() {
+ if (!this.passwordProgress) {
+ this.set(
+ "passwordProgress",
+ I18n.t("user.change_password.in_progress")
);
- }
- },
-
- @computed("model.associated_accounts")
- associatedAccountsLoaded(associatedAccounts) {
- return typeof associatedAccounts !== "undefined";
- },
-
- @computed("model.associated_accounts.[]")
- authProviders(accounts) {
- const allMethods = findAll();
-
- const result = allMethods.map(method => {
- return {
- method,
- account: accounts.find(account => account.name === method.name) // Will be undefined if no account
- };
- });
-
- return result.filter(value => value.account || value.method.can_connect);
- },
-
- disableConnectButtons: propertyNotEqual("model.id", "currentUser.id"),
-
- @computed(
- "model.second_factor_enabled",
- "canCheckEmails",
- "model.is_anonymous"
- )
- canUpdateAssociatedAccounts(
- secondFactorEnabled,
- canCheckEmails,
- isAnonymous
- ) {
- if (secondFactorEnabled || !canCheckEmails || isAnonymous) {
- return false;
- }
- return findAll().length > 0;
- },
-
- @computed("showAllAuthTokens", "model.user_auth_tokens")
- authTokens(showAllAuthTokens, tokens) {
- 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
- : tokens.slice(0, DEFAULT_AUTH_TOKENS_COUNT);
- },
-
- canShowAllAuthTokens: Ember.computed.gt(
- "model.user_auth_tokens.length",
- DEFAULT_AUTH_TOKENS_COUNT
- ),
-
- actions: {
- save() {
- this.set("saved", false);
-
- this.model.setProperties({
- name: this.newNameInput,
- title: this.newTitleInput
- });
-
return this.model
- .save(this.saveAttrNames)
- .then(() => this.set("saved", true))
- .catch(popupAjaxError);
- },
-
- changePassword() {
- if (!this.passwordProgress) {
- this.set(
- "passwordProgress",
- I18n.t("user.change_password.in_progress")
- );
- return this.model
- .changePassword()
- .then(() => {
- // password changed
- this.setProperties({
- changePasswordProgress: false,
- passwordProgress: I18n.t("user.change_password.success")
- });
- })
- .catch(() => {
- // password failed to change
- this.setProperties({
- changePasswordProgress: false,
- passwordProgress: I18n.t("user.change_password.error")
- });
+ .changePassword()
+ .then(() => {
+ // password changed
+ this.setProperties({
+ changePasswordProgress: false,
+ passwordProgress: I18n.t("user.change_password.success")
});
- }
- },
-
- delete() {
- this.set("deleting", true);
- const message = I18n.t("user.delete_account_confirm"),
- model = this.model,
- buttons = [
- {
- label: I18n.t("cancel"),
- class: "d-modal-cancel",
- link: true,
- callback: () => this.set("deleting", false)
- },
- {
- label:
- iconHTML("exclamation-triangle") +
- I18n.t("user.delete_account"),
- class: "btn btn-danger",
- callback() {
- model.delete().then(
- () => {
- bootbox.alert(
- I18n.t("user.deleted_yourself"),
- () => (window.location.pathname = Discourse.getURL("/"))
- );
- },
- () => {
- bootbox.alert(I18n.t("user.delete_yourself_not_allowed"));
- this.set("deleting", false);
- }
- );
- }
- }
- ];
- bootbox.dialog(message, buttons, { classes: "delete-account" });
- },
-
- revokeAccount(account) {
- this.set(`revoking.${account.name}`, true);
-
- this.model
- .revokeAssociatedAccount(account.name)
- .then(result => {
- if (result.success) {
- this.model.associated_accounts.removeObject(account);
- } else {
- bootbox.alert(result.message);
- }
})
- .catch(popupAjaxError)
- .finally(() => this.set(`revoking.${account.name}`, false));
- },
-
- toggleShowAllAuthTokens() {
- this.toggleProperty("showAllAuthTokens");
- },
-
- revokeAuthToken(token) {
- ajax(
- userPath(
- `${this.get("model.username_lower")}/preferences/revoke-auth-token`
- ),
- {
- type: "POST",
- data: token ? { token_id: token.id } : {}
- }
- );
- },
-
- showToken(token) {
- showModal("auth-token", { model: token });
- },
-
- connectAccount(method) {
- method.doLogin({ reconnect: true, fullScreenLogin: false });
+ .catch(() => {
+ // password failed to change
+ this.setProperties({
+ changePasswordProgress: false,
+ passwordProgress: I18n.t("user.change_password.error")
+ });
+ });
}
+ },
+
+ delete() {
+ this.set("deleting", true);
+ const message = I18n.t("user.delete_account_confirm"),
+ model = this.model,
+ buttons = [
+ {
+ label: I18n.t("cancel"),
+ class: "d-modal-cancel",
+ link: true,
+ callback: () => {
+ this.set("deleting", false);
+ }
+ },
+ {
+ label:
+ iconHTML("exclamation-triangle") + I18n.t("user.delete_account"),
+ class: "btn btn-danger",
+ callback() {
+ model.delete().then(
+ () => {
+ bootbox.alert(
+ I18n.t("user.deleted_yourself"),
+ () => (window.location = Discourse.getURL("/"))
+ );
+ },
+ () => {
+ bootbox.alert(I18n.t("user.delete_yourself_not_allowed"));
+ this.set("deleting", false);
+ }
+ );
+ }
+ }
+ ];
+ bootbox.dialog(message, buttons, { classes: "delete-account" });
+ },
+
+ revokeAccount(account) {
+ this.set(`revoking.${account.name}`, true);
+
+ this.model
+ .revokeAssociatedAccount(account.name)
+ .then(result => {
+ if (result.success) {
+ this.model.associated_accounts.removeObject(account);
+ } else {
+ bootbox.alert(result.message);
+ }
+ })
+ .catch(popupAjaxError)
+ .finally(() => this.set(`revoking.${account.name}`, false));
+ },
+
+ toggleShowAllAuthTokens() {
+ this.toggleProperty("showAllAuthTokens");
+ },
+
+ revokeAuthToken(token) {
+ ajax(
+ userPath(
+ `${this.get("model.username_lower")}/preferences/revoke-auth-token`
+ ),
+ {
+ type: "POST",
+ data: token ? { token_id: token.id } : {}
+ }
+ )
+ .then(() => {
+ if (!token) logout(); // All sessions revoked
+ })
+ .catch(popupAjaxError);
+ },
+
+ showToken(token) {
+ showModal("auth-token", { model: token });
+ },
+
+ connectAccount(method) {
+ method.doLogin({ reconnect: true });
}
}
-);
+});
diff --git a/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6
index d45e3fad89..4628234524 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6
@@ -1,8 +1,10 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { or } from "@ember/object/computed";
+import Controller from "@ember/controller";
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Controller.extend(PreferencesTabController, {
+export default Controller.extend(PreferencesTabController, {
init() {
this._super(...arguments);
@@ -14,7 +16,7 @@ export default Ember.Controller.extend(PreferencesTabController, {
];
},
- @computed(
+ @discourseComputed(
"model.watchedCategories",
"model.watchedFirstPostCategories",
"model.trackedCategories",
@@ -24,17 +26,17 @@ export default Ember.Controller.extend(PreferencesTabController, {
return [].concat(watched, watchedFirst, tracked, muted).filter(t => t);
},
- @computed
+ @discourseComputed
canSee() {
return this.get("currentUser.id") === this.get("model.id");
},
- @computed("siteSettings.remove_muted_tags_from_latest")
+ @discourseComputed("siteSettings.remove_muted_tags_from_latest")
hideMutedTags() {
return this.siteSettings.remove_muted_tags_from_latest !== "never";
},
- canSave: Ember.computed.or("canSee", "currentUser.admin"),
+ canSave: or("canSee", "currentUser.admin"),
actions: {
save() {
diff --git a/app/assets/javascripts/discourse/controllers/preferences/email.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/email.js.es6
index c7ad39668d..413638e0db 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/email.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/email.js.es6
@@ -1,18 +1,20 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { empty, or } from "@ember/object/computed";
+import Controller from "@ember/controller";
import { propertyEqual } from "discourse/lib/computed";
-import InputValidation from "discourse/models/input-validation";
+import EmberObject from "@ember/object";
import { emailValid } from "discourse/lib/utilities";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Controller.extend({
+export default Controller.extend({
taken: false,
saving: false,
error: false,
success: false,
newEmail: null,
- newEmailEmpty: Ember.computed.empty("newEmail"),
+ newEmailEmpty: empty("newEmail"),
- saveDisabled: Ember.computed.or(
+ saveDisabled: or(
"saving",
"newEmailEmpty",
"taken",
@@ -22,26 +24,26 @@ export default Ember.Controller.extend({
unchanged: propertyEqual("newEmailLower", "currentUser.email"),
- @computed("newEmail")
+ @discourseComputed("newEmail")
newEmailLower(newEmail) {
return newEmail.toLowerCase().trim();
},
- @computed("saving")
+ @discourseComputed("saving")
saveButtonText(saving) {
if (saving) return I18n.t("saving");
return I18n.t("user.change");
},
- @computed("newEmail")
+ @discourseComputed("newEmail")
invalidEmail(newEmail) {
return !emailValid(newEmail);
},
- @computed("invalidEmail")
+ @discourseComputed("invalidEmail")
emailValidation(invalidEmail) {
if (invalidEmail) {
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason: I18n.t("user.email.invalid")
});
diff --git a/app/assets/javascripts/discourse/controllers/preferences/emails.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/emails.js.es6
index 2db23b6f4d..2f7d0832c9 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/emails.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/emails.js.es6
@@ -1,5 +1,7 @@
+import { equal } from "@ember/object/computed";
+import Controller from "@ember/controller";
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
const EMAIL_LEVELS = {
@@ -8,12 +10,12 @@ const EMAIL_LEVELS = {
NEVER: 2
};
-export default Ember.Controller.extend(PreferencesTabController, {
- emailMessagesLevelAway: Ember.computed.equal(
+export default Controller.extend(PreferencesTabController, {
+ emailMessagesLevelAway: equal(
"model.user_option.email_messages_level",
EMAIL_LEVELS.ONLY_WHEN_AWAY
),
- emailLevelAway: Ember.computed.equal(
+ emailLevelAway: equal(
"model.user_option.email_level",
EMAIL_LEVELS.ONLY_WHEN_AWAY
),
@@ -58,7 +60,7 @@ export default Ember.Controller.extend(PreferencesTabController, {
];
},
- @computed()
+ @discourseComputed()
frequencyEstimate() {
var estimate = this.get("model.mailing_list_posts_per_day");
if (!estimate || estimate < 2) {
@@ -70,7 +72,7 @@ export default Ember.Controller.extend(PreferencesTabController, {
}
},
- @computed()
+ @discourseComputed()
mailingListModeOptions() {
return [
{ name: this.frequencyEstimate, value: 1 },
@@ -78,7 +80,7 @@ export default Ember.Controller.extend(PreferencesTabController, {
];
},
- @computed()
+ @discourseComputed()
emailFrequencyInstructions() {
if (this.siteSettings.email_time_window_mins) {
return I18n.t("user.email.frequency", {
diff --git a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6
index 072c42c3b6..1fbed8353b 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6
@@ -1,16 +1,20 @@
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
import { setDefaultHomepage } from "discourse/lib/utilities";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import {
listThemes,
previewTheme,
setLocalTheme
} from "discourse/lib/theme-selector";
import { popupAjaxError } from "discourse/lib/ajax-error";
-import { safariHacksDisabled, isiPad } from "discourse/lib/utilities";
+import {
+ safariHacksDisabled,
+ isiPad,
+ iOSWithVisualViewport
+} from "discourse/lib/utilities";
+import { computed } from "@ember/object";
const USER_HOMES = {
1: "latest",
@@ -23,8 +27,8 @@ const USER_HOMES = {
const TEXT_SIZES = ["smaller", "normal", "larger", "largest"];
const TITLE_COUNT_MODES = ["notifications", "contextual"];
-export default Ember.Controller.extend(PreferencesTabController, {
- @computed("makeThemeDefault")
+export default Controller.extend(PreferencesTabController, {
+ @discourseComputed("makeThemeDefault")
saveAttrNames(makeDefault) {
let attrs = [
"locale",
@@ -47,43 +51,56 @@ export default Ember.Controller.extend(PreferencesTabController, {
return attrs;
},
- preferencesController: Ember.inject.controller("preferences"),
+ preferencesController: inject("preferences"),
- @computed()
+ @discourseComputed()
isiPad() {
- return isiPad();
+ // TODO: remove this preference checkbox when iOS adoption > 90%
+ // (currently only applies to iOS 12 and below)
+ return isiPad() && !iOSWithVisualViewport();
},
- @computed()
+ @discourseComputed()
disableSafariHacks() {
return safariHacksDisabled();
},
- @computed()
+ @discourseComputed()
availableLocales() {
return JSON.parse(this.siteSettings.available_locales);
},
- @computed
+ @discourseComputed
textSizes() {
return TEXT_SIZES.map(value => {
return { name: I18n.t(`user.text_size.${value}`), value };
});
},
- @computed
+ homepageId: computed(
+ "model.user_option.homepage_id",
+ "userSelectableHome.[]",
+ function() {
+ return (
+ this.model.user_option.homepage_id ||
+ this.userSelectableHome.firstObject.value
+ );
+ }
+ ),
+
+ @discourseComputed
titleCountModes() {
return TITLE_COUNT_MODES.map(value => {
return { name: I18n.t(`user.title_count_mode.${value}`), value };
});
},
- @computed
+ @discourseComputed
userSelectableThemes() {
return listThemes(this.site);
},
- @computed("userSelectableThemes")
+ @discourseComputed("userSelectableThemes")
showThemeSelector(themes) {
return themes && themes.length > 1;
},
@@ -94,12 +111,12 @@ export default Ember.Controller.extend(PreferencesTabController, {
previewTheme([id]);
},
- @computed("model.user_option.theme_ids", "themeId")
+ @discourseComputed("model.user_option.theme_ids", "themeId")
showThemeSetDefault(userOptionThemes, selectedTheme) {
return !userOptionThemes || userOptionThemes[0] !== selectedTheme;
},
- @computed("model.user_option.text_size", "textSize")
+ @discourseComputed("model.user_option.text_size", "textSize")
showTextSetDefault(userOptionTextSize, selectedTextSize) {
return userOptionTextSize !== selectedTextSize;
},
@@ -111,7 +128,7 @@ export default Ember.Controller.extend(PreferencesTabController, {
setDefaultHomepage(userHome || siteHome);
},
- @computed()
+ @discourseComputed()
userSelectableHome() {
let homeValues = {};
Object.keys(USER_HOMES).forEach(newValue => {
@@ -190,6 +207,8 @@ export default Ember.Controller.extend(PreferencesTabController, {
// Force refresh when leaving this screen
Discourse.set("assetVersion", "forceRefresh");
+
+ this.set("textSize", newSize);
}
}
});
diff --git a/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6
index 77690846d5..8030885efb 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6
@@ -1,8 +1,9 @@
+import Controller from "@ember/controller";
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
import { NotificationLevels } from "discourse/lib/notification-levels";
import { popupAjaxError } from "discourse/lib/ajax-error";
-export default Ember.Controller.extend(PreferencesTabController, {
+export default Controller.extend(PreferencesTabController, {
init() {
this._super(...arguments);
diff --git a/app/assets/javascripts/discourse/controllers/preferences/profile.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/profile.js.es6
index 5d1dde4b76..5d2e65f045 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/profile.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/profile.js.es6
@@ -1,9 +1,14 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import { isEmpty } from "@ember/utils";
+import EmberObject from "@ember/object";
+import Controller from "@ember/controller";
+import discourseComputed from "discourse-common/utils/decorators";
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { cookAsync } from "discourse/lib/text";
+import { ajax } from "discourse/lib/ajax";
+import showModal from "discourse/lib/show-modal";
-export default Ember.Controller.extend(PreferencesTabController, {
+export default Controller.extend(PreferencesTabController, {
init() {
this._super(...arguments);
@@ -15,14 +20,15 @@ export default Ember.Controller.extend(PreferencesTabController, {
"user_fields",
"profile_background_upload_url",
"card_background_upload_url",
- "date_of_birth"
+ "date_of_birth",
+ "timezone"
];
},
- @computed("model.user_fields.@each.value")
+ @discourseComputed("model.user_fields.@each.value")
userFields() {
let siteUserFields = this.site.get("user_fields");
- if (!Ember.isEmpty(siteUserFields)) {
+ if (!isEmpty(siteUserFields)) {
const userFields = this.get("model.user_fields");
// Staff can edit fields that are not `editable`
@@ -33,17 +39,41 @@ export default Ember.Controller.extend(PreferencesTabController, {
const value = userFields
? userFields[field.get("id").toString()]
: null;
- return Ember.Object.create({ value, field });
+ return EmberObject.create({ value, field });
});
}
},
- @computed("model.can_change_bio")
+ @discourseComputed("model.can_change_bio")
canChangeBio(canChangeBio) {
return canChangeBio;
},
actions: {
+ showFeaturedTopicModal() {
+ showModal("feature-topic-on-profile", {
+ model: this.model,
+ title: "user.feature_topic_on_profile.title"
+ });
+ },
+
+ clearFeaturedTopicFromProfile() {
+ bootbox.confirm(
+ I18n.t("user.feature_topic_on_profile.clear.warning"),
+ result => {
+ if (result) {
+ ajax(`/u/${this.model.username}/clear-featured-topic`, {
+ type: "PUT"
+ })
+ .then(() => {
+ this.model.set("featured_topic", null);
+ })
+ .catch(popupAjaxError);
+ }
+ }
+ );
+ },
+
save() {
this.set("saved", false);
@@ -51,9 +81,9 @@ export default Ember.Controller.extend(PreferencesTabController, {
userFields = this.userFields;
// Update the user fields
- if (!Ember.isEmpty(userFields)) {
+ if (!isEmpty(userFields)) {
const modelFields = model.get("user_fields");
- if (!Ember.isEmpty(modelFields)) {
+ if (!isEmpty(modelFields)) {
userFields.forEach(function(uf) {
modelFields[uf.get("field.id").toString()] = uf.get("value");
});
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
deleted file mode 100644
index e5817ac9fa..0000000000
--- a/app/assets/javascripts/discourse/controllers/preferences/second-factor-backup.js.es6
+++ /dev/null
@@ -1,126 +0,0 @@
-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,
- errorMessage: null,
- successMessage: null,
- backupEnabled: Ember.computed.alias("model.second_factor_backup_enabled"),
- remainingCodes: Ember.computed.alias(
- "model.second_factor_remaining_backup_codes"
- ),
- backupCodes: null,
- secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
-
- @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")
- isDisabledGenerateBackupCodeBtn(isValid, backupEnabled, loading) {
- return !isValid || loading;
- },
-
- @computed("isValidSecondFactorToken", "backupEnabled", "loading")
- isDisabledDisableBackupCodeBtn(isValid, backupEnabled, loading) {
- return !isValid || !backupEnabled || loading;
- },
-
- @computed("backupEnabled")
- generateBackupCodeBtnLabel(backupEnabled) {
- return backupEnabled
- ? "user.second_factor_backup.regenerate"
- : "user.second_factor_backup.enable";
- },
-
- actions: {
- copyBackupCode(successful) {
- if (successful) {
- this.set(
- "successMessage",
- I18n.t("user.second_factor_backup.copied_to_clipboard")
- );
- } else {
- this.set(
- "errorMessage",
- I18n.t("user.second_factor_backup.copy_to_clipboard_error")
- );
- }
-
- this._hideCopyMessage();
- },
-
- disableSecondFactorBackup() {
- this.set("backupCodes", []);
-
- if (!this.secondFactorToken) return;
-
- this.set("loading", true);
-
- this.model
- .toggleSecondFactor(
- this.secondFactorToken,
- this.secondFactorMethod,
- SECOND_FACTOR_METHODS.BACKUP_CODE,
- false
- )
- .then(response => {
- if (response.error) {
- this.set("errorMessage", response.error);
- return;
- }
-
- this.set("errorMessage", null);
-
- const usernameLower = this.model.username.toLowerCase();
- DiscourseURL.redirectTo(userPath(`${usernameLower}/preferences`));
- })
- .catch(popupAjaxError)
- .finally(() => this.set("loading", false));
- },
-
- generateSecondFactorCodes() {
- if (!this.secondFactorToken) return;
- this.set("loading", true);
- this.model
- .generateSecondFactorCodes(
- this.secondFactorToken,
- this.secondFactorMethod
- )
- .then(response => {
- if (response.error) {
- this.set("errorMessage", response.error);
- return;
- }
-
- this.setProperties({
- errorMessage: null,
- backupCodes: response.backup_codes,
- backupEnabled: true,
- remainingCodes: response.backup_codes.length
- });
- })
- .catch(popupAjaxError)
- .finally(() => {
- this.setProperties({
- loading: false,
- secondFactorToken: null
- });
- });
- }
- },
-
- _hideCopyMessage() {
- Ember.run.later(
- () => this.setProperties({ successMessage: null, errorMessage: null }),
- 2000
- );
- }
-});
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 82be216d0d..67c7b58c83 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6
@@ -1,98 +1,99 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
-import { default as DiscourseURL, userPath } from "discourse/lib/url";
+import { alias, and } from "@ember/object/computed";
+import Controller from "@ember/controller";
+import discourseComputed from "discourse-common/utils/decorators";
+import CanCheckEmails from "discourse/mixins/can-check-emails";
+import 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";
+import showModal from "discourse/lib/show-modal";
-export default Ember.Controller.extend({
+export default Controller.extend(CanCheckEmails, {
loading: false,
+ dirty: false,
resetPasswordLoading: false,
resetPasswordProgress: "",
password: null,
- secondFactorImage: null,
- secondFactorKey: null,
- showSecondFactorKey: false,
errorMessage: null,
newUsername: null,
- backupEnabled: Ember.computed.alias("model.second_factor_backup_enabled"),
+ backupEnabled: alias("model.second_factor_backup_enabled"),
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
+ totps: null,
- loaded: Ember.computed.and("secondFactorImage", "secondFactorKey"),
+ loaded: and("secondFactorImage", "secondFactorKey"),
- @computed("loading")
- submitButtonText(loading) {
- return loading ? "loading" : "continue";
+ init() {
+ this._super(...arguments);
+ this.set("totps", []);
},
- @computed("loading")
- enableButtonText(loading) {
- return loading ? "loading" : "enable";
- },
-
- @computed("loading")
- disableButtonText(loading) {
- return loading ? "loading" : "disable";
- },
-
- @computed
+ @discourseComputed
displayOAuthWarning() {
return findAll().length > 0;
},
- @computed("currentUser")
+ @discourseComputed("currentUser")
showEnforcedNotice(user) {
- return user && user.get("enforcedSecondFactor");
+ return user && user.enforcedSecondFactor;
},
- toggleSecondFactor(enable) {
- if (!this.secondFactorToken) return;
+ handleError(error) {
+ if (error.jqXHR) {
+ error = error.jqXHR;
+ }
+ let parsedJSON = error.responseJSON;
+ if (parsedJSON.error_type === "invalid_access") {
+ const usernameLower = this.model.username.toLowerCase();
+ DiscourseURL.redirectTo(
+ userPath(`${usernameLower}/preferences/second-factor`)
+ );
+ } else {
+ popupAjaxError(error);
+ }
+ },
+
+ loadSecondFactors() {
+ if (this.dirty === false) {
+ return;
+ }
this.set("loading", true);
this.model
- .toggleSecondFactor(
- this.secondFactorToken,
- this.secondFactorMethod,
- SECOND_FACTOR_METHODS.TOTP,
- enable
- )
+ .loadSecondFactorCodes(this.password)
.then(response => {
if (response.error) {
this.set("errorMessage", response.error);
return;
}
- this.set("errorMessage", null);
- DiscourseURL.redirectTo(
- userPath(`${this.model.username.toLowerCase()}/preferences`)
+ this.setProperties({
+ errorMessage: null,
+ loaded: true,
+ totps: response.totps,
+ security_keys: response.security_keys,
+ password: null,
+ dirty: false
+ });
+ this.set(
+ "model.second_factor_enabled",
+ (response.totps && response.totps.length > 0) ||
+ (response.security_keys && response.security_keys.length > 0)
);
})
- .catch(error => {
- popupAjaxError(error);
- })
+ .catch(e => this.handleError(e))
.finally(() => this.set("loading", false));
},
+ markDirty() {
+ this.set("dirty", true);
+ },
+
actions: {
confirmPassword() {
if (!this.password) return;
- this.set("loading", true);
-
- this.model
- .loadSecondFactorCodes(this.password)
- .then(response => {
- if (response.error) {
- this.set("errorMessage", response.error);
- return;
- }
-
- this.setProperties({
- errorMessage: null,
- secondFactorKey: response.key,
- secondFactorImage: response.qr
- });
- })
- .catch(popupAjaxError)
- .finally(() => this.set("loading", false));
+ this.markDirty();
+ this.loadSecondFactors();
+ this.set("password", null);
},
resetPassword() {
@@ -113,16 +114,91 @@ export default Ember.Controller.extend({
.finally(() => this.set("resetPasswordLoading", false));
},
- showSecondFactorKey() {
- this.set("showSecondFactorKey", true);
+ disableAllSecondFactors() {
+ if (this.loading) {
+ return;
+ }
+ bootbox.confirm(
+ I18n.t("user.second_factor.disable_confirm"),
+ I18n.t("cancel"),
+ I18n.t("user.second_factor.disable"),
+ result => {
+ if (result) {
+ this.model
+ .disableAllSecondFactors()
+ .then(() => {
+ const usernameLower = this.model.username.toLowerCase();
+ DiscourseURL.redirectTo(
+ userPath(`${usernameLower}/preferences`)
+ );
+ })
+ .catch(e => this.handleError(e))
+ .finally(() => this.set("loading", false));
+ }
+ }
+ );
},
- enableSecondFactor() {
- this.toggleSecondFactor(true);
+ createTotp() {
+ const controller = showModal("second-factor-add-totp", {
+ model: this.model,
+ title: "user.second_factor.totp.add"
+ });
+ controller.setProperties({
+ onClose: () => this.loadSecondFactors(),
+ markDirty: () => this.markDirty(),
+ onError: e => this.handleError(e)
+ });
},
- disableSecondFactor() {
- this.toggleSecondFactor(false);
+ createSecurityKey() {
+ const controller = showModal("second-factor-add-security-key", {
+ model: this.model,
+ title: "user.second_factor.security_key.add"
+ });
+ controller.setProperties({
+ onClose: () => this.loadSecondFactors(),
+ markDirty: () => this.markDirty(),
+ onError: e => this.handleError(e)
+ });
+ },
+
+ editSecurityKey(security_key) {
+ const controller = showModal("second-factor-edit-security-key", {
+ model: security_key,
+ title: "user.second_factor.security_key.edit"
+ });
+ controller.setProperties({
+ user: this.model,
+ onClose: () => this.loadSecondFactors(),
+ markDirty: () => this.markDirty(),
+ onError: e => this.handleError(e)
+ });
+ },
+
+ editSecondFactor(second_factor) {
+ const controller = showModal("second-factor-edit", {
+ model: second_factor,
+ title: "user.second_factor.edit_title"
+ });
+ controller.setProperties({
+ user: this.model,
+ onClose: () => this.loadSecondFactors(),
+ markDirty: () => this.markDirty(),
+ onError: e => this.handleError(e)
+ });
+ },
+
+ editSecondFactorBackup() {
+ const controller = showModal("second-factor-backup-edit", {
+ model: this.model,
+ title: "user.second_factor_backup.title"
+ });
+ controller.setProperties({
+ onClose: () => this.loadSecondFactors(),
+ markDirty: () => this.markDirty(),
+ onError: e => this.handleError(e)
+ });
}
}
});
diff --git a/app/assets/javascripts/discourse/controllers/preferences/tags.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/tags.js.es6
index e149711bd3..1aede6792d 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/tags.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/tags.js.es6
@@ -1,8 +1,9 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Controller from "@ember/controller";
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
-import computed from "ember-addons/ember-computed-decorators";
-export default Ember.Controller.extend(PreferencesTabController, {
+export default Controller.extend(PreferencesTabController, {
init() {
this._super(...arguments);
@@ -14,7 +15,7 @@ export default Ember.Controller.extend(PreferencesTabController, {
];
},
- @computed(
+ @discourseComputed(
"model.watched_tags.[]",
"model.watching_first_post_tags.[]",
"model.tracked_tags.[]",
diff --git a/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/username.js.es6
index 367f83903c..3b5cbfc477 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/username.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/username.js.es6
@@ -1,13 +1,14 @@
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import { isEmpty } from "@ember/utils";
+import { empty, or } from "@ember/object/computed";
+import Controller from "@ember/controller";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { setting, propertyEqual } from "discourse/lib/computed";
import DiscourseURL from "discourse/lib/url";
import { userPath } from "discourse/lib/url";
import { popupAjaxError } from "discourse/lib/ajax-error";
+import User from "discourse/models/user";
-export default Ember.Controller.extend({
+export default Controller.extend({
taken: false,
saving: false,
errorMessage: null,
@@ -15,8 +16,8 @@ export default Ember.Controller.extend({
maxLength: setting("max_username_length"),
minLength: setting("min_username_length"),
- newUsernameEmpty: Ember.computed.empty("newUsername"),
- saveDisabled: Ember.computed.or(
+ newUsernameEmpty: empty("newUsername"),
+ saveDisabled: or(
"saving",
"newUsernameEmpty",
"taken",
@@ -35,24 +36,22 @@ export default Ember.Controller.extend({
this.set("taken", false);
this.set("errorMessage", null);
- if (Ember.isEmpty(this.newUsername)) return;
+ if (isEmpty(this.newUsername)) return;
if (this.unchanged) return;
- Discourse.User.checkUsername(
- newUsername,
- undefined,
- this.get("model.id")
- ).then(result => {
- if (result.errors) {
- this.set("errorMessage", result.errors.join(" "));
- } else if (result.available === false) {
- this.set("taken", true);
+ User.checkUsername(newUsername, undefined, this.get("model.id")).then(
+ result => {
+ if (result.errors) {
+ this.set("errorMessage", result.errors.join(" "));
+ } else if (result.available === false) {
+ this.set("taken", true);
+ }
}
- });
+ );
}
},
- @computed("saving")
+ @discourseComputed("saving")
saveButtonText(saving) {
if (saving) return I18n.t("saving");
return I18n.t("user.change");
diff --git a/app/assets/javascripts/discourse/controllers/preferences/users.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/users.js.es6
index 16c7630429..ba3e866901 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/users.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/users.js.es6
@@ -1,12 +1,26 @@
+import { makeArray } from "discourse-common/lib/helpers";
+import { alias, gte, or } from "@ember/object/computed";
+import { action, computed } from "@ember/object";
+import Controller from "@ember/controller";
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
-import showModal from "discourse/lib/show-modal";
-import User from "discourse/models/user";
-export default Ember.Controller.extend(PreferencesTabController, {
- ignoredUsernames: Ember.computed.alias("model.ignored_usernames"),
- userIsMemberOrAbove: Ember.computed.gte("model.trust_level", 2),
- ignoredEnabled: Ember.computed.or("userIsMemberOrAbove", "model.staff"),
+export default Controller.extend(PreferencesTabController, {
+ ignoredUsernames: alias("model.ignored_usernames"),
+ userIsMemberOrAbove: gte("model.trust_level", 2),
+ ignoredEnabled: or("userIsMemberOrAbove", "model.staff"),
+
+ mutedUsernames: computed("model.muted_usernames", {
+ get() {
+ let usernames = this.model.muted_usernames;
+
+ if (typeof usernames === "string") {
+ usernames = usernames.split(",").filter(Boolean);
+ }
+
+ return makeArray(usernames).uniq();
+ }
+ }),
init() {
this._super(...arguments);
@@ -14,41 +28,18 @@ export default Ember.Controller.extend(PreferencesTabController, {
this.saveAttrNames = ["muted_usernames", "ignored_usernames"];
},
- actions: {
- ignoredUsernamesChanged(previous, current) {
- if (current.length > previous.length) {
- const username = current.pop();
- if (username) {
- User.findByUsername(username).then(user => {
- if (user.get("ignored")) {
- return;
- }
- const controller = showModal("ignore-duration", {
- model: user
- });
- controller.setProperties({
- onClose: () => {
- if (!user.get("ignored")) {
- const usernames = this.ignoredUsernames
- .split(",")
- .removeAt(this.ignoredUsernames.split(",").length - 1)
- .join(",");
- this.set("ignoredUsernames", usernames);
- }
- }
- });
- });
- }
- } else {
- return this.model.save(["ignored_usernames"]).catch(popupAjaxError);
- }
- },
- save() {
- this.set("saved", false);
- return this.model
- .save(this.saveAttrNames)
- .then(() => this.set("saved", true))
- .catch(popupAjaxError);
- }
+ @action
+ onChangeMutedUsernames(usernames) {
+ this.model.set("muted_usernames", usernames.uniq().join(","));
+ },
+
+ @action
+ save() {
+ this.set("saved", false);
+
+ return this.model
+ .save(this.saveAttrNames)
+ .then(() => this.set("saved", true))
+ .catch(popupAjaxError);
}
});
diff --git a/app/assets/javascripts/discourse/controllers/raw-email.js.es6 b/app/assets/javascripts/discourse/controllers/raw-email.js.es6
index e671f3eef3..fbdeb103a5 100644
--- a/app/assets/javascripts/discourse/controllers/raw-email.js.es6
+++ b/app/assets/javascripts/discourse/controllers/raw-email.js.es6
@@ -1,18 +1,20 @@
+import { equal } from "@ember/object/computed";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import Post from "discourse/models/post";
import IncomingEmail from "admin/models/incoming-email";
// This controller handles displaying of raw email
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
rawEmail: "",
textPart: "",
htmlPart: "",
tab: "raw",
- showRawEmail: Ember.computed.equal("tab", "raw"),
- showTextPart: Ember.computed.equal("tab", "text_part"),
- showHtmlPart: Ember.computed.equal("tab", "html_part"),
+ showRawEmail: equal("tab", "raw"),
+ showTextPart: equal("tab", "text_part"),
+ showHtmlPart: equal("tab", "html_part"),
onShow() {
this.send("displayRaw");
diff --git a/app/assets/javascripts/discourse/controllers/rename-tag.js.es6 b/app/assets/javascripts/discourse/controllers/rename-tag.js.es6
index 9a7a01d1ef..4bb35a34ca 100644
--- a/app/assets/javascripts/discourse/controllers/rename-tag.js.es6
+++ b/app/assets/javascripts/discourse/controllers/rename-tag.js.es6
@@ -1,10 +1,11 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-import computed from "ember-addons/ember-computed-decorators";
import BufferedContent from "discourse/mixins/buffered-content";
import { extractError } from "discourse/lib/ajax-error";
-export default Ember.Controller.extend(ModalFunctionality, BufferedContent, {
- @computed("buffered.id", "id")
+export default Controller.extend(ModalFunctionality, BufferedContent, {
+ @discourseComputed("buffered.id", "id")
renameDisabled(inputTagName, currentTagName) {
const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
const newTagName = inputTagName
diff --git a/app/assets/javascripts/discourse/controllers/reorder-categories.js.es6 b/app/assets/javascripts/discourse/controllers/reorder-categories.js.es6
index d893fa5b0d..41df556554 100644
--- a/app/assets/javascripts/discourse/controllers/reorder-categories.js.es6
+++ b/app/assets/javascripts/discourse/controllers/reorder-categories.js.es6
@@ -1,153 +1,136 @@
+import { sort } from "@ember/object/computed";
+import Evented from "@ember/object/evented";
+import EmberObjectProxy from "@ember/object/proxy";
+import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import ModalFunctionality from "discourse/mixins/modal-functionality";
const BufferedProxy = window.BufferedProxy; // import BufferedProxy from 'ember-buffered-proxy/proxy';
import { popupAjaxError } from "discourse/lib/ajax-error";
-import {
- on,
- default as computed
-} from "ember-addons/ember-computed-decorators";
-import Ember from "ember";
+import discourseComputed, { on } from "discourse-common/utils/decorators";
-export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
+export default Controller.extend(ModalFunctionality, Evented, {
init() {
this._super(...arguments);
this.categoriesSorting = ["position"];
},
- @on("init")
- _fixOrder() {
- this.fixIndices();
- },
-
- @computed("site.categories")
+ @discourseComputed("site.categories")
categoriesBuffered(categories) {
- const bufProxy = Ember.ObjectProxy.extend(BufferedProxy);
+ const bufProxy = EmberObjectProxy.extend(BufferedProxy);
return categories.map(c => bufProxy.create({ content: c }));
},
- categoriesOrdered: Ember.computed.sort(
- "categoriesBuffered",
- "categoriesSorting"
- ),
-
- @computed("categoriesBuffered.@each.hasBufferedChanges")
- showApplyAll() {
- let anyChanged = false;
- this.categoriesBuffered.forEach(bc => {
- anyChanged = anyChanged || bc.get("hasBufferedChanges");
- });
- return anyChanged;
- },
-
- moveDir(cat, dir) {
- const cats = this.categoriesOrdered;
- const curIdx = cat.get("position");
- let desiredIdx = curIdx + dir;
- if (desiredIdx >= 0 && desiredIdx < cats.get("length")) {
- let otherCat = cats.objectAt(desiredIdx);
-
- // Respect children
- const parentIdx = otherCat.get("parent_category_id");
- if (parentIdx && parentIdx !== cat.get("parent_category_id")) {
- if (parentIdx === cat.get("id")) {
- // We want to move down
- for (let i = curIdx + 1; i < cats.get("length"); i++) {
- let tmpCat = cats.objectAt(i);
- if (!tmpCat.get("parent_category_id")) {
- desiredIdx = cats.indexOf(tmpCat);
- otherCat = tmpCat;
- break;
- }
- }
- } else {
- // We want to move up
- cats.forEach(function(tmpCat) {
- if (tmpCat.get("id") === parentIdx) {
- desiredIdx = cats.indexOf(tmpCat);
- otherCat = tmpCat;
- }
- });
- }
- }
-
- otherCat.set("position", curIdx);
- cat.set("position", desiredIdx);
- this.send("commit");
- }
- },
+ categoriesOrdered: sort("categoriesBuffered", "categoriesSorting"),
/**
- 1. Make sure all categories have unique position numbers.
- 2. Place sub-categories after their parent categories while maintaining the
- same relative order.
+ * 1. Make sure all categories have unique position numbers.
+ * 2. Place sub-categories after their parent categories while maintaining
+ * the same relative order.
+ *
+ * e.g.
+ * parent/c2/c1 parent
+ * parent/c1 parent/c1
+ * parent => parent/c2
+ * other parent/c2/c1
+ * parent/c2 other
+ *
+ **/
+ @on("init")
+ reorder() {
+ const reorderChildren = (categoryId, depth, index) => {
+ this.categoriesOrdered.forEach(category => {
+ if (
+ (categoryId === null && !category.get("parent_category_id")) ||
+ category.get("parent_category_id") === categoryId
+ ) {
+ category.setProperties({ depth, position: index++ });
+ index = reorderChildren(category.get("id"), depth + 1, index);
+ }
+ });
- e.g.
- parent/c1 parent
- parent => parent/c1
- other parent/c2
- parent/c2 other
- **/
- fixIndices() {
- const categories = this.categoriesOrdered;
- const subcategories = {};
+ return index;
+ };
- categories.forEach(category => {
- const parentCategoryId = category.get("parent_category_id");
+ reorderChildren(null, 0, 0);
- if (parentCategoryId) {
- subcategories[parentCategoryId] = subcategories[parentCategoryId] || [];
- subcategories[parentCategoryId].push(category);
+ this.categoriesBuffered.forEach(bc => {
+ if (bc.get("hasBufferedChanges")) {
+ bc.applyBufferedChanges();
}
});
- for (let i = 0, position = 0; i < categories.get("length"); ++i) {
- const category = categories.objectAt(i);
+ this.notifyPropertyChange("categoriesBuffered");
+ },
- if (!category.get("parent_category_id")) {
- category.set("position", position++);
- (subcategories[category.get("id")] || []).forEach(subcategory =>
- subcategory.set("position", position++)
- );
- }
+ move(category, direction) {
+ let otherCategory;
+
+ if (direction === -1) {
+ // First category above current one
+ const categoriesOrderedDesc = this.categoriesOrdered.reverse();
+ otherCategory = categoriesOrderedDesc.find(
+ c =>
+ category.get("parent_category_id") === c.get("parent_category_id") &&
+ c.get("position") < category.get("position")
+ );
+ } else if (direction === 1) {
+ // First category under current one
+ otherCategory = this.categoriesOrdered.find(
+ c =>
+ category.get("parent_category_id") === c.get("parent_category_id") &&
+ c.get("position") > category.get("position")
+ );
+ } else {
+ // Find category occupying target position
+ otherCategory = this.categoriesOrdered.find(
+ c => c.get("position") === category.get("position") + direction
+ );
}
+
+ if (otherCategory) {
+ // Try to swap positions of the two categories
+ if (category.get("id") !== otherCategory.get("id")) {
+ const currentPosition = category.get("position");
+ category.set("position", otherCategory.get("position"));
+ otherCategory.set("position", currentPosition);
+ }
+ } else if (direction < 0) {
+ category.set("position", -1);
+ } else if (direction > 0) {
+ category.set("position", this.categoriesOrdered.length);
+ }
+
+ this.reorder();
},
actions: {
- change(cat, e) {
- let position = parseInt($(e.target).val());
- let amount = Math.min(
- Math.max(position, 0),
+ change(category, event) {
+ let newPosition = parseInt(event.target.value, 10);
+ newPosition = Math.min(
+ Math.max(newPosition, 0),
this.categoriesOrdered.length - 1
);
- this.moveDir(cat, amount - cat.get("position"));
+
+ this.move(category, newPosition - category.get("position"));
},
- moveUp(cat) {
- this.moveDir(cat, -1);
- },
- moveDown(cat) {
- this.moveDir(cat, 1);
+ moveUp(category) {
+ this.move(category, -1);
},
- commit() {
- this.fixIndices();
-
- this.categoriesBuffered.forEach(bc => {
- if (bc.get("hasBufferedChanges")) {
- bc.applyBufferedChanges();
- }
- });
- this.notifyPropertyChange("categoriesBuffered");
+ moveDown(category) {
+ this.move(category, 1);
},
- saveOrder() {
- this.send("commit");
+ save() {
+ this.reorder();
const data = {};
this.categoriesBuffered.forEach(cat => {
data[cat.get("id")] = cat.get("position");
});
+
ajax("/categories/reorder", {
type: "POST",
data: { mapping: JSON.stringify(data) }
diff --git a/app/assets/javascripts/discourse/controllers/request-group-membership-form.js.es6 b/app/assets/javascripts/discourse/controllers/request-group-membership-form.js.es6
index dd3a995160..bbb54d03dd 100644
--- a/app/assets/javascripts/discourse/controllers/request-group-membership-form.js.es6
+++ b/app/assets/javascripts/discourse/controllers/request-group-membership-form.js.es6
@@ -1,20 +1,23 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import { alias } from "@ember/object/computed";
+import Controller from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
import DiscourseURL from "discourse/lib/url";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
loading: false,
- reason: Ember.computed.alias("model.membership_request_template"),
+ reason: alias("model.membership_request_template"),
- @computed("model.name")
+ @discourseComputed("model.name")
title(groupName) {
return I18n.t("groups.membership_request.title", { group_name: groupName });
},
- @computed("loading", "reason")
+ @discourseComputed("loading", "reason")
disableSubmit(loading, reason) {
- return loading || Ember.isEmpty(reason);
+ return loading || isEmpty(reason);
},
actions: {
diff --git a/app/assets/javascripts/discourse/controllers/review-index.js.es6 b/app/assets/javascripts/discourse/controllers/review-index.js.es6
index f65fd2129b..aeb61d7d61 100644
--- a/app/assets/javascripts/discourse/controllers/review-index.js.es6
+++ b/app/assets/javascripts/discourse/controllers/review-index.js.es6
@@ -1,6 +1,7 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Controller from "@ember/controller";
-export default Ember.Controller.extend({
+export default Controller.extend({
queryParams: [
"priority",
"type",
@@ -8,7 +9,10 @@ export default Ember.Controller.extend({
"category_id",
"topic_id",
"username",
- "sort_order"
+ "from_date",
+ "to_date",
+ "sort_order",
+ "additional_filters"
],
type: null,
status: "pending",
@@ -18,7 +22,10 @@ export default Ember.Controller.extend({
topic_id: null,
filtersExpanded: false,
username: "",
+ from_date: null,
+ to_date: null,
sort_order: "priority",
+ additional_filters: null,
init(...args) {
this._super(...args);
@@ -26,7 +33,7 @@ export default Ember.Controller.extend({
this.set("filtersExpanded", !this.site.mobileView);
},
- @computed("reviewableTypes")
+ @discourseComputed("reviewableTypes")
allTypes() {
return (this.reviewableTypes || []).map(type => {
return {
@@ -36,7 +43,7 @@ export default Ember.Controller.extend({
});
},
- @computed
+ @discourseComputed
priorities() {
return ["low", "medium", "high"].map(priority => {
return {
@@ -46,7 +53,7 @@ export default Ember.Controller.extend({
});
},
- @computed
+ @discourseComputed
sortOrders() {
return ["priority", "priority_asc", "created_at", "created_at_asc"].map(
order => {
@@ -58,7 +65,7 @@ export default Ember.Controller.extend({
);
},
- @computed
+ @discourseComputed
statuses() {
return [
"pending",
@@ -73,11 +80,20 @@ export default Ember.Controller.extend({
});
},
- @computed("filtersExpanded")
+ @discourseComputed("filtersExpanded")
toggleFiltersIcon(filtersExpanded) {
return filtersExpanded ? "chevron-up" : "chevron-down";
},
+ setRange(range) {
+ if (range.from) {
+ this.set("from", new Date(range.from).toISOString().split("T")[0]);
+ }
+ if (range.to) {
+ this.set("to", new Date(range.to).toISOString().split("T")[0]);
+ }
+ },
+
actions: {
remove(ids) {
if (!ids) {
@@ -102,8 +118,12 @@ export default Ember.Controller.extend({
status: this.filterStatus,
category_id: this.filterCategoryId,
username: this.filterUsername,
- sort_order: this.filterSortOrder
+ from_date: this.filterFromDate,
+ to_date: this.filterToDate,
+ sort_order: this.filterSortOrder,
+ additional_filters: JSON.stringify(this.additionalFilters)
});
+
this.send("refreshRoute");
},
diff --git a/app/assets/javascripts/discourse/controllers/review-settings.js.es6 b/app/assets/javascripts/discourse/controllers/review-settings.js.es6
index d3ee32ccf7..8e670adb2a 100644
--- a/app/assets/javascripts/discourse/controllers/review-settings.js.es6
+++ b/app/assets/javascripts/discourse/controllers/review-settings.js.es6
@@ -1,7 +1,8 @@
+import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
-export default Ember.Controller.extend({
+export default Controller.extend({
saving: false,
saved: false,
diff --git a/app/assets/javascripts/discourse/controllers/search-help.js.es6 b/app/assets/javascripts/discourse/controllers/search-help.js.es6
deleted file mode 100644
index be04358f4b..0000000000
--- a/app/assets/javascripts/discourse/controllers/search-help.js.es6
+++ /dev/null
@@ -1,9 +0,0 @@
-import ModalFunctionality from "discourse/mixins/modal-functionality";
-import computed from "ember-addons/ember-computed-decorators";
-
-export default Ember.Controller.extend(ModalFunctionality, {
- @computed
- showGoogleSearch() {
- return !Discourse.SiteSettings.login_required;
- }
-});
diff --git a/app/assets/javascripts/discourse/controllers/second-factor-add-security-key.js.es6 b/app/assets/javascripts/discourse/controllers/second-factor-add-security-key.js.es6
new file mode 100644
index 0000000000..54f41fc405
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/second-factor-add-security-key.js.es6
@@ -0,0 +1,137 @@
+import Controller from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+import {
+ bufferToBase64,
+ stringToBuffer,
+ isWebauthnSupported
+} from "discourse/lib/webauthn";
+
+// model for this controller is user.js.es6
+export default Controller.extend(ModalFunctionality, {
+ loading: false,
+ errorMessage: null,
+
+ onShow() {
+ // clear properties every time because the controller is a singleton
+ this.setProperties({
+ errorMessage: null,
+ loading: true,
+ securityKeyName: I18n.t("user.second_factor.security_key.default_name"),
+ webauthnUnsupported: !isWebauthnSupported()
+ });
+
+ this.model
+ .requestSecurityKeyChallenge()
+ .then(response => {
+ if (response.error) {
+ this.set("errorMessage", response.error);
+ return;
+ }
+
+ this.setProperties({
+ errorMessage: isWebauthnSupported()
+ ? null
+ : I18n.t("login.security_key_support_missing_error"),
+ loading: false,
+ challenge: response.challenge,
+ relayingParty: {
+ id: response.rp_id,
+ name: response.rp_name
+ },
+ supported_algoriths: response.supported_algoriths,
+ user_secure_id: response.user_secure_id,
+ existing_active_credential_ids:
+ response.existing_active_credential_ids
+ });
+ })
+ .catch(error => {
+ this.send("closeModal");
+ this.onError(error);
+ })
+ .finally(() => this.set("loading", false));
+ },
+
+ actions: {
+ registerSecurityKey() {
+ const publicKeyCredentialCreationOptions = {
+ challenge: Uint8Array.from(this.challenge, c => c.charCodeAt(0)),
+ rp: {
+ name: this.relayingParty.name,
+ id: this.relayingParty.id
+ },
+ user: {
+ id: Uint8Array.from(this.user_secure_id, c => c.charCodeAt(0)),
+ displayName: this.model.username_lower,
+ name: this.model.username_lower
+ },
+ pubKeyCredParams: this.supported_algoriths.map(alg => {
+ return { type: "public-key", alg: alg };
+ }),
+ excludeCredentials: this.existing_active_credential_ids.map(
+ credentialId => {
+ return {
+ type: "public-key",
+ id: stringToBuffer(atob(credentialId))
+ };
+ }
+ ),
+ timeout: 20000,
+ attestation: "none",
+ authenticatorSelection: {
+ // see https://chromium.googlesource.com/chromium/src/+/master/content/browser/webauth/uv_preferred.md for why
+ // default value of preferred is not necesarrily what we want, it limits webauthn to only devices that support
+ // user verification, which usually requires entering a PIN
+ userVerification: "discouraged"
+ }
+ };
+
+ navigator.credentials
+ .create({
+ publicKey: publicKeyCredentialCreationOptions
+ })
+ .then(
+ credential => {
+ let serverData = {
+ id: credential.id,
+ rawId: bufferToBase64(credential.rawId),
+ type: credential.type,
+ attestation: bufferToBase64(
+ credential.response.attestationObject
+ ),
+ clientData: bufferToBase64(credential.response.clientDataJSON),
+ name: this.securityKeyName
+ };
+
+ this.model
+ .registerSecurityKey(serverData)
+ .then(response => {
+ if (response.error) {
+ this.set("errorMessage", response.error);
+ return;
+ }
+ this.markDirty();
+ this.set("errorMessage", null);
+ this.send("closeModal");
+ })
+ .catch(error => this.onError(error))
+ .finally(() => this.set("loading", false));
+ },
+ err => {
+ if (err.name === "InvalidStateError") {
+ return this.set(
+ "errorMessage",
+ I18n.t("user.second_factor.security_key.already_added_error")
+ );
+ }
+ if (err.name === "NotAllowedError") {
+ return this.set(
+ "errorMessage",
+ I18n.t("user.second_factor.security_key.not_allowed_error")
+ );
+ }
+ this.set("errorMessage", err.message);
+ }
+ );
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/second-factor-add-totp.js.es6 b/app/assets/javascripts/discourse/controllers/second-factor-add-totp.js.es6
new file mode 100644
index 0000000000..eab4f2943f
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/second-factor-add-totp.js.es6
@@ -0,0 +1,66 @@
+import Controller from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+
+export default Controller.extend(ModalFunctionality, {
+ loading: false,
+ secondFactorImage: null,
+ secondFactorKey: null,
+ showSecondFactorKey: false,
+ errorMessage: null,
+
+ onShow() {
+ this.setProperties({
+ errorMessage: null,
+ secondFactorKey: null,
+ secondFactorName: null,
+ secondFactorToken: null,
+ showSecondFactorKey: false,
+ secondFactorImage: null,
+ loading: true
+ });
+ this.model
+ .createSecondFactorTotp()
+ .then(response => {
+ if (response.error) {
+ this.set("errorMessage", response.error);
+ return;
+ }
+
+ this.setProperties({
+ errorMessage: null,
+ secondFactorKey: response.key,
+ secondFactorImage: response.qr
+ });
+ })
+ .catch(error => {
+ this.send("closeModal");
+ this.onError(error);
+ })
+ .finally(() => this.set("loading", false));
+ },
+
+ actions: {
+ showSecondFactorKey() {
+ this.set("showSecondFactorKey", true);
+ },
+
+ enableSecondFactor() {
+ if (!this.secondFactorToken) return;
+ this.set("loading", true);
+
+ this.model
+ .enableSecondFactorTotp(this.secondFactorToken, this.secondFactorName)
+ .then(response => {
+ if (response.error) {
+ this.set("errorMessage", response.error);
+ return;
+ }
+ this.markDirty();
+ this.set("errorMessage", null);
+ this.send("closeModal");
+ })
+ .catch(error => this.onError(error))
+ .finally(() => this.set("loading", false));
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/second-factor-backup-edit.js.es6 b/app/assets/javascripts/discourse/controllers/second-factor-backup-edit.js.es6
new file mode 100644
index 0000000000..aa3e0c1143
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/second-factor-backup-edit.js.es6
@@ -0,0 +1,110 @@
+import { alias } from "@ember/object/computed";
+import { later } from "@ember/runloop";
+import Controller from "@ember/controller";
+import discourseComputed from "discourse-common/utils/decorators";
+import { SECOND_FACTOR_METHODS } from "discourse/models/user";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+
+export default Controller.extend(ModalFunctionality, {
+ loading: false,
+ errorMessage: null,
+ successMessage: null,
+ backupEnabled: alias("model.second_factor_backup_enabled"),
+ remainingCodes: alias("model.second_factor_remaining_backup_codes"),
+ backupCodes: null,
+ secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
+
+ @discourseComputed("backupEnabled")
+ generateBackupCodeBtnLabel(backupEnabled) {
+ return backupEnabled
+ ? "user.second_factor_backup.regenerate"
+ : "user.second_factor_backup.enable";
+ },
+
+ onShow() {
+ this.setProperties({
+ loading: false,
+ errorMessage: null,
+ successMessage: null,
+ backupCodes: null
+ });
+ },
+
+ actions: {
+ copyBackupCode(successful) {
+ if (successful) {
+ this.set(
+ "successMessage",
+ I18n.t("user.second_factor_backup.copied_to_clipboard")
+ );
+ } else {
+ this.set(
+ "errorMessage",
+ I18n.t("user.second_factor_backup.copy_to_clipboard_error")
+ );
+ }
+
+ this._hideCopyMessage();
+ },
+
+ disableSecondFactorBackup() {
+ this.set("backupCodes", []);
+ this.set("loading", true);
+
+ this.model
+ .updateSecondFactor(0, "", true, SECOND_FACTOR_METHODS.BACKUP_CODE)
+ .then(response => {
+ if (response.error) {
+ this.set("errorMessage", response.error);
+ return;
+ }
+
+ this.set("errorMessage", null);
+ this.model.set("second_factor_backup_enabled", false);
+ this.markDirty();
+ this.send("closeModal");
+ })
+ .catch(error => {
+ this.send("closeModal");
+ this.onError(error);
+ })
+ .finally(() => this.set("loading", false));
+ },
+
+ generateSecondFactorCodes() {
+ this.set("loading", true);
+ this.model
+ .generateSecondFactorCodes()
+ .then(response => {
+ if (response.error) {
+ this.set("errorMessage", response.error);
+ return;
+ }
+
+ this.markDirty();
+ this.setProperties({
+ errorMessage: null,
+ backupCodes: response.backup_codes,
+ backupEnabled: true,
+ remainingCodes: response.backup_codes.length
+ });
+ })
+ .catch(error => {
+ this.send("closeModal");
+ this.onError(error);
+ })
+ .finally(() => {
+ this.setProperties({
+ loading: false
+ });
+ });
+ }
+ },
+
+ _hideCopyMessage() {
+ later(
+ () => this.setProperties({ successMessage: null, errorMessage: null }),
+ 2000
+ );
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/second-factor-edit-security-key.js.es6 b/app/assets/javascripts/discourse/controllers/second-factor-edit-security-key.js.es6
new file mode 100644
index 0000000000..8507448607
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/second-factor-edit-security-key.js.es6
@@ -0,0 +1,43 @@
+import Controller from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+
+export default Controller.extend(ModalFunctionality, {
+ actions: {
+ disableSecurityKey() {
+ this.user
+ .updateSecurityKey(this.model.id, this.model.name, true)
+ .then(response => {
+ if (response.error) {
+ return;
+ }
+ this.markDirty();
+ })
+ .catch(error => {
+ this.send("closeModal");
+ this.onError(error);
+ })
+ .finally(() => {
+ this.set("loading", false);
+ this.send("closeModal");
+ });
+ },
+
+ editSecurityKey() {
+ this.user
+ .updateSecurityKey(this.model.id, this.model.name, false)
+ .then(response => {
+ if (response.error) {
+ return;
+ }
+ this.markDirty();
+ })
+ .catch(error => {
+ this.onError(error);
+ })
+ .finally(() => {
+ this.set("loading", false);
+ this.send("closeModal");
+ });
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/second-factor-edit.js.es6 b/app/assets/javascripts/discourse/controllers/second-factor-edit.js.es6
new file mode 100644
index 0000000000..c11b278158
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/second-factor-edit.js.es6
@@ -0,0 +1,54 @@
+import Controller from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+
+export default Controller.extend(ModalFunctionality, {
+ actions: {
+ disableSecondFactor() {
+ this.user
+ .updateSecondFactor(
+ this.model.id,
+ this.model.name,
+ true,
+ this.model.method
+ )
+ .then(response => {
+ if (response.error) {
+ return;
+ }
+ this.markDirty();
+ })
+ .catch(error => {
+ this.send("closeModal");
+ this.onError(error);
+ })
+ .finally(() => {
+ this.set("loading", false);
+ this.send("closeModal");
+ });
+ },
+
+ editSecondFactor() {
+ this.user
+ .updateSecondFactor(
+ this.model.id,
+ this.model.name,
+ false,
+ this.model.method
+ )
+ .then(response => {
+ if (response.error) {
+ return;
+ }
+ this.markDirty();
+ })
+ .catch(error => {
+ this.send("closeModal");
+ this.onError(error);
+ })
+ .finally(() => {
+ this.set("loading", false);
+ this.send("closeModal");
+ });
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/static.js.es6 b/app/assets/javascripts/discourse/controllers/static.js.es6
index 82fd514e16..ad4f953105 100644
--- a/app/assets/javascripts/discourse/controllers/static.js.es6
+++ b/app/assets/javascripts/discourse/controllers/static.js.es6
@@ -1,16 +1,19 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { equal } from "@ember/object/computed";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
-import computed from "ember-addons/ember-computed-decorators";
import { userPath } from "discourse/lib/url";
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+export default Controller.extend({
+ application: inject(),
- showLoginButton: Ember.computed.equal("model.path", "login"),
+ showLoginButton: equal("model.path", "login"),
- @computed("model.path")
+ @discourseComputed("model.path")
bodyClass: path => `static-${path}`,
- @computed("model.path")
+ @discourseComputed("model.path")
showSignupButton() {
return (
this.get("model.path") === "login" && this.get("application.canSignUp")
diff --git a/app/assets/javascripts/discourse/controllers/tag-groups-edit.js.es6 b/app/assets/javascripts/discourse/controllers/tag-groups-edit.js.es6
new file mode 100644
index 0000000000..067a2f615b
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/tag-groups-edit.js.es6
@@ -0,0 +1,15 @@
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
+
+export default Controller.extend({
+ tagGroups: inject(),
+
+ actions: {
+ onDestroy() {
+ const tagGroups = this.tagGroups.model;
+ tagGroups.removeObject(this.model);
+
+ this.transitionToRoute("tagGroups.index");
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/tag-groups-new.js.es6 b/app/assets/javascripts/discourse/controllers/tag-groups-new.js.es6
new file mode 100644
index 0000000000..d2d3705866
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/tag-groups-new.js.es6
@@ -0,0 +1,14 @@
+import Controller, { inject } from "@ember/controller";
+
+export default Controller.extend({
+ tagGroups: inject(),
+
+ actions: {
+ onSave() {
+ const tagGroups = this.tagGroups.model;
+ tagGroups.pushObject(this.model);
+
+ this.transitionToRoute("tagGroups.edit", this.model);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/tag-groups-show.js.es6 b/app/assets/javascripts/discourse/controllers/tag-groups-show.js.es6
deleted file mode 100644
index 7b80def0b7..0000000000
--- a/app/assets/javascripts/discourse/controllers/tag-groups-show.js.es6
+++ /dev/null
@@ -1,26 +0,0 @@
-export default Ember.Controller.extend({
- tagGroups: Ember.inject.controller(),
-
- actions: {
- save() {
- this.model.save();
- },
-
- destroy() {
- return bootbox.confirm(
- I18n.t("tagging.groups.confirm_delete"),
- I18n.t("no_value"),
- I18n.t("yes_value"),
- destroy => {
- if (destroy) {
- const c = this.get("tagGroups.model");
- return this.model.destroy().then(() => {
- c.removeObject(this.model);
- this.transitionToRoute("tagGroups");
- });
- }
- }
- );
- }
- }
-});
diff --git a/app/assets/javascripts/discourse/controllers/tag-groups.js.es6 b/app/assets/javascripts/discourse/controllers/tag-groups.js.es6
index e4515d90b9..725f8846f1 100644
--- a/app/assets/javascripts/discourse/controllers/tag-groups.js.es6
+++ b/app/assets/javascripts/discourse/controllers/tag-groups.js.es6
@@ -1,24 +1,9 @@
-import TagGroup from "discourse/models/tag-group";
+import Controller from "@ember/controller";
-export default Ember.Controller.extend({
+export default Controller.extend({
actions: {
- selectTagGroup(tagGroup) {
- if (this.selectedItem) {
- this.selectedItem.set("selected", false);
- }
- this.set("selectedItem", tagGroup);
- tagGroup.set("selected", true);
- tagGroup.set("savingStatus", null);
- this.transitionToRoute("tagGroups.show", tagGroup);
- },
-
newTagGroup() {
- const newTagGroup = TagGroup.create({
- id: "new",
- name: I18n.t("tagging.groups.new_name")
- });
- this.model.pushObject(newTagGroup);
- this.send("selectTagGroup", newTagGroup);
+ this.transitionToRoute("tagGroups.new");
}
}
});
diff --git a/app/assets/javascripts/discourse/controllers/tags-index.js.es6 b/app/assets/javascripts/discourse/controllers/tags-index.js.es6
index fa20f97f2f..14473df9a5 100644
--- a/app/assets/javascripts/discourse/controllers/tags-index.js.es6
+++ b/app/assets/javascripts/discourse/controllers/tags-index.js.es6
@@ -1,15 +1,17 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { alias, notEmpty } from "@ember/object/computed";
+import Controller from "@ember/controller";
import showModal from "discourse/lib/show-modal";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
-export default Ember.Controller.extend({
+export default Controller.extend({
sortedByCount: true,
sortedByName: false,
- canAdminTags: Ember.computed.alias("currentUser.staff"),
- groupedByCategory: Ember.computed.notEmpty("model.extras.categories"),
- groupedByTagGroup: Ember.computed.notEmpty("model.extras.tag_groups"),
+ canAdminTags: alias("currentUser.staff"),
+ groupedByCategory: notEmpty("model.extras.categories"),
+ groupedByTagGroup: notEmpty("model.extras.tag_groups"),
init() {
this._super(...arguments);
@@ -17,7 +19,7 @@ export default Ember.Controller.extend({
this.sortProperties = ["totalCount:desc", "id"];
},
- @computed("groupedByCategory", "groupedByTagGroup")
+ @discourseComputed("groupedByCategory", "groupedByTagGroup")
otherTagsTitleKey(groupedByCategory, groupedByTagGroup) {
if (!groupedByCategory && !groupedByTagGroup) {
return "tagging.all_tags";
@@ -26,7 +28,7 @@ export default Ember.Controller.extend({
}
},
- @computed
+ @discourseComputed
actionsMapping() {
return {
manageGroups: () => this.send("showTagGroups"),
diff --git a/app/assets/javascripts/discourse/controllers/tags-show.js.es6 b/app/assets/javascripts/discourse/controllers/tags-show.js.es6
index 0c1c77e610..c4d79e8643 100644
--- a/app/assets/javascripts/discourse/controllers/tags-show.js.es6
+++ b/app/assets/javascripts/discourse/controllers/tags-show.js.es6
@@ -1,61 +1,19 @@
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import { alias } from "@ember/object/computed";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
-import {
- default as NavItem,
- extraNavItemProperties,
- customNavItemHref
-} from "discourse/models/nav-item";
+import NavItem from "discourse/models/nav-item";
+import FilterModeMixin from "discourse/mixins/filter-mode";
+import { queryParams } from "discourse/controllers/discovery-sortable";
-if (extraNavItemProperties) {
- extraNavItemProperties(function(text, opts) {
- if (opts && opts.tagId) {
- return { tagId: opts.tagId };
- } else {
- return {};
- }
- });
-}
-
-if (customNavItemHref) {
- customNavItemHref(function(navItem) {
- if (navItem.get("tagId")) {
- const name = navItem.get("name");
-
- if (!Discourse.Site.currentProp("filters").includes(name)) {
- return null;
- }
-
- let path = "/tags/";
- const category = navItem.get("category");
-
- if (category) {
- path += "c/";
- path += Discourse.Category.slugFor(category);
- if (navItem.get("noSubcategories")) {
- path += "/none";
- }
- path += "/";
- }
-
- path += `${navItem.get("tagId")}/l/`;
- return `${path}${name.replace(" ", "-")}`;
- } else {
- return null;
- }
- });
-}
-
-export default Ember.Controller.extend(BulkTopicSelection, {
- application: Ember.inject.controller(),
+export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
+ application: inject(),
tag: null,
additionalTags: null,
list: null,
- canAdminTag: Ember.computed.alias("currentUser.staff"),
- filterMode: null,
+ canAdminTag: alias("currentUser.staff"),
navMode: "latest",
loading: false,
canCreateTopic: false,
@@ -66,15 +24,16 @@ export default Ember.Controller.extend(BulkTopicSelection, {
search: null,
max_posts: null,
q: null,
+ showInfo: false,
- categories: Ember.computed.alias("site.categoriesList"),
+ categories: alias("site.categoriesList"),
- @computed("list", "list.draft")
+ @discourseComputed("list", "list.draft")
createTopicLabel(list, listDraft) {
return listDraft ? "topic.open_draft" : "topic.create";
},
- @computed(
+ @discourseComputed(
"canCreateTopic",
"category",
"canCreateTopicOnCategory",
@@ -95,32 +54,25 @@ export default Ember.Controller.extend(BulkTopicSelection, {
);
},
- queryParams: [
- "order",
- "ascending",
- "status",
- "state",
- "search",
- "max_posts",
- "q"
- ],
+ queryParams: Object.keys(queryParams),
- @computed("category", "tag.id", "filterMode")
- navItems(category, tagId, filterMode) {
+ @discourseComputed("category", "tag.id", "filterType", "noSubcategories")
+ navItems(category, tagId, filterType, noSubcategories) {
return NavItem.buildList(category, {
tagId,
- filterMode
+ filterType,
+ noSubcategories
});
},
- @computed("category")
+ @discourseComputed("category")
showTagFilter() {
return Discourse.SiteSettings.show_filter_by_tag;
},
- @computed("additionalTags", "canAdminTag", "category")
- showAdminControls(additionalTags, canAdminTag, category) {
- return !additionalTags && canAdminTag && !category;
+ @discourseComputed("additionalTags", "category", "tag.id")
+ showToggleInfo(additionalTags, category, tagId) {
+ return !additionalTags && !category && tagId !== "none";
},
loadMoreTopics() {
@@ -132,7 +84,7 @@ export default Ember.Controller.extend(BulkTopicSelection, {
this.set("application.showFooter", !this.get("list.canLoadMore"));
},
- @computed("navMode", "list.topics.length", "loading")
+ @discourseComputed("navMode", "list.topics.length", "loading")
footerMessage(navMode, listTopicsLength, loading) {
if (loading || listTopicsLength !== 0) {
return;
@@ -157,7 +109,13 @@ export default Ember.Controller.extend(BulkTopicSelection, {
this.setProperties({ order, ascending: false });
}
- this.send("invalidateModel");
+ this.transitionToRoute({
+ queryParams: { order, ascending: this.ascending }
+ });
+ },
+
+ toggleInfo() {
+ this.toggleProperty("showInfo");
},
refresh() {
@@ -170,15 +128,23 @@ export default Ember.Controller.extend(BulkTopicSelection, {
});
},
- deleteTag() {
+ deleteTag(tagInfo) {
const numTopics =
this.get("list.topic_list.tags.firstObject.topic_count") || 0;
- const confirmText =
+ let confirmText =
numTopics === 0
? I18n.t("tagging.delete_confirm_no_topics")
: I18n.t("tagging.delete_confirm", { count: numTopics });
+ if (tagInfo.synonyms.length > 0) {
+ confirmText +=
+ " " +
+ I18n.t("tagging.delete_confirm_synonyms", {
+ count: tagInfo.synonyms.length
+ });
+ }
+
bootbox.confirm(confirmText, result => {
if (!result) return;
@@ -189,9 +155,8 @@ export default Ember.Controller.extend(BulkTopicSelection, {
});
},
- changeTagNotification(id) {
- const tagNotification = this.tagNotification;
- tagNotification.update({ notification_level: id });
+ changeTagNotificationLevel(notificationLevel) {
+ this.tagNotification.update({ notification_level: notificationLevel });
}
}
});
diff --git a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6
index 9f5752e5c6..723e7fee54 100644
--- a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6
@@ -1,4 +1,8 @@
+import { empty, alias } from "@ember/object/computed";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
+import Topic from "discourse/models/topic";
+import Category from "discourse/models/category";
const _buttons = [];
@@ -61,20 +65,26 @@ if (Discourse.SiteSettings.tagging_enabled) {
class: "btn-default"
});
}
-addBulkButton("deleteTopics", "delete", { icon: "trash", class: "btn-danger" });
+addBulkButton("deleteTopics", "delete", {
+ icon: "trash-alt",
+ class: "btn-danger"
+});
// Modal for performing bulk actions on topics
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Controller.extend(ModalFunctionality, {
tags: null,
- emptyTags: Ember.computed.empty("tags"),
- categoryId: Ember.computed.alias("model.category.id"),
+ emptyTags: empty("tags"),
+ categoryId: alias("model.category.id"),
onShow() {
const topics = this.get("model.topics");
// const relistButtonIndex = _buttons.findIndex(b => b.action === 'relistTopics');
- this.set("buttons", _buttons.filter(b => b.buttonVisible(topics)));
+ this.set(
+ "buttons",
+ _buttons.filter(b => b.buttonVisible(topics))
+ );
this.set("modal.modalClass", "topic-bulk-actions-modal small");
this.send("changeBulkTemplate", "modal/bulk-actions-buttons");
},
@@ -83,7 +93,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
this.set("loading", true);
const topics = this.get("model.topics");
- return Discourse.Topic.bulkOperation(topics, operation)
+ return Topic.bulkOperation(topics, operation)
.then(result => {
this.set("loading", false);
if (result && result.topic_ids) {
@@ -169,7 +179,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
changeCategory() {
const categoryId = parseInt(this.newCategoryId, 10) || 0;
- const category = Discourse.Category.findById(categoryId);
+ const category = Category.findById(categoryId);
this.perform({ type: "change_category", category_id: categoryId }).then(
topics => {
diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6
index 95290fba5b..ec53fe427d 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic.js.es6
@@ -1,3 +1,10 @@
+import { isPresent, isEmpty } from "@ember/utils";
+import { or, and, not, alias } from "@ember/object/computed";
+import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import Composer from "discourse/models/composer";
import DiscourseURL from "discourse/lib/url";
@@ -5,18 +12,17 @@ import Post from "discourse/models/post";
import Quote from "discourse/lib/quote";
import QuoteState from "discourse/lib/quote-state";
import Topic from "discourse/models/topic";
-import debounce from "discourse/lib/debounce";
+import discourseDebounce from "discourse/lib/debounce";
import isElementInViewport from "discourse/lib/is-element-in-viewport";
import { ajax } from "discourse/lib/ajax";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { extractLinkMeta } from "discourse/lib/render-topic-featured-link";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { spinnerHTML } from "discourse/helpers/loading-spinner";
import { userPath } from "discourse/lib/url";
import showModal from "discourse/lib/show-modal";
+import TopicTimer from "discourse/models/topic-timer";
+import { Promise } from "rsvp";
let customPostMessageCallbacks = {};
@@ -32,14 +38,14 @@ export function registerCustomPostMessageCallback(type, callback) {
customPostMessageCallbacks[type] = callback;
}
-export default Ember.Controller.extend(bufferedProperty("model"), {
- composer: Ember.inject.controller(),
- application: Ember.inject.controller(),
+export default Controller.extend(bufferedProperty("model"), {
+ composer: inject(),
+ application: inject(),
multiSelect: false,
selectedPostIds: null,
editingTopic: false,
queryParams: ["filter", "username_filters"],
- loadedAllPosts: Ember.computed.or(
+ loadedAllPosts: or(
"model.postStream.loadedAllPosts",
"model.postStream.loadingLastPost"
),
@@ -53,7 +59,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
filter: null,
quoteState: null,
- canRemoveTopicFeaturedLink: Ember.computed.and(
+ canRemoveTopicFeaturedLink: and(
"canEditTopicFeaturedLink",
"buffered.featured_link"
),
@@ -65,13 +71,13 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
@observes("model.title", "category")
_titleChanged() {
const title = this.get("model.title");
- if (!Ember.isEmpty(title)) {
+ if (!isEmpty(title)) {
// force update lazily loaded titles
this.send("refreshTitle");
}
},
- @computed("model.details.can_create_post")
+ @discourseComputed("model.details.can_create_post")
embedQuoteButton(canCreatePost) {
return (
canCreatePost &&
@@ -80,28 +86,31 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
);
},
- @computed("model.postStream.loaded", "model.category_id")
+ @discourseComputed("model.postStream.loaded", "model.category_id")
showSharedDraftControls(loaded, categoryId) {
let draftCat = this.site.shared_drafts_category_id;
return loaded && draftCat && categoryId && draftCat === categoryId;
},
- @computed("site.mobileView", "model.posts_count")
+ @discourseComputed("site.mobileView", "model.posts_count")
showSelectedPostsAtBottom(mobileView, postsCount) {
return mobileView && postsCount > 3;
},
- @computed("model.postStream.posts", "model.postStream.postsWithPlaceholders")
+ @discourseComputed(
+ "model.postStream.posts",
+ "model.postStream.postsWithPlaceholders"
+ )
postsToRender(posts, postsWithPlaceholders) {
return this.capabilities.isAndroid ? posts : postsWithPlaceholders;
},
- @computed("model.postStream.loadingFilter")
+ @discourseComputed("model.postStream.loadingFilter")
androidLoading(loading) {
return this.capabilities.isAndroid && loading;
},
- @computed("model")
+ @discourseComputed("model")
pmPath(topic) {
return this.currentUser && this.currentUser.pmPath(topic);
},
@@ -129,12 +138,12 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
return;
}
- Ember.run.scheduleOnce("afterRender", () => {
+ scheduleOnce("afterRender", () => {
this.send("showHistory", post, revision);
});
},
- showCategoryChooser: Ember.computed.not("model.isPrivateMessage"),
+ showCategoryChooser: not("model.isPrivateMessage"),
gotoInbox(name) {
let url = userPath(this.get("currentUser.username_lower") + "/messages");
@@ -144,12 +153,12 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
DiscourseURL.routeTo(url);
},
- @computed
+ @discourseComputed
selectedQuery() {
return post => this.postSelected(post);
},
- @computed("model.isPrivateMessage", "model.category.id")
+ @discourseComputed("model.isPrivateMessage", "model.category.id")
canEditTopicFeaturedLink(isPrivateMessage, categoryId) {
if (!this.siteSettings.topic_featured_link_enabled || isPrivateMessage) {
return false;
@@ -165,12 +174,12 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
);
},
- @computed("model")
+ @discourseComputed("model")
featuredLinkDomain(topic) {
return extractLinkMeta(topic).domain;
},
- @computed("model.isPrivateMessage")
+ @discourseComputed("model.isPrivateMessage")
canEditTags(isPrivateMessage) {
return (
this.site.get("can_tag_topics") &&
@@ -233,8 +242,12 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
},
actions: {
- topicCategoryChanged(selection) {
- this.set("buffered.category_id", selection.value);
+ topicCategoryChanged(categoryId) {
+ this.set("buffered.category_id", categoryId);
+ },
+
+ topicTagsChanged(value) {
+ this.set("buffered.tags", value);
},
deletePending(pending) {
@@ -253,16 +266,16 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
this.send("showFeatureTopic");
},
- selectText(postId, buffer) {
+ selectText(postId, buffer, opts) {
const loadedPost = this.get("model.postStream").findLoadedPost(postId);
const promise = loadedPost
- ? Ember.RSVP.resolve(loadedPost)
+ ? Promise.resolve(loadedPost)
: this.get("model.postStream").loadPost(postId);
return promise.then(post => {
const composer = this.composer;
const viewOpen = composer.get("model.viewOpen");
- const quotedText = Quote.build(post, buffer);
+ const quotedText = Quote.build(post, buffer, opts);
// If we can't create a post, delegate to reply as new topic
if (!viewOpen && !this.get("model.details.can_create_post")) {
@@ -431,9 +444,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
: "/";
ajax("/t/" + topic.get("id") + "/timings.json?last=1", { type: "DELETE" })
.then(() => {
- const highestSeenByTopic = Discourse.Session.currentProp(
- "highestSeenByTopic"
- );
+ const highestSeenByTopic = this.session.get("highestSeenByTopic");
highestSeenByTopic[topic.get("id")] = null;
DiscourseURL.routeTo(goToPath);
})
@@ -466,6 +477,8 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
const quoteState = this.quoteState;
const postStream = this.get("model.postStream");
+ this.appEvents.trigger("page:compose-reply", topic);
+
if (!postStream || !topic || !topic.get("details.can_create_post")) {
return;
}
@@ -522,6 +535,16 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
if (user.get("staff") && hasReplies) {
ajax(`/posts/${post.id}/reply-ids.json`).then(replies => {
+ if (replies.length === 0) {
+ return post
+ .destroy(user)
+ .then(refresh)
+ .catch(error => {
+ popupAjaxError(error);
+ post.undoDeleteState();
+ });
+ }
+
const buttons = [];
buttons.push({
@@ -653,6 +676,16 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
}
},
+ toggleBookmarkWithReminder(post) {
+ if (!this.currentUser) {
+ return bootbox.alert(I18n.t("bookmarks.not_bookmarked"));
+ } else if (post) {
+ return post.toggleBookmarkWithReminder();
+ } else {
+ return this.model.toggleBookmarkWithReminder();
+ }
+ },
+
jumpToIndex(index) {
this._jumpToIndex(index);
},
@@ -675,20 +708,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
},
jumpToPost(postNumber) {
- if (this.get("model.postStream.isMegaTopic")) {
- this._jumpToPostNumber(postNumber);
- } else {
- const postStream = this.get("model.postStream");
- let postId = postStream.findPostIdForPostNumber(postNumber);
-
- // If we couldn't find the post, find the closest post to it
- if (!postId) {
- const closest = postStream.closestPostNumberFor(postNumber);
- postId = postStream.findPostIdForPostNumber(closest);
- }
-
- this._jumpToPostId(postId);
- }
+ this._jumpToPostNumber(postNumber);
},
jumpTop() {
@@ -703,6 +723,16 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
});
},
+ jumpEnd() {
+ this.appEvents.trigger(
+ "topic:jump-to-post",
+ this.get("model.highest_post_number")
+ );
+ DiscourseURL.routeTo(this.get("model.lastPostUrl"), {
+ jumpEnd: true
+ });
+ },
+
jumpUnread() {
this._jumpToPostId(this.get("model.last_read_post_id"));
},
@@ -711,11 +741,6 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
this._jumpToPostId(postId);
},
- hideMultiSelect() {
- this.set("multiSelect", false);
- this._forceRefreshPostStream();
- },
-
toggleMultiSelect() {
this.toggleProperty("multiSelect");
this._forceRefreshPostStream();
@@ -814,7 +839,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
},
addNotice(post) {
- return new Ember.RSVP.Promise(function(resolve, reject) {
+ return new Promise(function(resolve, reject) {
const controller = showModal("add-post-notice");
controller.setProperties({ post, resolve, reject });
});
@@ -960,13 +985,13 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
options = {
action: Composer.PRIVATE_MESSAGE,
archetypeId: "private_message",
- draftKey: Composer.REPLY_AS_NEW_PRIVATE_MESSAGE_KEY,
+ draftKey: post.topic.draft_key,
usernames: usernames
};
} else {
options = {
action: Composer.CREATE_TOPIC,
- draftKey: Composer.REPLY_AS_NEW_TOPIC_KEY,
+ draftKey: post.topic.draft_key,
categoryId: this.get("model.category.id")
};
}
@@ -974,7 +999,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
composerController
.open(options)
.then(() => {
- return Ember.isEmpty(quotedText) ? "" : quotedText;
+ return isEmpty(quotedText) ? "" : quotedText;
})
.then(q => {
const postUrl = `${location.protocol}//${location.host}${post.get(
@@ -1022,11 +1047,17 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
},
convertToPublicTopic() {
- this.model.convertTopic("public");
+ showModal("convert-to-public-topic", {
+ model: this.model,
+ modalClass: "convert-to-public-topic"
+ });
},
convertToPrivateMessage() {
- this.model.convertTopic("private");
+ this.model
+ .convertTopic("private")
+ .then(() => window.location.reload())
+ .catch(popupAjaxError);
},
removeFeaturedLink() {
@@ -1035,6 +1066,18 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
resetBumpDate() {
this.model.resetBumpDate();
+ },
+
+ removeTopicTimer(statusType, topicTimer) {
+ TopicTimer.updateStatus(
+ this.get("model.id"),
+ null,
+ null,
+ statusType,
+ null
+ )
+ .then(() => this.set(`model.${topicTimer}`, EmberObject.create({})))
+ .catch(error => popupAjaxError(error));
}
},
@@ -1119,14 +1162,14 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
}
},
- hasError: Ember.computed.or("model.notFoundHtml", "model.message"),
- noErrorYet: Ember.computed.not("hasError"),
+ hasError: or("model.errorHtml", "model.errorMessage"),
+ noErrorYet: not("hasError"),
- categories: Ember.computed.alias("site.categoriesList"),
+ categories: alias("site.categoriesList"),
- selectedPostsCount: Ember.computed.alias("selectedPostIds.length"),
+ selectedPostsCount: alias("selectedPostIds.length"),
- @computed(
+ @discourseComputed(
"selectedPostIds",
"model.postStream.posts",
"selectedPostIds.[]",
@@ -1138,7 +1181,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
.filter(post => post !== undefined);
},
- @computed("selectedPostsCount", "selectedPosts", "selectedPosts.[]")
+ @discourseComputed("selectedPostsCount", "selectedPosts", "selectedPosts.[]")
selectedPostsUsername(selectedPostsCount, selectedPosts) {
if (selectedPosts.length < 1 || selectedPostsCount > selectedPosts.length) {
return undefined;
@@ -1149,7 +1192,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
: undefined;
},
- @computed(
+ @discourseComputed(
"selectedPostsCount",
"model.postStream.isMegaTopic",
"model.postStream.stream.length",
@@ -1168,14 +1211,14 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
}
},
- @computed("selectedAllPosts", "model.postStream.isMegaTopic")
+ @discourseComputed("selectedAllPosts", "model.postStream.isMegaTopic")
canSelectAll(selectedAllPosts, isMegaTopic) {
return isMegaTopic ? false : !selectedAllPosts;
},
- canDeselectAll: Ember.computed.alias("selectedAllPosts"),
+ canDeselectAll: alias("selectedAllPosts"),
- @computed(
+ @discourseComputed(
"currentUser.staff",
"selectedPostsCount",
"selectedAllPosts",
@@ -1194,19 +1237,23 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
);
},
- @computed("model.details.can_move_posts", "selectedPostsCount")
+ @discourseComputed("model.details.can_move_posts", "selectedPostsCount")
canMergeTopic(canMovePosts, selectedPostsCount) {
return canMovePosts && selectedPostsCount > 0;
},
- @computed("currentUser.admin", "selectedPostsCount", "selectedPostsUsername")
+ @discourseComputed(
+ "currentUser.admin",
+ "selectedPostsCount",
+ "selectedPostsUsername"
+ )
canChangeOwner(isAdmin, selectedPostsCount, selectedPostsUsername) {
return (
isAdmin && selectedPostsCount > 0 && selectedPostsUsername !== undefined
);
},
- @computed(
+ @discourseComputed(
"selectedPostsCount",
"selectedPostsUsername",
"selectedPosts",
@@ -1229,7 +1276,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
return this.selectedAllPost || this.selectedPostIds.includes(post.id);
},
- @computed
+ @discourseComputed
loadingHTML() {
return spinnerHTML;
},
@@ -1252,7 +1299,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
data => {
const topic = this.model;
- if (Ember.isPresent(data.notification_level_change)) {
+ if (isPresent(data.notification_level_change)) {
topic.set(
"details.notification_level",
data.notification_level_change
@@ -1284,6 +1331,12 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
})
.then(() => refresh({ id: data.id, refreshLikes: true }));
break;
+ case "read": {
+ postStream
+ .triggerReadPost(data.id, data.readers_count)
+ .then(() => refresh({ id: data.id, refreshLikes: true }));
+ break;
+ }
case "revised":
case "rebaked": {
postStream
@@ -1323,7 +1376,8 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
if (callback) {
callback(this, data);
} else {
- Ember.Logger.warn("unknown topic bus message type", data);
+ // eslint-disable-next-line no-console
+ console.warn("unknown topic bus message type", data);
}
}
}
@@ -1356,7 +1410,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
);
},
- _scrollToPost: debounce(function(postNumber) {
+ _scrollToPost: discourseDebounce(function(postNumber) {
const $post = $(`.topic-post article#post_${postNumber}`);
if ($post.length === 0 || isElementInViewport($post)) return;
@@ -1394,7 +1448,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
// automatically unpin topics when the user reaches the bottom
const max = _.max(postNumbers);
if (topic.get("pinned") && max >= topic.get("highest_post_number")) {
- Ember.run.next(() => topic.clearPin());
+ next(() => topic.clearPin());
}
}
}
diff --git a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6
index 73397bb07a..eae9a3a2bb 100644
--- a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6
+++ b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6
@@ -1,51 +1,47 @@
+import { equal } from "@ember/object/computed";
+import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
import {
allowsAttachments,
- authorizesAllExtensions,
authorizedExtensions,
+ authorizesAllExtensions,
uploadIcon
-} from "discourse/lib/utilities";
+} from "discourse/lib/uploads";
-function uploadTranslate(key) {
- if (allowsAttachments()) {
+function uploadTranslate(key, user) {
+ if (allowsAttachments(user.staff)) {
key += "_with_attachments";
}
return `upload_selector.${key}`;
}
-export default Ember.Controller.extend(ModalFunctionality, {
- showMore: false,
+export default Controller.extend(ModalFunctionality, {
imageUrl: null,
- imageLink: null,
- local: Ember.computed.equal("selection", "local"),
- remote: Ember.computed.equal("selection", "remote"),
+ local: equal("selection", "local"),
+ remote: equal("selection", "remote"),
selection: "local",
- @computed()
- uploadIcon: () => uploadIcon(),
-
- @computed()
- title: () => uploadTranslate("title"),
-
- @computed("selection")
- tip(selection) {
- const authorized_extensions = authorizesAllExtensions()
- ? ""
- : `(${authorizedExtensions()})`;
- return I18n.t(uploadTranslate(`${selection}_tip`), {
- authorized_extensions
- });
+ @discourseComputed()
+ uploadIcon() {
+ return uploadIcon(this.currentUser.staff);
},
- @observes("selection")
- _selectionChanged() {
- if (this.local) {
- this.set("showMore", false);
- }
+ @discourseComputed()
+ title() {
+ return uploadTranslate("title", this.currentUser);
+ },
+
+ @discourseComputed("selection")
+ tip(selection) {
+ const authorized_extensions = authorizesAllExtensions(
+ this.currentUser.staff
+ )
+ ? ""
+ : `(${authorizedExtensions(this.currentUser.staff)})`;
+ return I18n.t(uploadTranslate(`${selection}_tip`, this.currentUser), {
+ authorized_extensions
+ });
},
actions: {
@@ -56,22 +52,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
});
} else {
const imageUrl = this.imageUrl || "";
- const imageLink = this.imageLink || "";
const toolbarEvent = this.toolbarEvent;
- if (this.showMore && imageLink.length > 3) {
- toolbarEvent.addText(`[](${imageLink})`);
- } else if (imageUrl.match(/\.(jpg|jpeg|png|gif)$/)) {
+ if (imageUrl.match(/\.(jpg|jpeg|png|gif)$/)) {
toolbarEvent.addText(``);
} else {
toolbarEvent.addText(imageUrl);
}
}
this.send("closeModal");
- },
-
- toggleShowMore() {
- this.toggleProperty("showMore");
}
}
});
diff --git a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity.js.es6
index e214e58826..473f24c753 100644
--- a/app/assets/javascripts/discourse/controllers/user-activity.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-activity.js.es6
@@ -1,12 +1,19 @@
+import { alias } from "@ember/object/computed";
+import { inject as service } from "@ember/service";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import { exportUserArchive } from "discourse/lib/export-csv";
+import { observes } from "discourse-common/utils/decorators";
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
- user: Ember.inject.controller(),
+export default Controller.extend({
+ application: inject(),
+ router: service(),
+ user: inject(),
userActionType: null,
- canDownloadPosts: Ember.computed.alias("user.viewingSelf"),
+ canDownloadPosts: alias("user.viewingSelf"),
+ @observes("userActionType", "model.stream.itemsLoaded")
_showFooter: function() {
var showFooter;
if (this.userActionType) {
@@ -20,7 +27,7 @@ export default Ember.Controller.extend({
this.get("model.stream.itemsLoaded");
}
this.set("application.showFooter", showFooter);
- }.observes("userActionType", "model.stream.itemsLoaded"),
+ },
actions: {
exportUserArchive() {
diff --git a/app/assets/javascripts/discourse/controllers/user-badges.js.es6 b/app/assets/javascripts/discourse/controllers/user-badges.js.es6
index 9046c373bd..01df9566af 100644
--- a/app/assets/javascripts/discourse/controllers/user-badges.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-badges.js.es6
@@ -1,7 +1,10 @@
-export default Ember.Controller.extend({
- user: Ember.inject.controller(),
- username: Ember.computed.alias("user.model.username_lower"),
- sortedBadges: Ember.computed.sort("model", "badgeSortOrder"),
+import { alias, sort } from "@ember/object/computed";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
+export default Controller.extend({
+ user: inject(),
+ username: alias("user.model.username_lower"),
+ sortedBadges: sort("model", "badgeSortOrder"),
init() {
this._super(...arguments);
diff --git a/app/assets/javascripts/discourse/controllers/user-card.js.es6 b/app/assets/javascripts/discourse/controllers/user-card.js.es6
index cf84b13232..862fc92e73 100644
--- a/app/assets/javascripts/discourse/controllers/user-card.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-card.js.es6
@@ -1,12 +1,11 @@
-import {
- default as DiscourseURL,
- userPath,
- groupPath
-} from "discourse/lib/url";
+import { inject as service } from "@ember/service";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
+import DiscourseURL, { userPath, groupPath } from "discourse/lib/url";
-export default Ember.Controller.extend({
- topic: Ember.inject.controller(),
- application: Ember.inject.controller(),
+export default Controller.extend({
+ topic: inject(),
+ router: service(),
actions: {
togglePosts(user) {
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 d7450ae3bc..9282265e78 100644
--- a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6
@@ -1,12 +1,11 @@
+import { equal, reads, gte } from "@ember/object/computed";
+import Controller from "@ember/controller";
import Invite from "discourse/models/invite";
-import debounce from "discourse/lib/debounce";
+import discourseDebounce from "discourse/lib/debounce";
import { popupAjaxError } from "discourse/lib/ajax-error";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
-export default Ember.Controller.extend({
+export default Controller.extend({
user: null,
model: null,
filter: null,
@@ -25,15 +24,17 @@ export default Ember.Controller.extend({
},
@observes("searchTerm")
- _searchTermChanged: debounce(function() {
- Invite.findInvitedBy(this.user, this.filter, this.searchTerm).then(
- invites => this.set("model", invites)
- );
+ _searchTermChanged: discourseDebounce(function() {
+ Invite.findInvitedBy(
+ this.user,
+ this.filter,
+ this.searchTerm
+ ).then(invites => this.set("model", invites));
}, 250),
- inviteRedeemed: Ember.computed.equal("filter", "redeemed"),
+ inviteRedeemed: equal("filter", "redeemed"),
- @computed("filter")
+ @discourseComputed("filter")
showBulkActionButtons(filter) {
return (
filter === "pending" &&
@@ -42,19 +43,13 @@ export default Ember.Controller.extend({
);
},
- @computed
- canInviteToForum() {
- return Discourse.User.currentProp("can_invite_to_forum");
- },
+ canInviteToForum: reads("currentUser.can_invite_to_forum"),
- @computed
- canBulkInvite() {
- return Discourse.User.currentProp("admin");
- },
+ canBulkInvite: reads("currentUser.admin"),
- showSearch: Ember.computed.gte("totalInvites", 10),
+ showSearch: gte("totalInvites", 10),
- @computed("invitesCount.total", "invitesCount.pending")
+ @discourseComputed("invitesCount.total", "invitesCount.pending")
pendingLabel(invitesCountTotal, invitesCountPending) {
if (invitesCountTotal > 50) {
return I18n.t("user.invited.pending_tab_with_count", {
@@ -65,7 +60,7 @@ export default Ember.Controller.extend({
}
},
- @computed("invitesCount.total", "invitesCount.redeemed")
+ @discourseComputed("invitesCount.total", "invitesCount.redeemed")
redeemedLabel(invitesCountTotal, invitesCountRedeemed) {
if (invitesCountTotal > 50) {
return I18n.t("user.invited.redeemed_tab_with_count", {
diff --git a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6
index bb94ee6f50..23efcfc214 100644
--- a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6
@@ -1,23 +1,26 @@
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
+import { readOnly } from "@ember/object/computed";
+import { inject as service } from "@ember/service";
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+export default Controller.extend({
+ application: inject(),
+ router: service(),
+ currentPath: readOnly("router._router.currentPath"),
@observes("model.canLoadMore")
_showFooter() {
this.set("application.showFooter", !this.get("model.canLoadMore"));
},
- @computed("model.content.length")
+ @discourseComputed("model.content.length")
hasNotifications(length) {
return length > 0;
},
- @computed("model.content.@each.read")
+ @discourseComputed("model.content.@each.read")
allNotificationsRead() {
return !this.get("model.content").some(
notification => !notification.get("read")
diff --git a/app/assets/javascripts/discourse/controllers/user-posts.js.es6 b/app/assets/javascripts/discourse/controllers/user-posts.js.es6
index 25c730d97a..e5de7e9995 100644
--- a/app/assets/javascripts/discourse/controllers/user-posts.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-posts.js.es6
@@ -1,7 +1,12 @@
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
+import { observes } from "discourse-common/utils/decorators";
+export default Controller.extend({
+ application: inject(),
+
+ @observes("model.canLoadMore")
_showFooter: function() {
this.set("application.showFooter", !this.get("model.canLoadMore"));
- }.observes("model.canLoadMore")
+ }
});
diff --git a/app/assets/javascripts/discourse/controllers/user-private-messages-tags.js.es6 b/app/assets/javascripts/discourse/controllers/user-private-messages-tags.js.es6
index 1be6e948c4..d1c311f893 100644
--- a/app/assets/javascripts/discourse/controllers/user-private-messages-tags.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-private-messages-tags.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Controller.extend({
+import Controller from "@ember/controller";
+export default Controller.extend({
sortProperties: ["count:desc", "id"],
tagsForUser: null,
sortedByCount: true,
diff --git a/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 b/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6
index 62fc7a2a5c..8a21698516 100644
--- a/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6
@@ -1,39 +1,38 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { alias, equal, and } from "@ember/object/computed";
+import { inject as service } from "@ember/service";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import Topic from "discourse/models/topic";
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
- userTopicsList: Ember.inject.controller("user-topics-list"),
- user: Ember.inject.controller(),
+export default Controller.extend({
+ router: service(),
+ userTopicsList: inject("user-topics-list"),
+ user: inject(),
pmView: false,
- viewingSelf: Ember.computed.alias("user.viewingSelf"),
- isGroup: Ember.computed.equal("pmView", "groups"),
- currentPath: Ember.computed.alias("application.currentPath"),
- selected: Ember.computed.alias("userTopicsList.selected"),
- bulkSelectEnabled: Ember.computed.alias("userTopicsList.bulkSelectEnabled"),
+ viewingSelf: alias("user.viewingSelf"),
+ isGroup: equal("pmView", "groups"),
+ currentPath: alias("router._router.currentPath"),
+ selected: alias("userTopicsList.selected"),
+ bulkSelectEnabled: alias("userTopicsList.bulkSelectEnabled"),
showToggleBulkSelect: true,
- pmTaggingEnabled: Ember.computed.alias("site.can_tag_pms"),
+ pmTaggingEnabled: alias("site.can_tag_pms"),
tagId: null,
- @computed("user.viewingSelf")
- showNewPM(viewingSelf) {
- return (
- viewingSelf && Discourse.User.currentProp("can_send_private_messages")
- );
- },
+ showNewPM: and("user.viewingSelf", "currentUser.can_send_private_messages"),
- @computed("selected.[]", "bulkSelectEnabled")
+ @discourseComputed("selected.[]", "bulkSelectEnabled")
hasSelection(selected, bulkSelectEnabled) {
return bulkSelectEnabled && selected && selected.length > 0;
},
- @computed("hasSelection", "pmView", "archive")
+ @discourseComputed("hasSelection", "pmView", "archive")
canMoveToInbox(hasSelection, pmView, archive) {
return hasSelection && (pmView === "archive" || archive);
},
- @computed("hasSelection", "pmView", "archive")
+ @discourseComputed("hasSelection", "pmView", "archive")
canArchive(hasSelection, pmView, archive) {
return hasSelection && pmView !== "archive" && !archive;
},
@@ -60,6 +59,9 @@ export default Ember.Controller.extend({
},
actions: {
+ changeGroupNotificationLevel(notificationLevel) {
+ this.group.setNotification(notificationLevel, this.get("user.id"));
+ },
archive() {
this.bulkOperation("archive_messages");
},
diff --git a/app/assets/javascripts/discourse/controllers/user-summary.js.es6 b/app/assets/javascripts/discourse/controllers/user-summary.js.es6
index 2e88396cb7..0a1a37572e 100644
--- a/app/assets/javascripts/discourse/controllers/user-summary.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-summary.js.es6
@@ -1,29 +1,32 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { alias } from "@ember/object/computed";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import { durationTiny } from "discourse/lib/formatter";
// should be kept in sync with 'UserSummary::MAX_BADGES'
const MAX_BADGES = 6;
-export default Ember.Controller.extend({
- userController: Ember.inject.controller("user"),
- user: Ember.computed.alias("userController.model"),
+export default Controller.extend({
+ userController: inject("user"),
+ user: alias("userController.model"),
- @computed("model.badges.length")
+ @discourseComputed("model.badges.length")
moreBadges(badgesLength) {
return badgesLength >= MAX_BADGES;
},
- @computed("model.time_read")
+ @discourseComputed("model.time_read")
timeRead(timeReadSeconds) {
return durationTiny(timeReadSeconds);
},
- @computed("model.time_read", "model.recent_time_read")
+ @discourseComputed("model.time_read", "model.recent_time_read")
showRecentTimeRead(timeRead, recentTimeRead) {
return timeRead !== recentTimeRead && recentTimeRead !== 0;
},
- @computed("model.recent_time_read")
+ @discourseComputed("model.recent_time_read")
recentTimeRead(recentTimeReadSeconds) {
return recentTimeReadSeconds > 0
? durationTiny(recentTimeReadSeconds)
diff --git a/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 b/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6
index a4d822a565..f541ac01ca 100644
--- a/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6
@@ -1,8 +1,10 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
// Lists of topics on a user's page.
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+export default Controller.extend({
+ application: inject(),
hideCategory: false,
showPosters: false,
@@ -16,11 +18,16 @@ export default Ember.Controller.extend({
this.newIncoming = [];
},
+ saveScrollPosition: function() {
+ this.session.set("topicListScrollPosition", $(window).scrollTop());
+ },
+
+ @observes("model.canLoadMore")
_showFooter: function() {
this.set("application.showFooter", !this.get("model.canLoadMore"));
- }.observes("model.canLoadMore"),
+ },
- @computed("incomingCount")
+ @discourseComputed("incomingCount")
hasIncoming(incomingCount) {
return incomingCount > 0;
},
diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6
index 0051d0377e..90eeb3a837 100644
--- a/app/assets/javascripts/discourse/controllers/user.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user.js.es6
@@ -1,49 +1,59 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { isEmpty } from "@ember/utils";
+import { alias, or, gt, not, and } from "@ember/object/computed";
+import EmberObject from "@ember/object";
+import { inject as service } from "@ember/service";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
import CanCheckEmails from "discourse/mixins/can-check-emails";
-import computed from "ember-addons/ember-computed-decorators";
import User from "discourse/models/user";
import optionalService from "discourse/lib/optional-service";
+import { prioritizeNameInUx } from "discourse/lib/settings";
+import { set, computed } from "@ember/object";
-export default Ember.Controller.extend(CanCheckEmails, {
+export default Controller.extend(CanCheckEmails, {
indexStream: false,
- application: Ember.inject.controller(),
- userNotifications: Ember.inject.controller("user-notifications"),
- currentPath: Ember.computed.alias("application.currentPath"),
+ router: service(),
+ userNotifications: inject("user-notifications"),
+ currentPath: alias("router._router.currentPath"),
adminTools: optionalService(),
- @computed("model.username")
+ @discourseComputed("model.username")
viewingSelf(username) {
let currentUser = this.currentUser;
return currentUser && username === currentUser.get("username");
},
- @computed("viewingSelf", "model.profile_hidden")
+ @discourseComputed("viewingSelf", "model.profile_hidden")
canExpandProfile(viewingSelf, profileHidden) {
return !profileHidden && viewingSelf;
},
- @computed("model.profileBackgroundUrl")
+ @discourseComputed("model.profileBackgroundUrl")
hasProfileBackgroundUrl(background) {
- return !Ember.isEmpty(background.toString());
+ return !isEmpty(background.toString());
},
- @computed("model.profile_hidden", "indexStream", "viewingSelf", "forceExpand")
+ @discourseComputed(
+ "model.profile_hidden",
+ "indexStream",
+ "viewingSelf",
+ "forceExpand"
+ )
collapsedInfo(profileHidden, indexStream, viewingSelf, forceExpand) {
if (profileHidden) {
return true;
}
return (!indexStream || viewingSelf) && !forceExpand;
},
- canMuteOrIgnoreUser: Ember.computed.or(
- "model.can_ignore_user",
- "model.can_mute_user"
- ),
- hasGivenFlags: Ember.computed.gt("model.number_of_flags_given", 0),
- hasFlaggedPosts: Ember.computed.gt("model.number_of_flagged_posts", 0),
- hasDeletedPosts: Ember.computed.gt("model.number_of_deleted_posts", 0),
- hasBeenSuspended: Ember.computed.gt("model.number_of_suspensions", 0),
- hasReceivedWarnings: Ember.computed.gt("model.warnings_received_count", 0),
+ canMuteOrIgnoreUser: or("model.can_ignore_user", "model.can_mute_user"),
+ hasGivenFlags: gt("model.number_of_flags_given", 0),
+ hasFlaggedPosts: gt("model.number_of_flagged_posts", 0),
+ hasDeletedPosts: gt("model.number_of_deleted_posts", 0),
+ hasBeenSuspended: gt("model.number_of_suspensions", 0),
+ hasReceivedWarnings: gt("model.warnings_received_count", 0),
- showStaffCounters: Ember.computed.or(
+ showStaffCounters: or(
"hasGivenFlags",
"hasFlaggedPosts",
"hasDeletedPosts",
@@ -51,85 +61,96 @@ export default Ember.Controller.extend(CanCheckEmails, {
"hasReceivedWarnings"
),
- @computed("model.suspended", "currentUser.staff")
+ showFeaturedTopic: and(
+ "model.featured_topic",
+ "siteSettings.allow_featured_topic_on_user_profiles"
+ ),
+
+ @discourseComputed("model.suspended", "currentUser.staff")
isNotSuspendedOrIsStaff(suspended, isStaff) {
return !suspended || isStaff;
},
- linkWebsite: Ember.computed.not("model.isBasic"),
+ linkWebsite: not("model.isBasic"),
- @computed("model.trust_level")
+ @discourseComputed("model.trust_level")
removeNoFollow(trustLevel) {
return trustLevel > 2 && !this.siteSettings.tl3_links_no_follow;
},
- @computed("viewingSelf", "currentUser.admin")
+ @discourseComputed("viewingSelf", "currentUser.admin")
showBookmarks(viewingSelf, isAdmin) {
return viewingSelf || isAdmin;
},
- @computed("viewingSelf")
+ @discourseComputed("viewingSelf")
showDrafts(viewingSelf) {
return viewingSelf;
},
- @computed("viewingSelf", "currentUser.admin")
+ @discourseComputed("viewingSelf", "currentUser.admin")
showPrivateMessages(viewingSelf, isAdmin) {
return (
this.siteSettings.enable_personal_messages && (viewingSelf || isAdmin)
);
},
- @computed("viewingSelf", "currentUser.staff")
+ @discourseComputed("viewingSelf", "currentUser.staff")
showNotificationsTab(viewingSelf, staff) {
return viewingSelf || staff;
},
- @computed("model.name")
+ @discourseComputed("model.name")
nameFirst(name) {
- return (
- !this.get("siteSettings.prioritize_username_in_ux") &&
- name &&
- name.trim().length > 0
- );
+ return prioritizeNameInUx(name, this.siteSettings);
},
- @computed("model.badge_count")
+ @discourseComputed("model.badge_count")
showBadges(badgeCount) {
return Discourse.SiteSettings.enable_badges && badgeCount > 0;
},
- @computed()
+ @discourseComputed()
canInviteToForum() {
return User.currentProp("can_invite_to_forum");
},
- canDeleteUser: Ember.computed.and(
- "model.can_be_deleted",
- "model.can_delete_all_posts"
- ),
+ canDeleteUser: and("model.can_be_deleted", "model.can_delete_all_posts"),
- @computed("model.user_fields.@each.value")
+ @discourseComputed("model.user_fields.@each.value")
publicUserFields() {
const siteUserFields = this.site.get("user_fields");
- if (!Ember.isEmpty(siteUserFields)) {
+ if (!isEmpty(siteUserFields)) {
const userFields = this.get("model.user_fields");
return siteUserFields
.filterBy("show_on_profile", true)
.sortBy("position")
.map(field => {
- Ember.set(field, "dasherized_name", field.get("name").dasherize());
+ set(field, "dasherized_name", field.get("name").dasherize());
const value = userFields
? userFields[field.get("id").toString()]
: null;
- return Ember.isEmpty(value)
- ? null
- : Ember.Object.create({ value, field });
+ return isEmpty(value) ? null : EmberObject.create({ value, field });
})
.compact();
}
},
+ userNotificationLevel: computed(
+ "currentUser.ignored_ids",
+ "model.ignored",
+ "model.muted",
+ function() {
+ if (this.get("model.ignored")) {
+ return "changeToIgnored";
+ } else if (this.get("model.muted")) {
+ return "changeToMuted";
+ } else {
+ return "changeToNormal";
+ }
+ }
+ ),
+
actions: {
collapseProfile() {
this.set("forceExpand", false);
diff --git a/app/assets/javascripts/discourse/controllers/users.js.es6 b/app/assets/javascripts/discourse/controllers/users.js.es6
index 2fa9a8de59..820e50b0af 100644
--- a/app/assets/javascripts/discourse/controllers/users.js.es6
+++ b/app/assets/javascripts/discourse/controllers/users.js.es6
@@ -1,7 +1,11 @@
-import debounce from "discourse/lib/debounce";
+import { equal } from "@ember/object/computed";
+import { inject } from "@ember/controller";
+import Controller from "@ember/controller";
+import discourseDebounce from "discourse/lib/debounce";
+import { observes } from "discourse-common/utils/decorators";
-export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+export default Controller.extend({
+ application: inject(),
queryParams: ["period", "order", "asc", "name", "group", "exclude_usernames"],
period: "weekly",
order: "likes_received",
@@ -10,15 +14,17 @@ export default Ember.Controller.extend({
group: null,
exclude_usernames: null,
- showTimeRead: Ember.computed.equal("period", "all"),
+ showTimeRead: equal("period", "all"),
- _setName: debounce(function() {
+ @observes("nameInput")
+ _setName: discourseDebounce(function() {
this.set("name", this.nameInput);
- }, 500).observes("nameInput"),
+ }, 500),
+ @observes("model.canLoadMore")
_showFooter: function() {
this.set("application.showFooter", !this.get("model.canLoadMore"));
- }.observes("model.canLoadMore"),
+ },
actions: {
loadMore() {
diff --git a/app/assets/javascripts/discourse/helpers/application.js.es6 b/app/assets/javascripts/discourse/helpers/application.js.es6
index 0710d8b939..e6a88ded97 100644
--- a/app/assets/javascripts/discourse/helpers/application.js.es6
+++ b/app/assets/javascripts/discourse/helpers/application.js.es6
@@ -22,7 +22,10 @@ registerUnbound("number", (orig, params) => {
let title = I18n.toNumber(orig, { precision: 0 });
if (params.numberKey) {
- title = I18n.t(params.numberKey, { number: title, count: parseInt(orig) });
+ title = I18n.t(params.numberKey, {
+ number: title,
+ count: parseInt(orig, 10)
+ });
}
let classNames = "number";
diff --git a/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6 b/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6
index 30cb9aa3bd..b08e483b2e 100644
--- a/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6
+++ b/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6
@@ -1,8 +1,9 @@
+import { isEmpty } from "@ember/utils";
import { htmlHelper } from "discourse-common/lib/helpers";
import { avatarImg } from "discourse/lib/utilities";
export default htmlHelper((avatarTemplate, size) => {
- if (Ember.isEmpty(avatarTemplate)) {
+ if (isEmpty(avatarTemplate)) {
return "
";
} else {
return avatarImg({ size, avatarTemplate });
diff --git a/app/assets/javascripts/discourse/helpers/bound-avatar.js.es6 b/app/assets/javascripts/discourse/helpers/bound-avatar.js.es6
index 077e823220..6504dc1c08 100644
--- a/app/assets/javascripts/discourse/helpers/bound-avatar.js.es6
+++ b/app/assets/javascripts/discourse/helpers/bound-avatar.js.es6
@@ -1,12 +1,14 @@
+import { get } from "@ember/object";
+import { isEmpty } from "@ember/utils";
import { htmlHelper } from "discourse-common/lib/helpers";
import { avatarImg } from "discourse/lib/utilities";
import { addExtraUserClasses } from "discourse/helpers/user-avatar";
export default htmlHelper((user, size) => {
- if (Ember.isEmpty(user)) {
+ if (isEmpty(user)) {
return "
";
}
- const avatarTemplate = Ember.get(user, "avatar_template");
+ const avatarTemplate = get(user, "avatar_template");
return avatarImg(addExtraUserClasses(user, { size, avatarTemplate }));
});
diff --git a/app/assets/javascripts/discourse/helpers/category-link.js.es6 b/app/assets/javascripts/discourse/helpers/category-link.js.es6
index 3b62c82170..5199eda96a 100644
--- a/app/assets/javascripts/discourse/helpers/category-link.js.es6
+++ b/app/assets/javascripts/discourse/helpers/category-link.js.es6
@@ -1,10 +1,11 @@
+import { get } from "@ember/object";
import { registerUnbound } from "discourse-common/lib/helpers";
import { isRTL } from "discourse/lib/text-direction";
import { iconHTML } from "discourse-common/lib/icon-library";
+import Category from "discourse/models/category";
+import Site from "discourse/models/site";
-var get = Ember.get,
- escapeExpression = Handlebars.Utils.escapeExpression;
-
+let escapeExpression = Handlebars.Utils.escapeExpression;
let _renderer = defaultCategoryLinkRenderer;
export function replaceCategoryLinkRenderer(fn) {
@@ -24,7 +25,9 @@ function categoryStripe(color, classes) {
@param {String} [opts.url] The url that we want the category badge to link to.
@param {Boolean} [opts.allowUncategorized] If false, returns an empty string for the uncategorized category.
@param {Boolean} [opts.link] If false, the category badge will not be a link.
- @param {Boolean} [opts.hideParaent] If true, parent category will be hidden in the badge.
+ @param {Boolean} [opts.hideParent] If true, parent category will be hidden in the badge.
+ @param {Boolean} [opts.recursive] If true, the function will be called recursively for all parent categories
+ @param {Number} [opts.depth] Current category depth, used for limiting recursive calls
**/
export function categoryBadgeHTML(category, opts) {
opts = opts || {};
@@ -32,12 +35,18 @@ export function categoryBadgeHTML(category, opts) {
if (
!category ||
(!opts.allowUncategorized &&
- Ember.get(category, "id") ===
- Discourse.Site.currentProp("uncategorized_category_id") &&
+ get(category, "id") === Site.currentProp("uncategorized_category_id") &&
Discourse.SiteSettings.suppress_uncategorized_badge)
)
return "";
+ const depth = (opts.depth || 1) + 1;
+ if (opts.recursive && depth <= Discourse.SiteSettings.max_category_nesting) {
+ const parentCategory = Category.findById(category.parent_category_id);
+ opts.depth = depth;
+ return categoryBadgeHTML(parentCategory, opts) + _renderer(category, opts);
+ }
+
return _renderer(category, opts);
}
@@ -66,6 +75,9 @@ export function categoryLinkHTML(category, options) {
if (options.categoryStyle) {
categoryOptions.categoryStyle = options.categoryStyle;
}
+ if (options.recursive) {
+ categoryOptions.recursive = true;
+ }
}
return new Handlebars.SafeString(
categoryBadgeHTML(category, categoryOptions)
@@ -74,12 +86,21 @@ export function categoryLinkHTML(category, options) {
registerUnbound("category-link", categoryLinkHTML);
+function buildTopicCount(count) {
+ return `
× ${count} `;
+}
+
function defaultCategoryLinkRenderer(category, opts) {
- let description = get(category, "description_text");
+ let descriptionText = get(category, "description_text");
let restricted = get(category, "read_restricted");
let url = opts.url
? opts.url
- : Discourse.getURL("/c/") + Discourse.Category.slugFor(category);
+ : Discourse.getURL(
+ `/c/${Category.slugFor(category)}/${get(category, "id")}`
+ );
let href = opts.link === false ? "" : url;
let tagName = opts.link === false || opts.link === "false" ? "span" : "a";
let extraClasses = opts.extraClasses ? " " + opts.extraClasses : "";
@@ -89,9 +110,7 @@ function defaultCategoryLinkRenderer(category, opts) {
let categoryDir = "";
if (!opts.hideParent) {
- parentCat = Discourse.Category.findById(
- get(category, "parent_category_id")
- );
+ parentCat = Category.findById(get(category, "parent_category_id"));
}
const categoryStyle =
@@ -121,7 +140,7 @@ function defaultCategoryLinkRenderer(category, opts) {
'data-drop-close="true" class="' +
classNames +
'"' +
- (description ? 'title="' + escapeExpression(description) + '" ' : "") +
+ (descriptionText ? 'title="' + descriptionText + '" ' : "") +
">";
let categoryName = escapeExpression(get(category, "name"));
@@ -139,10 +158,19 @@ function defaultCategoryLinkRenderer(category, opts) {
}
html += "";
+ if (opts.topicCount && categoryStyle !== "box") {
+ html += buildTopicCount(opts.topicCount);
+ }
+
if (href) {
href = ` href="${href}" `;
}
extraClasses = categoryStyle ? categoryStyle + extraClasses : extraClasses;
- return `<${tagName} class="badge-wrapper ${extraClasses}" ${href}>${html}${tagName}>`;
+
+ let afterBadgeWrapper = "";
+ if (opts.topicCount && categoryStyle === "box") {
+ afterBadgeWrapper += buildTopicCount(opts.topicCount);
+ }
+ return `<${tagName} class="badge-wrapper ${extraClasses}" ${href}>${html}${tagName}>${afterBadgeWrapper}`;
}
diff --git a/app/assets/javascripts/discourse/helpers/dash-if-empty.js.es6 b/app/assets/javascripts/discourse/helpers/dash-if-empty.js.es6
index 18370df0d2..96d594e650 100644
--- a/app/assets/javascripts/discourse/helpers/dash-if-empty.js.es6
+++ b/app/assets/javascripts/discourse/helpers/dash-if-empty.js.es6
@@ -1,3 +1,4 @@
+import { isEmpty } from "@ember/utils";
import { htmlHelper } from "discourse-common/lib/helpers";
-export default htmlHelper(str => (Ember.isEmpty(str) ? "—" : str));
+export default htmlHelper(str => (isEmpty(str) ? "—" : str));
diff --git a/app/assets/javascripts/discourse/helpers/dasherize.js.es6 b/app/assets/javascripts/discourse/helpers/dasherize.js.es6
index b824006169..ce9aaed3af 100644
--- a/app/assets/javascripts/discourse/helpers/dasherize.js.es6
+++ b/app/assets/javascripts/discourse/helpers/dasherize.js.es6
@@ -1,5 +1,7 @@
+import Helper from "@ember/component/helper";
+
function dasherize([value]) {
return (value || "").replace(".", "-").dasherize();
}
-export default Ember.Helper.helper(dasherize);
+export default Helper.helper(dasherize);
diff --git a/app/assets/javascripts/discourse/helpers/editable-value.js.es6 b/app/assets/javascripts/discourse/helpers/editable-value.js.es6
index 799815590a..ae67cedf5e 100644
--- a/app/assets/javascripts/discourse/helpers/editable-value.js.es6
+++ b/app/assets/javascripts/discourse/helpers/editable-value.js.es6
@@ -1,10 +1,13 @@
+import { get } from "@ember/object";
+import Helper from "@ember/component/helper";
+
export function formatCurrency([reviewable, fieldId]) {
// The field `category_id` corresponds to `category`
if (fieldId === "category_id") {
fieldId = "category.id";
}
- let value = Ember.get(reviewable, fieldId);
+ let value = get(reviewable, fieldId);
// If it's an array, say tags, make a copy so we aren't mutating the original
if (Array.isArray(value)) {
@@ -14,4 +17,4 @@ export function formatCurrency([reviewable, fieldId]) {
return value;
}
-export default Ember.Helper.helper(formatCurrency);
+export default Helper.helper(formatCurrency);
diff --git a/app/assets/javascripts/discourse/helpers/float.js.es6 b/app/assets/javascripts/discourse/helpers/float.js.es6
new file mode 100644
index 0000000000..4d0fa564a2
--- /dev/null
+++ b/app/assets/javascripts/discourse/helpers/float.js.es6
@@ -0,0 +1,5 @@
+import { registerUnbound } from "discourse-common/lib/helpers";
+
+registerUnbound("float", function(n) {
+ return parseFloat(n).toFixed(1);
+});
diff --git a/app/assets/javascripts/discourse/helpers/icon-or-image.js.es6 b/app/assets/javascripts/discourse/helpers/icon-or-image.js.es6
index 8e48171eee..f038d40eb4 100644
--- a/app/assets/javascripts/discourse/helpers/icon-or-image.js.es6
+++ b/app/assets/javascripts/discourse/helpers/icon-or-image.js.es6
@@ -1,12 +1,13 @@
+import { isEmpty } from "@ember/utils";
import { htmlHelper } from "discourse-common/lib/helpers";
import { iconHTML, convertIconClass } from "discourse-common/lib/icon-library";
export default htmlHelper(function({ icon, image }) {
- if (!Ember.isEmpty(image)) {
+ if (!isEmpty(image)) {
return `
`;
}
- if (Ember.isEmpty(icon) || icon.indexOf("fa-") < 0) {
+ if (isEmpty(icon)) {
return "";
}
diff --git a/app/assets/javascripts/discourse/helpers/raw-plugin-outlet.js.es6 b/app/assets/javascripts/discourse/helpers/raw-plugin-outlet.js.es6
index 62de97d18c..4aa8484a55 100644
--- a/app/assets/javascripts/discourse/helpers/raw-plugin-outlet.js.es6
+++ b/app/assets/javascripts/discourse/helpers/raw-plugin-outlet.js.es6
@@ -1,6 +1,7 @@
import { rawConnectorsFor } from "discourse/lib/plugin-connectors";
+import RawHandlebars from "discourse-common/lib/raw-handlebars";
-Handlebars.registerHelper("raw-plugin-outlet", function(args) {
+RawHandlebars.registerHelper("raw-plugin-outlet", function(args) {
const connectors = rawConnectorsFor(args.hash.name);
if (connectors.length) {
const output = connectors.map(c => c.template({ context: this }));
diff --git a/app/assets/javascripts/discourse/helpers/reviewable-status.js.es6 b/app/assets/javascripts/discourse/helpers/reviewable-status.js.es6
index 5807cb0e64..4dcc12d3a4 100644
--- a/app/assets/javascripts/discourse/helpers/reviewable-status.js.es6
+++ b/app/assets/javascripts/discourse/helpers/reviewable-status.js.es6
@@ -1,6 +1,5 @@
import { htmlHelper } from "discourse-common/lib/helpers";
import { iconHTML } from "discourse-common/lib/icon-library";
-
import {
PENDING,
APPROVED,
@@ -20,7 +19,7 @@ function dataFor(status) {
case IGNORED:
return { icon: "external-link-alt", name: "ignored" };
case DELETED:
- return { icon: "trash", name: "deleted" };
+ return { icon: "trash-alt", name: "deleted" };
}
}
diff --git a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6
index ff09e4e2fc..b46a49d6ff 100644
--- a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6
+++ b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6
@@ -1,3 +1,4 @@
+import { get } from "@ember/object";
import { registerUnbound } from "discourse-common/lib/helpers";
import { avatarImg, formatUsername } from "discourse/lib/utilities";
@@ -30,9 +31,9 @@ function renderAvatar(user, options) {
options = options || {};
if (user) {
- const name = Ember.get(user, options.namePath || "name");
- const username = Ember.get(user, options.usernamePath || "username");
- const avatarTemplate = Ember.get(
+ const name = get(user, options.namePath || "name");
+ const username = get(user, options.usernamePath || "username");
+ const avatarTemplate = get(
user,
options.avatarTemplatePath || "avatar_template"
);
@@ -46,11 +47,11 @@ function renderAvatar(user, options) {
let title = options.title;
if (!title && !options.ignoreTitle) {
// first try to get a title
- title = Ember.get(user, "title");
+ title = get(user, "title");
// if there was no title provided
if (!title) {
// try to retrieve a description
- const description = Ember.get(user, "description");
+ const description = get(user, "description");
// if a description has been provided
if (description && description.length > 0) {
// preprend the username before the description
@@ -61,7 +62,7 @@ function renderAvatar(user, options) {
return avatarImg({
size: options.imageSize,
- extraClasses: Ember.get(user, "extras") || options.extraClasses,
+ extraClasses: get(user, "extras") || options.extraClasses,
title: title || displayName,
avatarTemplate: avatarTemplate
});
diff --git a/app/assets/javascripts/discourse/initializers/asset-version.js.es6 b/app/assets/javascripts/discourse/initializers/asset-version.js.es6
index 21faedd650..f685d1b091 100644
--- a/app/assets/javascripts/discourse/initializers/asset-version.js.es6
+++ b/app/assets/javascripts/discourse/initializers/asset-version.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
// Subscribe to "asset-version" change events via the Message Bus
export default {
name: "asset-version",
@@ -16,7 +17,7 @@ export default {
if (!timeoutIsSet && Discourse.get("requiresRefresh")) {
// Since we can do this transparently for people browsing the forum
// hold back the message 24 hours.
- Ember.run.later(() => {
+ later(() => {
bootbox.confirm(I18n.t("assets_changed_confirm"), function(result) {
if (result) {
document.location.reload();
diff --git a/app/assets/javascripts/discourse/initializers/auth-complete.js.es6 b/app/assets/javascripts/discourse/initializers/auth-complete.js.es6
index cecf831405..31101d9010 100644
--- a/app/assets/javascripts/discourse/initializers/auth-complete.js.es6
+++ b/app/assets/javascripts/discourse/initializers/auth-complete.js.es6
@@ -1,14 +1,11 @@
+import { next } from "@ember/runloop";
export default {
name: "auth-complete",
after: "inject-objects",
initialize(container) {
let lastAuthResult;
- if (window.location.search.indexOf("authComplete=true") !== -1) {
- // Happens when a popup social login loses connection to the parent window
- lastAuthResult = localStorage.getItem("lastAuthResult");
- localStorage.removeItem("lastAuthResult");
- } else if (document.getElementById("data-authentication")) {
+ if (document.getElementById("data-authentication")) {
// Happens for full screen logins
lastAuthResult = document.getElementById("data-authentication").dataset
.authenticationData;
@@ -17,7 +14,7 @@ export default {
if (lastAuthResult) {
const router = container.lookup("router:main");
router.one("didTransition", () => {
- Ember.run.next(() =>
+ next(() =>
Discourse.authenticationComplete(JSON.parse(lastAuthResult))
);
});
diff --git a/app/assets/javascripts/discourse/initializers/auto-load-modules.js.es6 b/app/assets/javascripts/discourse/initializers/auto-load-modules.js.es6
index ae3c15e420..3e1505a5dd 100644
--- a/app/assets/javascripts/discourse/initializers/auto-load-modules.js.es6
+++ b/app/assets/javascripts/discourse/initializers/auto-load-modules.js.es6
@@ -1,4 +1,6 @@
import { registerHelpers } from "discourse-common/lib/helpers";
+import RawHandlebars from "discourse-common/lib/raw-handlebars";
+import { registerRawHelpers } from "discourse-common/lib/raw-handlebars-helpers";
export function autoLoadModules(container, registry) {
Object.keys(requirejs.entries).forEach(entry => {
@@ -10,6 +12,7 @@ export function autoLoadModules(container, registry) {
}
});
registerHelpers(registry);
+ registerRawHelpers(RawHandlebars, Handlebars);
}
export default {
diff --git a/app/assets/javascripts/discourse/initializers/avatar-select.js.es6 b/app/assets/javascripts/discourse/initializers/avatar-select.js.es6
index f35ee67540..9eaac83db9 100644
--- a/app/assets/javascripts/discourse/initializers/avatar-select.js.es6
+++ b/app/assets/javascripts/discourse/initializers/avatar-select.js.es6
@@ -10,7 +10,7 @@ export default {
).selectable_avatars_enabled;
container
- .lookup("app-events:main")
+ .lookup("service:app-events")
.on("show-avatar-select", this, "_showAvatarSelect");
},
diff --git a/app/assets/javascripts/discourse/initializers/badging.js.es6 b/app/assets/javascripts/discourse/initializers/badging.js.es6
index b80416c231..f406604dfa 100644
--- a/app/assets/javascripts/discourse/initializers/badging.js.es6
+++ b/app/assets/javascripts/discourse/initializers/badging.js.es6
@@ -4,7 +4,7 @@ export default {
after: "message-bus",
initialize(container) {
- if (!window.ExperimentalBadge) return; // must have the Badging API
+ if (!navigator.setAppBadge) return; // must have the Badging API
const user = container.lookup("current-user:main");
if (!user) return; // must be logged in
@@ -13,11 +13,11 @@ export default {
user.unread_notifications + user.unread_private_messages;
container
- .lookup("app-events:main")
+ .lookup("service:app-events")
.on("notifications:changed", this, "_updateBadge");
},
_updateBadge() {
- window.ExperimentalBadge.set(this.notifications);
+ navigator.setAppBadge(this.notifications);
}
};
diff --git a/app/assets/javascripts/discourse/initializers/banner.js.es6 b/app/assets/javascripts/discourse/initializers/banner.js.es6
index 46e3899b30..ccec6a95b4 100644
--- a/app/assets/javascripts/discourse/initializers/banner.js.es6
+++ b/app/assets/javascripts/discourse/initializers/banner.js.es6
@@ -1,3 +1,4 @@
+import EmberObject from "@ember/object";
import PreloadStore from "preload-store";
export default {
@@ -5,7 +6,7 @@ export default {
after: "message-bus",
initialize(container) {
- const banner = Ember.Object.create(PreloadStore.get("banner") || {}),
+ const banner = EmberObject.create(PreloadStore.get("banner") || {}),
site = container.lookup("site:main");
site.set("banner", banner);
@@ -16,7 +17,7 @@ export default {
}
messageBus.subscribe("/site/banner", function(ban) {
- site.set("banner", Ember.Object.create(ban || {}));
+ site.set("banner", EmberObject.create(ban || {}));
});
}
};
diff --git a/app/assets/javascripts/discourse/initializers/live-development.js.es6 b/app/assets/javascripts/discourse/initializers/live-development.js.es6
index 8104bdfd0a..38afdbc9b7 100644
--- a/app/assets/javascripts/discourse/initializers/live-development.js.es6
+++ b/app/assets/javascripts/discourse/initializers/live-development.js.es6
@@ -1,5 +1,6 @@
import DiscourseURL from "discourse/lib/url";
import { currentThemeIds, refreshCSS } from "discourse/lib/theme-selector";
+import ENV from "discourse-common/config/environment";
// Use the message bus for live reloading of components for faster development.
export default {
@@ -12,7 +13,10 @@ export default {
window.location.search.indexOf("?preview_theme_id=") === 0
) {
// force preview theme id to always be carried along
- const themeId = parseInt(window.location.search.slice(18).split("&")[0]);
+ const themeId = parseInt(
+ window.location.search.slice(18).split("&")[0],
+ 10
+ );
if (!isNaN(themeId)) {
const patchState = function(f) {
const patched = window.history[f];
@@ -43,7 +47,7 @@ export default {
});
// Useful to export this for debugging purposes
- if (Discourse.Environment === "development" && !Ember.testing) {
+ if (Discourse.Environment === "development" && ENV.environment !== "test") {
window.DiscourseURL = DiscourseURL;
}
diff --git a/app/assets/javascripts/discourse/initializers/localization.js.es6 b/app/assets/javascripts/discourse/initializers/localization.js.es6
index fc9eee1536..cbe50e53d5 100644
--- a/app/assets/javascripts/discourse/initializers/localization.js.es6
+++ b/app/assets/javascripts/discourse/initializers/localization.js.es6
@@ -1,5 +1,3 @@
-import PreloadStore from "preload-store";
-
export default {
name: "localization",
after: "inject-objects",
@@ -21,20 +19,9 @@ export default {
}
// Merge any overrides into our object
- const overrides = PreloadStore.get("translationOverrides") || {};
+ const overrides = I18n._overrides || {};
Object.keys(overrides).forEach(k => {
const v = overrides[k];
-
- // Special case: Message format keys are functions
- if (/_MF$/.test(k)) {
- k = k.replace(/^[a-z_]*js\./, "");
- I18n._compiledMFs[k] = new Function(
- "transKey",
- `return (${v})(transKey);`
- );
- return;
- }
-
k = k.replace("admin_js", "js");
const segs = k.split(".");
@@ -52,6 +39,14 @@ export default {
}
});
+ const mfOverrides = I18n._mfOverrides || {};
+ Object.keys(mfOverrides).forEach(k => {
+ const v = mfOverrides[k];
+
+ k = k.replace(/^[a-z_]*js\./, "");
+ I18n._compiledMFs[k] = v;
+ });
+
bootbox.addLocale(I18n.currentLocale(), {
OK: I18n.t("composer.modal_ok"),
CANCEL: I18n.t("composer.modal_cancel"),
diff --git a/app/assets/javascripts/discourse/initializers/mobile.js.es6 b/app/assets/javascripts/discourse/initializers/mobile.js.es6
index a536398e94..b0708fca85 100644
--- a/app/assets/javascripts/discourse/initializers/mobile.js.es6
+++ b/app/assets/javascripts/discourse/initializers/mobile.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
import Mobile from "discourse/lib/mobile";
import { setResolverOption } from "discourse-common/resolver";
import { isAppWebview, postRNWebviewMessage } from "discourse/lib/utilities";
@@ -17,7 +18,7 @@ export default {
setResolverOption("mobileView", Mobile.mobileView);
if (isAppWebview()) {
- Ember.run.later(() => {
+ later(() => {
postRNWebviewMessage(
"headerBg",
$(".d-header").css("background-color")
diff --git a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6
index 06653ce175..71fa6f25ae 100644
--- a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6
+++ b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6
@@ -1,6 +1,7 @@
import { cleanDOM } from "discourse/lib/clean-dom";
import {
startPageTracking,
+ resetPageTracking,
googleTagManagerPageChanged
} from "discourse/lib/page-tracker";
import { viewTrackingRequired } from "discourse/lib/ajax";
@@ -16,7 +17,7 @@ export default {
router.on("routeWillChange", viewTrackingRequired);
router.on("routeDidChange", cleanDOM);
- let appEvents = container.lookup("app-events:main");
+ let appEvents = container.lookup("service:app-events");
startPageTracking(router, appEvents);
@@ -49,5 +50,9 @@ export default {
}
});
}
+ },
+
+ teardown() {
+ resetPageTracking();
}
};
diff --git a/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 b/app/assets/javascripts/discourse/initializers/post-decorations.js.es6
index a9e1cc24d0..35504de3bb 100644
--- a/app/assets/javascripts/discourse/initializers/post-decorations.js.es6
+++ b/app/assets/javascripts/discourse/initializers/post-decorations.js.es6
@@ -26,7 +26,10 @@ export default {
const players = $("audio", $elem);
if (players.length) {
players.on("play", () => {
- const postId = parseInt($elem.closest("article").data("post-id"));
+ const postId = parseInt(
+ $elem.closest("article").data("post-id"),
+ 10
+ );
if (postId) {
api.preventCloak(postId);
}
diff --git a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6
index b78044e11f..c0099d7f30 100644
--- a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6
+++ b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6
@@ -9,7 +9,8 @@ export default {
const isSupported = isSecured && "serviceWorker" in navigator;
if (isSupported) {
- const isApple = !!navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i);
+ const caps = Discourse.__container__.lookup("capabilities:main");
+ const isApple = caps.isSafari || caps.isIOS || caps.isIpadOS;
if (Discourse.ServiceWorkerURL && !isApple) {
navigator.serviceWorker.getRegistrations().then(registrations => {
diff --git a/app/assets/javascripts/discourse/initializers/relative-ages.js.es6 b/app/assets/javascripts/discourse/initializers/relative-ages.js.es6
index 30a0118d3e..aac39ca3c6 100644
--- a/app/assets/javascripts/discourse/initializers/relative-ages.js.es6
+++ b/app/assets/javascripts/discourse/initializers/relative-ages.js.es6
@@ -3,9 +3,17 @@ import { updateRelativeAge } from "discourse/lib/formatter";
// Updates the relative ages of dates on the screen.
export default {
name: "relative-ages",
- initialize: function() {
- setInterval(function() {
+
+ initialize() {
+ this._interval = setInterval(function() {
updateRelativeAge($(".relative-date"));
}, 60 * 1000);
+ },
+
+ teardown() {
+ if (this._interval) {
+ clearInterval(this._interval);
+ this._interval = null;
+ }
}
};
diff --git a/app/assets/javascripts/discourse/initializers/sharing-sources.js.es6 b/app/assets/javascripts/discourse/initializers/sharing-sources.js.es6
index b619969ac5..d3ca4fde2a 100644
--- a/app/assets/javascripts/discourse/initializers/sharing-sources.js.es6
+++ b/app/assets/javascripts/discourse/initializers/sharing-sources.js.es6
@@ -22,7 +22,7 @@ export default {
Sharing.addSource({
id: "facebook",
- icon: "fab-facebook-square",
+ icon: "fab-facebook",
title: I18n.t("share.facebook"),
generateUrl: function(link, title) {
return (
diff --git a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6
index 44f4025408..8efb204c36 100644
--- a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6
+++ b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6
@@ -1,3 +1,4 @@
+import EmberObject from "@ember/object";
// Subscribes to user events on the message bus
import {
init as initDesktopNotifications,
@@ -10,6 +11,8 @@ import {
unsubscribe as unsubscribePushNotifications,
isPushNotificationsEnabled
} from "discourse/lib/push-notifications";
+import { set } from "@ember/object";
+import ENV from "discourse-common/config/environment";
export default {
name: "subscribe-user-notifications",
@@ -18,7 +21,7 @@ export default {
initialize(container) {
const user = container.lookup("current-user:main");
const bus = container.lookup("message-bus:main");
- const appEvents = container.lookup("app-events:main");
+ const appEvents = container.lookup("service:app-events");
if (user) {
bus.subscribe("/reviewable_counts", data => {
@@ -82,7 +85,7 @@ export default {
}
oldNotifications.insertAt(
insertPosition,
- Ember.Object.create(lastNotification)
+ EmberObject.create(lastNotification)
);
}
@@ -119,24 +122,19 @@ export default {
});
bus.subscribe("/client_settings", data =>
- Ember.set(siteSettings, data.name, data.value)
+ set(siteSettings, data.name, data.value)
);
bus.subscribe("/refresh_client", data =>
Discourse.set("assetVersion", data)
);
- if (!Ember.testing) {
+ if (ENV.environment !== "test") {
bus.subscribe(alertChannel(user), data => onNotification(data, user));
initDesktopNotifications(bus, appEvents);
if (isPushNotificationsEnabled(user, site.mobileView)) {
disableDesktopNotifications();
- registerPushNotifications(
- Discourse.User.current(),
- site.mobileView,
- router,
- appEvents
- );
+ registerPushNotifications(user, site.mobileView, router, appEvents);
} else {
unsubscribePushNotifications(user);
}
diff --git a/app/assets/javascripts/discourse/initializers/title-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/title-notifications.js.es6
index ba24755d23..9801e1f688 100644
--- a/app/assets/javascripts/discourse/initializers/title-notifications.js.es6
+++ b/app/assets/javascripts/discourse/initializers/title-notifications.js.es6
@@ -6,15 +6,20 @@ export default {
const user = container.lookup("current-user:main");
if (!user) return; // must be logged in
- this.notifications =
- user.unread_notifications + user.unread_private_messages;
+ this.container = container;
container
- .lookup("app-events:main")
+ .lookup("service:app-events")
.on("notifications:changed", this, "_updateTitle");
},
_updateTitle() {
- Discourse.updateNotificationCount(this.notifications);
+ const user = this.container.lookup("current-user:main");
+ if (!user) return; // must be logged in
+
+ const notifications =
+ user.unread_notifications + user.unread_private_messages;
+
+ Discourse.updateNotificationCount(notifications);
}
};
diff --git a/app/assets/javascripts/discourse/lib/ajax.js.es6 b/app/assets/javascripts/discourse/lib/ajax.js.es6
index c5468e1195..54de18d4c8 100644
--- a/app/assets/javascripts/discourse/lib/ajax.js.es6
+++ b/app/assets/javascripts/discourse/lib/ajax.js.es6
@@ -1,5 +1,9 @@
+import { run } from "@ember/runloop";
import pageVisible from "discourse/lib/page-visible";
import logout from "discourse/lib/logout";
+import Session from "discourse/models/session";
+import { Promise } from "rsvp";
+import Site from "discourse/models/site";
let _trackView = false;
let _transientHeader = null;
@@ -14,7 +18,7 @@ export function viewTrackingRequired() {
}
export function handleLogoff(xhr) {
- if (xhr.getResponseHeader("Discourse-Logged-Out") && !_showingLogout) {
+ if (xhr && xhr.getResponseHeader("Discourse-Logged-Out") && !_showingLogout) {
_showingLogout = true;
const messageBus = Discourse.__container__.lookup("message-bus:main");
messageBus.stop();
@@ -40,6 +44,12 @@ function handleRedirect(data) {
}
}
+export function updateCsrfToken() {
+ return ajax("/session/csrf").then(result => {
+ Session.currentProp("csrfToken", result.csrf);
+ });
+}
+
/**
Our own $.ajax method. Makes sure the .then method executes in an Ember runloop
for performance reasons. Also automatically adjusts the URL to support installs
@@ -90,10 +100,10 @@ export function ajax() {
handleRedirect(data);
handleLogoff(xhr);
- Ember.run(() => {
- Discourse.Site.currentProp(
+ run(() => {
+ Site.currentProp(
"isReadOnly",
- !!xhr.getResponseHeader("Discourse-Readonly")
+ !!(xhr && xhr.getResponseHeader("Discourse-Readonly"))
);
});
@@ -101,7 +111,7 @@ export function ajax() {
data = { result: data, xhr: xhr };
}
- Ember.run(null, resolve, data);
+ run(null, resolve, data);
};
args.error = (xhr, textStatus, errorThrown) => {
@@ -112,7 +122,7 @@ export function ajax() {
// note: for bad CSRF we don't loop an extra request right away.
// this allows us to eliminate the possibility of having a loop.
if (xhr.status === 403 && xhr.responseText === '["BAD CSRF"]') {
- Discourse.Session.current().set("csrfToken", null);
+ Session.current().set("csrfToken", null);
}
// If it's a parsererror, don't reject
@@ -122,7 +132,7 @@ export function ajax() {
xhr.jqTextStatus = textStatus;
xhr.requestedUrl = url;
- Ember.run(null, reject, {
+ run(null, reject, {
jqXHR: xhr,
textStatus: textStatus,
errorThrown: errorThrown
@@ -140,7 +150,7 @@ export function ajax() {
}
if (args.type === "GET" && args.cache !== true) {
- args.cache = false;
+ args.cache = true; // Disable JQuery cache busting param, which was created to deal with IE8
}
ajaxObj = $.ajax(Discourse.getURL(url), args);
@@ -154,18 +164,15 @@ export function ajax() {
args.type &&
args.type.toUpperCase() !== "GET" &&
url !== Discourse.getURL("/clicks/track") &&
- !Discourse.Session.currentProp("csrfToken")
+ !Session.currentProp("csrfToken")
) {
- promise = new Ember.RSVP.Promise((resolve, reject) => {
- ajaxObj = $.ajax(Discourse.getURL("/session/csrf"), {
- cache: false
- }).done(result => {
- Discourse.Session.currentProp("csrfToken", result.csrf);
+ promise = new Promise((resolve, reject) => {
+ ajaxObj = updateCsrfToken().then(() => {
performAjax(resolve, reject);
});
});
} else {
- promise = new Ember.RSVP.Promise(performAjax);
+ promise = new Promise(performAjax);
}
promise.abort = () => {
diff --git a/app/assets/javascripts/discourse/lib/app-events.js.es6 b/app/assets/javascripts/discourse/lib/app-events.js.es6
deleted file mode 100644
index aff71a2a40..0000000000
--- a/app/assets/javascripts/discourse/lib/app-events.js.es6
+++ /dev/null
@@ -1,48 +0,0 @@
-import deprecated from "discourse-common/lib/deprecated";
-
-export default Ember.Object.extend(Ember.Evented, {
- _events: {},
-
- on() {
- if (arguments.length === 2) {
- let [name, fn] = arguments;
- let target = {};
- this._events[name] = this._events[name] || [];
- this._events[name].push({ target, fn });
-
- this._super(name, target, fn);
- } else if (arguments.length === 3) {
- let [name, target, fn] = arguments;
- this._events[name] = this._events[name] || [];
- this._events[name].push({ target, fn });
-
- this._super(...arguments);
- }
- return this;
- },
-
- off() {
- let name = arguments[0];
- let fn = arguments[2];
-
- if (this._events[name]) {
- if (arguments.length === 1) {
- deprecated(
- "Removing all event listeners at once is deprecated, please remove each listener individually."
- );
-
- this._events[name].forEach(ref => {
- this._super(name, ref.target, ref.fn);
- });
- delete this._events[name];
- } else if (arguments.length === 3) {
- this._super(...arguments);
-
- this._events[name] = this._events[name].filter(e => e.fn !== fn);
- if (this._events[name].length === 0) delete this._events[name];
- }
- }
-
- return this;
- }
-});
diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6
index 18fc9a98e2..aeb7b836a2 100644
--- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6
+++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6
@@ -1,12 +1,16 @@
+import { cancel } from "@ember/runloop";
+import { later } from "@ember/runloop";
+import { iconHTML } from "discourse-common/lib/icon-library";
+import { setCaretPosition, caretPosition } from "discourse/lib/utilities";
+import Site from "discourse/models/site";
+
/**
This is a jQuery plugin to support autocompleting values in our text fields.
@module $.fn.autocomplete
**/
-import { iconHTML } from "discourse-common/lib/icon-library";
-export const CANCELLED_STATUS = "__CANCELLED";
-import { setCaretPosition, caretPosition } from "discourse/lib/utilities";
+export const CANCELLED_STATUS = "__CANCELLED";
const allowedLettersRegex = /[\s\t\[\{\(\/]/;
const keys = {
@@ -43,7 +47,7 @@ export default function(options) {
if (this.length === 0) return;
if (options === "destroy" || options.updateData) {
- Ember.run.cancel(inputTimeout);
+ cancel(inputTimeout);
$(this)
.off("keyup.autocomplete")
@@ -208,11 +212,9 @@ export default function(options) {
}
if (options.single && !options.width) {
- this.css("width", "100%");
+ this.attr("class", `${this.attr("class")} fullwidth-input`);
} else if (options.width) {
this.css("width", options.width);
- } else {
- this.width(150);
}
this.attr(
@@ -317,7 +319,7 @@ export default function(options) {
vOffset = BELOW;
}
- if (Discourse.Site.currentProp("mobileView")) {
+ if (Site.currentProp("mobileView")) {
if (me.height() / 2 >= pos.top) {
vOffset = BELOW;
}
@@ -402,7 +404,7 @@ export default function(options) {
$(this).on("click.autocomplete", () => closeAutocomplete());
$(this).on("paste.autocomplete", () => {
- Ember.run.later(() => me.trigger("keydown"), 50);
+ later(() => me.trigger("keydown"), 50);
});
function checkTriggerRule(opts) {
@@ -455,8 +457,8 @@ export default function(options) {
if (options.allowAny) {
// saves us wiring up a change event as well
- Ember.run.cancel(inputTimeout);
- inputTimeout = Ember.run.later(function() {
+ cancel(inputTimeout);
+ inputTimeout = later(function() {
if (inputSelectedItems.length === 0) {
inputSelectedItems.push("");
}
diff --git a/app/assets/javascripts/discourse/lib/cached-topic-list.js.es6 b/app/assets/javascripts/discourse/lib/cached-topic-list.js.es6
new file mode 100644
index 0000000000..78cd2c0c51
--- /dev/null
+++ b/app/assets/javascripts/discourse/lib/cached-topic-list.js.es6
@@ -0,0 +1,12 @@
+export function findOrResetCachedTopicList(session, filter) {
+ const lastTopicList = session.get("topicList");
+ if (lastTopicList && lastTopicList.filter === filter) {
+ return lastTopicList;
+ } else {
+ session.setProperties({
+ topicList: null,
+ topicListScrollPosition: null
+ });
+ return false;
+ }
+}
diff --git a/app/assets/javascripts/discourse/lib/category-hashtags.js.es6 b/app/assets/javascripts/discourse/lib/category-hashtags.js.es6
index cac265f259..28042fff16 100644
--- a/app/assets/javascripts/discourse/lib/category-hashtags.js.es6
+++ b/app/assets/javascripts/discourse/lib/category-hashtags.js.es6
@@ -1,5 +1,9 @@
export const SEPARATOR = ":";
-import { caretRowCol } from "discourse/lib/utilities";
+import {
+ caretRowCol,
+ caretPosition,
+ inCodeBlock
+} from "discourse/lib/utilities";
export function replaceSpan($elem, categorySlug, categoryLink) {
$elem.replaceWith(
@@ -21,10 +25,14 @@ export function categoryHashtagTriggerRule(textarea, opts) {
if (/^#{1}\w+/.test(line)) return false;
}
- if (col < 6) {
- // Don't trigger autocomplete when ATX-style headers are used
- return line.slice(0, col) !== "#".repeat(col);
- } else {
- return true;
+ // Don't trigger autocomplete when ATX-style headers are used
+ if (col < 6 && line.slice(0, col) === "#".repeat(col)) {
+ return false;
}
+
+ if (inCodeBlock(textarea.value, caretPosition(textarea))) {
+ return false;
+ }
+
+ return true;
}
diff --git a/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 b/app/assets/javascripts/discourse/lib/category-tag-search.js.es6
index 164839fabf..ca1ddcc85e 100644
--- a/app/assets/javascripts/discourse/lib/category-tag-search.js.es6
+++ b/app/assets/javascripts/discourse/lib/category-tag-search.js.es6
@@ -1,8 +1,9 @@
-import debounce from "discourse/lib/debounce";
+import discourseDebounce from "discourse/lib/debounce";
import { CANCELLED_STATUS } from "discourse/lib/autocomplete";
import Category from "discourse/models/category";
import { TAG_HASHTAG_POSTFIX } from "discourse/lib/tag-hashtags";
import { SEPARATOR } from "discourse/lib/category-hashtags";
+import { Promise } from "rsvp";
var cache = {};
var cacheTime;
@@ -15,12 +16,12 @@ function updateCache(term, results) {
}
function searchTags(term, categories, limit) {
- return new Ember.RSVP.Promise(resolve => {
+ return new Promise(resolve => {
const clearPromise = setTimeout(() => {
resolve(CANCELLED_STATUS);
}, 5000);
- const debouncedSearch = debounce((q, cats, resultFunc) => {
+ const debouncedSearch = discourseDebounce((q, cats, resultFunc) => {
oldSearch = $.ajax(Discourse.getURL("/tags/filter/search"), {
type: "GET",
cache: true,
diff --git a/app/assets/javascripts/discourse/lib/clean-dom.js.es6 b/app/assets/javascripts/discourse/lib/clean-dom.js.es6
index 59157189f6..82cdb2d537 100644
--- a/app/assets/javascripts/discourse/lib/clean-dom.js.es6
+++ b/app/assets/javascripts/discourse/lib/clean-dom.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
function _clean() {
if (window.MiniProfiler) {
window.MiniProfiler.pageTransition();
@@ -30,10 +31,10 @@ function _clean() {
}
// TODO: Avoid container lookup here
- const appEvents = Discourse.__container__.lookup("app-events:main");
+ const appEvents = Discourse.__container__.lookup("service:app-events");
appEvents.trigger("dom:clean");
}
export function cleanDOM() {
- Ember.run.scheduleOnce("afterRender", _clean);
+ scheduleOnce("afterRender", _clean);
}
diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6
index 42a65623a6..534c04a6c4 100644
--- a/app/assets/javascripts/discourse/lib/click-track.js.es6
+++ b/app/assets/javascripts/discourse/lib/click-track.js.es6
@@ -1,24 +1,20 @@
+import { later } from "@ember/runloop";
import { ajax } from "discourse/lib/ajax";
import DiscourseURL from "discourse/lib/url";
import { wantsNewWindow } from "discourse/lib/intercept-click";
import { selectedText } from "discourse/lib/utilities";
+import { Promise } from "rsvp";
+import ENV from "discourse-common/config/environment";
+import User from "discourse/models/user";
export function isValidLink($link) {
- // Do not track:
- // - lightboxes
- // - links with disabled tracking
- // - category links
- // - quote back button
+ // .hashtag == category/tag link
+ // .back == quote back ^ button
if ($link.is(".lightbox, .no-track-link, .hashtag, .back")) {
return false;
}
- // Do not track links in quotes or in elided part
- if ($link.parents("aside.quote, .elided").length !== 0) {
- return false;
- }
-
- if ($link.parents(".expanded-embed").length !== 0) {
+ if ($link.parents("aside.quote, .elided, .expanded-embed").length !== 0) {
return false;
}
@@ -61,7 +57,7 @@ export default {
// Warn the user if they cannot download the file.
if (
Discourse.SiteSettings.prevent_anons_from_downloading_files &&
- !Discourse.User.current()
+ !User.current()
) {
bootbox.alert(I18n.t("post.errors.attachment_download_requires_login"));
} else if (wantsNewWindow(e)) {
@@ -80,7 +76,7 @@ export default {
const postId = $article.data("post-id");
const topicId = $("#topic").data("topic-id") || $article.data("topic-id");
const userId = $link.data("user-id") || $article.data("user-id");
- const ownLink = userId && userId === Discourse.User.currentProp("id");
+ const ownLink = userId && userId === User.currentProp("id");
// Update badge clicks unless it's our own.
if (tracking && !ownLink) {
@@ -95,9 +91,9 @@ export default {
}
}
- let trackPromise = Ember.RSVP.resolve();
+ let trackPromise = Promise.resolve();
if (tracking) {
- if (!Ember.testing && navigator.sendBeacon) {
+ if (ENV.environment !== "test" && navigator.sendBeacon) {
const data = new FormData();
data.append("url", href);
data.append("post_id", postId);
@@ -116,9 +112,7 @@ export default {
}
const isInternal = DiscourseURL.isInternal(href);
- const openExternalInNewTab = Discourse.User.currentProp(
- "external_links_in_new_tab"
- );
+ const openExternalInNewTab = User.currentProp("external_links_in_new_tab");
if (!wantsNewWindow(e)) {
if (!isInternal && openExternalInNewTab) {
@@ -134,7 +128,7 @@ export default {
$link.attr("href", null);
$link.data("auto-route", true);
- Ember.run.later(() => {
+ later(() => {
$link.removeClass("no-href");
$link.attr("href", $link.data("href"));
$link.data("href", null);
diff --git a/app/assets/javascripts/discourse/lib/computed.js.es6 b/app/assets/javascripts/discourse/lib/computed.js.es6
index 2683d944fe..992029d293 100644
--- a/app/assets/javascripts/discourse/lib/computed.js.es6
+++ b/app/assets/javascripts/discourse/lib/computed.js.es6
@@ -6,7 +6,7 @@ import addonFmt from "ember-addons/fmt";
@method propertyEqual
@params {String} p1 the first property
@params {String} p2 the second property
- @return {Function} computedProperty function
+ @return {Function} discourseComputedProperty function
**/
export function propertyEqual(p1, p2) {
@@ -21,7 +21,7 @@ export function propertyEqual(p1, p2) {
@method propertyNotEqual
@params {String} p1 the first property
@params {String} p2 the second property
- @return {Function} computedProperty function
+ @return {Function} discourseComputedProperty function
**/
export function propertyNotEqual(p1, p2) {
return Ember.computed(p1, p2, function() {
@@ -47,14 +47,13 @@ export function propertyLessThan(p1, p2) {
@method i18n
@params {String} properties* to format
@params {String} format the i18n format string
- @return {Function} computedProperty function
+ @return {Function} discourseComputedProperty function
**/
export function i18n(...args) {
const format = args.pop();
- const computed = Ember.computed(function() {
+ return Ember.computed(...args, function() {
return I18n.t(addonFmt(format, ...args.map(a => this.get(a))));
});
- return computed.property.apply(computed, args);
}
/**
@@ -64,14 +63,13 @@ export function i18n(...args) {
@method fmt
@params {String} properties* to format
@params {String} format the format string
- @return {Function} computedProperty function
+ @return {Function} discourseComputedProperty function
**/
export function fmt(...args) {
const format = args.pop();
- const computed = Ember.computed(function() {
+ return Ember.computed(...args, function() {
return addonFmt(format, ...args.map(a => this.get(a)));
});
- return computed.property.apply(computed, args);
}
/**
@@ -81,14 +79,13 @@ export function fmt(...args) {
@method url
@params {String} properties* to format
@params {String} format the format string for the URL
- @return {Function} computedProperty function returning a URL
+ @return {Function} discourseComputedProperty function returning a URL
**/
export function url(...args) {
const format = args.pop();
- const computed = Ember.computed(function() {
+ return Ember.computed(...args, function() {
return Discourse.getURL(addonFmt(format, ...args.map(a => this.get(a))));
});
- return computed.property.apply(computed, args);
}
/**
@@ -97,12 +94,12 @@ export function url(...args) {
@method endWith
@params {String} properties* to check
@params {String} substring the substring
- @return {Function} computedProperty function
+ @return {Function} discourseComputedProperty function
**/
export function endWith() {
const args = Array.prototype.slice.call(arguments, 0);
const substring = args.pop();
- const computed = Ember.computed(function() {
+ return Ember.computed(...args, function() {
return args
.map(a => this.get(a))
.every(s => {
@@ -111,7 +108,6 @@ export function endWith() {
return lastIndex !== -1 && lastIndex === position;
});
});
- return computed.property.apply(computed, args);
}
/**
diff --git a/app/assets/javascripts/discourse/lib/debounce.js.es6 b/app/assets/javascripts/discourse/lib/debounce.js.es6
index 6ab2eb1e58..f41b71f0d8 100644
--- a/app/assets/javascripts/discourse/lib/debounce.js.es6
+++ b/app/assets/javascripts/discourse/lib/debounce.js.es6
@@ -1,3 +1,4 @@
+import { debounce } from "@ember/runloop";
/**
Debounce a Javascript function. This means if it's called many times in a time limit it
should only be executed once (at the end of the limit counted from the last call made).
@@ -13,6 +14,6 @@ export default function(func, wait) {
self = this;
args = arguments;
- Ember.run.debounce(null, later, wait);
+ debounce(null, later, wait);
};
}
diff --git a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6
index 41b86bc30e..394d9a6dd3 100644
--- a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6
+++ b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6
@@ -1,6 +1,10 @@
+import { later } from "@ember/runloop";
import DiscourseURL from "discourse/lib/url";
import KeyValueStore from "discourse/lib/key-value-store";
import { formatUsername } from "discourse/lib/utilities";
+import { Promise } from "rsvp";
+import Site from "discourse/models/site";
+import User from "discourse/models/user";
let primaryTab = false;
let liveEnabled = false;
@@ -19,21 +23,23 @@ function init(messageBus, appEvents) {
liveEnabled = false;
mbClientId = messageBus.clientId;
- if (!Discourse.User.current()) {
+ if (!User.current()) {
return;
}
try {
keyValueStore.getItem(focusTrackerKey);
} catch (e) {
- Ember.Logger.info(
+ // eslint-disable-next-line no-console
+ console.info(
"Discourse desktop notifications are disabled - localStorage denied."
);
return;
}
if (!("Notification" in window)) {
- Ember.Logger.info(
+ // eslint-disable-next-line no-console
+ console.info(
"Discourse desktop notifications are disabled - not supported by browser"
);
return;
@@ -47,7 +53,8 @@ function init(messageBus, appEvents) {
return;
}
} catch (e) {
- Ember.Logger.warn(
+ // eslint-disable-next-line no-console
+ console.warn(
"Unexpected error, Notification is defined on window but not a responding correctly " +
e
);
@@ -58,7 +65,8 @@ function init(messageBus, appEvents) {
// Preliminary checks passed, continue with setup
setupNotifications(appEvents);
} catch (e) {
- Ember.Logger.error(e);
+ // eslint-disable-next-line no-console
+ console.error(e);
}
}
@@ -79,7 +87,7 @@ function confirmNotification() {
const clickEventHandler = () => notification.close();
notification.addEventListener("click", clickEventHandler);
- Ember.run.later(() => {
+ later(() => {
notification.close();
notification.removeEventListener("click", clickEventHandler);
}, 10 * 1000);
@@ -177,7 +185,7 @@ function onNotification(data) {
}
notification.addEventListener("click", clickEventHandler);
- Ember.run.later(() => {
+ later(() => {
notification.close();
notification.removeEventListener("click", clickEventHandler);
}, 10 * 1000);
@@ -188,11 +196,11 @@ function onNotification(data) {
// Wraps Notification.requestPermission in a Promise
function requestPermission() {
if (havePermission === true) {
- return Ember.RSVP.resolve();
+ return Promise.resolve();
} else if (havePermission === false) {
- return Ember.RSVP.reject();
+ return Promise.reject();
} else {
- return new Ember.RSVP.Promise(function(resolve, reject) {
+ return new Promise(function(resolve, reject) {
Notification.requestPermission(function(status) {
if (status === "granted") {
resolve();
@@ -207,7 +215,7 @@ function requestPermission() {
function i18nKey(notification_type) {
return (
"notifications.popup." +
- Discourse.Site.current().get("notificationLookup")[notification_type]
+ Site.current().get("notificationLookup")[notification_type]
);
}
diff --git a/app/assets/javascripts/discourse/lib/discourse-location.js.es6 b/app/assets/javascripts/discourse/lib/discourse-location.js.es6
index 4fd1c6628e..20e8ad8c5d 100644
--- a/app/assets/javascripts/discourse/lib/discourse-location.js.es6
+++ b/app/assets/javascripts/discourse/lib/discourse-location.js.es6
@@ -1,4 +1,6 @@
+import EmberObject from "@ember/object";
import { defaultHomepage } from "discourse/lib/utilities";
+import { guidFor } from "@ember/object/internals";
let popstateFired = false;
const supportsHistoryState = window.history && "state" in window.history;
const popstateCallbacks = [];
@@ -9,13 +11,14 @@ const popstateCallbacks = [];
@class DiscourseLocation
@namespace Discourse
- @extends Ember.Object
+ @extends @ember/object
*/
-const DiscourseLocation = Ember.Object.extend({
+const DiscourseLocation = EmberObject.extend({
init() {
this._super(...arguments);
this.set("location", this.location || window.location);
+
this.initState();
},
@@ -101,7 +104,7 @@ const DiscourseLocation = Ember.Object.extend({
const state = this.getState();
path = this.formatURL(path);
- if (state && state.path !== path) {
+ if (!state || state.path !== path) {
this.replaceState(path);
}
},
@@ -173,7 +176,7 @@ const DiscourseLocation = Ember.Object.extend({
@param callback {Function}
*/
onUpdateURL(callback) {
- const guid = Ember.guidFor(this);
+ const guid = guidFor(this);
$(window).on(`popstate.ember-location-${guid}`, () => {
const url = this.getURL();
@@ -214,7 +217,7 @@ const DiscourseLocation = Ember.Object.extend({
willDestroy() {
this._super(...arguments);
- const guid = Ember.guidFor(this);
+ const guid = guidFor(this);
$(window).off(`popstate.ember-location-${guid}`);
}
});
diff --git a/app/assets/javascripts/discourse/lib/export-csv.js.es6 b/app/assets/javascripts/discourse/lib/export-csv.js.es6
index 9f604f6e45..39b19771f3 100644
--- a/app/assets/javascripts/discourse/lib/export-csv.js.es6
+++ b/app/assets/javascripts/discourse/lib/export-csv.js.es6
@@ -1,4 +1,6 @@
import { ajax } from "discourse/lib/ajax";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+
function exportEntityByType(type, entity, args) {
return ajax("/export_csv/export_entity.json", {
method: "POST",
@@ -11,9 +13,7 @@ export function exportUserArchive() {
.then(function() {
bootbox.alert(I18n.t("user.download_archive.success"));
})
- .catch(function() {
- bootbox.alert(I18n.t("user.download_archive.rate_limit_error"));
- });
+ .catch(popupAjaxError);
}
export function exportEntity(entity, args) {
diff --git a/app/assets/javascripts/discourse/lib/eyeline.js.es6 b/app/assets/javascripts/discourse/lib/eyeline.js.es6
index e18f0a4554..bebbfa1623 100644
--- a/app/assets/javascripts/discourse/lib/eyeline.js.es6
+++ b/app/assets/javascripts/discourse/lib/eyeline.js.es6
@@ -1,19 +1,42 @@
-// Track visible elemnts on the screen.
+import ENV from "discourse-common/config/environment";
+import { EventTarget } from "rsvp";
+
+let _skipUpdate;
+let _rootElement;
+
+export function configureEyeline(opts) {
+ if (opts) {
+ _skipUpdate = opts.skipUpdate;
+ _rootElement = opts.rootElement;
+ } else {
+ _skipUpdate = ENV.environment === "test";
+ _rootElement = null;
+ }
+}
+
+configureEyeline();
+
+// Track visible elements on the screen.
const Eyeline = function Eyeline(selector) {
this.selector = selector;
};
Eyeline.prototype.update = function() {
- if (Ember.testing) {
+ if (_skipUpdate) {
return;
}
- const docViewTop = $(window).scrollTop(),
- windowHeight = $(window).height(),
- docViewBottom = docViewTop + windowHeight,
- $elements = $(this.selector),
- bottomOffset = $elements.last().offset(),
- self = this;
+ const docViewTop = _rootElement
+ ? $(_rootElement).scrollTop()
+ : $(window).scrollTop();
+ const windowHeight = _rootElement
+ ? $(_rootElement).height()
+ : $(window).height();
+ const docViewBottom = docViewTop + windowHeight;
+ const $elements = $(this.selector);
+ const bottomOffset = _rootElement
+ ? $elements.last().position()
+ : $elements.last().offset();
let atBottom = false;
if (bottomOffset) {
@@ -21,9 +44,9 @@ Eyeline.prototype.update = function() {
bottomOffset.top <= docViewBottom && bottomOffset.top >= docViewTop;
}
- return $elements.each(function(i, elem) {
+ return $elements.each((i, elem) => {
const $elem = $(elem),
- elemTop = $elem.offset().top,
+ elemTop = _rootElement ? $elem.position().top : $elem.offset().top,
elemBottom = elemTop + $elem.height();
let markSeen = false;
@@ -45,32 +68,30 @@ Eyeline.prototype.update = function() {
// If you hit the bottom we mark all the elements as seen. Otherwise, just the first one
if (!atBottom) {
- self.trigger("saw", { detail: $elem });
+ this.trigger("saw", { detail: $elem });
if (i === 0) {
- self.trigger("sawTop", { detail: $elem });
+ this.trigger("sawTop", { detail: $elem });
}
return false;
}
if (i === 0) {
- self.trigger("sawTop", { detail: $elem });
+ this.trigger("sawTop", { detail: $elem });
}
if (i === $elements.length - 1) {
- return self.trigger("sawBottom", { detail: $elem });
+ return this.trigger("sawBottom", { detail: $elem });
}
});
};
// Call this when we know aren't loading any more elements. Mark the rest as seen
Eyeline.prototype.flushRest = function() {
- if (Ember.testing) {
+ if (ENV.environment === "test") {
return;
}
- const self = this;
- $(this.selector).each(function(i, elem) {
- return self.trigger("saw", { detail: $(elem) });
- });
+
+ $(this.selector).each((i, elem) => this.trigger("saw", { detail: $(elem) }));
};
-RSVP.EventTarget.mixin(Eyeline.prototype);
+EventTarget.mixin(Eyeline.prototype);
export default Eyeline;
diff --git a/app/assets/javascripts/discourse/lib/formatter.js.es6 b/app/assets/javascripts/discourse/lib/formatter.js.es6
index 2d5961a552..2a93f590d7 100644
--- a/app/assets/javascripts/discourse/lib/formatter.js.es6
+++ b/app/assets/javascripts/discourse/lib/formatter.js.es6
@@ -1,54 +1,3 @@
-/* global BreakString:true */
-
-/*
- * memoize.js
- * by @philogb and @addyosmani
- * with further optimizations by @mathias
- * and @DmitryBaranovsk
- * perf tests: http://bit.ly/q3zpG3
- * Released under an MIT license.
- *
- * modified with cap by Sam
- */
-function cappedMemoize(fn, max) {
- fn.maxMemoize = max;
- fn.memoizeLength = 0;
-
- return function() {
- const args = Array.prototype.slice.call(arguments);
- let hash = "";
- let i = args.length;
- let currentArg = null;
- while (i--) {
- currentArg = args[i];
- hash +=
- currentArg === new Object(currentArg)
- ? JSON.stringify(currentArg)
- : currentArg;
- if (!fn.memoize) {
- fn.memoize = {};
- }
- }
- if (hash in fn.memoize) {
- return fn.memoize[hash];
- } else {
- fn.memoizeLength++;
- if (fn.memoizeLength > max) {
- fn.memoizeLength = 0;
- fn.memoize = {};
- }
- const result = fn.apply(this, args);
- fn.memoize[hash] = result;
- return result;
- }
- };
-}
-
-const breakUp = cappedMemoize(function(str, hint) {
- return new BreakString(str).break(hint);
-}, 100);
-export { breakUp };
-
export function shortDate(date) {
return moment(date).format(I18n.t("dates.medium.date_year"));
}
@@ -195,11 +144,11 @@ export function durationTiny(distance, ageOpts) {
const numYears = distanceInMinutes / 525600.0;
const remainder = numYears % 1;
if (remainder < 0.25) {
- formatted = t("about_x_years", { count: parseInt(numYears) });
+ formatted = t("about_x_years", { count: Math.floor(numYears) });
} else if (remainder < 0.75) {
- formatted = t("over_x_years", { count: parseInt(numYears) });
+ formatted = t("over_x_years", { count: Math.floor(numYears) });
} else {
- formatted = t("almost_x_years", { count: parseInt(numYears) + 1 });
+ formatted = t("almost_x_years", { count: Math.floor(numYears) + 1 });
}
break;
@@ -262,7 +211,7 @@ function relativeAgeTinyShowsYear(relativeAgeString) {
return relativeAgeString.match(/'[\d]{2}$/);
}
-function relativeAgeMediumSpan(distance, leaveAgo) {
+export function relativeAgeMediumSpan(distance, leaveAgo) {
let formatted;
const distanceInMinutes = Math.round(distance / 60.0);
@@ -283,14 +232,24 @@ function relativeAgeMediumSpan(distance, leaveAgo) {
case distanceInMinutes >= 90 && distanceInMinutes <= 1409:
formatted = t("x_hours", { count: Math.round(distanceInMinutes / 60.0) });
break;
- case distanceInMinutes >= 1410 && distanceInMinutes <= 2159:
+ case distanceInMinutes >= 1410 && distanceInMinutes <= 2519:
formatted = t("x_days", { count: 1 });
break;
- case distanceInMinutes >= 2160:
+ case distanceInMinutes >= 2520 && distanceInMinutes <= 129599:
formatted = t("x_days", {
count: Math.round((distanceInMinutes - 720.0) / 1440.0)
});
break;
+ case distanceInMinutes >= 129600 && distanceInMinutes <= 525599:
+ formatted = t("x_months", {
+ count: Math.round(distanceInMinutes / 43200.0)
+ });
+ break;
+ default:
+ formatted = t("x_years", {
+ count: Math.round(distanceInMinutes / 525600.0)
+ });
+ break;
}
return formatted || "—";
}
diff --git a/app/assets/javascripts/discourse/lib/key-value-store.js.es6 b/app/assets/javascripts/discourse/lib/key-value-store.js.es6
index c45501cf05..a0a0a5dcaa 100644
--- a/app/assets/javascripts/discourse/lib/key-value-store.js.es6
+++ b/app/assets/javascripts/discourse/lib/key-value-store.js.es6
@@ -67,7 +67,7 @@ KeyValueStore.prototype = {
if (!safeLocalStorage) {
return def;
}
- const result = parseInt(this.get(key));
+ const result = parseInt(this.get(key), 10);
if (!isFinite(result)) {
return def;
}
diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
index 590f916155..409ebc3190 100644
--- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
+++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
@@ -1,3 +1,5 @@
+import { run } from "@ember/runloop";
+import { later } from "@ember/runloop";
import DiscourseURL from "discourse/lib/url";
import Composer from "discourse/models/composer";
import { minimumOffset } from "discourse/lib/offset-calculator";
@@ -65,9 +67,11 @@ const bindings = {
"shift+p": { handler: "pinUnpinTopic" },
"shift+r": { handler: "replyToTopic" },
"shift+s": { click: "#topic-footer-buttons button.share", anonymous: true }, // share topic
- "shift+u": { handler: "goToUnreadPost" },
+ "shift+l": { handler: "goToUnreadPost" },
"shift+z shift+z": { handler: "logout" },
"shift+f11": { handler: "fullscreenComposer", global: true },
+ "shift+u": { handler: "deferTopic" },
+ "shift+a": { handler: "toggleAdminActions" },
t: { postAction: "replyAsNewTopic" },
u: { handler: "goBack", anonymous: true },
"x r": {
@@ -85,7 +89,7 @@ export default {
this._stopCallback();
this.searchService = this.container.lookup("search-service:main");
- this.appEvents = this.container.lookup("app-events:main");
+ this.appEvents = this.container.lookup("service:app-events");
this.currentUser = this.container.lookup("current-user:main");
let siteSettings = this.container.lookup("site-settings:main");
@@ -139,7 +143,7 @@ export default {
quoteReply() {
this.sendToSelectedPost("replyToPost");
// lazy but should work for now
- Ember.run.later(() => $(".d-editor .quote").click(), 500);
+ later(() => $(".d-editor .quote").click(), 500);
return false;
},
@@ -209,7 +213,7 @@ export default {
},
showPageSearch(event) {
- Ember.run(() => {
+ run(() => {
this.appEvents.trigger("header:keyboard-trigger", {
type: "page-search",
event
@@ -218,7 +222,7 @@ export default {
},
printTopic(event) {
- Ember.run(() => {
+ run(() => {
if ($(".container.posts").length) {
event.preventDefault(); // We need to stop printing the current page in Firefox
this.container.lookup("controller:topic").print();
@@ -240,7 +244,7 @@ export default {
this.container.lookup("controller:composer").open({
action: Composer.CREATE_TOPIC,
- draftKey: Composer.CREATE_TOPIC
+ draftKey: Composer.NEW_TOPIC_KEY
});
},
@@ -438,9 +442,22 @@ export default {
$selected = $articles.filter("[data-islastviewedtopic=true]");
}
+ // Discard selection if it is not in viewport, so users can combine
+ // keyboard shortcuts with mouse scrolling.
+ if ($selected.length !== 0 && !fast) {
+ const offset = minimumOffset();
+ const beginScreen = $(window).scrollTop() - offset;
+ const endScreen = beginScreen + window.innerHeight + offset;
+ const beginArticle = $selected.offset().top;
+ const endArticle = $selected.offset().top + $selected.height();
+ if (beginScreen > endArticle || beginArticle > endScreen) {
+ $selected = null;
+ }
+ }
+
// If still nothing is selected, select the first post that is
// visible and cancel move operation.
- if ($selected.length === 0) {
+ if (!$selected || $selected.length === 0) {
const offset = minimumOffset();
$selected = $articles
.toArray()
@@ -618,5 +635,13 @@ export default {
_replyToPost() {
this.container.lookup("controller:topic").send("replyToPost");
+ },
+
+ deferTopic() {
+ this.container.lookup("controller:topic").send("deferTopic");
+ },
+
+ toggleAdminActions() {
+ this.appEvents.trigger("topic:toggle-actions");
}
};
diff --git a/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6 b/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6
index 12f78fc3f5..a8f30a3998 100644
--- a/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6
+++ b/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6
@@ -40,7 +40,18 @@ function show(image) {
image.srcset = copyImg.srcset;
}
image.classList.remove("d-lazyload-hidden");
- image.parentNode.removeChild(copyImg);
+
+ if (image.onload) {
+ // don't bother fighting with existing handler
+ // this can mean a slight flash on mobile
+ image.parentNode.removeChild(copyImg);
+ } else {
+ image.onload = () => {
+ image.parentNode.removeChild(copyImg);
+ image.onload = null;
+ };
+ }
+
copyImg.onload = null;
};
@@ -50,25 +61,23 @@ function show(image) {
copyImg.srcset = imageData.srcset;
}
+ // width of image may not match, use computed style which
+ // is the actual size of the image
+ const computedStyle = window.getComputedStyle(image);
+ const actualWidth = parseInt(computedStyle.width, 10);
+ const actualHeight = parseInt(computedStyle.height, 10);
+
copyImg.style.position = "absolute";
copyImg.style.top = `${image.offsetTop}px`;
copyImg.style.left = `${image.offsetLeft}px`;
+ copyImg.style.width = `${actualWidth}px`;
+ copyImg.style.height = `${actualHeight}px`;
+
copyImg.className = imageData.className;
- let inOnebox = false;
- for (let element = image; element; element = element.parentElement) {
- if (element.classList.contains("onebox")) {
- inOnebox = true;
- break;
- }
- }
-
- if (!inOnebox) {
- copyImg.style.width = `${imageData.width}px`;
- copyImg.style.height = `${imageData.height}px`;
- }
-
- image.parentNode.insertBefore(copyImg, image);
+ // insert after the current element so styling still will
+ // apply to original image firstChild selectors
+ image.parentNode.insertBefore(copyImg, image.nextSibling);
} else {
image.classList.remove("d-lazyload-hidden");
}
diff --git a/app/assets/javascripts/discourse/lib/lightbox.js.es6 b/app/assets/javascripts/discourse/lib/lightbox.js.es6
index c84d878d6a..34330c44c8 100644
--- a/app/assets/javascripts/discourse/lib/lightbox.js.es6
+++ b/app/assets/javascripts/discourse/lib/lightbox.js.es6
@@ -3,6 +3,7 @@ import { escapeExpression } from "discourse/lib/utilities";
import { renderIcon } from "discourse-common/lib/icon-library";
import { isAppWebview, postRNWebviewMessage } from "discourse/lib/utilities";
import { spinnerHTML } from "discourse/helpers/loading-spinner";
+import User from "discourse/models/user";
export default function($elem) {
if (!$elem) {
@@ -77,7 +78,7 @@ export default function($elem) {
];
if (
!Discourse.SiteSettings.prevent_anons_from_downloading_files ||
- Discourse.User.current()
+ User.current()
) {
src.push(
'
{
+ schedule("afterRender", () => {
$hashtags.each((index, hashtag) => {
const categorySlug = categorySlugs[index];
const link = validCategoryHashtags[categorySlug];
diff --git a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 b/app/assets/javascripts/discourse/lib/link-mentions.js.es6
index 16bc7304c1..30bdcdda95 100644
--- a/app/assets/javascripts/discourse/lib/link-mentions.js.es6
+++ b/app/assets/javascripts/discourse/lib/link-mentions.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
import { ajax } from "discourse/lib/ajax";
import { userPath } from "discourse/lib/url";
import { formatUsername } from "discourse/lib/utilities";
@@ -10,9 +11,7 @@ function replaceSpan($e, username, opts) {
if (opts && opts.group) {
if (opts.mentionable) {
- extra = `data-name='${username}' data-mentionable-user-count='${
- opts.mentionable.user_count
- }' data-max-mentions='${maxGroupMention}'`;
+ extra = `data-name='${username}' data-mentionable-user-count='${opts.mentionable.user_count}' data-max-mentions='${maxGroupMention}'`;
extraClass = "notify";
}
$e.replaceWith(
@@ -41,17 +40,19 @@ const checked = {};
const cannotSee = [];
function updateFound($mentions, usernames) {
- Ember.run.scheduleOnce("afterRender", function() {
+ scheduleOnce("afterRender", function() {
$mentions.each((i, e) => {
const $e = $(e);
const username = usernames[i];
if (found[username.toLowerCase()]) {
replaceSpan($e, username, { cannot_see: cannotSee[username] });
- } else if (foundGroups[username]) {
+ } else if (mentionableGroups[username]) {
replaceSpan($e, username, {
group: true,
mentionable: mentionableGroups[username]
});
+ } else if (foundGroups[username]) {
+ replaceSpan($e, username, { group: true });
} else if (checked[username]) {
$e.addClass("mention-tested");
}
diff --git a/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6 b/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6
index a0b2c95c46..5aa3f6d612 100644
--- a/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6
+++ b/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
import { ajax } from "discourse/lib/ajax";
import { replaceSpan } from "discourse/lib/category-hashtags";
import { TAG_HASHTAG_POSTFIX } from "discourse/lib/tag-hashtags";
@@ -7,7 +8,7 @@ const checkedTagHashtags = [];
const testedClass = "tag-hashtag-tested";
function updateFound($hashtags, tagValues) {
- Ember.run.schedule("afterRender", () => {
+ schedule("afterRender", () => {
$hashtags.each((index, hashtag) => {
const tagValue = tagValues[index];
const link = validTagHashtags[tagValue];
diff --git a/app/assets/javascripts/discourse/lib/load-script.js.es6 b/app/assets/javascripts/discourse/lib/load-script.js.es6
index 34b3f2897b..1efec2ea78 100644
--- a/app/assets/javascripts/discourse/lib/load-script.js.es6
+++ b/app/assets/javascripts/discourse/lib/load-script.js.es6
@@ -1,4 +1,7 @@
+import { run } from "@ember/runloop";
import { ajax } from "discourse/lib/ajax";
+import { Promise } from "rsvp";
+
const _loaded = {};
const _loading = {};
@@ -22,7 +25,7 @@ function loadWithTag(path, cb) {
) {
s = s.onload = s.onreadystatechange = null;
if (!abort) {
- Ember.run(null, cb);
+ run(null, cb);
}
}
};
@@ -37,7 +40,7 @@ export function loadCSS(url) {
export default function loadScript(url, opts) {
// TODO: Remove this once plugins have been updated not to use it:
if (url === "defer/html-sanitizer-bundle") {
- return Ember.RSVP.Promise.resolve();
+ return Promise.resolve();
}
opts = opts || {};
@@ -54,7 +57,7 @@ export default function loadScript(url, opts) {
}
});
- return new Ember.RSVP.Promise(function(resolve) {
+ return new Promise(function(resolve) {
// If we already loaded this url
if (_loaded[url]) {
return resolve();
@@ -64,7 +67,7 @@ export default function loadScript(url, opts) {
}
let done;
- _loading[url] = new Ember.RSVP.Promise(function(_done) {
+ _loading[url] = new Promise(function(_done) {
done = _done;
});
diff --git a/app/assets/javascripts/discourse/lib/lock-on.js.es6 b/app/assets/javascripts/discourse/lib/lock-on.js.es6
index 41f578ab29..90cc641dfe 100644
--- a/app/assets/javascripts/discourse/lib/lock-on.js.es6
+++ b/app/assets/javascripts/discourse/lib/lock-on.js.es6
@@ -35,7 +35,7 @@ export default class LockOn {
elementTop() {
const $selected = $(this.selector);
- if ($selected && $selected.offset && $selected.offset()) {
+ if ($selected.length && $selected.offset && $selected.offset()) {
return $selected.offset().top - minimumOffset();
}
}
@@ -49,13 +49,18 @@ export default class LockOn {
}
lock() {
- let previousTop = this.elementTop();
const startedAt = new Date().getTime();
-
- $(window).scrollTop(previousTop);
+ let previousTop = this.elementTop();
+ previousTop && $(window).scrollTop(previousTop);
const interval = setInterval(() => {
- const top = Math.max(0, this.elementTop());
+ const elementTop = this.elementTop();
+ if (!previousTop && !elementTop) {
+ // we can't find the element yet, wait a little bit more
+ return;
+ }
+
+ const top = Math.max(0, elementTop);
const scrollTop = $(window).scrollTop();
if (typeof top === "undefined" || isNaN(top)) {
@@ -67,7 +72,7 @@ export default class LockOn {
previousTop = top;
}
- // Commit suicide after a little while
+ // Stop after a little while
if (new Date().getTime() - startedAt > LOCK_DURATION_MS) {
return this.clearLock(interval);
}
diff --git a/app/assets/javascripts/discourse/lib/logout.js.es6 b/app/assets/javascripts/discourse/lib/logout.js.es6
index 888b07eca9..23160f8e44 100644
--- a/app/assets/javascripts/discourse/lib/logout.js.es6
+++ b/app/assets/javascripts/discourse/lib/logout.js.es6
@@ -1,3 +1,6 @@
+import { isEmpty } from "@ember/utils";
+import { findAll } from "discourse/models/login-method";
+
export default function logout(siteSettings, keyValueStore) {
if (!siteSettings || !keyValueStore) {
const container = Discourse.__container__;
@@ -8,9 +11,21 @@ export default function logout(siteSettings, keyValueStore) {
keyValueStore.abandonLocal();
const redirect = siteSettings.logout_redirect;
- if (Ember.isEmpty(redirect)) {
- window.location.pathname = Discourse.getURL("/");
- } else {
+ if (!isEmpty(redirect)) {
window.location.href = redirect;
+ return;
}
+
+ const sso = siteSettings.enable_sso;
+ const oneAuthenticator =
+ !siteSettings.enable_local_logins && findAll().length === 1;
+
+ if (siteSettings.login_required && (sso || oneAuthenticator)) {
+ // In this situation visiting most URLs will start the auth process again
+ // Go to the `/login` page to avoid an immediate redirect
+ window.location.href = Discourse.getURL("/login");
+ return;
+ }
+
+ window.location.href = Discourse.getURL("/");
}
diff --git a/app/assets/javascripts/discourse/lib/mobile.js.es6 b/app/assets/javascripts/discourse/lib/mobile.js.es6
index de9a10697b..96ee5f7099 100644
--- a/app/assets/javascripts/discourse/lib/mobile.js.es6
+++ b/app/assets/javascripts/discourse/lib/mobile.js.es6
@@ -1,4 +1,5 @@
import deprecated from "discourse-common/lib/deprecated";
+import ENV from "discourse-common/config/environment";
let mobileForced = false;
@@ -12,7 +13,7 @@ const Mobile = {
this.isMobileDevice = mobileForced || $html.hasClass("mobile-device");
this.mobileView = mobileForced || $html.hasClass("mobile-view");
- if (Ember.testing || mobileForced) {
+ if (ENV.environment === "test" || mobileForced) {
return;
}
diff --git a/app/assets/javascripts/discourse/lib/page-tracker.js.es6 b/app/assets/javascripts/discourse/lib/page-tracker.js.es6
index 0c9e9445c9..4acbdc4ea3 100644
--- a/app/assets/javascripts/discourse/lib/page-tracker.js.es6
+++ b/app/assets/javascripts/discourse/lib/page-tracker.js.es6
@@ -1,6 +1,6 @@
+import { next } from "@ember/runloop";
let _started = false;
-
-const cache = {};
+let cache = {};
let transitionCount = 0;
export function setTransient(key, data, count) {
@@ -11,6 +11,12 @@ export function getTransient(key) {
return cache[key];
}
+export function resetPageTracking() {
+ _started = false;
+ transitionCount = 0;
+ cache = {};
+}
+
export function startPageTracking(router, appEvents) {
if (_started) {
return;
@@ -26,13 +32,13 @@ export function startPageTracking(router, appEvents) {
// Refreshing the title is debounced, so we need to trigger this in the
// next runloop to have the correct title.
- Ember.run.next(() => {
+ next(() => {
let title = Discourse.get("_docTitle");
appEvents.trigger("page:changed", {
url,
title,
- currentRouteName: router.get("currentRouteName"),
+ currentRouteName: router.currentRouteName,
replacedOnlyQueryParams
});
});
diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6
index 1fa2f449d0..2fcedd76b9 100644
--- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6
+++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6
@@ -1,6 +1,7 @@
import deprecated from "discourse-common/lib/deprecated";
import { iconNode } from "discourse-common/lib/icon-library";
import { addDecorator } from "discourse/widgets/post-cooked";
+import { addPluginOutletDecorator } from "discourse/components/plugin-connector";
import ComposerEditor from "discourse/components/composer-editor";
import DiscourseBanner from "discourse/components/discourse-banner";
import { addButton } from "discourse/widgets/post-menu";
@@ -8,6 +9,7 @@ import { includeAttributes } from "discourse/lib/transform-post";
import { registerHighlightJSLanguage } from "discourse/lib/highlight-syntax";
import { addToolbarCallback } from "discourse/components/d-editor";
import { addWidgetCleanCallback } from "discourse/components/mount-widget";
+import { addGlobalNotice } from "discourse/components/global-notice";
import {
createWidget,
reopenWidget,
@@ -40,11 +42,17 @@ import { registerCustomAvatarHelper } from "discourse/helpers/user-avatar";
import { disableNameSuppression } from "discourse/widgets/poster-name";
import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1 } from "discourse/controllers/topic";
import Sharing from "discourse/lib/sharing";
-import { addComposerUploadHandler } from "discourse/components/composer-editor";
+import {
+ addComposerUploadHandler,
+ addComposerUploadMarkdownResolver
+} from "discourse/components/composer-editor";
import { addCategorySortCriteria } from "discourse/components/edit-category-settings";
+import { queryRegistry } from "discourse/widgets/widget";
+import Composer from "discourse/models/composer";
+import { on } from "@ember/object/evented";
// If you add any methods to the API ensure you bump up this number
-const PLUGIN_API_VERSION = "0.8.31";
+const PLUGIN_API_VERSION = "0.8.38";
class PluginApi {
constructor(version, container) {
@@ -325,7 +333,9 @@ class PluginApi {
* ```
**/
attachWidgetAction(widget, actionName, fn) {
- const widgetClass = this.container.factoryFor(`widget:${widget}`).class;
+ const widgetClass =
+ queryRegistry(widget) ||
+ this.container.factoryFor(`widget:${widget}`).class;
widgetClass.prototype[actionName] = fn;
}
@@ -421,45 +431,45 @@ class PluginApi {
}
/**
- Called whenever the "page" changes. This allows us to set up analytics
- and other tracking.
+ Called whenever the "page" changes. This allows us to set up analytics
+ and other tracking.
- To get notified when the page changes, you can install a hook like so:
+ To get notified when the page changes, you can install a hook like so:
- ```javascript
- api.onPageChange((url, title) => {
+ ```javascript
+ api.onPageChange((url, title) => {
console.log('the page changed to: ' + url + ' and title ' + title);
});
- ```
- **/
+ ```
+ **/
onPageChange(fn) {
this.onAppEvent("page:changed", data => fn(data.url, data.title));
}
/**
- Listen for a triggered `AppEvent` from Discourse.
+ Listen for a triggered `AppEvent` from Discourse.
- ```javascript
- api.onAppEvent('inserted-custom-html', () => {
+ ```javascript
+ api.onAppEvent('inserted-custom-html', () => {
console.log('a custom footer was rendered');
});
- ```
- **/
+ ```
+ **/
onAppEvent(name, fn) {
- const appEvents = this._lookupContainer("app-events:main");
+ const appEvents = this._lookupContainer("service:app-events");
appEvents && appEvents.on(name, fn);
}
/**
- Registers a function to generate custom avatar CSS classes
- for a particular user.
+ Registers a function to generate custom avatar CSS classes
+ for a particular user.
- Takes a function that will accept a user as a parameter
- and return an array of CSS classes to apply.
+ Takes a function that will accept a user as a parameter
+ and return an array of CSS classes to apply.
- ```javascript
- api.customUserAvatarClasses(user => {
- if (Ember.get(user, 'primary_group_name') === 'managers') {
+ ```javascript
+ api.customUserAvatarClasses(user => {
+ if (get(user, 'primary_group_name') === 'managers') {
return ['managers'];
}
});
@@ -526,7 +536,7 @@ class PluginApi {
/**
* Exposes the widget creating ability to plugins. Plugins can
- * register their own plugins and attach them with decorators.
+ * register their own widgets and attach them with decorators.
* See `createWidget` in `discourse/widgets/widget` for more info.
**/
createWidget(name, args) {
@@ -712,7 +722,7 @@ class PluginApi {
/**
*
- * Adds a new item in the navigation bar.
+ * Adds a new item in the navigation bar. Returns the NavItem object created.
*
* Example:
*
@@ -725,13 +735,20 @@ class PluginApi {
* An optional `customFilter` callback can be included to not display the
* nav item on certain routes
*
+ * An optional `init` callback can be included to run custom code on menu
+ * init
+ *
* Example:
*
* addNavigationBarItem({
* name: "link-to-bugs-category",
* displayName: "bugs"
* href: "/c/bugs",
- * customFilter: (category, args) => { category && category.get('name') !== 'bug' }
+ * init: (navItem, category) => { if (category) { navItem.set("category", category) } }
+ * customFilter: (category, args, router) => { category && category.name !== 'bug' }
+ * customHref: (category, args, router) => { if (category && category.name) === 'not-a-bug') "/a-feature"; },
+ * before: "top",
+ * forceActive(category, args, router) => router.currentURL === "/a/b/c/d";
* })
*/
addNavigationBarItem(item) {
@@ -742,6 +759,38 @@ class PluginApi {
item
);
} else {
+ const customHref = item.customHref;
+ if (customHref) {
+ const router = this.container.lookup("service:router");
+ item.customHref = function(category, args) {
+ return customHref(category, args, router);
+ };
+ }
+
+ const customFilter = item.customFilter;
+ if (customFilter) {
+ const router = this.container.lookup("service:router");
+ item.customFilter = function(category, args) {
+ return customFilter(category, args, router);
+ };
+ }
+
+ const forceActive = item.forceActive;
+ if (forceActive) {
+ const router = this.container.lookup("service:router");
+ item.forceActive = function(category, args) {
+ return forceActive(category, args, router);
+ };
+ }
+
+ const init = item.init;
+ if (init) {
+ const router = this.container.lookup("service:router");
+ item.init = function(navItem, category, args) {
+ init(navItem, category, args, router);
+ };
+ }
+
addNavItem(item);
}
}
@@ -770,7 +819,7 @@ class PluginApi {
*
* Example:
*
- * modifySelectKit("topic-footer-mobile-dropdown").appendContent(() => [{
+ * api.modifySelectKit("topic-footer-mobile-dropdown").appendContent(() => [{
* name: "discourse",
* id: 1
* }])
@@ -786,7 +835,7 @@ class PluginApi {
*
* Example:
*
- * addGTMPageChangedCallback( gtmData => gtmData.locale = I18n.currentLocale() )
+ * api.addGTMPageChangedCallback( gtmData => gtmData.locale = I18n.currentLocale() )
*
*/
addGTMPageChangedCallback(fn) {
@@ -800,7 +849,7 @@ class PluginApi {
* Example:
*
* // read /discourse/lib/sharing.js.es6 for options
- * addSharingSource(options)
+ * api.addSharingSource(options)
*
*/
addSharingSource(options) {
@@ -816,14 +865,56 @@ class PluginApi {
*
* Example:
*
- * addComposerUploadHandler(["mp4", "mov"], (file, editor) => {
- * console.log("Handling upload for", file.name);
+ * api.addComposerUploadHandler(["mp4", "mov"], (file, editor) => {
+ * console.log("Handling upload for", file.name);
* })
*/
addComposerUploadHandler(extensions, method) {
addComposerUploadHandler(extensions, method);
}
+ /**
+ * Registers a function to generate Markdown after a file has been uploaded.
+ *
+ * Example:
+ *
+ * api.addComposerUploadMarkdownResolver(upload => {
+ * return `_uploaded ${upload.original_filename}_`;
+ * })
+ */
+ addComposerUploadMarkdownResolver(resolver) {
+ addComposerUploadMarkdownResolver(resolver);
+ }
+
+ /**
+ * Registers a "beforeSave" function on the composer. This allows you to
+ * implement custom logic that will happen before the user makes a post.
+ *
+ * Example:
+ *
+ * api.composerBeforeSave(() => {
+ * console.log("Before saving, do something!");
+ * })
+ */
+ composerBeforeSave(method) {
+ Composer.reopen({ beforeSave: method });
+ }
+
+ /**
+ * Adds a field to draft serializer
+ *
+ * Example:
+ *
+ * api.serializeToDraft('key_set_in_model', 'field_name_in_payload');
+ *
+ * to keep both of them same
+ * api.serializeToDraft('field_name');
+ *
+ */
+ serializeToDraft(fieldName, property) {
+ Composer.serializeToDraft(fieldName, property);
+ }
+
/**
* Registers a criteria that can be used as default topic order on category
* pages.
@@ -881,6 +972,41 @@ class PluginApi {
registerHighlightJSLanguage(name, fn) {
registerHighlightJSLanguage(name, fn);
}
+
+ /**
+ * Adds global notices to display.
+ *
+ * Example:
+ *
+ * api.addGlobalNotice("text", "foo", { html: "bar
" })
+ *
+ **/
+ addGlobalNotice(id, text, options) {
+ addGlobalNotice(id, text, options);
+ }
+
+ /**
+ * Used for decorating the rendered HTML content of a plugin-outlet after it's been rendered
+ *
+ * `callback` will be called when it is time to decorate it.
+ *
+ * For example, to add a yellow background to a connector:
+ *
+ * ```
+ * api.decoratePluginOutlet(
+ * "discovery-list-container-top",
+ * (elem, args) => {
+ * if (elem.classList.contains("foo")) {
+ * elem.style.backgroundColor = "yellow";
+ * }
+ * }
+ * );
+ * ```
+ *
+ **/
+ decoratePluginOutlet(outletName, callback, opts) {
+ addPluginOutletDecorator(outletName, callback, opts || {});
+ }
}
let _pluginv01;
@@ -958,12 +1084,12 @@ function decorate(klass, evt, cb, id) {
}
const mixin = {};
- mixin["_decorate_" + _decorateId++] = function($elem) {
- $elem = $elem || this.$();
+ mixin["_decorate_" + _decorateId++] = on(evt, function($elem) {
+ $elem = $elem || $(this.element);
if ($elem) {
cb($elem);
}
- }.on(evt);
+ });
klass.reopen(mixin);
}
diff --git a/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6 b/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6
index 3b817746a4..8c6efa86a2 100644
--- a/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6
+++ b/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6
@@ -1,3 +1,6 @@
+import Site from "discourse/models/site";
+import deprecated from "discourse-common/lib/deprecated";
+
let _connectorCache;
let _rawConnectorCache;
let _extraConnectorClasses = {};
@@ -17,11 +20,12 @@ export function extraConnectorClass(name, obj) {
const DefaultConnectorClass = {
actions: {},
shouldRender: () => true,
- setupComponent() {}
+ setupComponent() {},
+ teardownComponent() {}
};
function findOutlets(collection, callback) {
- const disabledPlugins = Discourse.Site.currentProp("disabled_plugins") || [];
+ const disabledPlugins = Site.currentProp("disabled_plugins") || [];
Object.keys(collection).forEach(function(res) {
if (res.indexOf("/connectors/") !== -1) {
@@ -69,6 +73,7 @@ function buildConnectorCache() {
_connectorCache[outletName] = _connectorCache[outletName] || [];
_connectorCache[outletName].push({
+ outletName,
templateName: resource.replace("javascripts/", ""),
template: Ember.TEMPLATES[resource],
classNames: `${outletName}-outlet ${uniqueName}`,
@@ -106,3 +111,23 @@ export function rawConnectorsFor(outletName) {
}
return _rawConnectorCache[outletName] || [];
}
+
+export function buildArgsWithDeprecations(args, deprecatedArgs) {
+ const output = {};
+
+ Object.keys(args).forEach(key => {
+ Object.defineProperty(output, key, { value: args[key] });
+ });
+
+ Object.keys(deprecatedArgs).forEach(key => {
+ Object.defineProperty(output, key, {
+ get() {
+ deprecated(`${key} is deprecated`);
+
+ return deprecatedArgs[key];
+ }
+ });
+ });
+
+ return output;
+}
diff --git a/app/assets/javascripts/discourse/lib/posts-with-placeholders.js.es6 b/app/assets/javascripts/discourse/lib/posts-with-placeholders.js.es6
index 03271c6d6b..449c70ab79 100644
--- a/app/assets/javascripts/discourse/lib/posts-with-placeholders.js.es6
+++ b/app/assets/javascripts/discourse/lib/posts-with-placeholders.js.es6
@@ -1,10 +1,11 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import EmberObject from "@ember/object";
+import discourseComputed from "discourse-common/utils/decorators";
export function Placeholder(viewName) {
this.viewName = viewName;
}
-export default Ember.Object.extend(Ember.Array, {
+export default EmberObject.extend(Ember.Array, {
posts: null,
_appendingIds: null,
@@ -12,7 +13,7 @@ export default Ember.Object.extend(Ember.Array, {
this._appendingIds = {};
},
- @computed
+ @discourseComputed
length() {
return (
this.get("posts.length") + Object.keys(this._appendingIds || {}).length
diff --git a/app/assets/javascripts/discourse/lib/push-notifications.js.es6 b/app/assets/javascripts/discourse/lib/push-notifications.js.es6
index 00f15d0fab..20040a8b11 100644
--- a/app/assets/javascripts/discourse/lib/push-notifications.js.es6
+++ b/app/assets/javascripts/discourse/lib/push-notifications.js.es6
@@ -22,7 +22,7 @@ function userAgentVersionChecker(agent, version, mobileView) {
new RegExp(`${agent}\/(\\d+)\\.\\d`)
);
if (uaMatch && mobileView) return false;
- if (!uaMatch || parseInt(uaMatch[1]) < version) return false;
+ if (!uaMatch || parseInt(uaMatch[1], 10) < version) return false;
return true;
}
@@ -49,10 +49,10 @@ export function isPushNotificationsSupported(mobileView) {
if (
!(
"serviceWorker" in navigator &&
- (ServiceWorkerRegistration &&
- typeof Notification !== "undefined" &&
- "showNotification" in ServiceWorkerRegistration.prototype &&
- "PushManager" in window)
+ ServiceWorkerRegistration &&
+ typeof Notification !== "undefined" &&
+ "showNotification" in ServiceWorkerRegistration.prototype &&
+ "PushManager" in window
)
) {
return false;
diff --git a/app/assets/javascripts/discourse/lib/pwa-utils.js.es6 b/app/assets/javascripts/discourse/lib/pwa-utils.js.es6
index c1833da32b..23746a0cb3 100644
--- a/app/assets/javascripts/discourse/lib/pwa-utils.js.es6
+++ b/app/assets/javascripts/discourse/lib/pwa-utils.js.es6
@@ -1,6 +1,8 @@
+import { Promise } from "rsvp";
+
export function nativeShare(data) {
const caps = Discourse.__container__.lookup("capabilities:main");
- return new Ember.RSVP.Promise((resolve, reject) => {
+ return new Promise((resolve, reject) => {
if (!(caps.isIOS || caps.isAndroid || caps.isWinphone)) {
reject();
return;
@@ -24,3 +26,18 @@ export function nativeShare(data) {
}
});
}
+
+export function getNativeContact(properties, multiple) {
+ const caps = Discourse.__container__.lookup("capabilities:main");
+ return new Promise((resolve, reject) => {
+ if (!caps.hasContactPicker) {
+ reject();
+ return;
+ }
+
+ navigator.contacts
+ .select(properties, { multiple })
+ .then(resolve)
+ .catch(reject);
+ });
+}
diff --git a/app/assets/javascripts/discourse/lib/quote-state.js.es6 b/app/assets/javascripts/discourse/lib/quote-state.js.es6
index 56e2843887..63f7337aec 100644
--- a/app/assets/javascripts/discourse/lib/quote-state.js.es6
+++ b/app/assets/javascripts/discourse/lib/quote-state.js.es6
@@ -3,13 +3,15 @@ export default class QuoteState {
this.clear();
}
- selected(postId, buffer) {
+ selected(postId, buffer, opts) {
this.postId = postId;
this.buffer = buffer;
+ this.opts = opts;
}
clear() {
this.buffer = "";
this.postId = null;
+ this.opts = null;
}
}
diff --git a/app/assets/javascripts/discourse/lib/quote.js.es6 b/app/assets/javascripts/discourse/lib/quote.js.es6
index 568cf987c8..7a39c6da30 100644
--- a/app/assets/javascripts/discourse/lib/quote.js.es6
+++ b/app/assets/javascripts/discourse/lib/quote.js.es6
@@ -8,6 +8,7 @@ export default {
}
if (!contents) contents = "";
+ if (!opts) opts = {};
const sansQuotes = contents.replace(this.REGEXP, "").trim();
if (sansQuotes.length === 0) {
@@ -26,9 +27,9 @@ export default {
stripped.replace(/\W/g, "") === contents.replace(/\W/g, "");
const params = [
- post.get("username"),
- `post:${post.get("post_number")}`,
- `topic:${post.get("topic_id")}`
+ opts.username || post.username,
+ `post:${opts.post || post.post_number}`,
+ `topic:${opts.topic || post.topic_id}`
];
opts = opts || {};
diff --git a/app/assets/javascripts/discourse/lib/register-topic-footer-button.js.es6 b/app/assets/javascripts/discourse/lib/register-topic-footer-button.js.es6
index 789122618d..c2dd9eb96c 100644
--- a/app/assets/javascripts/discourse/lib/register-topic-footer-button.js.es6
+++ b/app/assets/javascripts/discourse/lib/register-topic-footer-button.js.es6
@@ -1,8 +1,11 @@
+import error from "@ember/error";
+import { computed } from "@ember/object";
+
let _topicFooterButtons = {};
export function registerTopicFooterButton(button) {
if (!button.id) {
- Ember.error(`Attempted to register a topic button: ${button} with no id.`);
+ error(`Attempted to register a topic button: ${button} with no id.`);
return;
}
@@ -31,7 +34,7 @@ export function registerTopicFooterButton(button) {
// css class appended to the button
classNames: [],
- // computed properties which should force a button state refresh
+ // discourseComputed properties which should force a button state refresh
// eg: ["topic.bookmarked", "topic.category_id"]
dependentKeys: [],
@@ -52,10 +55,8 @@ export function registerTopicFooterButton(button) {
!normalizedButton.title &&
!normalizedButton.translatedTitle
) {
- Ember.error(
- `Attempted to register a topic button: ${
- button.id
- } with no icon or title.`
+ error(
+ `Attempted to register a topic button: ${button.id} with no icon or title.`
);
return;
}
@@ -70,7 +71,7 @@ export function getTopicFooterButtons() {
.filter(x => x)
);
- const computedFunc = Ember.computed({
+ return computed(...dependentKeys, {
get() {
const _isFunction = descriptor =>
descriptor && typeof descriptor === "function";
@@ -88,44 +89,42 @@ export function getTopicFooterButtons() {
return Object.values(_topicFooterButtons)
.filter(button => _compute(button, "displayed"))
.map(button => {
- const computedButon = {};
+ const discourseComputedButon = {};
- computedButon.id = button.id;
+ discourseComputedButon.id = button.id;
const label = _compute(button, "label");
- computedButon.label = label
+ discourseComputedButon.label = label
? I18n.t(label)
: _compute(button, "translatedLabel");
const title = _compute(button, "title");
- computedButon.title = title
+ discourseComputedButon.title = title
? I18n.t(title)
: _compute(button, "translatedTitle");
- computedButon.classNames = (
+ discourseComputedButon.classNames = (
_compute(button, "classNames") || []
).join(" ");
- computedButon.icon = _compute(button, "icon");
- computedButon.disabled = _compute(button, "disabled");
- computedButon.dropdown = _compute(button, "dropdown");
- computedButon.priority = _compute(button, "priority");
+ discourseComputedButon.icon = _compute(button, "icon");
+ discourseComputedButon.disabled = _compute(button, "disabled");
+ discourseComputedButon.dropdown = _compute(button, "dropdown");
+ discourseComputedButon.priority = _compute(button, "priority");
if (_isFunction(button.action)) {
- computedButon.action = () => button.action.apply(this);
+ discourseComputedButon.action = () => button.action.apply(this);
} else {
const actionName = button.action;
- computedButon.action = () => this[actionName]();
+ discourseComputedButon.action = () => this[actionName]();
}
- return computedButon;
+ return discourseComputedButon;
})
.sortBy("priority")
.reverse();
}
});
-
- return computedFunc.property.apply(computedFunc, dependentKeys);
}
export function clearTopicFooterButtons() {
diff --git a/app/assets/javascripts/discourse/lib/render-tag.js.es6 b/app/assets/javascripts/discourse/lib/render-tag.js.es6
index 4e0597d4a4..f9230f0255 100644
--- a/app/assets/javascripts/discourse/lib/render-tag.js.es6
+++ b/app/assets/javascripts/discourse/lib/render-tag.js.es6
@@ -1,10 +1,12 @@
+import User from "discourse/models/user";
+
let _renderer = defaultRenderTag;
export function replaceTagRenderer(fn) {
_renderer = fn;
}
-function defaultRenderTag(tag, params) {
+export function defaultRenderTag(tag, params) {
params = params || {};
const visibleName = Handlebars.Utils.escapeExpression(tag);
tag = visibleName.toLowerCase();
@@ -12,13 +14,13 @@ function defaultRenderTag(tag, params) {
const tagName = params.tagName || "a";
let path;
if (tagName === "a" && !params.noHref) {
- if (params.isPrivateMessage && Discourse.User.current()) {
+ if ((params.isPrivateMessage || params.pmOnly) && User.current()) {
const username = params.tagsForUser
? params.tagsForUser
- : Discourse.User.current().username;
+ : User.current().username;
path = `/u/${username}/messages/tags/${tag}`;
} else {
- path = `/tags/${tag}`;
+ path = `/tag/${tag}`;
}
}
const href = path ? ` href='${Discourse.getURL(path)}' ` : "";
@@ -26,6 +28,9 @@ function defaultRenderTag(tag, params) {
if (Discourse.SiteSettings.tag_style || params.style) {
classes.push(params.style || Discourse.SiteSettings.tag_style);
}
+ if (params.size) {
+ classes.push(params.size);
+ }
let val =
"<" +
diff --git a/app/assets/javascripts/discourse/lib/render-tags.js.es6 b/app/assets/javascripts/discourse/lib/render-tags.js.es6
index b6a8a32880..55538088d1 100644
--- a/app/assets/javascripts/discourse/lib/render-tags.js.es6
+++ b/app/assets/javascripts/discourse/lib/render-tags.js.es6
@@ -21,6 +21,7 @@ export default function(topic, params) {
let tags = topic.tags;
let buffer = "";
let tagsForUser = null;
+ let tagName;
const isPrivateMessage = topic.get("isPrivateMessage");
if (params) {
@@ -30,6 +31,9 @@ export default function(topic, params) {
if (params.tagsForUser) {
tagsForUser = params.tagsForUser;
}
+ if (params.tagName) {
+ tagName = params.tagName;
+ }
}
let customHtml = null;
@@ -50,7 +54,8 @@ export default function(topic, params) {
buffer = "";
if (tags) {
for (let i = 0; i < tags.length; i++) {
- buffer += renderTag(tags[i], { isPrivateMessage, tagsForUser }) + " ";
+ buffer +=
+ renderTag(tags[i], { isPrivateMessage, tagsForUser, tagName }) + " ";
}
}
diff --git a/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6 b/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6
index 7fa722a28d..b4751954aa 100644
--- a/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6
+++ b/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6
@@ -1,5 +1,6 @@
import { h } from "virtual-dom";
import { renderIcon } from "discourse-common/lib/icon-library";
+import User from "discourse/models/user";
const _decorators = [];
@@ -9,9 +10,7 @@ export function addFeaturedLinkMetaDecorator(decorator) {
export function extractLinkMeta(topic) {
const href = topic.get("featured_link");
- const target = Discourse.User.currentProp("external_links_in_new_tab")
- ? "_blank"
- : "";
+ const target = User.currentProp("external_links_in_new_tab") ? "_blank" : "";
if (!href) {
return;
@@ -21,7 +20,7 @@ export function extractLinkMeta(topic) {
target: target,
href,
domain: topic.get("featured_link_root_domain"),
- rel: "nofollow"
+ rel: "nofollow ugc"
};
if (_decorators.length) {
diff --git a/app/assets/javascripts/discourse/lib/reports-loader.js.es6 b/app/assets/javascripts/discourse/lib/reports-loader.js.es6
index fefd6c4c4d..1d41d0a1cf 100644
--- a/app/assets/javascripts/discourse/lib/reports-loader.js.es6
+++ b/app/assets/javascripts/discourse/lib/reports-loader.js.es6
@@ -1,5 +1,6 @@
+import { run } from "@ember/runloop";
import { ajax } from "discourse/lib/ajax";
-const { debounce } = Ember.run;
+const { debounce } = run;
let _queue = [];
let _processing = 0;
diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
index 809adbf6ba..799d3c904c 100644
--- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
+++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
@@ -1,5 +1,12 @@
-import debounce from "discourse/lib/debounce";
-import { isAppleDevice, safariHacksDisabled } from "discourse/lib/utilities";
+import { later } from "@ember/runloop";
+import discourseDebounce from "discourse/lib/debounce";
+import {
+ safariHacksDisabled,
+ iOSWithVisualViewport
+} from "discourse/lib/utilities";
+
+// TODO: remove calcHeight once iOS 13 adoption > 90%
+// In iOS 13 and up we use visualViewport API to calculate height
// we can't tell what the actual visible window height is
// because we cannot account for the height of the mobile keyboard
@@ -41,6 +48,11 @@ function calcHeight() {
withoutKeyboard = smallViewport ? 340 : 370;
}
+ // iPhone Xs Max and iPhone Xʀ
+ if (window.screen.height === 896) {
+ withoutKeyboard = smallViewport ? 410 : 440;
+ }
+
// iPad can use innerHeight cause it renders nothing in the footer
if (window.innerHeight > 920) {
withoutKeyboard -= 45;
@@ -58,7 +70,6 @@ function calcHeight() {
}
let workaroundActive = false;
-let composingTopic = false;
export function isWorkaroundActive() {
return workaroundActive;
@@ -66,28 +77,26 @@ 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() || safariHacksDisabled()) {
+ const caps = Discourse.__container__.lookup("capabilities:main");
+
+ if (!caps.isIOS || safariHacksDisabled()) {
return;
}
const fixedElement = $fixedElement[0];
const oldHeight = fixedElement.style.height;
- var done = false;
var originalScrollTop = 0;
+ let lastTouchedElement = null;
positioningWorkaround.blur = function(evt) {
if (workaroundActive) {
- done = true;
+ $("body").removeClass("ios-safari-composer-hacks");
- $("#main-outlet").show();
- $("header").show();
-
- fixedElement.style.position = "";
- fixedElement.style.top = "";
- fixedElement.style.height = oldHeight;
-
- Ember.run.later(() => $(fixedElement).removeClass("no-transition"), 500);
+ if (!iOSWithVisualViewport()) {
+ fixedElement.style.height = oldHeight;
+ later(() => $(fixedElement).removeClass("no-transition"), 500);
+ }
$(window).scrollTop(originalScrollTop);
@@ -99,66 +108,110 @@ function positioningWorkaround($fixedElement) {
};
var blurredNow = function(evt) {
+ // we cannot use evt.relatedTarget to get the last focused element in safari iOS
+ // document.activeElement is also unreliable (iOS does not mark buttons as focused)
+ // so instead, we store the last touched element and check against it
+
+ // cancel blur event if user is:
+ // - switching to another iOS app
+ // - invoking a select-kit dropdown
+ // - invoking mentions
+ // - invoking emoji dropdown via : and hitting return
+ // - invoking a toolbar button
+
if (
- !done &&
- $(document.activeElement)
- .parents()
- .toArray()
- .indexOf(fixedElement) > -1
+ lastTouchedElement &&
+ (document.visibilityState === "hidden" ||
+ $(lastTouchedElement).hasClass("select-kit-header") ||
+ $(lastTouchedElement).closest(".autocomplete").length ||
+ (lastTouchedElement.nodeName.toLowerCase() === "textarea" &&
+ document.activeElement === lastTouchedElement) ||
+ ["span", "svg", "button"].includes(
+ lastTouchedElement.nodeName.toLowerCase()
+ ))
) {
- // something in focus so skip
return;
}
positioningWorkaround.blur(evt);
};
- var blurred = debounce(blurredNow, 250);
+ var blurred = discourseDebounce(blurredNow, 250);
var positioningHack = function(evt) {
- const self = this;
- done = false;
-
// we need this, otherwise changing focus means we never clear
- self.addEventListener("blur", blurred);
+ this.addEventListener("blur", blurred);
- if (fixedElement.style.top === "0px") {
- if (this !== document.activeElement) {
- evt.preventDefault();
- self.focus();
- }
- return;
+ // resets focus out of select-kit elements
+ // might become redundant after select-kit refactoring
+ $fixedElement.find(".select-kit.is-expanded > button").trigger("click");
+ $fixedElement
+ .find(".select-kit > button.is-focused")
+ .removeClass("is-focused");
+
+ if ($(window).scrollTop() > 0) {
+ originalScrollTop = $(window).scrollTop();
}
- originalScrollTop = $(window).scrollTop();
-
- // take care of body
-
- $("#main-outlet").hide();
- $("header").hide();
-
- $(window).scrollTop(0);
-
- let i = 20;
- let interval = setInterval(() => {
- $(window).scrollTop(0);
- if (i-- === 0) {
- clearInterval(interval);
+ setTimeout(function() {
+ if (iOSWithVisualViewport()) {
+ // disable hacks when using a hardware keyboard
+ // by default, a hardware keyboard will show the keyboard accessory bar
+ // whose height is currently 55px (using 75 for a bit of a buffer)
+ let heightDiff = window.innerHeight - window.visualViewport.height;
+ if (heightDiff < 75) {
+ return;
+ }
}
- }, 10);
- fixedElement.style.top = "0px";
+ if (fixedElement.style.top === "0px") {
+ if (this !== document.activeElement) {
+ evt.preventDefault();
- composingTopic = $("#reply-control .category-chooser").length > 0;
+ // this tricks safari into assuming current input is at top of the viewport
+ // via https://stackoverflow.com/questions/38017771/mobile-safari-prevent-scroll-page-when-focus-on-input
+ this.style.transform = "translateY(-200px)";
+ this.focus();
+ let _this = this;
+ setTimeout(function() {
+ _this.style.transform = "none";
+ }, 30);
+ }
+ return;
+ }
- const height = calcHeight(composingTopic);
- fixedElement.style.height = height + "px";
+ // don't trigger keyboard on disabled element (happens when a category is required)
+ if (this.disabled) {
+ return;
+ }
- $(fixedElement).addClass("no-transition");
+ $("body").addClass("ios-safari-composer-hacks");
+ $(window).scrollTop(0);
- evt.preventDefault();
- self.focus();
- workaroundActive = true;
+ let i = 20;
+ let interval = setInterval(() => {
+ $(window).scrollTop(0);
+ if (i-- === 0) {
+ clearInterval(interval);
+ }
+ }, 10);
+
+ if (!iOSWithVisualViewport()) {
+ const height = calcHeight();
+ fixedElement.style.height = height + "px";
+ $(fixedElement).addClass("no-transition");
+ }
+
+ evt.preventDefault();
+ this.focus();
+ workaroundActive = true;
+ }, 350);
+ };
+
+ var lastTouched = function(evt) {
+ if (evt && evt.target) {
+ lastTouchedElement = evt.target;
+ }
};
function attachTouchStart(elem, fn) {
@@ -168,31 +221,9 @@ function positioningWorkaround($fixedElement) {
}
}
- const checkForInputs = debounce(function() {
- $fixedElement
- .find(
- "button:not(.hide-preview),a:not(.mobile-file-upload):not(.toggle-toolbar)"
- )
- .each(function(idx, elem) {
- if ($(elem).parents(".emoji-picker").length > 0) {
- return;
- }
+ const checkForInputs = discourseDebounce(function() {
+ attachTouchStart(fixedElement, lastTouched);
- if ($(elem).parents(".autocomplete").length > 0) {
- return;
- }
-
- if ($(elem).parents(".d-editor-button-bar").length > 0) {
- return;
- }
-
- attachTouchStart(this, function(evt) {
- done = true;
- $(document.activeElement).blur();
- evt.preventDefault();
- $(this).click();
- });
- });
$fixedElement.find("input[type=text],textarea").each(function() {
attachTouchStart(this, positioningHack);
});
diff --git a/app/assets/javascripts/discourse/lib/screen-track.js.es6 b/app/assets/javascripts/discourse/lib/screen-track.js.es6
index a36b15cb21..91e0d42a83 100644
--- a/app/assets/javascripts/discourse/lib/screen-track.js.es6
+++ b/app/assets/javascripts/discourse/lib/screen-track.js.es6
@@ -1,3 +1,4 @@
+import { bind } from "@ember/runloop";
import { ajax } from "discourse/lib/ajax";
// We use this class to track how long posts in a topic are on the screen.
@@ -26,7 +27,7 @@ export default class {
// Create an interval timer if we don't have one.
if (!this._interval) {
this._interval = setInterval(() => this.tick(), 1000);
- this._boundScrolled = Ember.run.bind(this, this.scrolled);
+ this._boundScrolled = bind(this, this.scrolled);
$(window).on("scroll.screentrack", this._boundScrolled);
}
@@ -186,7 +187,7 @@ export default class {
// Save unique topic IDs up to a max
let topicIds = storage.get("anon-topic-ids");
if (topicIds) {
- topicIds = topicIds.split(",").map(e => parseInt(e));
+ topicIds = topicIds.split(",").map(e => parseInt(e, 10));
} else {
topicIds = [];
}
diff --git a/app/assets/javascripts/discourse/lib/search.js.es6 b/app/assets/javascripts/discourse/lib/search.js.es6
index 28380ddc93..d6c76dd56b 100644
--- a/app/assets/javascripts/discourse/lib/search.js.es6
+++ b/app/assets/javascripts/discourse/lib/search.js.es6
@@ -1,9 +1,12 @@
+import { isEmpty } from "@ember/utils";
+import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { findRawTemplate } from "discourse/lib/raw-templates";
import Category from "discourse/models/category";
import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
import userSearch from "discourse/lib/user-search";
import { userPath } from "discourse/lib/url";
+import { emojiUnescape } from "discourse/lib/text";
import User from "discourse/models/user";
import Post from "discourse/models/post";
import Topic from "discourse/models/topic";
@@ -31,6 +34,7 @@ export function translateResults(results, opts) {
}
post = Post.create(post);
post.set("topic", topicMap[post.topic_id]);
+ post.blurb = emojiUnescape(post.blurb);
return post;
});
@@ -50,7 +54,7 @@ export function translateResults(results, opts) {
const fullName = Handlebars.Utils.escapeExpression(
group.full_name || group.display_name
);
- const flairUrl = Ember.isEmpty(group.flair_url)
+ const flairUrl = isEmpty(group.flair_url)
? null
: Handlebars.Utils.escapeExpression(group.flair_url);
const flairColor = Handlebars.Utils.escapeExpression(group.flair_color);
@@ -73,9 +77,9 @@ export function translateResults(results, opts) {
results.tags = results.tags
.map(function(tag) {
const tagName = Handlebars.Utils.escapeExpression(tag.name);
- return Ember.Object.create({
+ return EmberObject.create({
id: tagName,
- url: Discourse.getURL("/tags/" + tagName)
+ url: Discourse.getURL("/tag/" + tagName)
});
})
.compact();
@@ -125,7 +129,7 @@ export function translateResults(results, opts) {
!results.categories.length
);
- return noResults ? null : Ember.Object.create(results);
+ return noResults ? null : EmberObject.create(results);
}
export function searchForTerm(term, opts) {
@@ -141,7 +145,8 @@ export function searchForTerm(term, opts) {
if (opts.searchContext) {
data.search_context = {
type: opts.searchContext.type,
- id: opts.searchContext.id
+ id: opts.searchContext.id,
+ name: opts.searchContext.name
};
}
@@ -163,6 +168,8 @@ export function searchContextDescription(type, name) {
return I18n.t("search.context.user", { username: name });
case "category":
return I18n.t("search.context.category", { category: name });
+ case "tag":
+ return I18n.t("search.context.tag", { tag: name });
case "private_messages":
return I18n.t("search.context.private_messages");
}
diff --git a/app/assets/javascripts/discourse/lib/settings.js.es6 b/app/assets/javascripts/discourse/lib/settings.js.es6
new file mode 100644
index 0000000000..f4f6ae6fa6
--- /dev/null
+++ b/app/assets/javascripts/discourse/lib/settings.js.es6
@@ -0,0 +1,7 @@
+export function prioritizeNameInUx(name, siteSettings) {
+ siteSettings = siteSettings || Discourse.SiteSettings;
+
+ return (
+ !siteSettings.prioritize_username_in_ux && name && name.trim().length > 0
+ );
+}
diff --git a/app/assets/javascripts/discourse/lib/sharing.js.es6 b/app/assets/javascripts/discourse/lib/sharing.js.es6
index 4e57b9dfa5..2a3b4d191b 100644
--- a/app/assets/javascripts/discourse/lib/sharing.js.es6
+++ b/app/assets/javascripts/discourse/lib/sharing.js.es6
@@ -19,6 +19,11 @@
return "http://twitter.com/intent/tweet?url=" + encodeURIComponent(link) + "&text=" + encodeURIComponent(title);
},
+ // If provided, handle by custom javascript rather than default url open
+ clickHandler: function(link, title){
+ alert("Hello!")
+ }
+
// If true, opens in a popup of `popupHeight` size. If false it's opened in a new tab
shouldOpenInPopup: true,
popupHeight: 265
@@ -48,23 +53,27 @@ export default {
},
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);
+ if (source.clickHandler) {
+ source.clickHandler(data.url, data.title);
} else {
- window.open(url, "_blank");
+ 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");
+ }
}
},
diff --git a/app/assets/javascripts/discourse/lib/show-modal.js.es6 b/app/assets/javascripts/discourse/lib/show-modal.js.es6
index 8cf5a2b0be..968f1e399d 100644
--- a/app/assets/javascripts/discourse/lib/show-modal.js.es6
+++ b/app/assets/javascripts/discourse/lib/show-modal.js.es6
@@ -1,3 +1,5 @@
+import { dasherize } from "@ember/string";
+
export default function(name, opts) {
opts = opts || {};
const container = Discourse.__container__;
@@ -13,7 +15,7 @@ export default function(name, opts) {
modalController.set("name", controllerName);
let controller = container.lookup("controller:" + controllerName);
- const templateName = opts.templateName || Ember.String.dasherize(name);
+ const templateName = opts.templateName || dasherize(name);
const renderArgs = { into: "modal", outlet: "modalBody" };
if (controller) {
diff --git a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6
index efc97411c0..91cda0f0be 100644
--- a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6
+++ b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6
@@ -1,5 +1,6 @@
+import DiscourseRoute from "discourse/routes/discourse";
import StaticPage from "discourse/models/static-page";
-import { default as DiscourseURL, jumpToElement } from "discourse/lib/url";
+import DiscourseURL, { jumpToElement } from "discourse/lib/url";
const configs = {
faq: "faq_url",
@@ -8,7 +9,7 @@ const configs = {
};
export default function(page) {
- return Discourse.Route.extend({
+ return DiscourseRoute.extend({
renderTemplate() {
this.render("static");
},
diff --git a/app/assets/javascripts/discourse/lib/text.js.es6 b/app/assets/javascripts/discourse/lib/text.js.es6
index ee51cb53d1..7f75cb497d 100644
--- a/app/assets/javascripts/discourse/lib/text.js.es6
+++ b/app/assets/javascripts/discourse/lib/text.js.es6
@@ -1,9 +1,10 @@
-import { default as PrettyText, buildOptions } from "pretty-text/pretty-text";
+import PrettyText, { buildOptions } from "pretty-text/pretty-text";
import { performEmojiUnescape, buildEmojiUrl } from "pretty-text/emoji";
import WhiteLister from "pretty-text/white-lister";
import { sanitize as textSanitize } from "pretty-text/sanitizer";
import loadScript from "discourse/lib/load-script";
import { formatUsername } from "discourse/lib/utilities";
+import { Promise } from "rsvp";
function getOpts(opts) {
const siteSettings = Discourse.__container__.lookup("site-settings:main"),
@@ -13,7 +14,7 @@ function getOpts(opts) {
{
getURL: Discourse.getURLWithCDN,
currentUser: Discourse.__container__.lookup("current-user:main"),
- censoredWords: site.censored_words,
+ censoredRegexp: site.censored_regexp,
siteSettings,
formatUsername
},
@@ -51,7 +52,7 @@ function loadMarkdownIt() {
console.error(e);
});
} else {
- return Ember.RSVP.Promise.resolve();
+ return Promise.resolve();
}
}
@@ -67,7 +68,8 @@ function emojiOptions() {
return {
getURL: Discourse.getURLWithCDN,
emojiSet: Discourse.SiteSettings.emoji_set,
- enableEmojiShortcuts: Discourse.SiteSettings.enable_emoji_shortcuts
+ enableEmojiShortcuts: Discourse.SiteSettings.enable_emoji_shortcuts,
+ inlineEmoji: Discourse.SiteSettings.enable_inline_emoji_translation
};
}
diff --git a/app/assets/javascripts/discourse/lib/theme-selector.js.es6 b/app/assets/javascripts/discourse/lib/theme-selector.js.es6
index 60d61c20c5..e0ba455d4f 100644
--- a/app/assets/javascripts/discourse/lib/theme-selector.js.es6
+++ b/app/assets/javascripts/discourse/lib/theme-selector.js.es6
@@ -43,16 +43,12 @@ export function setLocalTheme(ids, themeSeq) {
}
}
-export function refreshCSS(node, hash, newHref, options) {
+export function refreshCSS(node, hash, newHref) {
let $orig = $(node);
if ($orig.data("reloading")) {
- if (options && options.force) {
- clearTimeout($orig.data("timeout"));
- $orig.data("copy").remove();
- } else {
- return;
- }
+ clearTimeout($orig.data("timeout"));
+ $orig.data("copy").remove();
}
if (!$orig.data("orig")) {
@@ -99,7 +95,7 @@ export function previewTheme(ids = []) {
`link[rel=stylesheet][data-target=${theme.target}]`
)[0];
if (node) {
- refreshCSS(node, null, theme.new_href, { force: true });
+ refreshCSS(node, null, theme.new_href);
}
});
}
diff --git a/app/assets/javascripts/discourse/lib/throttle.js.es6 b/app/assets/javascripts/discourse/lib/throttle.js.es6
deleted file mode 100644
index 2e8673203b..0000000000
--- a/app/assets/javascripts/discourse/lib/throttle.js.es6
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- Throttle a Javascript function. This means if it's called many times in a time limit it
- should only be executed one time at most during this time limit
- Original function will be called with the context and arguments from the last call made.
-**/
-export default function(func, spacing, immediate) {
- let self, args;
- const later = function() {
- func.apply(self, args);
- };
-
- return function() {
- self = this;
- args = arguments;
-
- Ember.run.throttle(null, later, spacing, immediate);
- };
-}
diff --git a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 b/app/assets/javascripts/discourse/lib/to-markdown.js.es6
index 72ae8aaa92..83d7b28d04 100644
--- a/app/assets/javascripts/discourse/lib/to-markdown.js.es6
+++ b/app/assets/javascripts/discourse/lib/to-markdown.js.es6
@@ -44,7 +44,6 @@ export class Tag {
return [
"address",
"article",
- "aside",
"dd",
"div",
"dl",
@@ -89,6 +88,7 @@ export class Tag {
...Tag.blocks(),
...Tag.headings(),
...Tag.slices(),
+ "aside",
"li",
"td",
"th",
@@ -102,6 +102,10 @@ export class Tag {
];
}
+ static whitelists() {
+ return ["ins", "del", "small", "big", "kbd", "ruby", "rt", "rb", "rp"];
+ }
+
static block(name, prefix, suffix) {
return class extends Tag {
constructor() {
@@ -122,6 +126,45 @@ export class Tag {
};
}
+ static aside() {
+ return class extends Tag.block("aside") {
+ constructor() {
+ super();
+ }
+
+ toMarkdown() {
+ if (!/\bquote\b/.test(this.element.attributes.class)) {
+ return super.toMarkdown();
+ }
+
+ const blockquote = this.element.children.find(
+ child => child.name === "blockquote"
+ );
+
+ if (!blockquote) {
+ return super.toMarkdown();
+ }
+
+ let text = Element.parse([blockquote], this.element) || "";
+ text = text.trim().replace(/^>/g, "");
+ if (text.length === 0) {
+ return "";
+ }
+
+ const username = this.element.attributes["data-username"];
+ const post = this.element.attributes["data-post"];
+ const topic = this.element.attributes["data-topic"];
+
+ const prefix =
+ username && post && topic
+ ? `[quote="${username}, post:${post}, topic:${topic}"]`
+ : "[quote]";
+
+ return `\n\n${prefix}\n${text}\n[/quote]\n\n`;
+ }
+ };
+ }
+
static heading(name, i) {
const prefix = `${[...Array(i)].map(() => "#").join("")} `;
return Tag.block(name, prefix, "");
@@ -149,7 +192,7 @@ export class Tag {
};
}
- static keep(name) {
+ static whitelist(name) {
return class extends Tag {
constructor() {
super(name, `<${name}>`, `${name}>`);
@@ -206,8 +249,16 @@ export class Tag {
["lightbox", "d-lazyload"].includes(attr.class) &&
hasChild(e, "img")
) {
+ let href = attr.href;
+ const img = (e.children || []).find(c => c.name === "img");
+ const base62SHA1 = img.attributes["data-base62-sha1"];
text = attr.title || "";
- return "";
+
+ if (base62SHA1) {
+ href = `upload://${base62SHA1}`;
+ }
+
+ return "";
}
if (attr.href && text !== attr.href) {
@@ -313,7 +364,8 @@ export class Tag {
if (msoListClasses.includes(attrs.class)) {
try {
const level = parseInt(
- attrs.style.match(/level./)[0].replace("level", "")
+ attrs.style.match(/level./)[0].replace("level", ""),
+ 10
);
indent = Array(level).join("\t") + indent;
} finally {
@@ -440,7 +492,7 @@ export class Tag {
const bullet = text.match(/\n\t*\*/)[0];
for (
- let i = parseInt(this.element.attributes.start || 1);
+ let i = parseInt(this.element.attributes.start || 1, 10);
text.includes(bullet);
i++
) {
@@ -470,16 +522,13 @@ function tags() {
...Tag.headings().map((h, i) => Tag.heading(h, i + 1)),
...Tag.slices().map(s => Tag.slice(s, "\n")),
...Tag.emphases().map(e => Tag.emphasis(e[0], e[1])),
+ ...Tag.whitelists().map(t => Tag.whitelist(t)),
+ Tag.aside(),
Tag.cell("td"),
Tag.cell("th"),
Tag.replace("br", "\n"),
Tag.replace("hr", "\n---\n"),
Tag.replace("head", ""),
- Tag.keep("ins"),
- Tag.keep("del"),
- Tag.keep("small"),
- Tag.keep("big"),
- Tag.keep("kbd"),
Tag.li(),
Tag.link(),
Tag.image(),
diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6
index 7a1872b4fe..f6d19c6b46 100644
--- a/app/assets/javascripts/discourse/lib/transform-post.js.es6
+++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6
@@ -1,3 +1,4 @@
+import { isEmpty } from "@ember/utils";
import { userPath } from "discourse/lib/url";
const _additionalAttributes = [];
@@ -34,6 +35,8 @@ export function transformBasicPost(post) {
username: post.username,
avatar_template: post.avatar_template,
bookmarked: post.bookmarked,
+ bookmarkedWithReminder: post.bookmarked_with_reminder,
+ bookmarkReminderAt: post.bookmark_reminder_at,
yours: post.yours,
shareUrl: post.get("shareUrl"),
staff: post.staff,
@@ -48,7 +51,7 @@ export function transformBasicPost(post) {
showFlagDelete: false,
canRecover: post.can_recover,
canEdit: post.can_edit,
- canFlag: !Ember.isEmpty(post.get("flagsAvailable")),
+ canFlag: !isEmpty(post.get("flagsAvailable")),
canReviewTopic: false,
reviewableId: post.reviewable_id,
reviewableScoreCount: post.reviewable_score_count,
@@ -71,7 +74,8 @@ export function transformBasicPost(post) {
expandablePost: false,
replyCount: post.reply_count,
locked: post.locked,
- userCustomFields: post.user_custom_fields
+ userCustomFields: post.user_custom_fields,
+ readCount: post.readers_count
};
_additionalAttributes.forEach(a => (postAtts[a] = post[a]));
@@ -237,7 +241,8 @@ export default function transformPost(
postAtts.showFlagDelete =
!postAtts.canDelete &&
postAtts.yours &&
- (currentUser && !currentUser.staff);
+ currentUser &&
+ !currentUser.staff;
} else {
postAtts.canRecover = postAtts.isDeleted && postAtts.canRecover;
postAtts.canDelete =
diff --git a/app/assets/javascripts/discourse/lib/uploads.js.es6 b/app/assets/javascripts/discourse/lib/uploads.js.es6
new file mode 100644
index 0000000000..37ded98e4f
--- /dev/null
+++ b/app/assets/javascripts/discourse/lib/uploads.js.es6
@@ -0,0 +1,290 @@
+import { isAppleDevice } from "discourse/lib/utilities";
+
+function isGUID(value) {
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
+ value
+ );
+}
+
+export function markdownNameFromFileName(fileName) {
+ let name = fileName.substr(0, fileName.lastIndexOf("."));
+
+ if (isAppleDevice() && isGUID(name)) {
+ name = I18n.t("upload_selector.default_image_alt_text");
+ }
+
+ return name.replace(/\[|\]|\|/g, "");
+}
+
+export function validateUploadedFiles(files, opts) {
+ if (!files || files.length === 0) {
+ return false;
+ }
+
+ if (files.length > 1) {
+ bootbox.alert(I18n.t("post.errors.too_many_uploads"));
+ return false;
+ }
+
+ const upload = files[0];
+
+ // CHROME ONLY: if the image was pasted, sets its name to a default one
+ if (typeof Blob !== "undefined" && typeof File !== "undefined") {
+ if (
+ upload instanceof Blob &&
+ !(upload instanceof File) &&
+ upload.type === "image/png"
+ ) {
+ upload.name = "image.png";
+ }
+ }
+
+ opts = opts || {};
+ opts.type = uploadTypeFromFileName(upload.name);
+
+ return validateUploadedFile(upload, opts);
+}
+
+function validateUploadedFile(file, opts) {
+ if (opts.skipValidation) return true;
+
+ opts = opts || {};
+ let user = opts.user;
+ let staff = user && user.staff;
+
+ if (!authorizesOneOrMoreExtensions(staff)) return false;
+
+ const name = file && file.name;
+
+ if (!name) {
+ return false;
+ }
+
+ // check that the uploaded file is authorized
+ if (opts.allowStaffToUploadAnyFileInPm && opts.isPrivateMessage) {
+ if (staff) {
+ return true;
+ }
+ }
+
+ if (opts.imagesOnly) {
+ if (!isImage(name) && !isAuthorizedImage(name, staff)) {
+ bootbox.alert(
+ I18n.t("post.errors.upload_not_authorized", {
+ authorized_extensions: authorizedImagesExtensions(staff)
+ })
+ );
+ return false;
+ }
+ } else if (opts.csvOnly) {
+ if (!/\.csv$/i.test(name)) {
+ bootbox.alert(I18n.t("user.invited.bulk_invite.error"));
+ return false;
+ }
+ } else {
+ if (!authorizesAllExtensions(staff) && !isAuthorizedFile(name, staff)) {
+ bootbox.alert(
+ I18n.t("post.errors.upload_not_authorized", {
+ authorized_extensions: authorizedExtensions(staff)
+ })
+ );
+ return false;
+ }
+ }
+
+ if (!opts.bypassNewUserRestriction) {
+ // ensures that new users can upload a file
+ if (user && !user.isAllowedToUploadAFile(opts.type)) {
+ bootbox.alert(
+ I18n.t(`post.errors.${opts.type}_upload_not_allowed_for_new_user`)
+ );
+ return false;
+ }
+ }
+
+ // everything went fine
+ return true;
+}
+
+const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|svg|ico)/i;
+
+function extensionsToArray(exts) {
+ return exts
+ .toLowerCase()
+ .replace(/[\s\.]+/g, "")
+ .split("|")
+ .filter(ext => ext.indexOf("*") === -1);
+}
+
+function extensions() {
+ return extensionsToArray(Discourse.SiteSettings.authorized_extensions);
+}
+
+function staffExtensions() {
+ return extensionsToArray(
+ Discourse.SiteSettings.authorized_extensions_for_staff
+ );
+}
+
+function imagesExtensions(staff) {
+ let exts = extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext));
+ if (staff) {
+ const staffExts = staffExtensions().filter(ext =>
+ IMAGES_EXTENSIONS_REGEX.test(ext)
+ );
+ exts = _.union(exts, staffExts);
+ }
+ return exts;
+}
+
+function extensionsRegex() {
+ return new RegExp("\\.(" + extensions().join("|") + ")$", "i");
+}
+
+function imagesExtensionsRegex(staff) {
+ return new RegExp("\\.(" + imagesExtensions(staff).join("|") + ")$", "i");
+}
+
+function staffExtensionsRegex() {
+ return new RegExp("\\.(" + staffExtensions().join("|") + ")$", "i");
+}
+
+function isAuthorizedFile(fileName, staff) {
+ if (staff && staffExtensionsRegex().test(fileName)) {
+ return true;
+ }
+ return extensionsRegex().test(fileName);
+}
+
+function isAuthorizedImage(fileName, staff) {
+ return imagesExtensionsRegex(staff).test(fileName);
+}
+
+export function authorizedExtensions(staff) {
+ const exts = staff ? [...extensions(), ...staffExtensions()] : extensions();
+ return exts.filter(ext => ext.length > 0).join(", ");
+}
+
+function authorizedImagesExtensions(staff) {
+ return authorizesAllExtensions(staff)
+ ? "png, jpg, jpeg, gif, svg, ico"
+ : imagesExtensions(staff).join(", ");
+}
+
+export function authorizesAllExtensions(staff) {
+ return (
+ Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0 ||
+ (Discourse.SiteSettings.authorized_extensions_for_staff.indexOf("*") >= 0 &&
+ staff)
+ );
+}
+
+export function authorizesOneOrMoreExtensions(staff) {
+ if (authorizesAllExtensions(staff)) return true;
+
+ return (
+ Discourse.SiteSettings.authorized_extensions.split("|").filter(ext => ext)
+ .length > 0
+ );
+}
+
+export function authorizesOneOrMoreImageExtensions(staff) {
+ if (authorizesAllExtensions(staff)) return true;
+ return imagesExtensions(staff).length > 0;
+}
+
+export function isImage(path) {
+ return /\.(png|webp|jpe?g|gif|svg|ico)$/i.test(path);
+}
+
+export function isVideo(path) {
+ return /\.(mov|mp4|webm|m4v|3gp|ogv|avi|mpeg|ogv)$/i.test(path);
+}
+
+export function isAudio(path) {
+ return /\.(mp3|og[ga]|opus|wav|m4[abpr]|aac|flac)$/i.test(path);
+}
+
+function uploadTypeFromFileName(fileName) {
+ return isImage(fileName) ? "image" : "attachment";
+}
+
+export function allowsImages(staff) {
+ return (
+ authorizesAllExtensions(staff) ||
+ IMAGES_EXTENSIONS_REGEX.test(authorizedExtensions(staff))
+ );
+}
+
+export function allowsAttachments(staff) {
+ return (
+ authorizesAllExtensions(staff) ||
+ authorizedExtensions(staff).split(", ").length >
+ imagesExtensions(staff).length
+ );
+}
+
+export function uploadIcon(staff) {
+ return allowsAttachments(staff) ? "upload" : "far-image";
+}
+
+function imageMarkdown(upload) {
+ return ``;
+}
+
+function playableMediaMarkdown(upload, type) {
+ return ``;
+}
+
+function attachmentMarkdown(upload) {
+ return `[${upload.original_filename}|attachment](${
+ upload.short_url
+ }) (${I18n.toHumanSize(upload.filesize)})`;
+}
+
+export function getUploadMarkdown(upload) {
+ if (isImage(upload.original_filename)) {
+ return imageMarkdown(upload);
+ } else if (isAudio(upload.original_filename)) {
+ return playableMediaMarkdown(upload, "audio");
+ } else if (isVideo(upload.original_filename)) {
+ return playableMediaMarkdown(upload, "video");
+ } else {
+ return attachmentMarkdown(upload);
+ }
+}
+
+export function displayErrorForUpload(data) {
+ if (data.jqXHR) {
+ switch (data.jqXHR.status) {
+ // cancelled by the user
+ case 0:
+ return;
+
+ // entity too large, usually returned from the web server
+ case 413:
+ const type = uploadTypeFromFileName(data.files[0].name);
+ const max_size_kb = Discourse.SiteSettings[`max_${type}_size_kb`];
+ bootbox.alert(I18n.t("post.errors.file_too_large", { max_size_kb }));
+ return;
+
+ // the error message is provided by the server
+ case 422:
+ if (data.jqXHR.responseJSON.message) {
+ bootbox.alert(data.jqXHR.responseJSON.message);
+ } else {
+ bootbox.alert(data.jqXHR.responseJSON.errors.join("\n"));
+ }
+ return;
+ }
+ } else if (data.errors && data.errors.length > 0) {
+ bootbox.alert(data.errors.join("\n"));
+ return;
+ }
+ // otherwise, display a generic error message
+ bootbox.alert(I18n.t("post.errors.upload"));
+}
diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6
index ec111f281a..3dc13d0d79 100644
--- a/app/assets/javascripts/discourse/lib/url.js.es6
+++ b/app/assets/javascripts/discourse/lib/url.js.es6
@@ -1,6 +1,11 @@
+import { isEmpty } from "@ember/utils";
+import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
import offsetCalculator from "discourse/lib/offset-calculator";
import LockOn from "discourse/lib/lock-on";
import { defaultHomepage } from "discourse/lib/utilities";
+import User from "discourse/models/user";
const rewrites = [];
const TOPIC_REGEXP = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/;
@@ -23,7 +28,8 @@ const SERVER_SIDE_ONLY = [
/\.rss$/,
/\.json$/,
/^\/admin\/upgrade$/,
- /^\/logs($|\/)/
+ /^\/logs($|\/)/,
+ /^\/admin\/logs\/watched_words\/action\/[^\/]+\/download$/
];
export function rewritePath(path) {
@@ -58,13 +64,13 @@ export function groupPath(subPath) {
let _jumpScheduled = false;
export function jumpToElement(elementId) {
- if (_jumpScheduled || Ember.isEmpty(elementId)) {
+ if (_jumpScheduled || isEmpty(elementId)) {
return;
}
const selector = `#${elementId}, a[name=${elementId}]`;
_jumpScheduled = true;
- Ember.run.schedule("afterRender", function() {
+ schedule("afterRender", function() {
const lockon = new LockOn(selector, {
finished() {
_jumpScheduled = false;
@@ -76,7 +82,7 @@ export function jumpToElement(elementId) {
let _transitioning = false;
-const DiscourseURL = Ember.Object.extend({
+const DiscourseURL = EmberObject.extend({
isJumpScheduled() {
return _transitioning || _jumpScheduled;
},
@@ -88,10 +94,28 @@ const DiscourseURL = Ember.Object.extend({
_transitioning = postNumber > 1;
- Ember.run.schedule("afterRender", () => {
+ schedule("afterRender", () => {
let elementId;
let holder;
+ if (opts.jumpEnd) {
+ let $holder = $(holderId);
+ let holderHeight = $holder.height();
+ let windowHeight = $(window).height() - offsetCalculator();
+
+ // scroll to the bottom of the post and if the post is yuge we go back up the
+ // timeline by a small % of the post height so we can see the bottom of the text.
+ //
+ // otherwise just jump to the top of the post using the lock & holder method.
+ if (holderHeight > windowHeight) {
+ $(window).scrollTop(
+ $holder.offset().top + (holderHeight - holderHeight / 10)
+ );
+ _transitioning = false;
+ return;
+ }
+ }
+
if (postNumber === 1 && !opts.anchor) {
$(window).scrollTop(0);
_transitioning = false;
@@ -148,7 +172,7 @@ const DiscourseURL = Ember.Object.extend({
// Always use replaceState in the next runloop to prevent weird routes changing
// while URLs are loading. For example, while a topic loads it sets `currentPost`
// which triggers a replaceState even though the topic hasn't fully loaded yet!
- Ember.run.next(() => {
+ next(() => {
const location = DiscourseURL.get("router.location");
if (location && location.replaceURL) {
location.replaceURL(path);
@@ -181,7 +205,7 @@ const DiscourseURL = Ember.Object.extend({
routeTo(path, opts) {
opts = opts || {};
- if (Ember.isEmpty(path)) {
+ if (isEmpty(path)) {
return;
}
@@ -219,7 +243,7 @@ const DiscourseURL = Ember.Object.extend({
// Rewrite /my/* urls
let myPath = `${baseUri}/my/`;
if (path.indexOf(myPath) === 0) {
- const currentUser = Discourse.User.current();
+ const currentUser = User.current();
if (currentUser) {
path = path.replace(
myPath,
@@ -242,7 +266,7 @@ const DiscourseURL = Ember.Object.extend({
path = rewritePath(path);
if (typeof opts.afterRouteComplete === "function") {
- Ember.run.schedule("afterRender", opts.afterRouteComplete);
+ schedule("afterRender", opts.afterRouteComplete);
}
if (this.navigatedToPost(oldPath, path, opts)) {
@@ -268,6 +292,10 @@ const DiscourseURL = Ember.Object.extend({
return this.handleURL(path, opts);
},
+ routeToUrl(url, opts = {}) {
+ this.routeTo(Discourse.getURL(url), opts);
+ },
+
rewrite(regexp, replacement, opts) {
rewrites.push({ regexp, replacement, opts: opts || {} });
},
@@ -346,7 +374,8 @@ const DiscourseURL = Ember.Object.extend({
this.appEvents.trigger("post:highlight", closest);
const jumpOpts = {
- skipIfOnScreen: routeOpts.skipIfOnScreen
+ skipIfOnScreen: routeOpts.skipIfOnScreen,
+ jumpEnd: routeOpts.jumpEnd
};
const m = /#.+$/.exec(path);
@@ -397,6 +426,9 @@ const DiscourseURL = Ember.Object.extend({
);
},
+ // TODO: These container calls can be replaced eventually if we migrate this to a service
+ // object.
+
/**
@private
@@ -405,11 +437,13 @@ const DiscourseURL = Ember.Object.extend({
@property router
**/
- router: function() {
+ get router() {
return Discourse.__container__.lookup("router:main");
- }
- .property()
- .volatile(),
+ },
+
+ get appEvents() {
+ return Discourse.__container__.lookup("service:app-events");
+ },
// Get a controller. Note that currently it uses `__container__` which is not
// advised but there is no other way to access the router.
@@ -428,13 +462,6 @@ const DiscourseURL = Ember.Object.extend({
if (opts.replaceURL) {
this.replaceState(path);
- } else {
- const discoveryTopics = this.controllerFor("discovery/topics");
- if (discoveryTopics) {
- discoveryTopics.resetParams();
- }
-
- router._routerMicrolib.updateURL(path);
}
const split = path.split("#");
@@ -445,7 +472,16 @@ const DiscourseURL = Ember.Object.extend({
elementId = split[1];
}
- const transition = router.handleURL(path);
+ // The default path has a hack to allow `/` to default to defaultHomepage
+ // via BareRouter.handleUrl
+ let transition;
+ if (path === "/" || path.substring(0, 2) === "/?") {
+ router._routerMicrolib.updateURL(path);
+ transition = router.handleURL(path);
+ } else {
+ transition = router.transitionTo(path);
+ }
+
transition._discourse_intercepted = true;
const promise = transition.promise || transition;
promise.then(() => jumpToElement(elementId));
diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6
index 753460f1fa..fcdc421427 100644
--- a/app/assets/javascripts/discourse/lib/user-search.js.es6
+++ b/app/assets/javascripts/discourse/lib/user-search.js.es6
@@ -1,10 +1,11 @@
-import debounce from "discourse/lib/debounce";
+import discourseDebounce from "discourse/lib/debounce";
import { CANCELLED_STATUS } from "discourse/lib/autocomplete";
import { userPath } from "discourse/lib/url";
import { emailValid } from "discourse/lib/utilities";
+import { Promise } from "rsvp";
var cache = {},
- cacheTopicId,
+ cacheKey,
cacheTime,
currentTerm,
oldSearch;
@@ -12,6 +13,7 @@ var cache = {},
function performSearch(
term,
topicId,
+ categoryId,
includeGroups,
includeMentionableGroups,
includeMessageableGroups,
@@ -25,11 +27,13 @@ function performSearch(
return;
}
- // 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 [];
+ const eagerComplete = eagerCompleteSearch(term, topicId || categoryId);
+
+ if (term === "" && !eagerComplete) {
+ // The server returns no results in this case, so no point checking
+ // do not return empty list, because autocomplete will get terminated
+ resultsFn(CANCELLED_STATUS);
+ return;
}
// need to be able to cancel this
@@ -37,6 +41,7 @@ function performSearch(
data: {
term: term,
topic_id: topicId,
+ category_id: categoryId,
include_groups: includeGroups,
include_mentionable_groups: includeMentionableGroups,
include_messageable_groups: includeMessageableGroups,
@@ -49,6 +54,18 @@ function performSearch(
oldSearch
.then(function(r) {
+ const hasResults = !!(
+ (r.users && r.users.length) ||
+ (r.groups && r.groups.length) ||
+ (r.emails && r.emails.length)
+ );
+
+ if (eagerComplete && !hasResults) {
+ // we are trying to eager load, but received no results
+ // do not return empty list, because autocomplete will get terminated
+ r = CANCELLED_STATUS;
+ }
+
cache[term] = r;
cacheTime = new Date();
// If there is a newer search term, return null
@@ -62,7 +79,7 @@ function performSearch(
});
}
-var debouncedSearch = debounce(performSearch, 300);
+var debouncedSearch = discourseDebounce(performSearch, 300);
function organizeResults(r, options) {
if (r === CANCELLED_STATUS) {
@@ -119,9 +136,9 @@ function organizeResults(r, options) {
// 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$/;
+const ignoreRegex = /([\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*,\/:;<=>?\[\]^`{|}~])|\s\s|^\s$|^[^+]*\+[^@]*$/;
-function skipSearch(term, allowEmails) {
+export function skipSearch(term, allowEmails) {
if (term.indexOf("@") > -1 && !allowEmails) {
return true;
}
@@ -129,6 +146,10 @@ function skipSearch(term, allowEmails) {
return !!term.match(ignoreRegex);
}
+export function eagerCompleteSearch(term, scopedId) {
+ return term === "" && !!scopedId;
+}
+
export default function userSearch(options) {
if (options.term && options.term.length > 0 && options.term[0] === "@") {
options.term = options.term.substring(1);
@@ -140,6 +161,7 @@ export default function userSearch(options) {
includeMessageableGroups = options.includeMessageableGroups,
allowedUsers = options.allowedUsers,
topicId = options.topicId,
+ categoryId = options.categoryId,
groupMembersOf = options.groupMembersOf;
if (oldSearch) {
@@ -149,12 +171,14 @@ export default function userSearch(options) {
currentTerm = term;
- return new Ember.RSVP.Promise(function(resolve) {
- if (new Date() - cacheTime > 30000 || cacheTopicId !== topicId) {
+ return new Promise(function(resolve) {
+ const newCacheKey = `${topicId}-${categoryId}`;
+
+ if (new Date() - cacheTime > 30000 || cacheKey !== newCacheKey) {
cache = {};
}
- cacheTopicId = topicId;
+ cacheKey = newCacheKey;
var clearPromise = setTimeout(function() {
resolve(CANCELLED_STATUS);
@@ -168,6 +192,7 @@ export default function userSearch(options) {
debouncedSearch(
term,
topicId,
+ categoryId,
includeGroups,
includeMentionableGroups,
includeMessageableGroups,
diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6
index be80d4c0ae..ce91d7c8dc 100644
--- a/app/assets/javascripts/discourse/lib/utilities.js.es6
+++ b/app/assets/javascripts/discourse/lib/utilities.js.es6
@@ -143,6 +143,13 @@ export function selectedText() {
return toMarkdown($div.html());
}
+export function selectedElement() {
+ const selection = window.getSelection();
+ if (selection.rangeCount > 0) {
+ return selection.getRangeAt(0).startContainer.parentElement;
+ }
+}
+
// Determine the row and col of the caret in an element
export function caretRowCol(el) {
var cp = caretPosition(el);
@@ -195,292 +202,6 @@ export function setCaretPosition(ctrl, pos) {
}
}
-export function validateUploadedFiles(files, opts) {
- if (!files || files.length === 0) {
- return false;
- }
-
- if (files.length > 1) {
- bootbox.alert(I18n.t("post.errors.too_many_uploads"));
- return false;
- }
-
- const upload = files[0];
-
- // CHROME ONLY: if the image was pasted, sets its name to a default one
- if (typeof Blob !== "undefined" && typeof File !== "undefined") {
- if (
- upload instanceof Blob &&
- !(upload instanceof File) &&
- upload.type === "image/png"
- ) {
- upload.name = "image.png";
- }
- }
-
- opts = opts || {};
- opts.type = uploadTypeFromFileName(upload.name);
-
- return validateUploadedFile(upload, opts);
-}
-
-export function validateUploadedFile(file, opts) {
- if (opts.skipValidation) return true;
- if (!authorizesOneOrMoreExtensions()) return false;
-
- opts = opts || {};
-
- const name = file && file.name;
-
- if (!name) {
- return false;
- }
-
- // check that the uploaded file is authorized
- if (opts.allowStaffToUploadAnyFileInPm && opts.isPrivateMessage) {
- if (Discourse.User.currentProp("staff")) {
- return true;
- }
- }
-
- if (opts.imagesOnly) {
- if (!isAnImage(name) && !isAuthorizedImage(name)) {
- bootbox.alert(
- I18n.t("post.errors.upload_not_authorized", {
- authorized_extensions: authorizedImagesExtensions()
- })
- );
- return false;
- }
- } else if (opts.csvOnly) {
- if (!/\.csv$/i.test(name)) {
- bootbox.alert(I18n.t("user.invited.bulk_invite.error"));
- return false;
- }
- } else {
- if (!authorizesAllExtensions() && !isAuthorizedFile(name)) {
- bootbox.alert(
- I18n.t("post.errors.upload_not_authorized", {
- authorized_extensions: authorizedExtensions()
- })
- );
- return false;
- }
- }
-
- if (!opts.bypassNewUserRestriction) {
- // ensures that new users can upload a file
- if (!Discourse.User.current().isAllowedToUploadAFile(opts.type)) {
- bootbox.alert(
- I18n.t(`post.errors.${opts.type}_upload_not_allowed_for_new_user`)
- );
- return false;
- }
- }
-
- // everything went fine
- return true;
-}
-
-const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|svg|ico)/i;
-
-function extensionsToArray(exts) {
- return exts
- .toLowerCase()
- .replace(/[\s\.]+/g, "")
- .split("|")
- .filter(ext => ext.indexOf("*") === -1);
-}
-
-function extensions() {
- return extensionsToArray(Discourse.SiteSettings.authorized_extensions);
-}
-
-function staffExtensions() {
- return extensionsToArray(
- Discourse.SiteSettings.authorized_extensions_for_staff
- );
-}
-
-function imagesExtensions() {
- let exts = extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext));
- if (Discourse.User.currentProp("staff")) {
- const staffExts = staffExtensions().filter(ext =>
- IMAGES_EXTENSIONS_REGEX.test(ext)
- );
- exts = _.union(exts, staffExts);
- }
- return exts;
-}
-
-function extensionsRegex() {
- return new RegExp("\\.(" + extensions().join("|") + ")$", "i");
-}
-
-function imagesExtensionsRegex() {
- return new RegExp("\\.(" + imagesExtensions().join("|") + ")$", "i");
-}
-
-function staffExtensionsRegex() {
- return new RegExp("\\.(" + staffExtensions().join("|") + ")$", "i");
-}
-
-function isAuthorizedFile(fileName) {
- if (
- Discourse.User.currentProp("staff") &&
- staffExtensionsRegex().test(fileName)
- ) {
- return true;
- }
- return extensionsRegex().test(fileName);
-}
-
-function isAuthorizedImage(fileName) {
- return imagesExtensionsRegex().test(fileName);
-}
-
-export function authorizedExtensions() {
- const exts = Discourse.User.currentProp("staff")
- ? [...extensions(), ...staffExtensions()]
- : extensions();
- return exts.filter(ext => ext.length > 0).join(", ");
-}
-
-export function authorizedImagesExtensions() {
- return authorizesAllExtensions()
- ? "png, jpg, jpeg, gif, svg, ico"
- : imagesExtensions().join(", ");
-}
-
-export function authorizesAllExtensions() {
- return (
- Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0 ||
- (Discourse.SiteSettings.authorized_extensions_for_staff.indexOf("*") >= 0 &&
- Discourse.User.currentProp("staff"))
- );
-}
-
-export function authorizesOneOrMoreExtensions() {
- if (authorizesAllExtensions()) return true;
-
- return (
- Discourse.SiteSettings.authorized_extensions.split("|").filter(ext => ext)
- .length > 0
- );
-}
-
-export function authorizesOneOrMoreImageExtensions() {
- if (authorizesAllExtensions()) return true;
-
- return imagesExtensions().length > 0;
-}
-
-export function isAnImage(path) {
- return /\.(png|jpe?g|gif|svg|ico)$/i.test(path);
-}
-
-function uploadTypeFromFileName(fileName) {
- return isAnImage(fileName) ? "image" : "attachment";
-}
-
-function isGUID(value) {
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
- value
- );
-}
-
-function imageNameFromFileName(fileName) {
- const split = fileName.split(".");
- let name = split[split.length - 2];
-
- if (exports.isAppleDevice() && isGUID(name)) {
- name = I18n.t("upload_selector.default_image_alt_text");
- }
-
- return encodeURIComponent(name);
-}
-
-export function allowsImages() {
- return (
- authorizesAllExtensions() ||
- IMAGES_EXTENSIONS_REGEX.test(authorizedExtensions())
- );
-}
-
-export function allowsAttachments() {
- return (
- authorizesAllExtensions() ||
- authorizedExtensions().split(", ").length > imagesExtensions().length
- );
-}
-
-export function uploadIcon() {
- return allowsAttachments() ? "upload" : "far-image";
-}
-
-export function uploadLocation(url) {
- if (Discourse.CDN) {
- url = Discourse.getURLWithCDN(url);
- return /^\/\//.test(url) ? "http:" + url : url;
- } else if (Discourse.S3BaseUrl) {
- return "https:" + url;
- } else {
- var protocol = window.location.protocol + "//",
- hostname = window.location.hostname,
- port = window.location.port ? ":" + window.location.port : "";
- return protocol + hostname + port + url;
- }
-}
-
-export function getUploadMarkdown(upload) {
- if (isAnImage(upload.original_filename)) {
- const name = imageNameFromFileName(upload.original_filename);
- return ``;
- } else if (
- !Discourse.SiteSettings.prevent_anons_from_downloading_files &&
- /\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i.test(upload.original_filename)
- ) {
- return uploadLocation(upload.url);
- } else {
- return `[${upload.original_filename}|attachment](${
- upload.short_url
- }) (${I18n.toHumanSize(upload.filesize)})`;
- }
-}
-
-export function displayErrorForUpload(data) {
- if (data.jqXHR) {
- switch (data.jqXHR.status) {
- // cancelled by the user
- case 0:
- return;
-
- // entity too large, usually returned from the web server
- case 413:
- const type = uploadTypeFromFileName(data.files[0].name);
- const max_size_kb = Discourse.SiteSettings[`max_${type}_size_kb`];
- bootbox.alert(I18n.t("post.errors.file_too_large", { max_size_kb }));
- return;
-
- // the error message is provided by the server
- case 422:
- if (data.jqXHR.responseJSON.message) {
- bootbox.alert(data.jqXHR.responseJSON.message);
- } else {
- bootbox.alert(data.jqXHR.responseJSON.errors.join("\n"));
- }
- return;
- }
- } else if (data.errors && data.errors.length > 0) {
- bootbox.alert(data.errors.join("\n"));
- return;
- }
- // otherwise, display a generic error message
- bootbox.alert(I18n.t("post.errors.upload"));
-}
-
export function defaultHomepage() {
let homepage = null;
let elem = _.first($(homepageSelector));
@@ -538,14 +259,16 @@ export function determinePostReplaceSelection({
export function isAppleDevice() {
// IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone
// This will apply hack on all iDevices
- return (
- navigator.userAgent.match(/(iPad|iPhone|iPod)/g) &&
- !navigator.userAgent.match(/Trident/g)
- );
+ const caps = Discourse.__container__.lookup("capabilities:main");
+ return caps.isIOS && !navigator.userAgent.match(/Trident/g);
}
let iPadDetected = undefined;
+export function iOSWithVisualViewport() {
+ return isAppleDevice() && window.visualViewport !== undefined;
+}
+
export function isiPad() {
if (iPadDetected === undefined) {
iPadDetected =
@@ -556,6 +279,8 @@ export function isiPad() {
}
export function safariHacksDisabled() {
+ if (iOSWithVisualViewport()) return false;
+
let pref = localStorage.getItem("safari-hacks-disabled");
let result = false;
if (pref !== null) {
@@ -588,7 +313,7 @@ export function clipboardData(e, canUpload) {
files = toArray(clipboard.items).filter(i => i.kind === "file");
}
- canUpload = files && canUpload && !types.includes("text/plain");
+ canUpload = files && canUpload && types.includes("Files");
const canUploadImage =
canUpload && files.filter(f => f.type.match("^image/"))[0];
const canPasteHtml =
@@ -599,8 +324,12 @@ export function clipboardData(e, canUpload) {
return { clipboard, types, canUpload, canPasteHtml };
}
+export function toNumber(input) {
+ return typeof input === "number" ? input : parseFloat(input);
+}
+
export function isNumeric(input) {
- return !isNaN(parseFloat(input)) && isFinite(input);
+ return !isNaN(toNumber(input)) && isFinite(input);
}
export function fillMissingDates(data, startDate, endDate) {
@@ -688,5 +417,31 @@ export function rescueThemeError(name, error, api) {
document.body.prepend(alertDiv);
}
+const CODE_BLOCKS_REGEX = /^( |\t).*|`[^`]+`|^```[^]*?^```|\[code\][^]*?\[\/code\]/gm;
+// | ^ | ^ | ^ | ^ |
+// | | | |
+// | | | code blocks between [code]
+// | | |
+// | | +--- code blocks between three backquote
+// | |
+// | +----- inline code between backquotes
+// |
+// +------- paragraphs starting with 4 spaces or tab
+
+export function inCodeBlock(text, pos) {
+ let result = false;
+
+ let match;
+ while ((match = CODE_BLOCKS_REGEX.exec(text)) !== null) {
+ const begin = match.index;
+ const end = match.index + match[0].length;
+ if (begin <= pos && pos <= end) {
+ result = true;
+ }
+ }
+
+ return result;
+}
+
// This prevents a mini racer crash
export default {};
diff --git a/app/assets/javascripts/discourse/lib/webauthn.js.es6 b/app/assets/javascripts/discourse/lib/webauthn.js.es6
new file mode 100644
index 0000000000..6b3e81ad4d
--- /dev/null
+++ b/app/assets/javascripts/discourse/lib/webauthn.js.es6
@@ -0,0 +1,78 @@
+export function stringToBuffer(str) {
+ let buffer = new ArrayBuffer(str.length);
+ let byteView = new Uint8Array(buffer);
+ for (let i = 0; i < str.length; i++) {
+ byteView[i] = str.charCodeAt(i);
+ }
+ return buffer;
+}
+
+export function bufferToBase64(buffer) {
+ return btoa(String.fromCharCode(...new Uint8Array(buffer)));
+}
+
+export function isWebauthnSupported() {
+ return typeof PublicKeyCredential !== "undefined";
+}
+
+export function getWebauthnCredential(
+ challenge,
+ allowedCredentialIds,
+ successCallback,
+ errorCallback
+) {
+ if (!isWebauthnSupported()) {
+ return errorCallback(I18n.t("login.security_key_support_missing_error"));
+ }
+
+ let challengeBuffer = stringToBuffer(challenge);
+ let allowCredentials = allowedCredentialIds.map(credentialId => {
+ return {
+ id: stringToBuffer(atob(credentialId)),
+ type: "public-key"
+ };
+ });
+
+ navigator.credentials
+ .get({
+ publicKey: {
+ challenge: challengeBuffer,
+ allowCredentials: allowCredentials,
+ timeout: 60000,
+
+ // see https://chromium.googlesource.com/chromium/src/+/master/content/browser/webauth/uv_preferred.md for why
+ // default value of preferred is not necesarrily what we want, it limits webauthn to only devices that support
+ // user verification, which usually requires entering a PIN
+ userVerification: "discouraged"
+ }
+ })
+ .then(credential => {
+ // 1. if there is a credential, check if the raw ID base64 matches
+ // any of the allowed credential ids
+ if (
+ !allowedCredentialIds.some(
+ credentialId => bufferToBase64(credential.rawId) === credentialId
+ )
+ ) {
+ return errorCallback(
+ I18n.t("login.security_key_no_matching_credential_error")
+ );
+ }
+
+ const credentialData = {
+ signature: bufferToBase64(credential.response.signature),
+ clientData: bufferToBase64(credential.response.clientDataJSON),
+ authenticatorData: bufferToBase64(
+ credential.response.authenticatorData
+ ),
+ credentialId: bufferToBase64(credential.rawId)
+ };
+ successCallback(credentialData);
+ })
+ .catch(err => {
+ if (err.name === "NotAllowedError") {
+ return errorCallback(I18n.t("login.security_key_not_allowed_error"));
+ }
+ errorCallback(err);
+ });
+}
diff --git a/app/assets/javascripts/discourse/mapping-router.js.es6 b/app/assets/javascripts/discourse/mapping-router.js.es6
index 52d25f5479..c50eb9112c 100644
--- a/app/assets/javascripts/discourse/mapping-router.js.es6
+++ b/app/assets/javascripts/discourse/mapping-router.js.es6
@@ -1,11 +1,13 @@
import { defaultHomepage } from "discourse/lib/utilities";
import { rewritePath } from "discourse/lib/url";
+import ENV from "discourse-common/config/environment";
+import Site from "discourse/models/site";
const rootURL = Discourse.BaseUri;
const BareRouter = Ember.Router.extend({
rootURL,
- location: Ember.testing ? "none" : "discourse-location",
+ location: ENV.environment === "test" ? "none" : "discourse-location",
handleURL(url) {
url = rewritePath(url);
@@ -32,7 +34,7 @@ class RouteNode {
this.children = [];
this.childrenByName = {};
this.paths = {};
- this.site = Discourse.Site.current();
+ this.site = Site.current();
if (!opts.path) {
opts.path = name;
diff --git a/app/assets/javascripts/discourse/mixins/add-archetype-class.js.es6 b/app/assets/javascripts/discourse/mixins/add-archetype-class.js.es6
index a8a2c3c528..1436f0def6 100644
--- a/app/assets/javascripts/discourse/mixins/add-archetype-class.js.es6
+++ b/app/assets/javascripts/discourse/mixins/add-archetype-class.js.es6
@@ -1,4 +1,4 @@
-import { on, observes } from "ember-addons/ember-computed-decorators";
+import { on, observes } from "discourse-common/utils/decorators";
// Mix this in to a view that has a `archetype` property to automatically
// add it to the body as the view is entered / left / model is changed.
diff --git a/app/assets/javascripts/discourse/mixins/badge-select-controller.js.es6 b/app/assets/javascripts/discourse/mixins/badge-select-controller.js.es6
deleted file mode 100644
index 449030dad2..0000000000
--- a/app/assets/javascripts/discourse/mixins/badge-select-controller.js.es6
+++ /dev/null
@@ -1,39 +0,0 @@
-import Badge from "discourse/models/badge";
-import computed from "ember-addons/ember-computed-decorators";
-
-export default Ember.Mixin.create({
- saving: false,
- saved: false,
-
- @computed("filteredList")
- selectableUserBadges(items) {
- items = _.uniq(items, false, function(e) {
- return e.get("badge.name");
- });
- items.unshiftObject(
- Ember.Object.create({
- badge: Badge.create({ name: I18n.t("badges.none") })
- })
- );
- return items;
- },
-
- @computed("saving")
- savingStatus(saving) {
- return saving ? I18n.t("saving") : I18n.t("save");
- },
-
- @computed("selectedUserBadgeId")
- selectedUserBadge(selectedUserBadgeId) {
- selectedUserBadgeId = parseInt(selectedUserBadgeId);
- let selectedUserBadge = null;
- this.selectableUserBadges.forEach(function(userBadge) {
- if (userBadge.get("id") === selectedUserBadgeId) {
- selectedUserBadge = userBadge;
- }
- });
- return selectedUserBadge;
- },
-
- disableSave: Ember.computed.alias("saving")
-});
diff --git a/app/assets/javascripts/discourse/mixins/buffered-content.js.es6 b/app/assets/javascripts/discourse/mixins/buffered-content.js.es6
index aefee9990e..beb145a3ec 100644
--- a/app/assets/javascripts/discourse/mixins/buffered-content.js.es6
+++ b/app/assets/javascripts/discourse/mixins/buffered-content.js.es6
@@ -1,11 +1,15 @@
+import EmberObjectProxy from "@ember/object/proxy";
+import Mixin from "@ember/object/mixin";
+import { computed } from "@ember/object";
+
/* global BufferedProxy: true */
export function bufferedProperty(property) {
const mixin = {
- buffered: function() {
- return Ember.ObjectProxy.extend(BufferedProxy).create({
+ buffered: computed(property, function() {
+ return EmberObjectProxy.extend(BufferedProxy).create({
content: this.get(property)
});
- }.property(property),
+ }),
rollbackBuffer: function() {
this.buffered.discardBufferedChanges();
@@ -19,7 +23,7 @@ export function bufferedProperty(property) {
// It's a good idea to null out fields when declaring objects
mixin.property = null;
- return Ember.Mixin.create(mixin);
+ return Mixin.create(mixin);
}
export default bufferedProperty("content");
diff --git a/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6 b/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6
index 7dd6331ba7..cc2dfc1d55 100644
--- a/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6
+++ b/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6
@@ -1,11 +1,14 @@
+import { alias } from "@ember/object/computed";
import { NotificationLevels } from "discourse/lib/notification-levels";
-import { on } from "ember-addons/ember-computed-decorators";
+import { on } from "discourse-common/utils/decorators";
+import Mixin from "@ember/object/mixin";
+import Topic from "discourse/models/topic";
-export default Ember.Mixin.create({
+export default Mixin.create({
bulkSelectEnabled: false,
selected: null,
- canBulkSelect: Ember.computed.alias("currentUser.staff"),
+ canBulkSelect: alias("currentUser.staff"),
@on("init")
resetSelected() {
@@ -18,7 +21,7 @@ export default Ember.Mixin.create({
this.selected.clear();
},
- dismissRead(operationType) {
+ dismissRead(operationType, categoryOptions) {
let operation;
if (operationType === "posts") {
operation = { type: "dismiss_posts" };
@@ -31,12 +34,13 @@ export default Ember.Mixin.create({
let promise;
if (this.selected.length > 0) {
- promise = Discourse.Topic.bulkOperation(this.selected, operation);
+ promise = Topic.bulkOperation(this.selected, operation);
} else {
- promise = Discourse.Topic.bulkOperationByFilter(
+ promise = Topic.bulkOperationByFilter(
"unread",
operation,
- this.get("category.id")
+ this.get("category.id"),
+ categoryOptions
);
}
diff --git a/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 b/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6
index a7eb59fe7b..9e203da744 100644
--- a/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6
+++ b/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6
@@ -1,14 +1,13 @@
+import { and, alias, or } from "@ember/object/computed";
import { propertyEqual, setting } from "discourse/lib/computed";
+import Mixin from "@ember/object/mixin";
-export default Ember.Mixin.create({
+export default Mixin.create({
isCurrentUser: propertyEqual("model.id", "currentUser.id"),
showEmailOnProfile: setting("moderators_view_emails"),
- canStaffCheckEmails: Ember.computed.and(
- "showEmailOnProfile",
- "currentUser.staff"
- ),
- canAdminCheckEmails: Ember.computed.alias("currentUser.admin"),
- canCheckEmails: Ember.computed.or(
+ canStaffCheckEmails: and("showEmailOnProfile", "currentUser.staff"),
+ canAdminCheckEmails: alias("currentUser.admin"),
+ canCheckEmails: or(
"isCurrentUser",
"canStaffCheckEmails",
"canAdminCheckEmails"
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 f81a573891..c5cdcc4a16 100644
--- a/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6
+++ b/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6
@@ -1,14 +1,19 @@
+import { alias, match } from "@ember/object/computed";
+import { throttle } from "@ember/runloop";
+import { next } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
import { wantsNewWindow } from "discourse/lib/intercept-click";
import afterTransition from "discourse/lib/after-transition";
import DiscourseURL from "discourse/lib/url";
+import Mixin from "@ember/object/mixin";
-export default Ember.Mixin.create({
+export default Mixin.create({
elementId: null, //click detection added for data-{elementId}
triggeringLinkClass: null, //the
classname where this card should appear
_showCallback: null, //username, $target - load up data for when show is called, should call this._positionCard($target) when it's done.
- postStream: Ember.computed.alias("topic.postStream"),
- viewingTopic: Ember.computed.match("currentPath", /^topic\./),
+ postStream: alias("topic.postStream"),
+ viewingTopic: match("currentPath", /^topic\./),
visible: false,
username: null,
@@ -73,7 +78,7 @@ export default Ember.Mixin.create({
didInsertElement() {
this._super(...arguments);
- afterTransition(this.$(), this._hide.bind(this));
+ afterTransition($(this.element), this._hide.bind(this));
const id = this.elementId;
const triggeringLinkClass = this.triggeringLinkClass;
const clickOutsideEventName = `mousedown.outside-${id}`;
@@ -121,7 +126,7 @@ export default Ember.Mixin.create({
if (wantsNewWindow(e)) {
return;
}
- const $target = $(e.target);
+ const $target = $(e.currentTarget);
return this._show($target.text().replace(/^@/, ""), $target);
});
@@ -142,7 +147,7 @@ export default Ember.Mixin.create({
_bindMobileScroll() {
const mobileScrollEvent = this.mobileScrollEvent;
const onScroll = () => {
- Ember.run.throttle(this, this._close, 1000);
+ throttle(this, this._close, 1000);
};
$(window).on(mobileScrollEvent, onScroll);
@@ -164,14 +169,14 @@ export default Ember.Mixin.create({
if (!target) {
return;
}
- const width = this.$().width();
+ const width = $(this.element).width();
const height = 175;
const isFixed = this.isFixed;
const isDocked = this.isDocked;
let verticalAdjustments = 0;
- Ember.run.schedule("afterRender", () => {
+ schedule("afterRender", () => {
if (target) {
if (!this.site.mobileView) {
let position = target.offset();
@@ -227,7 +232,7 @@ export default Ember.Mixin.create({
position.top = avatarOverflowSize;
}
- this.$().css(position);
+ $(this.element).css(position);
}
}
@@ -236,23 +241,26 @@ export default Ember.Mixin.create({
let position = target.offset();
position.top = "10%"; // match modal behaviour
position.left = 0;
- this.$().css(position);
+ $(this.element).css(position);
}
- this.$().toggleClass("docked-card", isDocked);
+ $(this.element).toggleClass("docked-card", isDocked);
// After the card is shown, focus on the first link
//
// note: we DO NOT use afterRender here cause _positionCard may
// run afterwards, if we allowed this to happen the usercard
// may be offscreen and we may scroll all the way to it on focus
- Ember.run.next(null, () => this.$("a:first").focus());
+ next(null, () => {
+ const firstLink = this.element.querySelector("a");
+ firstLink && firstLink.focus();
+ });
}
});
},
_hide() {
if (!this.visible) {
- this.$().css({ left: -9999, top: -9999 });
+ $(this.element).css({ left: -9999, top: -9999 });
if (this.site.mobileView) {
$(".card-cloak").addClass("hidden");
}
diff --git a/app/assets/javascripts/discourse/mixins/cleans-up.js.es6 b/app/assets/javascripts/discourse/mixins/cleans-up.js.es6
index 09d469af1c..ffbaf7a9ee 100644
--- a/app/assets/javascripts/discourse/mixins/cleans-up.js.es6
+++ b/app/assets/javascripts/discourse/mixins/cleans-up.js.es6
@@ -1,11 +1,14 @@
+import { on } from "@ember/object/evented";
+import Mixin from "@ember/object/mixin";
+
// Include this mixin if you want to be notified when the dom should be
// cleaned (usually on route change.)
-export default Ember.Mixin.create({
- _initializeChooser: function() {
+export default Mixin.create({
+ _initializeChooser: on("didInsertElement", function() {
this.appEvents.on("dom:clean", this, "cleanUp");
- }.on("didInsertElement"),
+ }),
- _clearChooser: function() {
+ _clearChooser: on("willDestroyElement", function() {
this.appEvents.off("dom:clean", this, "cleanUp");
- }.on("willDestroyElement")
+ })
});
diff --git a/app/assets/javascripts/discourse/mixins/docking.js.es6 b/app/assets/javascripts/discourse/mixins/docking.js.es6
index 800898ae70..3df9f80eda 100644
--- a/app/assets/javascripts/discourse/mixins/docking.js.es6
+++ b/app/assets/javascripts/discourse/mixins/docking.js.es6
@@ -1,3 +1,6 @@
+import Mixin from "@ember/object/mixin";
+import { debounce } from "@ember/runloop";
+
const helper = {
offset() {
const mainOffset = $("#main").offset();
@@ -6,13 +9,13 @@ const helper = {
}
};
-export default Ember.Mixin.create({
+export default Mixin.create({
queueDockCheck: null,
init() {
this._super(...arguments);
this.queueDockCheck = () => {
- Ember.run.debounce(this, this.safeDockCheck, 5);
+ debounce(this, this.safeDockCheck, 5);
};
},
diff --git a/app/assets/javascripts/discourse/mixins/filter-mode.js.es6 b/app/assets/javascripts/discourse/mixins/filter-mode.js.es6
new file mode 100644
index 0000000000..4287080762
--- /dev/null
+++ b/app/assets/javascripts/discourse/mixins/filter-mode.js.es6
@@ -0,0 +1,50 @@
+import Mixin from "@ember/object/mixin";
+import { computed } from "@ember/object";
+import Category from "discourse/models/category";
+
+export default Mixin.create({
+ filterModeInternal: computed(
+ "rawFilterMode",
+ "filterType",
+ "category",
+ "noSubcategories",
+ function() {
+ const rawFilterMode = this.rawFilterMode;
+ if (rawFilterMode) {
+ return rawFilterMode;
+ } else {
+ const category = this.category;
+ const filterType = this.filterType;
+
+ if (category) {
+ const noSubcategories = this.noSubcategories;
+
+ return `c/${Category.slugFor(category)}${
+ noSubcategories ? "/none" : ""
+ }/l/${filterType}`;
+ } else {
+ return filterType;
+ }
+ }
+ }
+ ),
+
+ filterMode: computed("filterModeInternal", {
+ get() {
+ return this.filterModeInternal;
+ },
+
+ set(key, value) {
+ this.set("rawFilterMode", value);
+ const parts = value.split("/");
+
+ if (parts.length >= 2 && parts[parts.length - 2] === "top") {
+ this.set("filterType", "top");
+ } else {
+ this.set("filterType", parts.pop());
+ }
+
+ return value;
+ }
+ })
+});
diff --git a/app/assets/javascripts/discourse/mixins/grant-badge-controller.js.es6 b/app/assets/javascripts/discourse/mixins/grant-badge-controller.js.es6
index cc0d82fd87..153ba29c01 100644
--- a/app/assets/javascripts/discourse/mixins/grant-badge-controller.js.es6
+++ b/app/assets/javascripts/discourse/mixins/grant-badge-controller.js.es6
@@ -1,9 +1,11 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { empty } from "@ember/object/computed";
import UserBadge from "discourse/models/user-badge";
import { convertIconClass } from "discourse-common/lib/icon-library";
+import Mixin from "@ember/object/mixin";
-export default Ember.Mixin.create({
- @computed("allBadges.[]", "userBadges.[]")
+export default Mixin.create({
+ @discourseComputed("allBadges.[]", "userBadges.[]")
grantableBadges(allBadges, userBadges) {
const granted = userBadges.reduce((map, badge) => {
map[badge.get("badge_id")] = true;
@@ -27,9 +29,9 @@ export default Ember.Mixin.create({
.sort((a, b) => a.get("name").localeCompare(b.get("name")));
},
- noGrantableBadges: Ember.computed.empty("grantableBadges"),
+ noGrantableBadges: empty("grantableBadges"),
- @computed("selectedBadgeId", "grantableBadges")
+ @discourseComputed("selectedBadgeId", "grantableBadges")
selectedBadgeGrantable(selectedBadgeId, grantableBadges) {
return (
grantableBadges &&
diff --git a/app/assets/javascripts/discourse/mixins/load-more.js.es6 b/app/assets/javascripts/discourse/mixins/load-more.js.es6
index caf86100ea..911e261e47 100644
--- a/app/assets/javascripts/discourse/mixins/load-more.js.es6
+++ b/app/assets/javascripts/discourse/mixins/load-more.js.es6
@@ -1,9 +1,10 @@
import Eyeline from "discourse/lib/eyeline";
import Scrolling from "discourse/mixins/scrolling";
-import { on } from "ember-addons/ember-computed-decorators";
+import { on } from "discourse-common/utils/decorators";
+import Mixin from "@ember/object/mixin";
// Provides the ability to load more items for a view which is scrolled to the bottom.
-export default Ember.Mixin.create(Scrolling, {
+export default Mixin.create(Scrolling, {
scrolled() {
const eyeline = this.eyeline;
return eyeline && eyeline.update();
@@ -20,6 +21,7 @@ export default Ember.Mixin.create(Scrolling, {
const eyeline = new Eyeline(this.eyelineSelector + ":last");
this.set("eyeline", eyeline);
eyeline.on("sawBottom", () => this.send("loadMore"));
+ eyeline.update(); // update once to consider current position
this.bindScrolling();
},
diff --git a/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6 b/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6
index 0f27500a09..b2eabe0815 100644
--- a/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6
+++ b/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6
@@ -1,7 +1,9 @@
+import { debounce } from "@ember/runloop";
+import Mixin from "@ember/object/mixin";
// Small buffer so that very tiny scrolls don't trigger mobile header switch
const MOBILE_SCROLL_TOLERANCE = 5;
-export default Ember.Mixin.create({
+export default Mixin.create({
_lastScroll: null,
_bottomHit: 0,
@@ -42,7 +44,7 @@ export default Ember.Mixin.create({
// If the user reaches the very bottom of the topic, we only want to reset
// this scroll direction after a second scrolldown. This is a nicer event
// similar to what Safari and Chrome do.
- Ember.run.debounce(() => {
+ debounce(() => {
this._bottomHit = 1;
}, 1000);
diff --git a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6
index cb4ba7717f..70b5bd010c 100644
--- a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6
+++ b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6
@@ -1,6 +1,7 @@
import showModal from "discourse/lib/show-modal";
+import Mixin from "@ember/object/mixin";
-export default Ember.Mixin.create({
+export default Mixin.create({
flash(text, messageClass) {
this.appEvents.trigger("modal-body:flash", { text, messageClass });
},
diff --git a/app/assets/javascripts/discourse/mixins/name-validation.js.es6 b/app/assets/javascripts/discourse/mixins/name-validation.js.es6
index 5b505c1359..3b377f095b 100644
--- a/app/assets/javascripts/discourse/mixins/name-validation.js.es6
+++ b/app/assets/javascripts/discourse/mixins/name-validation.js.es6
@@ -1,8 +1,10 @@
-import InputValidation from "discourse/models/input-validation";
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import { isEmpty } from "@ember/utils";
+import discourseComputed from "discourse-common/utils/decorators";
+import Mixin from "@ember/object/mixin";
+import EmberObject from "@ember/object";
-export default Ember.Mixin.create({
- @computed()
+export default Mixin.create({
+ @discourseComputed()
nameInstructions() {
return I18n.t(
this.siteSettings.full_name_required
@@ -12,15 +14,12 @@ export default Ember.Mixin.create({
},
// Validate the name.
- @computed("accountName")
+ @discourseComputed("accountName")
nameValidation() {
- if (
- this.siteSettings.full_name_required &&
- Ember.isEmpty(this.accountName)
- ) {
- return InputValidation.create({ failed: true });
+ if (this.siteSettings.full_name_required && isEmpty(this.accountName)) {
+ return EmberObject.create({ failed: true });
}
- return InputValidation.create({ ok: true });
+ return EmberObject.create({ ok: true });
}
});
diff --git a/app/assets/javascripts/discourse/mixins/open-composer.js.es6 b/app/assets/javascripts/discourse/mixins/open-composer.js.es6
index e3576cda08..480eae5783 100644
--- a/app/assets/javascripts/discourse/mixins/open-composer.js.es6
+++ b/app/assets/javascripts/discourse/mixins/open-composer.js.es6
@@ -1,12 +1,22 @@
// This mixin allows a route to open the composer
import Composer from "discourse/models/composer";
+import Mixin from "@ember/object/mixin";
-export default Ember.Mixin.create({
+export default Mixin.create({
openComposer(controller) {
+ let categoryId = controller.get("category.id");
+ if (
+ categoryId &&
+ controller.category.isUncategorizedCategory &&
+ !this.siteSettings.allow_uncategorized_topics
+ ) {
+ categoryId = null;
+ }
+
this.controllerFor("composer").open({
- categoryId: controller.get("category.id"),
+ categoryId,
action: Composer.CREATE_TOPIC,
- draftKey: controller.get("model.draft_key") || Composer.CREATE_TOPIC,
+ draftKey: controller.get("model.draft_key") || Composer.NEW_TOPIC_KEY,
draftSequence: controller.get("model.draft_sequence") || 0
});
},
@@ -24,15 +34,15 @@ export default Ember.Mixin.create({
topicBody,
topicCategoryId,
topicTags,
- draftKey: controller.get("model.draft_key"),
+ draftKey: controller.get("model.draft_key") || Composer.NEW_TOPIC_KEY,
draftSequence: controller.get("model.draft_sequence")
});
},
- openComposerWithMessageParams(usernames, topicTitle, topicBody) {
+ openComposerWithMessageParams(recipients, topicTitle, topicBody) {
this.controllerFor("composer").open({
action: Composer.PRIVATE_MESSAGE,
- usernames,
+ recipients,
topicTitle,
topicBody,
archetypeId: "private_message",
diff --git a/app/assets/javascripts/discourse/mixins/pan-events.js.es6 b/app/assets/javascripts/discourse/mixins/pan-events.js.es6
index d64f714917..fb05901205 100644
--- a/app/assets/javascripts/discourse/mixins/pan-events.js.es6
+++ b/app/assets/javascripts/discourse/mixins/pan-events.js.es6
@@ -1,3 +1,4 @@
+import Mixin from "@ember/object/mixin";
/**
Pan events is a mixin that allows components to detect and respond to swipe gestures
It fires callbacks for panStart, panEnd, panMove with the pan state, and the original event.
@@ -6,19 +7,19 @@ export const SWIPE_VELOCITY = 40;
export const SWIPE_DISTANCE_THRESHOLD = 50;
export const SWIPE_VELOCITY_THRESHOLD = 0.12;
export const MINIMUM_SWIPE_DISTANCE = 5;
-export default Ember.Mixin.create({
+export default Mixin.create({
//velocity is pixels per ms
_panState: null,
didInsertElement() {
this._super(...arguments);
- this.addTouchListeners(this.$());
+ this.addTouchListeners($(this.element));
},
willDestroyElement() {
this._super(...arguments);
- this.removeTouchListeners(this.$());
+ this.removeTouchListeners($(this.element));
},
addTouchListeners($element) {
diff --git a/app/assets/javascripts/discourse/mixins/password-validation.js.es6 b/app/assets/javascripts/discourse/mixins/password-validation.js.es6
index 50a98e8790..d660f5208b 100644
--- a/app/assets/javascripts/discourse/mixins/password-validation.js.es6
+++ b/app/assets/javascripts/discourse/mixins/password-validation.js.es6
@@ -1,7 +1,9 @@
-import InputValidation from "discourse/models/input-validation";
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import { isEmpty } from "@ember/utils";
+import discourseComputed from "discourse-common/utils/decorators";
+import Mixin from "@ember/object/mixin";
+import EmberObject from "@ember/object";
-export default Ember.Mixin.create({
+export default Mixin.create({
rejectedPasswords: null,
init() {
@@ -10,21 +12,21 @@ export default Ember.Mixin.create({
this.set("rejectedPasswordsMessages", new Map());
},
- @computed("passwordMinLength")
+ @discourseComputed("passwordMinLength")
passwordInstructions() {
return I18n.t("user.password.instructions", {
count: this.passwordMinLength
});
},
- @computed("isDeveloper", "admin")
+ @discourseComputed("isDeveloper", "admin")
passwordMinLength(isDeveloper, admin) {
return isDeveloper || admin
? this.siteSettings.min_admin_password_length
: this.siteSettings.min_password_length;
},
- @computed(
+ @discourseComputed(
"accountPassword",
"passwordRequired",
"rejectedPasswords.[]",
@@ -41,11 +43,11 @@ export default Ember.Mixin.create({
passwordMinLength
) {
if (!passwordRequired) {
- return InputValidation.create({ ok: true });
+ return EmberObject.create({ ok: true });
}
if (rejectedPasswords.includes(password)) {
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason:
this.rejectedPasswordsMessages.get(password) ||
@@ -54,34 +56,34 @@ export default Ember.Mixin.create({
}
// If blank, fail without a reason
- if (Ember.isEmpty(password)) {
- return InputValidation.create({ failed: true });
+ if (isEmpty(password)) {
+ return EmberObject.create({ failed: true });
}
// If too short
if (password.length < passwordMinLength) {
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason: I18n.t("user.password.too_short")
});
}
- if (!Ember.isEmpty(accountUsername) && password === accountUsername) {
- return InputValidation.create({
+ if (!isEmpty(accountUsername) && password === accountUsername) {
+ return EmberObject.create({
failed: true,
reason: I18n.t("user.password.same_as_username")
});
}
- if (!Ember.isEmpty(accountEmail) && password === accountEmail) {
- return InputValidation.create({
+ if (!isEmpty(accountEmail) && password === accountEmail) {
+ return EmberObject.create({
failed: true,
reason: I18n.t("user.password.same_as_email")
});
}
// Looks good!
- return InputValidation.create({
+ return EmberObject.create({
ok: true,
reason: I18n.t("user.password.ok")
});
diff --git a/app/assets/javascripts/discourse/mixins/preferences-tab-controller.js.es6 b/app/assets/javascripts/discourse/mixins/preferences-tab-controller.js.es6
index 080c81fe9f..4dabeb3fa0 100644
--- a/app/assets/javascripts/discourse/mixins/preferences-tab-controller.js.es6
+++ b/app/assets/javascripts/discourse/mixins/preferences-tab-controller.js.es6
@@ -1,9 +1,10 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Mixin from "@ember/object/mixin";
-export default Ember.Mixin.create({
+export default Mixin.create({
saved: false,
- @computed("model.isSaving")
+ @discourseComputed("model.isSaving")
saveButtonText(isSaving) {
return isSaving ? I18n.t("saving") : I18n.t("save");
}
diff --git a/app/assets/javascripts/discourse/mixins/scroll-top.js.es6 b/app/assets/javascripts/discourse/mixins/scroll-top.js.es6
index 630227b8fa..d636c2333f 100644
--- a/app/assets/javascripts/discourse/mixins/scroll-top.js.es6
+++ b/app/assets/javascripts/discourse/mixins/scroll-top.js.es6
@@ -1,9 +1,12 @@
+import { scheduleOnce } from "@ember/runloop";
import DiscourseURL from "discourse/lib/url";
import { deprecated } from "discourse/mixins/scroll-top";
+import Mixin from "@ember/object/mixin";
+import ENV from "discourse-common/config/environment";
const context = {
_scrollTop() {
- if (Ember.testing) {
+ if (ENV.environment === "test") {
return;
}
$(document).scrollTop(0);
@@ -14,10 +17,10 @@ function scrollTop() {
if (DiscourseURL.isJumpScheduled()) {
return;
}
- Ember.run.scheduleOnce("afterRender", context, context._scrollTop);
+ scheduleOnce("afterRender", context, context._scrollTop);
}
-export default Ember.Mixin.create({
+export default Mixin.create({
didInsertElement() {
deprecated(
"The `ScrollTop` mixin is deprecated. Replace it with a `{{d-section}}` component"
diff --git a/app/assets/javascripts/discourse/mixins/scrolling.js.es6 b/app/assets/javascripts/discourse/mixins/scrolling.js.es6
index 690bf1168f..2a15df47eb 100644
--- a/app/assets/javascripts/discourse/mixins/scrolling.js.es6
+++ b/app/assets/javascripts/discourse/mixins/scrolling.js.es6
@@ -1,4 +1,6 @@
-import debounce from "discourse/lib/debounce";
+import { scheduleOnce } from "@ember/runloop";
+import discourseDebounce from "discourse/lib/debounce";
+import Mixin from "@ember/object/mixin";
/**
This object provides the DOM methods we need for our Mixin to bind to scrolling
@@ -23,7 +25,7 @@ const ScrollingDOMMethods = {
}
};
-const Scrolling = Ember.Mixin.create({
+const Scrolling = Mixin.create({
// Begin watching for scroll events. By default they will be called at max every 100ms.
// call with {debounce: N} for a diff time
bindScrolling(opts) {
@@ -37,11 +39,11 @@ const Scrolling = Ember.Mixin.create({
if (router.activeTransition) {
return;
}
- return Ember.run.scheduleOnce("afterRender", this, "scrolled");
+ return scheduleOnce("afterRender", this, "scrolled");
};
if (opts.debounce) {
- onScrollMethod = debounce(onScrollMethod, opts.debounce);
+ onScrollMethod = discourseDebounce(onScrollMethod, opts.debounce);
}
ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name);
diff --git a/app/assets/javascripts/discourse/mixins/singleton.js.es6 b/app/assets/javascripts/discourse/mixins/singleton.js.es6
index 62a8265613..5c4ac8238d 100644
--- a/app/assets/javascripts/discourse/mixins/singleton.js.es6
+++ b/app/assets/javascripts/discourse/mixins/singleton.js.es6
@@ -8,7 +8,7 @@
```javascript
// Define your class and apply the Mixin
- User = Ember.Object.extend({});
+ User = EmberObject.extend({});
User.reopenClass(Singleton);
// Retrieve the current instance:
@@ -34,7 +34,7 @@
```javascript
// Define your class and apply the Mixin
- Foot = Ember.Object.extend({});
+ Foot = EmberObject.extend({});
Foot.reopenClass(Singleton, {
createCurrent: function() {
return Foot.create({toes: 5});
@@ -45,7 +45,9 @@
```
**/
-const Singleton = Ember.Mixin.create({
+import Mixin from "@ember/object/mixin";
+
+const Singleton = Mixin.create({
current() {
if (!this._current) {
this._current = this.createCurrent();
diff --git a/app/assets/javascripts/discourse/mixins/upload.js.es6 b/app/assets/javascripts/discourse/mixins/upload.js.es6
index 394b6a73f3..820e2e912a 100644
--- a/app/assets/javascripts/discourse/mixins/upload.js.es6
+++ b/app/assets/javascripts/discourse/mixins/upload.js.es6
@@ -1,10 +1,12 @@
import {
displayErrorForUpload,
validateUploadedFiles
-} from "discourse/lib/utilities";
+} from "discourse/lib/uploads";
import getUrl from "discourse-common/lib/get-url";
+import { on } from "@ember/object/evented";
+import Mixin from "@ember/object/mixin";
-export default Ember.Mixin.create({
+export default Mixin.create({
uploading: false,
uploadProgress: 0,
@@ -23,8 +25,6 @@ export default Ember.Mixin.create({
getUrl(this.getWithDefault("uploadUrl", "/uploads")) +
".json?client_id=" +
(this.messageBus && this.messageBus.clientId) +
- "&authenticity_token=" +
- encodeURIComponent(Discourse.Session.currentProp("csrfToken")) +
this.uploadUrlParams
);
},
@@ -35,8 +35,8 @@ export default Ember.Mixin.create({
return {};
},
- _initialize: function() {
- const $upload = this.$();
+ _initialize: on("didInsertElement", function() {
+ const $upload = $(this.element);
const reset = () =>
this.setProperties({ uploading: false, uploadProgress: 0 });
const maxFiles = this.getWithDefault(
@@ -78,7 +78,7 @@ export default Ember.Mixin.create({
$upload.on("fileuploadsubmit", (e, data) => {
const opts = _.merge(
- { bypassNewUserRestriction: true },
+ { bypassNewUserRestriction: true, user: this.currentUser },
this.validateUploadedFilesOptions()
);
const isValid = validateUploadedFiles(data.files, opts);
@@ -103,17 +103,17 @@ export default Ember.Mixin.create({
}
reset();
});
- }.on("didInsertElement"),
+ }),
- _destroy: function() {
+ _destroy: on("willDestroyElement", function() {
this.messageBus && this.messageBus.unsubscribe("/uploads/" + this.type);
- const $upload = this.$();
+ const $upload = $(this.element);
try {
$upload.fileupload("destroy");
} catch (e) {
/* wasn't initialized yet */
}
$upload.off();
- }.on("willDestroyElement")
+ })
});
diff --git a/app/assets/javascripts/discourse/mixins/user-fields-validation.js.es6 b/app/assets/javascripts/discourse/mixins/user-fields-validation.js.es6
index 57a4b9cec0..5a7ff18808 100644
--- a/app/assets/javascripts/discourse/mixins/user-fields-validation.js.es6
+++ b/app/assets/javascripts/discourse/mixins/user-fields-validation.js.es6
@@ -1,10 +1,9 @@
-import InputValidation from "discourse/models/input-validation";
-import {
- on,
- default as computed
-} from "ember-addons/ember-computed-decorators";
+import { isEmpty } from "@ember/utils";
+import EmberObject from "@ember/object";
+import discourseComputed, { on } from "discourse-common/utils/decorators";
+import Mixin from "@ember/object/mixin";
-export default Ember.Mixin.create({
+export default Mixin.create({
@on("init")
_createUserFields() {
if (!this.site) {
@@ -14,28 +13,28 @@ export default Ember.Mixin.create({
let userFields = this.site.get("user_fields");
if (userFields) {
userFields = _.sortBy(userFields, "position").map(function(f) {
- return Ember.Object.create({ value: null, field: f });
+ return EmberObject.create({ value: null, field: f });
});
}
this.set("userFields", userFields);
},
// Validate required fields
- @computed("userFields.@each.value")
+ @discourseComputed("userFields.@each.value")
userFieldsValidation() {
let userFields = this.userFields;
if (userFields) {
userFields = userFields.filterBy("field.required");
}
- if (!Ember.isEmpty(userFields)) {
+ if (!isEmpty(userFields)) {
const anyEmpty = userFields.any(uf => {
const val = uf.get("value");
- return !val || Ember.isEmpty(val);
+ return !val || isEmpty(val);
});
if (anyEmpty) {
- return InputValidation.create({ failed: true });
+ return EmberObject.create({ failed: true });
}
}
- return InputValidation.create({ ok: true });
+ return EmberObject.create({ ok: true });
}
});
diff --git a/app/assets/javascripts/discourse/mixins/username-validation.js.es6 b/app/assets/javascripts/discourse/mixins/username-validation.js.es6
index 892f587b85..fcd0ea89ba 100644
--- a/app/assets/javascripts/discourse/mixins/username-validation.js.es6
+++ b/app/assets/javascripts/discourse/mixins/username-validation.js.es6
@@ -1,20 +1,23 @@
-import InputValidation from "discourse/models/input-validation";
-import debounce from "discourse/lib/debounce";
+import { isEmpty } from "@ember/utils";
+import discourseDebounce from "discourse/lib/debounce";
import { setting } from "discourse/lib/computed";
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import Mixin from "@ember/object/mixin";
+import EmberObject from "@ember/object";
+import User from "discourse/models/user";
-export default Ember.Mixin.create({
+export default Mixin.create({
uniqueUsernameValidation: null,
maxUsernameLength: setting("max_username_length"),
minUsernameLength: setting("min_username_length"),
- fetchExistingUsername: debounce(function() {
- Discourse.User.checkUsername(null, this.accountEmail).then(result => {
+ fetchExistingUsername: discourseDebounce(function() {
+ User.checkUsername(null, this.accountEmail).then(result => {
if (
result.suggestion &&
- (Ember.isEmpty(this.accountUsername) ||
+ (isEmpty(this.accountUsername) ||
this.accountUsername === this.get("authOptions.username"))
) {
this.setProperties({
@@ -25,25 +28,25 @@ export default Ember.Mixin.create({
});
}, 500),
- @computed("accountUsername")
+ @discourseComputed("accountUsername")
basicUsernameValidation(accountUsername) {
this.set("uniqueUsernameValidation", null);
if (accountUsername && accountUsername === this.prefilledUsername) {
- return InputValidation.create({
+ return EmberObject.create({
ok: true,
reason: I18n.t("user.username.prefilled")
});
}
// If blank, fail without a reason
- if (Ember.isEmpty(accountUsername)) {
- return InputValidation.create({ failed: true });
+ if (isEmpty(accountUsername)) {
+ return EmberObject.create({ failed: true });
}
// If too short
if (accountUsername.length < this.siteSettings.min_username_length) {
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason: I18n.t("user.username.too_short")
});
@@ -51,7 +54,7 @@ export default Ember.Mixin.create({
// If too long
if (accountUsername.length > this.maxUsernameLength) {
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason: I18n.t("user.username.too_long")
});
@@ -59,7 +62,7 @@ export default Ember.Mixin.create({
this.checkUsernameAvailability();
// Let's check it out asynchronously
- return InputValidation.create({
+ return EmberObject.create({
failed: true,
reason: I18n.t("user.username.checking")
});
@@ -67,56 +70,55 @@ export default Ember.Mixin.create({
shouldCheckUsernameAvailability() {
return (
- !Ember.isEmpty(this.accountUsername) &&
+ !isEmpty(this.accountUsername) &&
this.accountUsername.length >= this.minUsernameLength
);
},
- checkUsernameAvailability: debounce(function() {
+ checkUsernameAvailability: discourseDebounce(function() {
if (this.shouldCheckUsernameAvailability()) {
- return Discourse.User.checkUsername(
- this.accountUsername,
- this.accountEmail
- ).then(result => {
- this.set("isDeveloper", false);
- if (result.available) {
- if (result.is_developer) {
- this.set("isDeveloper", true);
- }
- return this.set(
- "uniqueUsernameValidation",
- InputValidation.create({
- ok: true,
- reason: I18n.t("user.username.available")
- })
- );
- } else {
- if (result.suggestion) {
+ return User.checkUsername(this.accountUsername, this.accountEmail).then(
+ result => {
+ this.set("isDeveloper", false);
+ if (result.available) {
+ if (result.is_developer) {
+ this.set("isDeveloper", true);
+ }
return this.set(
"uniqueUsernameValidation",
- InputValidation.create({
- failed: true,
- reason: I18n.t("user.username.not_available", result)
+ EmberObject.create({
+ ok: true,
+ reason: I18n.t("user.username.available")
})
);
} else {
- return this.set(
- "uniqueUsernameValidation",
- InputValidation.create({
- failed: true,
- reason: result.errors
- ? result.errors.join(" ")
- : I18n.t("user.username.not_available_no_suggestion")
- })
- );
+ if (result.suggestion) {
+ return this.set(
+ "uniqueUsernameValidation",
+ EmberObject.create({
+ failed: true,
+ reason: I18n.t("user.username.not_available", result)
+ })
+ );
+ } else {
+ return this.set(
+ "uniqueUsernameValidation",
+ EmberObject.create({
+ failed: true,
+ reason: result.errors
+ ? result.errors.join(" ")
+ : I18n.t("user.username.not_available_no_suggestion")
+ })
+ );
+ }
}
}
- });
+ );
}
}, 500),
// Actually wait for the async name check before we're 100% sure we're good to go
- @computed("uniqueUsernameValidation", "basicUsernameValidation")
+ @discourseComputed("uniqueUsernameValidation", "basicUsernameValidation")
usernameValidation() {
const basicValidation = this.basicUsernameValidation;
const uniqueUsername = this.uniqueUsernameValidation;
diff --git a/app/assets/javascripts/discourse/models/action-summary.js.es6 b/app/assets/javascripts/discourse/models/action-summary.js.es6
index 283a76981d..aad8b48b8a 100644
--- a/app/assets/javascripts/discourse/models/action-summary.js.es6
+++ b/app/assets/javascripts/discourse/models/action-summary.js.es6
@@ -1,9 +1,10 @@
+import { or } from "@ember/object/computed";
import { ajax } from "discourse/lib/ajax";
import RestModel from "discourse/models/rest";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default RestModel.extend({
- canToggle: Ember.computed.or("can_undo", "can_act"),
+ canToggle: or("can_undo", "can_act"),
// Remove it
removeAction: function() {
@@ -59,10 +60,12 @@ export default RestModel.extend({
post.updateActionsSummary(data.result);
}
const remaining = parseInt(
- data.xhr.getResponseHeader("Discourse-Actions-Remaining") || 0
+ data.xhr.getResponseHeader("Discourse-Actions-Remaining") || 0,
+ 10
);
const max = parseInt(
- data.xhr.getResponseHeader("Discourse-Actions-Max") || 0
+ data.xhr.getResponseHeader("Discourse-Actions-Max") || 0,
+ 10
);
return { acted: true, remaining, max };
})
diff --git a/app/assets/javascripts/discourse/models/archetype.js.es6 b/app/assets/javascripts/discourse/models/archetype.js.es6
index 9ba6e6744b..912a45b45c 100644
--- a/app/assets/javascripts/discourse/models/archetype.js.es6
+++ b/app/assets/javascripts/discourse/models/archetype.js.es6
@@ -1,8 +1,9 @@
+import { gt, not } from "@ember/object/computed";
import { propertyEqual } from "discourse/lib/computed";
import RestModel from "discourse/models/rest";
export default RestModel.extend({
- hasOptions: Ember.computed.gt("options.length", 0),
+ hasOptions: gt("options.length", 0),
isDefault: propertyEqual("id", "site.default_archetype"),
- notDefault: Ember.computed.not("isDefault")
+ notDefault: not("isDefault")
});
diff --git a/app/assets/javascripts/discourse/models/badge-grouping.js.es6 b/app/assets/javascripts/discourse/models/badge-grouping.js.es6
index 573dff223d..6dbaa5c7c6 100644
--- a/app/assets/javascripts/discourse/models/badge-grouping.js.es6
+++ b/app/assets/javascripts/discourse/models/badge-grouping.js.es6
@@ -1,13 +1,13 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
import RestModel from "discourse/models/rest";
export default RestModel.extend({
- @computed("name")
+ @discourseComputed("name")
i18nNameKey() {
return this.name.toLowerCase().replace(/\s/g, "_");
},
- @computed("name")
+ @discourseComputed("name")
displayName() {
const i18nKey = `badges.badge_grouping.${this.i18nNameKey}.name`;
return I18n.t(i18nKey, { defaultValue: this.name });
diff --git a/app/assets/javascripts/discourse/models/badge.js.es6 b/app/assets/javascripts/discourse/models/badge.js.es6
index d70a5fbbe6..5b9e46cef9 100644
--- a/app/assets/javascripts/discourse/models/badge.js.es6
+++ b/app/assets/javascripts/discourse/models/badge.js.es6
@@ -1,12 +1,15 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { none } from "@ember/object/computed";
+import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import BadgeGrouping from "discourse/models/badge-grouping";
import RestModel from "discourse/models/rest";
-import computed from "ember-addons/ember-computed-decorators";
+import { Promise } from "rsvp";
const Badge = RestModel.extend({
- newBadge: Ember.computed.none("id"),
+ newBadge: none("id"),
- @computed
+ @discourseComputed
url() {
return Discourse.getURL(`/badges/${this.id}/${this.slug}`);
},
@@ -24,7 +27,7 @@ const Badge = RestModel.extend({
}
},
- @computed("badge_type.name")
+ @discourseComputed("badge_type.name")
badgeTypeClassName(type) {
type = type || "";
return `badge-type-${type.toLowerCase()}`;
@@ -51,7 +54,7 @@ const Badge = RestModel.extend({
},
destroy() {
- if (this.newBadge) return Ember.RSVP.resolve();
+ if (this.newBadge) return Promise.resolve();
return ajax(`/admin/badges/${this.id}`, {
type: "DELETE"
@@ -66,7 +69,7 @@ Badge.reopenClass({
if ("badge_types" in json) {
json.badge_types.forEach(
badgeTypeJson =>
- (badgeTypes[badgeTypeJson.id] = Ember.Object.create(badgeTypeJson))
+ (badgeTypes[badgeTypeJson.id] = EmberObject.create(badgeTypeJson))
);
}
diff --git a/app/assets/javascripts/discourse/models/bookmark.js.es6 b/app/assets/javascripts/discourse/models/bookmark.js.es6
new file mode 100644
index 0000000000..2ae4e63e4c
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/bookmark.js.es6
@@ -0,0 +1,24 @@
+import { none } from "@ember/object/computed";
+import { computed } from "@ember/object";
+import { ajax } from "discourse/lib/ajax";
+import { Promise } from "rsvp";
+import RestModel from "discourse/models/rest";
+
+const Bookmark = RestModel.extend({
+ newBookmark: none("id"),
+
+ @computed
+ get url() {
+ return Discourse.getURL(`/bookmarks/${this.id}`);
+ },
+
+ destroy() {
+ if (this.newBookmark) return Promise.resolve();
+
+ return ajax(this.url, {
+ type: "DELETE"
+ });
+ }
+});
+
+export default Bookmark;
diff --git a/app/assets/javascripts/discourse/models/category-list.js.es6 b/app/assets/javascripts/discourse/models/category-list.js.es6
index aff72e7fa3..b8437f706d 100644
--- a/app/assets/javascripts/discourse/models/category-list.js.es6
+++ b/app/assets/javascripts/discourse/models/category-list.js.es6
@@ -1,5 +1,8 @@
import PreloadStore from "preload-store";
import { ajax } from "discourse/lib/ajax";
+import Topic from "discourse/models/topic";
+import Category from "discourse/models/category";
+import Site from "discourse/models/site";
const CategoryList = Ember.ArrayProxy.extend({
init() {
@@ -11,7 +14,7 @@ const CategoryList = Ember.ArrayProxy.extend({
CategoryList.reopenClass({
categoriesFrom(store, result) {
const categories = CategoryList.create();
- const list = Discourse.Category.list();
+ const list = Category.list();
let statPeriod = "all";
const minCategories = result.category_list.categories.length * 0.66;
@@ -39,7 +42,7 @@ CategoryList.reopenClass({
if (c.topics) {
c.topics = c.topics.map(t => {
- const topic = Discourse.Topic.create(t);
+ const topic = Topic.create(t);
topic.set("category", c);
return topic;
});
@@ -74,7 +77,9 @@ CategoryList.reopenClass({
break;
}
- categories.pushObject(store.createRecord("category", c));
+ const record = Site.current().updateCategory(c);
+ record.setupGroupsAndPermissions();
+ categories.pushObject(record);
});
return categories;
},
diff --git a/app/assets/javascripts/discourse/models/category.js.es6 b/app/assets/javascripts/discourse/models/category.js.es6
index 5b663c48a0..bbbe590d0b 100644
--- a/app/assets/javascripts/discourse/models/category.js.es6
+++ b/app/assets/javascripts/discourse/models/category.js.es6
@@ -1,10 +1,16 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { get } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import RestModel from "discourse/models/rest";
-import computed from "ember-addons/ember-computed-decorators";
-import { on } from "ember-addons/ember-computed-decorators";
+import { on } from "discourse-common/utils/decorators";
import PermissionType from "discourse/models/permission-type";
+import { NotificationLevels } from "discourse/lib/notification-levels";
+import deprecated from "discourse-common/lib/deprecated";
+import Site from "discourse/models/site";
const Category = RestModel.extend({
+ permissions: null,
+
@on("init")
setupGroupsAndPermissions() {
const availableGroups = this.available_groups;
@@ -28,7 +34,14 @@ const Category = RestModel.extend({
}
},
- @computed
+ @on("init")
+ setupRequiredTagGroups() {
+ if (this.required_tag_group_name) {
+ this.set("required_tag_groups", [this.required_tag_group_name]);
+ }
+ },
+
+ @discourseComputed
availablePermissions() {
return [
PermissionType.create({ id: PermissionType.FULL }),
@@ -37,55 +50,93 @@ const Category = RestModel.extend({
];
},
- @computed("id")
+ @discourseComputed("id")
searchContext(id) {
return { type: "category", id, category: this };
},
- @computed("name")
- url() {
- return Discourse.getURL("/c/") + Category.slugFor(this);
+ @discourseComputed("parentCategory.ancestors")
+ ancestors(parentAncestors) {
+ return [...(parentAncestors || []), this];
},
- @computed
+ @discourseComputed("parentCategory.level")
+ level(parentLevel) {
+ return (parentLevel || -1) + 1;
+ },
+
+ @discourseComputed("subcategories")
+ isParent(subcategories) {
+ return subcategories && subcategories.length > 0;
+ },
+
+ @discourseComputed("subcategories")
+ isGrandParent(subcategories) {
+ return (
+ subcategories &&
+ subcategories.some(
+ cat => cat.subcategories && cat.subcategories.length > 0
+ )
+ );
+ },
+
+ @discourseComputed("notification_level")
+ isMuted(notificationLevel) {
+ return notificationLevel === NotificationLevels.MUTED;
+ },
+
+ @discourseComputed("notification_level")
+ notificationLevelString(notificationLevel) {
+ // Get the key from the value
+ const notificationLevelString = Object.keys(NotificationLevels).find(
+ key => NotificationLevels[key] === notificationLevel
+ );
+ if (notificationLevelString) return notificationLevelString.toLowerCase();
+ },
+
+ @discourseComputed("name")
+ url() {
+ return Discourse.getURL(`/c/${Category.slugFor(this)}/${this.id}`);
+ },
+
+ @discourseComputed
fullSlug() {
return Category.slugFor(this).replace(/\//g, "-");
},
- @computed("name")
+ @discourseComputed("name")
nameLower(name) {
return name.toLowerCase();
},
- @computed("url")
+ @discourseComputed("url")
unreadUrl(url) {
return `${url}/l/unread`;
},
- @computed("url")
+ @discourseComputed("url")
newUrl(url) {
return `${url}/l/new`;
},
- @computed("color", "text_color")
+ @discourseComputed("color", "text_color")
style(color, textColor) {
return `background-color: #${color}; color: #${textColor}`;
},
- @computed("topic_count")
+ @discourseComputed("topic_count")
moreTopics(topicCount) {
return topicCount > (this.num_featured_topics || 2);
},
- @computed("topic_count", "subcategories")
- totalTopicCount(topicCount, subcats) {
- let count = topicCount;
- if (subcats) {
- subcats.forEach(s => {
- count += s.get("topic_count");
+ @discourseComputed("topic_count", "subcategories.[]")
+ totalTopicCount(topicCount, subcategories) {
+ if (subcategories) {
+ subcategories.forEach(subcategory => {
+ topicCount += subcategory.topic_count;
});
}
- return count;
+ return topicCount;
},
save() {
@@ -114,11 +165,14 @@ const Category = RestModel.extend({
allow_badges: this.allow_badges,
custom_fields: this.custom_fields,
topic_template: this.topic_template,
- suppress_from_latest: this.suppress_from_latest,
all_topics_wiki: this.all_topics_wiki,
allowed_tags: this.allowed_tags,
allowed_tag_groups: this.allowed_tag_groups,
allow_global_tags: this.allow_global_tags,
+ required_tag_group_name: this.required_tag_groups
+ ? this.required_tag_groups[0]
+ : null,
+ min_tags_from_required_group: this.min_tags_from_required_group,
sort_order: this.sort_order,
sort_ascending: this.sort_ascending,
topic_featured_link_allowed: this.topic_featured_link_allowed,
@@ -161,35 +215,26 @@ const Category = RestModel.extend({
this.availableGroups.addObject(permission.group_name);
},
- @computed
- permissions() {
- return Ember.A([
- { group_name: "everyone", permission: PermissionType.create({ id: 1 }) },
- { group_name: "admins", permission: PermissionType.create({ id: 2 }) },
- { group_name: "crap", permission: PermissionType.create({ id: 3 }) }
- ]);
- },
-
- @computed("topics")
+ @discourseComputed("topics")
latestTopic(topics) {
if (topics && topics.length) {
return topics[0];
}
},
- @computed("topics")
+ @discourseComputed("topics")
featuredTopics(topics) {
if (topics && topics.length) {
return topics.slice(0, this.num_featured_topics || 2);
}
},
- @computed("id", "topicTrackingState.messageCount")
+ @discourseComputed("id", "topicTrackingState.messageCount")
unreadTopics(id) {
return this.topicTrackingState.countUnread(id);
},
- @computed("id", "topicTrackingState.messageCount")
+ @discourseComputed("id", "topicTrackingState.messageCount")
newTopics(id) {
return this.topicTrackingState.countNew(id);
},
@@ -200,9 +245,9 @@ const Category = RestModel.extend({
return ajax(url, { data: { notification_level }, type: "POST" });
},
- @computed("id")
+ @discourseComputed("id")
isUncategorizedCategory(id) {
- return id === Discourse.Site.currentProp("uncategorized_category_id");
+ return id === Site.currentProp("uncategorized_category_id");
}
});
@@ -214,7 +259,7 @@ Category.reopenClass({
_uncategorized ||
Category.list().findBy(
"id",
- Discourse.Site.currentProp("uncategorized_category_id")
+ Site.currentProp("uncategorized_category_id")
);
return _uncategorized;
},
@@ -222,15 +267,15 @@ Category.reopenClass({
slugFor(category, separator = "/") {
if (!category) return "";
- const parentCategory = Ember.get(category, "parentCategory");
+ const parentCategory = get(category, "parentCategory");
let result = "";
if (parentCategory) {
result = Category.slugFor(parentCategory) + separator;
}
- const id = Ember.get(category, "id"),
- slug = Ember.get(category, "slug");
+ const id = get(category, "id"),
+ slug = get(category, "slug");
return !slug || slug.trim().length === 0
? `${result}${id}-category`
@@ -238,26 +283,30 @@ Category.reopenClass({
},
list() {
- return Discourse.Site.currentProp("categoriesList");
+ return Site.currentProp("categoriesList");
},
listByActivity() {
- return Discourse.Site.currentProp("sortedCategories");
+ return Site.currentProp("sortedCategories");
},
- idMap() {
- return Discourse.Site.currentProp("categoriesById");
+ _idMap() {
+ return Site.currentProp("categoriesById");
},
findSingleBySlug(slug) {
- return Category.list().find(c => Category.slugFor(c) === slug);
+ if (Discourse.SiteSettings.slug_generation_method !== "encoded") {
+ return Category.list().find(c => Category.slugFor(c) === slug);
+ } else {
+ return Category.list().find(c => Category.slugFor(c) === encodeURI(slug));
+ }
},
findById(id) {
if (!id) {
return;
}
- return Category.idMap()[id];
+ return Category._idMap()[id];
},
findByIds(ids = []) {
@@ -271,6 +320,61 @@ Category.reopenClass({
return categories;
},
+ findBySlugAndParent(slug, parentCategory) {
+ if (Discourse.SiteSettings.slug_generation_method === "encoded") {
+ slug = encodeURI(slug);
+ }
+ return Category.list().find(category => {
+ return (
+ category.slug === slug &&
+ (category.parentCategory || null) === parentCategory
+ );
+ });
+ },
+
+ findBySlugPath(slugPath) {
+ let category = null;
+
+ for (const slug of slugPath) {
+ category = this.findBySlugAndParent(slug, category);
+
+ if (!category) {
+ return null;
+ }
+ }
+
+ return category;
+ },
+
+ findBySlugPathWithID(slugPathWithID) {
+ let parts = slugPathWithID.split("/").filter(Boolean);
+ // slugs found by star/glob pathing in emeber do not automatically url decode - ensure that these are decoded
+ if (Discourse.SiteSettings.slug_generation_method === "encoded") {
+ parts = parts.map(urlPart => decodeURI(urlPart));
+ }
+ let category = null;
+
+ if (parts.length > 0 && parts[parts.length - 1].match(/^\d+$/)) {
+ const id = parseInt(parts.pop(), 10);
+
+ category = Category.findById(id);
+ } else {
+ category = Category.findBySlugPath(parts);
+
+ if (
+ !category &&
+ parts.length > 0 &&
+ parts[parts.length - 1].match(/^\d+-category/)
+ ) {
+ const id = parseInt(parts.pop(), 10);
+
+ category = Category.findById(id);
+ }
+ }
+
+ return category;
+ },
+
findBySlug(slug, parentSlug) {
const categories = Category.list();
let category;
@@ -286,7 +390,11 @@ Category.reopenClass({
return (
item &&
item.get("parentCategory") === parentCategory &&
- Category.slugFor(item) === parentSlug + "/" + slug
+ ((Discourse.SiteSettings.slug_generation_method !== "encoded" &&
+ Category.slugFor(item) === parentSlug + "/" + slug) ||
+ (Discourse.SiteSettings.slug_generation_method === "encoded" &&
+ Category.slugFor(item) ===
+ encodeURI(parentSlug) + "/" + encodeURI(slug)))
);
});
}
@@ -315,6 +423,10 @@ Category.reopenClass({
: ajax(`/c/${slug}/find_by_slug.json`);
},
+ reloadBySlugPath(slugPath) {
+ return ajax(`/c/${slugPath}/find_by_slug.json`);
+ },
+
search(term, opts) {
var limit = 5;
@@ -388,4 +500,14 @@ Category.reopenClass({
}
});
+Object.defineProperty(Discourse, "Category", {
+ get() {
+ deprecated(
+ "Import the Category class instead of using Discourse.Category",
+ { since: "2.4.0", dropFrom: "2.5.0" }
+ );
+ return Category;
+ }
+});
+
export default Category;
diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6
index 7e4f38a585..2150396102 100644
--- a/app/assets/javascripts/discourse/models/composer.js.es6
+++ b/app/assets/javascripts/discourse/models/composer.js.es6
@@ -1,28 +1,40 @@
+import { isEmpty } from "@ember/utils";
+import { reads, equal, not, or, and } from "@ember/object/computed";
+import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
+import { cancel } from "@ember/runloop";
+import { later } from "@ember/runloop";
import RestModel from "discourse/models/rest";
import Topic from "discourse/models/topic";
import { throwAjaxError } from "discourse/lib/ajax-error";
import Quote from "discourse/lib/quote";
import Draft from "discourse/models/draft";
-import {
- default as computed,
+import discourseComputed, {
observes,
on
-} from "ember-addons/ember-computed-decorators";
-import { escapeExpression, tinyAvatar } from "discourse/lib/utilities";
+} from "discourse-common/utils/decorators";
+import {
+ escapeExpression,
+ tinyAvatar,
+ emailValid
+} from "discourse/lib/utilities";
import { propertyNotEqual } from "discourse/lib/computed";
-import throttle from "discourse/lib/throttle";
+import { throttle } from "@ember/runloop";
+import { Promise } from "rsvp";
+import { set } from "@ember/object";
+import Site from "discourse/models/site";
+import User from "discourse/models/user";
+import deprecated from "discourse-common/lib/deprecated";
// The actions the composer can take
export const CREATE_TOPIC = "createTopic",
CREATE_SHARED_DRAFT = "createSharedDraft",
EDIT_SHARED_DRAFT = "editSharedDraft",
PRIVATE_MESSAGE = "privateMessage",
- NEW_PRIVATE_MESSAGE_KEY = "new_private_message",
- NEW_TOPIC_KEY = "new_topic",
REPLY = "reply",
EDIT = "edit",
- REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic",
- REPLY_AS_NEW_PRIVATE_MESSAGE_KEY = "reply_as_new_private_message";
+ NEW_PRIVATE_MESSAGE_KEY = "new_private_message",
+ NEW_TOPIC_KEY = "new_topic";
function isEdit(action) {
return action === EDIT || action === EDIT_SHARED_DRAFT;
@@ -43,20 +55,38 @@ const CLOSED = "closed",
is_warning: "isWarning",
whisper: "whisper",
archetype: "archetypeId",
- target_usernames: "targetUsernames",
+ target_recipients: "targetRecipients",
typing_duration_msecs: "typingTime",
composer_open_duration_msecs: "composerTime",
tags: "tags",
featured_link: "featuredLink",
shared_draft: "sharedDraft",
- no_bump: "noBump"
+ no_bump: "noBump",
+ draft_key: "draftKey"
},
_edit_topic_serializer = {
title: "topic.title",
categoryId: "topic.category.id",
tags: "topic.tags",
featuredLink: "topic.featured_link"
- };
+ },
+ _draft_serializer = {
+ reply: "reply",
+ action: "action",
+ title: "title",
+ categoryId: "categoryId",
+ archetypeId: "archetypeId",
+ whisper: "whisper",
+ metaData: "metaData",
+ composerTime: "composerTime",
+ typingTime: "typingTime",
+ postId: "post.id",
+ // TODO remove together with 'targetUsername' deprecations
+ usernames: "targetUsernames",
+ recipients: "targetRecipients"
+ },
+ _add_draft_fields = {},
+ FAST_REPLY_LENGTH_THRESHOLD = 10000;
export const SAVE_LABELS = {
[EDIT]: "composer.save_edit",
@@ -69,11 +99,11 @@ export const SAVE_LABELS = {
export const SAVE_ICONS = {
[EDIT]: "pencil-alt",
- [EDIT_SHARED_DRAFT]: "clipboard",
+ [EDIT_SHARED_DRAFT]: "far-clipboard",
[REPLY]: "reply",
[CREATE_TOPIC]: "plus",
[PRIVATE_MESSAGE]: "envelope",
- [CREATE_SHARED_DRAFT]: "clipboard"
+ [CREATE_SHARED_DRAFT]: "far-clipboard"
};
const Composer = RestModel.extend({
@@ -83,11 +113,11 @@ const Composer = RestModel.extend({
draftSaving: false,
draftSaved: false,
- archetypes: Ember.computed.reads("site.archetypes"),
+ archetypes: reads("site.archetypes"),
- sharedDraft: Ember.computed.equal("action", CREATE_SHARED_DRAFT),
+ sharedDraft: equal("action", CREATE_SHARED_DRAFT),
- @computed
+ @discourseComputed
categoryId: {
get() {
return this._categoryId;
@@ -98,7 +128,7 @@ const Composer = RestModel.extend({
set(categoryId) {
const oldCategoryId = this._categoryId;
- if (Ember.isEmpty(categoryId)) {
+ if (isEmpty(categoryId)) {
categoryId = null;
}
this._categoryId = categoryId;
@@ -111,48 +141,53 @@ const Composer = RestModel.extend({
}
},
- @computed("categoryId")
+ @discourseComputed("categoryId")
category(categoryId) {
return categoryId ? this.site.categories.findBy("id", categoryId) : null;
},
- @computed("category")
+ @discourseComputed("category")
minimumRequiredTags(category) {
return category && category.minimum_required_tags > 0
? category.minimum_required_tags
: null;
},
- creatingTopic: Ember.computed.equal("action", CREATE_TOPIC),
- creatingSharedDraft: Ember.computed.equal("action", CREATE_SHARED_DRAFT),
- creatingPrivateMessage: Ember.computed.equal("action", PRIVATE_MESSAGE),
- notCreatingPrivateMessage: Ember.computed.not("creatingPrivateMessage"),
- notPrivateMessage: Ember.computed.not("privateMessage"),
+ creatingTopic: equal("action", CREATE_TOPIC),
+ creatingSharedDraft: equal("action", CREATE_SHARED_DRAFT),
+ creatingPrivateMessage: equal("action", PRIVATE_MESSAGE),
+ notCreatingPrivateMessage: not("creatingPrivateMessage"),
+ notPrivateMessage: not("privateMessage"),
- @computed("privateMessage", "archetype.hasOptions")
+ @discourseComputed("editingPost", "topic.details.can_edit")
+ disableTitleInput(editingPost, canEditTopic) {
+ return editingPost && !canEditTopic;
+ },
+
+ @discourseComputed("privateMessage", "archetype.hasOptions")
showCategoryChooser(isPrivateMessage, hasOptions) {
const manyCategories = this.site.categories.length > 1;
return !isPrivateMessage && (hasOptions || manyCategories);
},
- @computed("creatingPrivateMessage", "topic")
+ @discourseComputed("creatingPrivateMessage", "topic")
privateMessage(creatingPrivateMessage, topic) {
return (
creatingPrivateMessage || (topic && topic.archetype === "private_message")
);
},
- topicFirstPost: Ember.computed.or("creatingTopic", "editingFirstPost"),
+ topicFirstPost: or("creatingTopic", "editingFirstPost"),
- @computed("action")
+ @discourseComputed("action")
editingPost: isEdit,
- replyingToTopic: Ember.computed.equal("action", REPLY),
+ replyingToTopic: equal("action", REPLY),
- viewOpen: Ember.computed.equal("composeState", OPEN),
- viewDraft: Ember.computed.equal("composeState", DRAFT),
- viewFullscreen: Ember.computed.equal("composeState", FULLSCREEN),
- viewOpenOrFullscreen: Ember.computed.or("viewOpen", "viewFullscreen"),
+ viewOpen: equal("composeState", OPEN),
+ viewDraft: equal("composeState", DRAFT),
+ viewFullscreen: equal("composeState", FULLSCREEN),
+ viewOpenOrFullscreen: or("viewOpen", "viewFullscreen"),
@observes("composeState")
composeStateChanged() {
@@ -176,7 +211,7 @@ const Composer = RestModel.extend({
}
},
- @computed
+ @discourseComputed
composerTime: {
get() {
let total = this.composerTotalOpened || 0;
@@ -190,42 +225,45 @@ const Composer = RestModel.extend({
}
},
- @computed("archetypeId")
+ @discourseComputed("archetypeId")
archetype(archetypeId) {
return this.archetypes.findBy("id", archetypeId);
},
@observes("archetype")
archetypeChanged() {
- return this.set("metaData", Ember.Object.create());
+ return this.set("metaData", EmberObject.create());
},
- // view detected user is typing
- typing: throttle(
- function() {
- const typingTime = this.typingTime || 0;
- this.set("typingTime", typingTime + 100);
- },
- 100,
- false
- ),
+ // called whenever the user types to update the typing time
+ typing() {
+ throttle(
+ this,
+ function() {
+ const typingTime = this.typingTime || 0;
+ this.set("typingTime", typingTime + 100);
+ },
+ 100,
+ false
+ );
+ },
- editingFirstPost: Ember.computed.and("editingPost", "post.firstPost"),
+ editingFirstPost: and("editingPost", "post.firstPost"),
- canEditTitle: Ember.computed.or(
+ canEditTitle: or(
"creatingTopic",
"creatingPrivateMessage",
"editingFirstPost",
"creatingSharedDraft"
),
- canCategorize: Ember.computed.and(
+ canCategorize: and(
"canEditTitle",
"notCreatingPrivateMessage",
"notPrivateMessage"
),
- @computed("canEditTitle", "creatingPrivateMessage", "categoryId")
+ @discourseComputed("canEditTitle", "creatingPrivateMessage", "categoryId")
canEditTopicFeaturedLink(canEditTitle, creatingPrivateMessage, categoryId) {
if (
!this.siteSettings.topic_featured_link_enabled ||
@@ -251,14 +289,14 @@ const Composer = RestModel.extend({
);
},
- @computed("canEditTopicFeaturedLink")
+ @discourseComputed("canEditTopicFeaturedLink")
titlePlaceholder(canEditTopicFeaturedLink) {
return canEditTopicFeaturedLink
? "composer.title_or_link_placeholder"
: "composer.title_placeholder";
},
- @computed("action", "post", "topic", "topic.title")
+ @discourseComputed("action", "post", "topic", "topic.title")
replyOptions(action, post, topic, topicTitle) {
const options = {
userLink: null,
@@ -308,17 +346,36 @@ const Composer = RestModel.extend({
return options;
},
- @computed
- isStaffUser() {
- const currentUser = Discourse.User.current();
- return currentUser && currentUser.staff;
+ @discourseComputed("targetRecipients")
+ targetUsernames(targetRecipients) {
+ deprecated(
+ "`targetUsernames` is deprecated, use `targetRecipients` instead."
+ );
+ return targetRecipients;
},
- @computed(
+ @discourseComputed("targetRecipients")
+ targetRecipientsArray(targetRecipients) {
+ const recipients = targetRecipients ? targetRecipients.split(",") : [];
+ const groups = new Set(this.site.groups.map(g => g.name));
+
+ return recipients.map(item => {
+ if (groups.has(item)) {
+ return { type: "group", name: item };
+ } else if (emailValid(item)) {
+ return { type: "email", name: item };
+ } else {
+ return { type: "user", name: item };
+ }
+ });
+ },
+
+ @discourseComputed(
"loading",
"canEditTitle",
"titleLength",
- "targetUsernames",
+ "targetRecipients",
+ "targetRecipientsArray",
"replyLength",
"categoryId",
"missingReplyCharacters",
@@ -331,7 +388,8 @@ const Composer = RestModel.extend({
loading,
canEditTitle,
titleLength,
- targetUsernames,
+ targetRecipients,
+ targetRecipientsArray,
replyLength,
categoryId,
missingReplyCharacters,
@@ -363,18 +421,27 @@ const Composer = RestModel.extend({
}
}
+ if (topicFirstPost) {
+ // user should modify topic template
+ const category = this.category;
+ if (category && category.topic_template) {
+ if (this.reply.trim() === category.topic_template.trim()) {
+ bootbox.alert(I18n.t("composer.error.topic_template_not_modified"));
+ return true;
+ }
+ }
+ }
+
if (this.privateMessage) {
// need at least one user when sending a PM
- return (
- targetUsernames && (targetUsernames.trim() + ",").indexOf(",") === 0
- );
+ return targetRecipients && targetRecipientsArray.length === 0;
} else {
// has a category? (when needed)
return this.requiredCategoryMissing;
}
},
- @computed("canCategorize", "categoryId")
+ @discourseComputed("canCategorize", "categoryId")
requiredCategoryMissing(canCategorize, categoryId) {
return (
canCategorize &&
@@ -383,28 +450,28 @@ const Composer = RestModel.extend({
);
},
- @computed("minimumTitleLength", "titleLength", "post.static_doc")
+ @discourseComputed("minimumTitleLength", "titleLength", "post.static_doc")
titleLengthValid(minTitleLength, titleLength, staticDoc) {
if (this.user.admin && staticDoc && titleLength > 0) return true;
if (titleLength < minTitleLength) return false;
return titleLength <= this.siteSettings.max_topic_title_length;
},
- @computed("metaData")
+ @discourseComputed("metaData")
hasMetaData(metaData) {
- return metaData ? Ember.isEmpty(Ember.keys(metaData)) : false;
+ return metaData ? isEmpty(Ember.keys(metaData)) : false;
},
replyDirty: propertyNotEqual("reply", "originalText"),
titleDirty: propertyNotEqual("title", "originalTitle"),
- @computed("minimumTitleLength", "titleLength")
+ @discourseComputed("minimumTitleLength", "titleLength")
missingTitleCharacters(minimumTitleLength, titleLength) {
return minimumTitleLength - titleLength;
},
- @computed("privateMessage")
+ @discourseComputed("privateMessage")
minimumTitleLength(privateMessage) {
if (privateMessage) {
return this.siteSettings.min_personal_message_title_length;
@@ -413,7 +480,11 @@ const Composer = RestModel.extend({
}
},
- @computed("minimumPostLength", "replyLength", "canEditTopicFeaturedLink")
+ @discourseComputed(
+ "minimumPostLength",
+ "replyLength",
+ "canEditTopicFeaturedLink"
+ )
missingReplyCharacters(
minimumPostLength,
replyLength,
@@ -428,7 +499,11 @@ const Composer = RestModel.extend({
return minimumPostLength - replyLength;
},
- @computed("privateMessage", "topicFirstPost", "topic.pm_with_non_human_user")
+ @discourseComputed(
+ "privateMessage",
+ "topicFirstPost",
+ "topic.pm_with_non_human_user"
+ )
minimumPostLength(privateMessage, topicFirstPost, pmWithNonHumanUser) {
if (pmWithNonHumanUser) {
return 1;
@@ -442,19 +517,72 @@ const Composer = RestModel.extend({
}
},
- @computed("title")
+ @discourseComputed("title")
titleLength(title) {
title = title || "";
return title.replace(/\s+/gim, " ").trim().length;
},
- @computed("reply")
+ @discourseComputed("reply")
replyLength(reply) {
reply = reply || "";
- while (Quote.REGEXP.test(reply)) {
- reply = reply.replace(Quote.REGEXP, "");
+
+ if (reply.length > FAST_REPLY_LENGTH_THRESHOLD) {
+ return reply.length;
}
- return reply.replace(/\s+/gim, " ").trim().length;
+
+ while (Quote.REGEXP.test(reply)) {
+ // make it global so we can strip as many quotes at once
+ // keep in mind nested quotes mean we still need a loop here
+ const regex = new RegExp(Quote.REGEXP.source, "img");
+ reply = reply.replace(regex, "");
+ }
+
+ // This is in place so we do not generate any intermediate
+ // strings while calculating the length, this is issued
+ // every keypress in the composer so it needs to be very fast
+ let len = 0,
+ skipSpace = true;
+
+ for (let i = 0; i < reply.length; i++) {
+ const code = reply.charCodeAt(i);
+
+ let isSpace = false;
+ if (code >= 0x2000 && code <= 0x200a) {
+ isSpace = true;
+ } else {
+ switch (code) {
+ case 0x09: // \t
+ case 0x0a: // \n
+ case 0x0b: // \v
+ case 0x0c: // \f
+ case 0x0d: // \r
+ case 0x20:
+ case 0xa0:
+ case 0x1680:
+ case 0x202f:
+ case 0x205f:
+ case 0x3000:
+ isSpace = true;
+ }
+ }
+
+ if (isSpace) {
+ if (!skipSpace) {
+ len++;
+ skipSpace = true;
+ }
+ } else {
+ len++;
+ skipSpace = false;
+ }
+ }
+
+ if (len > 0 && skipSpace) {
+ len--;
+ }
+
+ return len;
},
@on("init")
@@ -529,7 +657,7 @@ const Composer = RestModel.extend({
}
}
- if (!Ember.isEmpty(reply)) {
+ if (!isEmpty(reply)) {
return;
}
@@ -552,13 +680,10 @@ const Composer = RestModel.extend({
if (!opts) opts = {};
this.set("loading", false);
- const replyBlank = Ember.isEmpty(this.reply);
+ const replyBlank = isEmpty(this.reply);
const composer = this;
- if (
- !replyBlank &&
- ((opts.reply || isEdit(opts.action)) && this.replyDirty)
- ) {
+ if (!replyBlank && (opts.reply || isEdit(opts.action)) && this.replyDirty) {
return;
}
@@ -572,13 +697,17 @@ const Composer = RestModel.extend({
throw new Error("draft sequence is required");
}
+ if (opts.usernames) {
+ deprecated("`usernames` is deprecated, use `recipients` instead.");
+ }
+
this.setProperties({
draftKey: opts.draftKey,
draftSequence: opts.draftSequence,
composeState: opts.composerState || OPEN,
action: opts.action,
topic: opts.topic,
- targetUsernames: opts.usernames,
+ targetRecipients: opts.usernames || opts.recipients,
composerTotalOpened: opts.composerTime,
typingTime: opts.typingTime,
whisper: opts.whisper,
@@ -601,7 +730,7 @@ const Composer = RestModel.extend({
this.setProperties({
archetypeId: opts.archetypeId || this.site.default_archetype,
- metaData: opts.metaData ? Ember.Object.create(opts.metaData) : null,
+ metaData: opts.metaData ? EmberObject.create(opts.metaData) : null,
reply: opts.reply || this.reply || ""
});
@@ -663,18 +792,30 @@ const Composer = RestModel.extend({
composer.appEvents.trigger("composer:reply-reloaded", composer);
}
+ // Ensure additional draft fields are set
+ Object.keys(_add_draft_fields).forEach(f => {
+ this.set(_add_draft_fields[f], opts[f]);
+ });
+
return false;
},
- save(opts) {
- if (!this.cantSubmitPost) {
- // change category may result in some effect for topic featured link
- if (!this.canEditTopicFeaturedLink) {
- this.set("featuredLink", null);
- }
+ // Overwrite to implement custom logic
+ beforeSave() {
+ return Promise.resolve();
+ },
- return this.editingPost ? this.editPost(opts) : this.createPost(opts);
- }
+ save(opts) {
+ return this.beforeSave().then(() => {
+ if (!this.cantSubmitPost) {
+ // change category may result in some effect for topic featured link
+ if (!this.canEditTopicFeaturedLink) {
+ this.set("featuredLink", null);
+ }
+
+ return this.editingPost ? this.editPost(opts) : this.createPost(opts);
+ }
+ });
},
clearState() {
@@ -698,29 +839,34 @@ const Composer = RestModel.extend({
editPost(opts) {
const post = this.post;
const oldCooked = post.cooked;
- let promise = Ember.RSVP.resolve();
+ let promise = Promise.resolve();
// Update the topic if we're editing the first post
- if (
- this.title &&
- post.post_number === 1 &&
- this.get("topic.details.can_edit")
- ) {
- const topicProps = this.getProperties(
- Object.keys(_edit_topic_serializer)
- );
-
+ if (this.title && post.post_number === 1) {
const topic = this.topic;
- // If we're editing a shared draft, keep the original category
- if (this.action === EDIT_SHARED_DRAFT) {
- const destinationCategoryId = topicProps.categoryId;
- promise = promise.then(() =>
- topic.updateDestinationCategory(destinationCategoryId)
+ if (topic.details.can_edit) {
+ const topicProps = this.getProperties(
+ Object.keys(_edit_topic_serializer)
);
- topicProps.categoryId = topic.get("category.id");
+ // frontend should have featuredLink but backend needs featured_link
+ if (topicProps.featuredLink) {
+ topicProps.featured_link = topicProps.featuredLink;
+ delete topicProps.featuredLink;
+ }
+
+ // If we're editing a shared draft, keep the original category
+ if (this.action === EDIT_SHARED_DRAFT) {
+ const destinationCategoryId = topicProps.categoryId;
+ promise = promise.then(() =>
+ topic.updateDestinationCategory(destinationCategoryId)
+ );
+ topicProps.categoryId = topic.get("category.id");
+ }
+ promise = promise.then(() => Topic.update(topic, topicProps));
+ } else if (topic.details.can_edit_tags) {
+ promise = promise.then(() => topic.updateTags(this.tags));
}
- promise = promise.then(() => Topic.update(topic, topicProps));
}
const props = {
@@ -759,13 +905,17 @@ const Composer = RestModel.extend({
Object.keys(serializer).forEach(f => {
const val = this.get(serializer[f]);
if (typeof val !== "undefined") {
- Ember.set(dest, f, val);
+ set(dest, f, val);
}
});
return dest;
},
createPost(opts) {
+ if (CREATE_TOPIC === this.action || PRIVATE_MESSAGE === this.action) {
+ this.set("topic", null);
+ }
+
const post = this.post;
const topic = this.topic;
const user = this.user;
@@ -877,6 +1027,11 @@ const Composer = RestModel.extend({
composer.clearState();
composer.set("createdPost", createdPost);
+ if (composer.replyingToTopic) {
+ this.appEvents.trigger("post:created", createdPost);
+ } else {
+ this.appEvents.trigger("topic:created", createdPost, composer);
+ }
if (addedToStream) {
composer.set("composeState", CLOSED);
@@ -895,7 +1050,7 @@ const Composer = RestModel.extend({
post.set("reply_count", post.reply_count - 1);
}
}
- Ember.run.next(() => composer.set("composeState", OPEN));
+ next(() => composer.set("composeState", OPEN));
})
);
},
@@ -946,35 +1101,26 @@ const Composer = RestModel.extend({
});
if (this._clearingStatus) {
- Ember.run.cancel(this._clearingStatus);
+ cancel(this._clearingStatus);
this._clearingStatus = null;
}
- let data = this.getProperties(
- "reply",
- "action",
- "title",
- "categoryId",
- "archetypeId",
- "whisper",
- "metaData",
- "composerTime",
- "typingTime",
- "tags",
- "noBump"
- );
+ let data = this.serialize(_draft_serializer);
- data = Object.assign(data, {
- usernames: this.targetUsernames,
- postId: this.get("post.id")
- });
-
- if (data.postId && !Ember.isEmpty(this.originalText)) {
+ if (data.postId && !isEmpty(this.originalText)) {
data.originalText = this.originalText;
}
- return Draft.save(this.draftKey, this.draftSequence, data)
+ return Draft.save(
+ this.draftKey,
+ this.draftSequence,
+ data,
+ this.messageBus.clientId
+ )
.then(result => {
+ if (result.draft_sequence) {
+ this.draftSequence = result.draft_sequence;
+ }
if (result.conflict_user) {
this.setProperties({
draftSaving: false,
@@ -989,10 +1135,27 @@ const Composer = RestModel.extend({
});
}
})
- .catch(() => {
+ .catch(e => {
+ let draftStatus;
+ const xhr = e && e.jqXHR;
+
+ if (
+ xhr &&
+ xhr.status === 409 &&
+ xhr.responseJSON &&
+ xhr.responseJSON.errors &&
+ xhr.responseJSON.errors.length
+ ) {
+ const json = e.jqXHR.responseJSON;
+ draftStatus = json.errors[0];
+ if (json.extras && json.extras.description) {
+ bootbox.alert(json.extras.description);
+ }
+ }
+
this.setProperties({
draftSaving: false,
- draftStatus: I18n.t("composer.drafts_offline"),
+ draftStatus: draftStatus || I18n.t("composer.drafts_offline"),
draftConflictUser: null
});
});
@@ -1003,7 +1166,7 @@ const Composer = RestModel.extend({
const draftStatus = this.draftStatus;
if (draftStatus && !this._clearingStatus) {
- this._clearingStatus = Ember.run.later(
+ this._clearingStatus = later(
this,
() => {
this.setProperties({ draftStatus: null, draftConflictUser: null });
@@ -1020,8 +1183,8 @@ Composer.reopenClass({
// TODO: Replace with injection
create(args) {
args = args || {};
- args.user = args.user || Discourse.User.current();
- args.site = args.site || Discourse.Site.current();
+ args.user = args.user || User.current();
+ args.site = args.site || Site.current();
args.siteSettings = args.siteSettings || Discourse.SiteSettings;
return this._super(args);
},
@@ -1044,6 +1207,18 @@ Composer.reopenClass({
return Object.keys(_create_serializer);
},
+ serializeToDraft(fieldName, property) {
+ if (!property) {
+ property = fieldName;
+ }
+ _draft_serializer[fieldName] = property;
+ _add_draft_fields[fieldName] = property;
+ },
+
+ serializedFieldsForDraft() {
+ return Object.keys(_draft_serializer);
+ },
+
// The status the compose view can have
CLOSED,
SAVING,
@@ -1061,8 +1236,7 @@ Composer.reopenClass({
// Draft key
NEW_PRIVATE_MESSAGE_KEY,
- REPLY_AS_NEW_TOPIC_KEY,
- REPLY_AS_NEW_PRIVATE_MESSAGE_KEY
+ NEW_TOPIC_KEY
});
export default Composer;
diff --git a/app/assets/javascripts/discourse/models/draft.js.es6 b/app/assets/javascripts/discourse/models/draft.js.es6
index abba8511f5..178c38f94a 100644
--- a/app/assets/javascripts/discourse/models/draft.js.es6
+++ b/app/assets/javascripts/discourse/models/draft.js.es6
@@ -1,5 +1,7 @@
import { ajax } from "discourse/lib/ajax";
-const Draft = Discourse.Model.extend();
+import EmberObject from "@ember/object";
+
+const Draft = EmberObject.extend();
Draft.reopenClass({
clear(key, sequence) {
@@ -21,11 +23,11 @@ Draft.reopenClass({
return current;
},
- save(key, sequence, data) {
+ save(key, sequence, data, clientId) {
data = typeof data === "string" ? data : JSON.stringify(data);
return ajax("/draft.json", {
type: "POST",
- data: { draft_key: key, sequence, data }
+ data: { draft_key: key, sequence, data, owner: clientId }
});
}
});
diff --git a/app/assets/javascripts/discourse/models/group-history.js.es6 b/app/assets/javascripts/discourse/models/group-history.js.es6
index 946741ae5d..8c22e9af5f 100644
--- a/app/assets/javascripts/discourse/models/group-history.js.es6
+++ b/app/assets/javascripts/discourse/models/group-history.js.es6
@@ -1,8 +1,8 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
import RestModel from "discourse/models/rest";
export default RestModel.extend({
- @computed("action")
+ @discourseComputed("action")
actionTitle(action) {
return I18n.t(`group_histories.actions.${action}`);
}
diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6
index c25f0ce8d5..c799f78314 100644
--- a/app/assets/javascripts/discourse/models/group.js.es6
+++ b/app/assets/javascripts/discourse/models/group.js.es6
@@ -1,82 +1,109 @@
+import EmberObject from "@ember/object";
+import { equal } from "@ember/object/computed";
+import { isEmpty } from "@ember/utils";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import Category from "discourse/models/category";
import GroupHistory from "discourse/models/group-history";
import RestModel from "discourse/models/rest";
-import Category from "discourse/models/category";
-import User from "discourse/models/user";
import Topic from "discourse/models/topic";
-import { popupAjaxError } from "discourse/lib/ajax-error";
+import User from "discourse/models/user";
+import { Promise } from "rsvp";
const Group = RestModel.extend({
- limit: 50,
- offset: 0,
user_count: 0,
+ limit: null,
+ offset: null,
+
+ request_count: 0,
+ requestersLimit: null,
+ requestersOffset: null,
init() {
this._super(...arguments);
-
- this.set("owners", []);
+ this.setProperties({ members: [], requesters: [] });
},
- hasOwners: Ember.computed.notEmpty("owners"),
-
- @computed("automatic_membership_email_domains")
+ @discourseComputed("automatic_membership_email_domains")
emailDomains(value) {
- return Ember.isEmpty(value) ? "" : value;
+ return isEmpty(value) ? "" : value;
},
- @computed("automatic")
+ @discourseComputed("automatic")
type(automatic) {
return automatic ? "automatic" : "custom";
},
- @computed("user_count")
- userCountDisplay(userCount) {
- // don't display zero its ugly
- if (userCount > 0) {
- return userCount;
+ findMembers(params, refresh) {
+ if (isEmpty(this.name) || !this.can_see_members) {
+ return Promise.reject();
}
+
+ if (refresh) {
+ this.setProperties({ limit: null, offset: null });
+ }
+
+ params = Object.assign(
+ { offset: (this.offset || 0) + (this.limit || 0) },
+ params
+ );
+
+ return Group.loadMembers(this.name, params).then(result => {
+ const ownerIds = new Set();
+ result.owners.forEach(owner => ownerIds.add(owner.id));
+
+ const members = refresh ? [] : this.members;
+ members.pushObjects(
+ result.members.map(member => {
+ member.owner = ownerIds.has(member.id);
+ return User.create(member);
+ })
+ );
+
+ this.setProperties({
+ members,
+ user_count: result.meta.total,
+ limit: result.meta.limit,
+ offset: result.meta.offset
+ });
+ });
},
- findMembers(params) {
- if (Ember.isEmpty(this.name)) {
- return;
+ findRequesters(params, refresh) {
+ if (isEmpty(this.name) || !this.can_see_members) {
+ return Promise.reject();
}
- const offset = Math.min(this.user_count, Math.max(this.offset, 0));
+ if (refresh) {
+ this.setProperties({ requestersOffset: null, requestersLimit: null });
+ }
- return Group.loadMembers(this.name, offset, this.limit, params).then(
- result => {
- const ownerIds = {};
- result.owners.forEach(owner => (ownerIds[owner.id] = true));
-
- this.setProperties({
- user_count: result.meta.total,
- limit: result.meta.limit,
- offset: result.meta.offset,
- members: result.members.map(member => {
- if (ownerIds[member.id]) {
- member.owner = true;
- }
- return User.create(member);
- }),
- owners: result.owners.map(owner => User.create(owner))
- });
- }
+ params = Object.assign(
+ {
+ offset: (this.requestersOffset || 0) + (this.requestersLimit || 0),
+ requesters: true
+ },
+ params
);
+
+ return Group.loadMembers(this.name, params).then(result => {
+ const requesters = refresh ? [] : this.requesters;
+ requesters.pushObjects(result.members.map(m => User.create(m)));
+
+ this.setProperties({
+ requesters,
+ request_count: result.meta.total,
+ requestersLimit: result.meta.limit,
+ requestersOffset: result.meta.offset
+ });
+ });
},
removeOwner(member) {
return ajax(`/admin/groups/${this.id}/owners.json`, {
type: "DELETE",
data: { user_id: member.id }
- }).then(() => {
- // reload member list
- this.findMembers();
- });
+ }).then(() => this.findMembers());
},
removeMember(member, params) {
@@ -116,30 +143,30 @@ const Group = RestModel.extend({
return this.findMembers({ filter: response.usernames.join(",") });
},
- @computed("display_name", "name")
+ @discourseComputed("display_name", "name")
displayName(groupDisplayName, name) {
return groupDisplayName || name;
},
- @computed("flair_bg_color")
+ @discourseComputed("flair_bg_color")
flairBackgroundHexColor(flairBgColor) {
return flairBgColor
? flairBgColor.replace(new RegExp("[^0-9a-fA-F]", "g"), "")
: null;
},
- @computed("flair_color")
+ @discourseComputed("flair_color")
flairHexColor(flairColor) {
return flairColor
? flairColor.replace(new RegExp("[^0-9a-fA-F]", "g"), "")
: null;
},
- canEveryoneMention: Ember.computed.equal("mentionable_level", 99),
+ canEveryoneMention: equal("mentionable_level", 99),
- @computed("visibility_level")
+ @discourseComputed("visibility_level")
isPrivate(visibilityLevel) {
- return visibilityLevel !== 0;
+ return visibilityLevel > 1;
},
@observes("isPrivate", "canEveryoneMention")
@@ -149,19 +176,13 @@ const Group = RestModel.extend({
}
},
- @observes("visibility_level")
- _updatePublic() {
- if (this.isPrivate) {
- this.setProperties({ public: false, allow_membership_requests: false });
- }
- },
-
asJSON() {
const attrs = {
name: this.name,
mentionable_level: this.mentionable_level,
messageable_level: this.messageable_level,
visibility_level: this.visibility_level,
+ members_visibility_level: this.members_visibility_level,
automatic_membership_email_domains: this.emailDomains,
automatic_membership_retroactive: !!this.automatic_membership_retroactive,
title: this.title,
@@ -177,7 +198,8 @@ const Group = RestModel.extend({
allow_membership_requests: this.allow_membership_requests,
full_name: this.full_name,
default_notification_level: this.default_notification_level,
- membership_request_template: this.membership_request_template
+ membership_request_template: this.membership_request_template,
+ publish_read_state: this.publish_read_state
};
if (!this.id) {
@@ -221,7 +243,7 @@ const Group = RestModel.extend({
return ajax(`/groups/${this.name}/logs.json`, {
data: { offset, filters }
}).then(results => {
- return Ember.Object.create({
+ return EmberObject.create({
logs: results["logs"].map(log => GroupHistory.create(log)),
all_loaded: results["all_loaded"]
});
@@ -238,7 +260,7 @@ const Group = RestModel.extend({
}
if (opts.categoryId) {
- data.category_id = parseInt(opts.categoryId);
+ data.category_id = parseInt(opts.categoryId, 10);
}
return ajax(`/groups/${this.name}/${type}.json`, { data }).then(posts => {
@@ -246,7 +268,7 @@ const Group = RestModel.extend({
p.user = User.create(p.user);
p.topic = Topic.create(p.topic);
p.category = Category.findById(p.category_id);
- return Ember.Object.create(p);
+ return EmberObject.create(p);
});
});
},
@@ -274,16 +296,8 @@ Group.reopenClass({
);
},
- loadMembers(name, offset, limit, params) {
- return ajax(`/groups/${name}/members.json`, {
- data: Object.assign(
- {
- limit: limit || 50,
- offset: offset || 0
- },
- params || {}
- )
- });
+ loadMembers(name, opts) {
+ return ajax(`/groups/${name}/members.json`, { data: opts });
},
mentionable(name) {
@@ -295,9 +309,7 @@ Group.reopenClass({
},
checkName(name) {
- return ajax("/groups/check-name", {
- data: { group_name: name }
- }).catch(popupAjaxError);
+ return ajax("/groups/check-name", { data: { group_name: name } });
}
});
diff --git a/app/assets/javascripts/discourse/models/input-validation.js.es6 b/app/assets/javascripts/discourse/models/input-validation.js.es6
deleted file mode 100644
index fcbd0a1536..0000000000
--- a/app/assets/javascripts/discourse/models/input-validation.js.es6
+++ /dev/null
@@ -1,4 +0,0 @@
-import Model from "discourse/models/model";
-
-// A trivial model we use to handle input validation
-export default Model.extend();
diff --git a/app/assets/javascripts/discourse/models/invite.js.es6 b/app/assets/javascripts/discourse/models/invite.js.es6
index 842a04a324..7eb142c0ab 100644
--- a/app/assets/javascripts/discourse/models/invite.js.es6
+++ b/app/assets/javascripts/discourse/models/invite.js.es6
@@ -1,8 +1,12 @@
+import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { userPath } from "discourse/lib/url";
+import { Promise } from "rsvp";
+import { isNone } from "@ember/utils";
+import User from "discourse/models/user";
-const Invite = Discourse.Model.extend({
+const Invite = EmberObject.extend({
rescind() {
ajax("/invites", {
type: "DELETE",
@@ -25,33 +29,33 @@ Invite.reopenClass({
create() {
const result = this._super.apply(this, arguments);
if (result.user) {
- result.user = Discourse.User.create(result.user);
+ result.user = User.create(result.user);
}
return result;
},
findInvitedBy(user, filter, search, offset) {
- if (!user) Ember.RSVP.resolve();
+ if (!user) Promise.resolve();
const data = {};
- if (!Ember.isNone(filter)) data.filter = filter;
- if (!Ember.isNone(search)) data.search = search;
+ if (!isNone(filter)) data.filter = filter;
+ if (!isNone(search)) data.search = search;
data.offset = offset || 0;
return ajax(userPath(`${user.username_lower}/invited.json`), {
data
}).then(result => {
result.invites = result.invites.map(i => Invite.create(i));
- return Ember.Object.create(result);
+ return EmberObject.create(result);
});
},
findInvitedCount(user) {
- if (!user) Ember.RSVP.resolve();
+ if (!user) Promise.resolve();
- return ajax(userPath(`${user.username_lower}/invited_count.json`)).then(
- result => Ember.Object.create(result.counts)
- );
+ return ajax(
+ userPath(`${user.username_lower}/invited_count.json`)
+ ).then(result => EmberObject.create(result.counts));
},
reinviteAll() {
diff --git a/app/assets/javascripts/discourse/models/live-post-counts.es6 b/app/assets/javascripts/discourse/models/live-post-counts.es6
index b28c525f8d..ac5f14d76f 100644
--- a/app/assets/javascripts/discourse/models/live-post-counts.es6
+++ b/app/assets/javascripts/discourse/models/live-post-counts.es6
@@ -1,5 +1,7 @@
import { ajax } from "discourse/lib/ajax";
-const LivePostCounts = Discourse.Model.extend({});
+import EmberObject from "@ember/object";
+
+const LivePostCounts = EmberObject.extend({});
LivePostCounts.reopenClass({
find() {
diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6
index 63fb07eb22..2a3ab489c4 100644
--- a/app/assets/javascripts/discourse/models/login-method.js.es6
+++ b/app/assets/javascripts/discourse/models/login-method.js.es6
@@ -1,71 +1,74 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import EmberObject from "@ember/object";
+import { updateCsrfToken } from "discourse/lib/ajax";
+import { Promise } from "rsvp";
+import Session from "discourse/models/session";
+import Site from "discourse/models/site";
-const LoginMethod = Ember.Object.extend({
- @computed
+const LoginMethod = EmberObject.extend({
+ @discourseComputed
title() {
return this.title_override || I18n.t(`login.${this.name}.title`);
},
- @computed
+ @discourseComputed
prettyName() {
return this.pretty_name_override || I18n.t(`login.${this.name}.name`);
},
- @computed
+ @discourseComputed
message() {
return this.message_override || I18n.t(`login.${this.name}.message`);
},
- doLogin({ reconnect = false, fullScreenLogin = true } = {}) {
- const name = this.name;
- const customLogin = this.customLogin;
-
- if (customLogin) {
- customLogin();
- } else {
- let authUrl = this.custom_url || Discourse.getURL(`/auth/${name}`);
-
- if (reconnect) {
- authUrl += "?reconnect=true";
- }
-
- if (reconnect || fullScreenLogin) {
- document.cookie = "fsl=true";
- window.location = authUrl;
- } else {
- this.set("authenticate", name);
- const left = this.lastX - 400;
- const top = this.lastY - 200;
-
- const height = this.frame_height || 400;
- const width = this.frame_width || 800;
-
- if (name === "facebook") {
- authUrl += authUrl.includes("?") ? "&" : "?";
- authUrl += "display=popup";
- }
-
- const windowState = window.open(
- authUrl,
- "_blank",
- "menubar=no,status=no,height=" +
- height +
- ",width=" +
- width +
- ",left=" +
- left +
- ",top=" +
- top
- );
-
- const timer = setInterval(() => {
- if (!windowState || windowState.closed) {
- clearInterval(timer);
- this.set("authenticate", null);
- }
- }, 1000);
- }
+ doLogin({ reconnect = false, params = {} } = {}) {
+ if (this.customLogin) {
+ this.customLogin();
+ return Promise.resolve();
}
+
+ if (this.custom_url) {
+ window.location = this.custom_url;
+ return Promise.resolve();
+ }
+
+ let authUrl = Discourse.getURL(`/auth/${this.name}`);
+
+ if (reconnect) {
+ params["reconnect"] = true;
+ }
+
+ const paramKeys = Object.keys(params);
+ if (paramKeys.length > 0) {
+ authUrl += "?";
+ authUrl += paramKeys
+ .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
+ .join("&");
+ }
+
+ return LoginMethod.buildPostForm(authUrl).then(form => form.submit());
+ }
+});
+
+LoginMethod.reopenClass({
+ buildPostForm(url) {
+ // Login always happens in an anonymous context, with no CSRF token
+ // So we need to fetch it before sending a POST request
+ return updateCsrfToken().then(() => {
+ const form = document.createElement("form");
+ form.setAttribute("style", "display:none;");
+ form.setAttribute("method", "post");
+ form.setAttribute("action", url);
+
+ const input = document.createElement("input");
+ input.setAttribute("name", "authenticity_token");
+ input.setAttribute("value", Session.currentProp("csrfToken"));
+ form.appendChild(input);
+
+ document.body.appendChild(form);
+
+ return form;
+ });
}
});
@@ -76,7 +79,7 @@ export function findAll() {
methods = [];
- Discourse.Site.currentProp("auth_providers").forEach(provider =>
+ Site.currentProp("auth_providers").forEach(provider =>
methods.pushObject(LoginMethod.create(provider))
);
diff --git a/app/assets/javascripts/discourse/models/model.js.es6 b/app/assets/javascripts/discourse/models/model.js.es6
deleted file mode 100644
index c6a0b3ec20..0000000000
--- a/app/assets/javascripts/discourse/models/model.js.es6
+++ /dev/null
@@ -1,17 +0,0 @@
-const Model = Ember.Object.extend();
-
-Model.reopenClass({
- extractByKey(collection, klass) {
- const retval = {};
- if (Ember.isEmpty(collection)) {
- return retval;
- }
-
- collection.forEach(function(item) {
- retval[item.id] = klass.create(item);
- });
- return retval;
- }
-});
-
-export default Model;
diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6
index 64a705de89..83330778e0 100644
--- a/app/assets/javascripts/discourse/models/nav-item.js.es6
+++ b/app/assets/javascripts/discourse/models/nav-item.js.es6
@@ -1,62 +1,41 @@
-import { toTitleCase } from "discourse/lib/formatter";
+import discourseComputed from "discourse-common/utils/decorators";
import { emojiUnescape } from "discourse/lib/text";
-import computed from "ember-addons/ember-computed-decorators";
+import Category from "discourse/models/category";
+import EmberObject from "@ember/object";
+import { reads } from "@ember/object/computed";
+import deprecated from "discourse-common/lib/deprecated";
+import Site from "discourse/models/site";
+import User from "discourse/models/user";
-const NavItem = Discourse.Model.extend({
- @computed("categoryName", "name")
- title(categoryName, name) {
+const NavItem = EmberObject.extend({
+ @discourseComputed("name")
+ title(name) {
const extra = {};
- if (categoryName) {
- name = "category";
- extra.categoryName = categoryName;
- }
-
return I18n.t("filters." + name.replace("/", ".") + ".help", extra);
},
- @computed("categoryName", "name", "count")
- displayName(categoryName, name, count) {
+ @discourseComputed("name", "count")
+ displayName(name, count) {
count = count || 0;
- if (name === "latest" && !Discourse.Site.currentProp("mobileView")) {
+ if (
+ name === "latest" &&
+ (!Site.currentProp("mobileView") || this.tagId !== undefined)
+ ) {
count = 0;
}
let extra = { count: count };
const titleKey = count === 0 ? ".title" : ".title_with_count";
- if (categoryName) {
- name = "category";
- extra.categoryName = toTitleCase(categoryName);
- }
-
return emojiUnescape(
I18n.t(`filters.${name.replace("/", ".") + titleKey}`, extra)
);
},
- @computed("name")
- categoryName(name) {
- const split = name.split("/");
- return split[0] === "category" ? split[1] : null;
- },
-
- @computed("name")
- categorySlug(name) {
- const split = name.split("/");
- if (split[0] === "category" && split[1]) {
- const cat = Discourse.Site.current().categories.findBy(
- "nameLower",
- split[1].toLowerCase()
- );
- return cat ? Discourse.Category.slugFor(cat) : null;
- }
- return null;
- },
-
- @computed("filterMode")
- href(filterMode) {
+ @discourseComputed("filterType", "category", "noSubcategories", "tagId")
+ href(filterType, category, noSubcategories, tagId) {
let customHref = null;
NavItem.customNavItemHrefs.forEach(function(cb) {
@@ -70,28 +49,27 @@ const NavItem = Discourse.Model.extend({
return customHref;
}
- return Discourse.getURL("/") + filterMode;
+ const context = { category, noSubcategories, tagId };
+ return NavItem.pathFor(filterType, context);
},
- @computed("name", "category", "categorySlug", "noSubcategories")
- filterMode(name, category, categorySlug, noSubcategories) {
- if (name.split("/")[0] === "category") {
- return "c/" + categorySlug;
- } else {
- let mode = "";
- if (category) {
- mode += "c/";
- mode += Discourse.Category.slugFor(category);
- if (noSubcategories) {
- mode += "/none";
- }
- mode += "/l/";
+ filterType: reads("name"),
+
+ @discourseComputed("name", "category", "noSubcategories")
+ filterMode(name, category, noSubcategories) {
+ let mode = "";
+ if (category) {
+ mode += "c/";
+ mode += Category.slugFor(category);
+ if (noSubcategories) {
+ mode += "/none";
}
- return mode + name.replace(" ", "-");
+ mode += "/l/";
}
+ return mode + name.replace(" ", "-");
},
- @computed("name", "category", "topicTrackingState.messageCount")
+ @discourseComputed("name", "category", "topicTrackingState.messageCount")
count(name, category) {
const state = this.topicTrackingState;
if (state) {
@@ -101,48 +79,103 @@ const NavItem = Discourse.Model.extend({
});
const ExtraNavItem = NavItem.extend({
- @computed("href")
- href: href => href,
+ href: discourseComputed("href", {
+ get() {
+ if (this._href) {
+ return this._href;
+ }
+
+ return this.href;
+ },
+
+ set(key, value) {
+ return (this._href = value);
+ }
+ }),
+
+ count: 0,
+
customFilter: null
});
NavItem.reopenClass({
extraArgsCallbacks: [],
customNavItemHrefs: [],
- extraNavItems: [],
+ extraNavItemDescriptors: [],
- // create a nav item from the text, will return null if there is not valid nav item for this particular text
- fromText(text, opts) {
- var split = text.split(","),
- name = split[0],
- testName = name.split("/")[0],
- anonymous = !Discourse.User.current();
+ pathFor(filterType, context) {
+ let path = Discourse.getURL("");
+ let includesCategoryContext = false;
+ let includesTagContext = false;
+
+ if (filterType === "categories") {
+ path += "/categories";
+ return path;
+ }
+
+ if (context.tagId && Site.currentProp("filters").includes(filterType)) {
+ includesTagContext = true;
+ path += "/tags";
+ }
+
+ if (context.category) {
+ includesCategoryContext = true;
+ path += `/c/${Category.slugFor(context.category)}/${context.category.id}`;
+
+ if (context.noSubcategories) {
+ path += "/none";
+ }
+ }
+
+ if (includesTagContext) {
+ path += `/${context.tagId}`;
+ }
+
+ if (includesTagContext || includesCategoryContext) {
+ path += "/l";
+ }
+
+ path += `/${filterType}`;
+
+ // In the case of top, the nav item doesn't include a period because the
+ // period has its own selector just below
+
+ return path;
+ },
+
+ // Create a nav item given a filterType. It returns null if there is not
+ // valid nav item. The name is a historical artifact.
+ fromText(filterType, opts) {
+ const anonymous = !User.current();
opts = opts || {};
- if (
- anonymous &&
- !Discourse.Site.currentProp("anonymous_top_menu_items").includes(testName)
- )
- return null;
+ if (anonymous) {
+ const topMenuItems = Site.currentProp("anonymous_top_menu_items");
+ if (!topMenuItems || !topMenuItems.includes(filterType)) {
+ return null;
+ }
+ }
- if (!Discourse.Category.list() && testName === "categories") return null;
- if (!Discourse.Site.currentProp("top_menu_items").includes(testName))
- return null;
+ if (!Category.list() && filterType === "categories") return null;
+ if (!Site.currentProp("top_menu_items").includes(filterType)) return null;
- var args = { name: name, hasIcon: name === "unread" },
- extra = null,
- self = this;
+ var args = { name: filterType, hasIcon: filterType === "unread" };
if (opts.category) {
args.category = opts.category;
}
+ if (opts.tagId) {
+ args.tagId = opts.tagId;
+ }
+ if (opts.persistedQueryParams) {
+ args.persistedQueryParams = opts.persistedQueryParams;
+ }
if (opts.noSubcategories) {
args.noSubcategories = true;
}
- NavItem.extraArgsCallbacks.forEach(cb => {
- extra = cb.call(self, text, opts);
- _.merge(args, extra);
- });
+ NavItem.extraArgsCallbacks.forEach(cb =>
+ _.merge(args, cb.call(this, filterType, opts))
+ );
const store = Discourse.__container__.lookup("service:store");
return store.createRecord("nav-item", args);
@@ -157,25 +190,71 @@ NavItem.reopenClass({
let items = Discourse.SiteSettings.top_menu.split("|");
- if (
- args.filterMode &&
- !items.some(i => i.indexOf(args.filterMode) !== -1)
- ) {
- items.push(args.filterMode);
+ const filterType = (args.filterMode || "").split("/").pop();
+
+ if (!items.some(i => filterType === i)) {
+ items.push(filterType);
}
items = items
- .map(i => Discourse.NavItem.fromText(i, args))
+ .map(i => NavItem.fromText(i, args))
.filter(
i => i !== null && !(category && i.get("name").indexOf("categor") === 0)
);
- const extraItems = NavItem.extraNavItems.filter(item => {
- if (!item.customFilter) return true;
- return item.customFilter.call(this, category, args);
+ const context = {
+ category: args.category,
+ tagId: args.tagId,
+ noSubcategories: args.noSubcategories
+ };
+
+ const extraItems = NavItem.extraNavItemDescriptors
+ .map(descriptor => ExtraNavItem.create(_.merge({}, context, descriptor)))
+ .filter(item => {
+ if (!item.customFilter) return true;
+ return item.customFilter(category, args);
+ });
+
+ let forceActive = false;
+
+ extraItems.forEach(item => {
+ if (item.init) {
+ item.init(item, category, args);
+ }
+
+ const before = item.before;
+ if (before) {
+ let i = 0;
+ for (i = 0; i < items.length; i++) {
+ if (items[i].name === before) {
+ break;
+ }
+ }
+ items.splice(i, 0, item);
+ } else {
+ items.push(item);
+ }
+
+ if (item.customHref) {
+ item.set("href", item.customHref(category, args));
+ }
+
+ if (item.forceActive && item.forceActive(category, args)) {
+ item.active = true;
+ forceActive = true;
+ } else {
+ item.active = undefined;
+ }
});
- return items.concat(extraItems);
+ if (forceActive) {
+ items.forEach(i => {
+ if (i.active === undefined) {
+ i.active = false;
+ }
+ });
+ }
+ return items;
}
});
@@ -190,5 +269,15 @@ export function customNavItemHref(cb) {
}
export function addNavItem(item) {
- NavItem.extraNavItems.push(ExtraNavItem.create(item));
+ NavItem.extraNavItemDescriptors.push(item);
}
+
+Object.defineProperty(Discourse, "NavItem", {
+ get() {
+ deprecated("Import the NavItem class instead of using Discourse.NavItem", {
+ since: "2.4.0",
+ dropFrom: "2.5.0"
+ });
+ return NavItem;
+ }
+});
diff --git a/app/assets/javascripts/discourse/models/permission-type.js.es6 b/app/assets/javascripts/discourse/models/permission-type.js.es6
index 858be5722f..f6ad56c928 100644
--- a/app/assets/javascripts/discourse/models/permission-type.js.es6
+++ b/app/assets/javascripts/discourse/models/permission-type.js.es6
@@ -1,7 +1,8 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import EmberObject from "@ember/object";
-const PermissionType = Discourse.Model.extend({
- @computed("id")
+const PermissionType = EmberObject.extend({
+ @discourseComputed("id")
description(id) {
var key = "";
diff --git a/app/assets/javascripts/discourse/models/post-action-type.js.es6 b/app/assets/javascripts/discourse/models/post-action-type.js.es6
index fc83f6282a..ef8541a168 100644
--- a/app/assets/javascripts/discourse/models/post-action-type.js.es6
+++ b/app/assets/javascripts/discourse/models/post-action-type.js.es6
@@ -1,7 +1,8 @@
+import { not } from "@ember/object/computed";
import RestModel from "discourse/models/rest";
export const MAX_MESSAGE_LENGTH = 500;
export default RestModel.extend({
- notCustomFlag: Ember.computed.not("is_custom_flag")
+ notCustomFlag: not("is_custom_flag")
});
diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6
index 51e536965e..4accbbe268 100644
--- a/app/assets/javascripts/discourse/models/post-stream.js.es6
+++ b/app/assets/javascripts/discourse/models/post-stream.js.es6
@@ -1,9 +1,14 @@
+import { get } from "@ember/object";
+import { isEmpty } from "@ember/utils";
+import { or, not, and } from "@ember/object/computed";
import { ajax } from "discourse/lib/ajax";
import DiscourseURL from "discourse/lib/url";
import RestModel from "discourse/models/rest";
import PostsWithPlaceholders from "discourse/lib/posts-with-placeholders";
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
import { loadTopicView } from "discourse/models/topic";
+import { Promise } from "rsvp";
+import User from "discourse/models/user";
export default RestModel.extend({
_identityMap: null,
@@ -43,41 +48,32 @@ export default RestModel.extend({
});
},
- loading: Ember.computed.or(
- "loadingAbove",
- "loadingBelow",
- "loadingFilter",
- "stagingPost"
- ),
- notLoading: Ember.computed.not("loading"),
+ loading: or("loadingAbove", "loadingBelow", "loadingFilter", "stagingPost"),
+ notLoading: not("loading"),
- @computed("isMegaTopic", "stream.length", "topic.highest_post_number")
+ @discourseComputed(
+ "isMegaTopic",
+ "stream.length",
+ "topic.highest_post_number"
+ )
filteredPostsCount(isMegaTopic, streamLength, topicHighestPostNumber) {
return isMegaTopic ? topicHighestPostNumber : streamLength;
},
- @computed("posts.[]")
+ @discourseComputed("posts.[]")
hasPosts() {
return this.get("posts.length") > 0;
},
- @computed("hasPosts", "filteredPostsCount")
+ @discourseComputed("hasPosts", "filteredPostsCount")
hasLoadedData(hasPosts, filteredPostsCount) {
return hasPosts && filteredPostsCount > 0;
},
- canAppendMore: Ember.computed.and(
- "notLoading",
- "hasPosts",
- "lastPostNotLoaded"
- ),
- canPrependMore: Ember.computed.and(
- "notLoading",
- "hasPosts",
- "firstPostNotLoaded"
- ),
+ canAppendMore: and("notLoading", "hasPosts", "lastPostNotLoaded"),
+ canPrependMore: and("notLoading", "hasPosts", "firstPostNotLoaded"),
- @computed("hasLoadedData", "firstPostId", "posts.[]")
+ @discourseComputed("hasLoadedData", "firstPostId", "posts.[]")
firstPostPresent(hasLoadedData, firstPostId) {
if (!hasLoadedData) {
return false;
@@ -85,22 +81,22 @@ export default RestModel.extend({
return !!this.posts.findBy("id", firstPostId);
},
- firstPostNotLoaded: Ember.computed.not("firstPostPresent"),
+ firstPostNotLoaded: not("firstPostPresent"),
firstId: null,
lastId: null,
- @computed("isMegaTopic", "stream.firstObject", "firstId")
+ @discourseComputed("isMegaTopic", "stream.firstObject", "firstId")
firstPostId(isMegaTopic, streamFirstId, firstId) {
return isMegaTopic ? firstId : streamFirstId;
},
- @computed("isMegaTopic", "stream.lastObject", "lastId")
+ @discourseComputed("isMegaTopic", "stream.lastObject", "lastId")
lastPostId(isMegaTopic, streamLastId, lastId) {
return isMegaTopic ? lastId : streamLastId;
},
- @computed("hasLoadedData", "lastPostId", "posts.@each.id")
+ @discourseComputed("hasLoadedData", "lastPostId", "posts.@each.id")
loadedAllPosts(hasLoadedData, lastPostId) {
if (!hasLoadedData) {
return false;
@@ -112,13 +108,13 @@ export default RestModel.extend({
return !!this.posts.findBy("id", lastPostId);
},
- lastPostNotLoaded: Ember.computed.not("loadedAllPosts"),
+ lastPostNotLoaded: not("loadedAllPosts"),
/**
Returns a JS Object of current stream filter options. It should match the query
params for the stream.
**/
- @computed("summary", "userFilters.[]")
+ @discourseComputed("summary", "userFilters.[]")
streamFilters(summary) {
const result = {};
if (summary) {
@@ -126,14 +122,14 @@ export default RestModel.extend({
}
const userFilters = this.userFilters;
- if (!Ember.isEmpty(userFilters)) {
+ if (!isEmpty(userFilters)) {
result.username_filters = userFilters.join(",");
}
return result;
},
- @computed("streamFilters.[]", "topic.posts_count", "posts.length")
+ @discourseComputed("streamFilters.[]", "topic.posts_count", "posts.length")
hasNoFilters() {
const streamFilters = this.streamFilters;
return !(
@@ -146,7 +142,7 @@ export default RestModel.extend({
Returns the window of posts above the current set in the stream, bound to the top of the stream.
This is the collection we'll ask for when scrolling upwards.
**/
- @computed("posts.[]", "stream.[]")
+ @discourseComputed("posts.[]", "stream.[]")
previousWindow() {
// If we can't find the last post loaded, bail
const firstPost = _.first(this.posts);
@@ -172,7 +168,7 @@ export default RestModel.extend({
Returns the window of posts below the current set in the stream, bound by the bottom of the
stream. This is the collection we use when scrolling downwards.
**/
- @computed("posts.lastObject", "stream.[]")
+ @discourseComputed("posts.lastObject", "stream.[]")
nextWindow(lastLoadedPost) {
// If we can't find the last post loaded, bail
if (!lastLoadedPost) {
@@ -265,7 +261,7 @@ export default RestModel.extend({
} else {
const postWeWant = this.posts.findBy("post_number", opts.nearPost);
if (postWeWant) {
- return Ember.RSVP.resolve();
+ return Promise.resolve();
}
}
@@ -327,7 +323,7 @@ export default RestModel.extend({
});
}
}
- return Ember.RSVP.resolve();
+ return Promise.resolve();
},
// Fill in a gap of posts after a particular post
@@ -343,14 +339,14 @@ export default RestModel.extend({
this.stream.arrayContentDidChange();
});
}
- return Ember.RSVP.resolve();
+ return Promise.resolve();
},
// Appends the next window of posts to the stream. Call it when scrolling downwards.
appendMore() {
// Make sure we can append more posts
if (!this.canAppendMore) {
- return Ember.RSVP.resolve();
+ return Promise.resolve();
}
const postsWithPlaceholders = this.postsWithPlaceholders;
@@ -373,7 +369,7 @@ export default RestModel.extend({
});
} else {
const postIds = this.nextWindow;
- if (Ember.isEmpty(postIds)) return Ember.RSVP.resolve();
+ if (isEmpty(postIds)) return Promise.resolve();
this.set("loadingBelow", true);
postsWithPlaceholders.appending(postIds);
@@ -393,7 +389,7 @@ export default RestModel.extend({
prependMore() {
// Make sure we can append more posts
if (!this.canPrependMore) {
- return Ember.RSVP.resolve();
+ return Promise.resolve();
}
if (this.isMegaTopic) {
@@ -414,7 +410,7 @@ export default RestModel.extend({
});
} else {
const postIds = this.previousWindow;
- if (Ember.isEmpty(postIds)) return Ember.RSVP.resolve();
+ if (isEmpty(postIds)) return Promise.resolve();
this.set("loadingAbove", true);
return this.findPostsByIds(postIds.reverse())
@@ -532,7 +528,7 @@ export default RestModel.extend({
},
removePosts(posts) {
- if (Ember.isEmpty(posts)) {
+ if (isEmpty(posts)) {
return;
}
@@ -590,7 +586,7 @@ export default RestModel.extend({
have no filters.
**/
triggerNewPostInStream(postId) {
- const resolved = Ember.RSVP.Promise.resolve();
+ const resolved = Promise.resolve();
if (!postId) {
return resolved;
@@ -610,8 +606,7 @@ export default RestModel.extend({
return this.findPostsByIds([postId])
.then(posts => {
const ignoredUsers =
- Discourse.User.current() &&
- Discourse.User.current().get("ignored_users");
+ User.current() && User.current().get("ignored_users");
posts.forEach(p => {
if (ignoredUsers && ignoredUsers.includes(p.username)) {
this.stream.removeObject(postId);
@@ -690,13 +685,13 @@ export default RestModel.extend({
this.removePosts([existing]);
});
}
- return Ember.RSVP.Promise.resolve();
+ return Promise.resolve();
},
triggerChangedPost(postId, updatedAt, opts) {
opts = opts || {};
- const resolved = Ember.RSVP.Promise.resolve();
+ const resolved = Promise.resolve();
if (!postId) {
return resolved;
}
@@ -716,6 +711,19 @@ export default RestModel.extend({
return resolved;
},
+ triggerReadPost(postId, readersCount) {
+ const resolved = Promise.resolve();
+ resolved.then(() => {
+ const post = this.findLoadedPost(postId);
+ if (post && readersCount > post.readers_count) {
+ post.set("readers_count", readersCount);
+ this.storePost(post);
+ }
+ });
+
+ return resolved;
+ },
+
postForPostNumber(postNumber) {
if (!this.hasPosts) {
return;
@@ -880,12 +888,12 @@ export default RestModel.extend({
than you supplied if the post has already been loaded.
**/
storePost(post) {
- // Calling `Ember.get(undefined)` raises an error
+ // Calling `get(undefined)` raises an error
if (!post) {
return;
}
- const postId = Ember.get(post, "id");
+ const postId = get(post, "id");
if (postId) {
const existing = this._identityMap[post.get("id")];
@@ -929,7 +937,7 @@ export default RestModel.extend({
this.set("topic.suggested_topics", result.suggested_topics);
}
- const posts = Ember.get(result, "post_stream.posts");
+ const posts = get(result, "post_stream.posts");
if (posts) {
posts.forEach(p => {
@@ -954,8 +962,8 @@ export default RestModel.extend({
},
loadIntoIdentityMap(postIds) {
- if (Ember.isEmpty(postIds)) {
- return Ember.RSVP.resolve([]);
+ if (isEmpty(postIds)) {
+ return Promise.resolve([]);
}
let includeSuggested = !this.get("topic.suggested_topics");
@@ -969,7 +977,7 @@ export default RestModel.extend({
this.set("topic.suggested_topics", result.suggested_topics);
}
- const posts = Ember.get(result, "post_stream.posts");
+ const posts = get(result, "post_stream.posts");
if (posts) {
posts.forEach(p => this.storePost(store.createRecord("post", p)));
@@ -1026,12 +1034,12 @@ export default RestModel.extend({
excerpt(streamPosition) {
if (this.isMegaTopic) {
- return new Ember.RSVP.Promise(resolve => resolve(""));
+ return new Promise(resolve => resolve(""));
}
const stream = this.stream;
- return new Ember.RSVP.Promise((resolve, reject) => {
+ return new Promise((resolve, reject) => {
let excerpt = this._excerpts && this._excerpts[stream[streamPosition]];
if (excerpt) {
@@ -1054,31 +1062,16 @@ export default RestModel.extend({
// Handles an error loading a topic based on a HTTP status code. Updates
// the text to the correct values.
errorLoading(result) {
- const status = result.jqXHR.status;
-
const topic = this.topic;
this.set("loadingFilter", false);
topic.set("errorLoading", true);
- // If the result was 404 the post is not found
- // If it was 410 the post is deleted and the user should not see it
- if (status === 404 || status === 410) {
- topic.set("notFoundHtml", result.jqXHR.responseText);
- return;
+ const json = result.jqXHR.responseJSON;
+ if (json && json.extras && json.extras.html) {
+ topic.set("errorHtml", json.extras.html);
+ } else {
+ topic.set("errorMessage", I18n.t("topic.server_error.description"));
+ topic.set("noRetry", result.jqXHR.status === 403);
}
-
- // If the result is 403 it means invalid access
- if (status === 403) {
- topic.set("noRetry", true);
- if (Discourse.User.current()) {
- topic.set("message", I18n.t("topic.invalid_access.description"));
- } else {
- topic.set("message", I18n.t("topic.invalid_access.login_required"));
- }
- return;
- }
-
- // Otherwise supply a generic error message
- topic.set("message", I18n.t("topic.server_error.description"));
}
});
diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6
index 00f60bb358..a08f960ef0 100644
--- a/app/assets/javascripts/discourse/models/post.js.es6
+++ b/app/assets/javascripts/discourse/models/post.js.es6
@@ -1,25 +1,39 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { computed, get } from "@ember/object";
+import { isEmpty } from "@ember/utils";
+import { equal, and, or, not } from "@ember/object/computed";
+import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import RestModel from "discourse/models/rest";
import { popupAjaxError } from "discourse/lib/ajax-error";
import ActionSummary from "discourse/models/action-summary";
import { propertyEqual } from "discourse/lib/computed";
import Quote from "discourse/lib/quote";
-import computed from "ember-addons/ember-computed-decorators";
import { postUrl } from "discourse/lib/utilities";
import { cookAsync } from "discourse/lib/text";
import { userPath } from "discourse/lib/url";
import Composer from "discourse/models/composer";
+import { Promise } from "rsvp";
+import Site from "discourse/models/site";
+import User from "discourse/models/user";
+import showModal from "discourse/lib/show-modal";
const Post = RestModel.extend({
- @computed()
- siteSettings() {
- // TODO: Remove this once one instantiate all `Discourse.Post` models via the store.
- return Discourse.SiteSettings;
- },
+ // TODO: Remove this once one instantiate all `Discourse.Post` models via the store.
+ siteSettings: computed({
+ get() {
+ return Discourse.SiteSettings;
+ },
- @computed("url")
+ // prevents model created from json to overridde this property
+ set() {
+ return Discourse.SiteSettings;
+ }
+ }),
+
+ @discourseComputed("url")
shareUrl(url) {
- const user = Discourse.User.current();
+ const user = User.current();
const userSuffix = user ? `?u=${user.username_lower}` : "";
if (this.firstPost) {
@@ -29,32 +43,32 @@ const Post = RestModel.extend({
}
},
- new_user: Ember.computed.equal("trust_level", 0),
- firstPost: Ember.computed.equal("post_number", 1),
+ new_user: equal("trust_level", 0),
+ firstPost: equal("post_number", 1),
// Posts can show up as deleted if the topic is deleted
- deletedViaTopic: Ember.computed.and("firstPost", "topic.deleted_at"),
- deleted: Ember.computed.or("deleted_at", "deletedViaTopic"),
- notDeleted: Ember.computed.not("deleted"),
+ deletedViaTopic: and("firstPost", "topic.deleted_at"),
+ deleted: or("deleted_at", "deletedViaTopic"),
+ notDeleted: not("deleted"),
- @computed("name", "username")
+ @discourseComputed("name", "username")
showName(name, username) {
return (
name && name !== username && Discourse.SiteSettings.display_name_on_posts
);
},
- @computed("firstPost", "deleted_by", "topic.deleted_by")
+ @discourseComputed("firstPost", "deleted_by", "topic.deleted_by")
postDeletedBy(firstPost, deletedBy, topicDeletedBy) {
return firstPost ? topicDeletedBy : deletedBy;
},
- @computed("firstPost", "deleted_at", "topic.deleted_at")
+ @discourseComputed("firstPost", "deleted_at", "topic.deleted_at")
postDeletedAt(firstPost, deletedAt, topicDeletedAt) {
return firstPost ? topicDeletedAt : deletedAt;
},
- @computed("post_number", "topic_id", "topic.slug")
+ @discourseComputed("post_number", "topic_id", "topic.slug")
url(post_number, topic_id, topicSlug) {
return postUrl(
topicSlug || this.topic_slug,
@@ -64,12 +78,13 @@ const Post = RestModel.extend({
},
// Don't drop the /1
- @computed("post_number", "url")
+ @discourseComputed("post_number", "url")
urlWithNumber(postNumber, baseUrl) {
return postNumber === 1 ? `${baseUrl}/1` : baseUrl;
},
- @computed("username") usernameUrl: userPath,
+ @discourseComputed("username")
+ usernameUrl: userPath,
topicOwner: propertyEqual("topic.details.created_by.id", "user_id"),
@@ -82,14 +97,14 @@ const Post = RestModel.extend({
.catch(popupAjaxError);
},
- @computed("link_counts.@each.internal")
+ @discourseComputed("link_counts.@each.internal")
internalLinks() {
- if (Ember.isEmpty(this.link_counts)) return null;
+ if (isEmpty(this.link_counts)) return null;
return this.link_counts.filterBy("internal").filterBy("title");
},
- @computed("actions_summary.@each.can_act")
+ @discourseComputed("actions_summary.@each.can_act")
flagsAvailable() {
// TODO: Investigate why `this.site` is sometimes null when running
// Search - Search with context
@@ -219,7 +234,7 @@ const Post = RestModel.extend({
});
}
- return promise || Ember.RSVP.Promise.resolve();
+ return promise || Promise.resolve();
},
/**
@@ -272,7 +287,7 @@ const Post = RestModel.extend({
if (key === "reply_to_user" && value && oldValue) {
skip =
value.username === oldValue.username ||
- Ember.get(value, "username") === Ember.get(oldValue, "username");
+ get(value, "username") === get(oldValue, "username");
}
if (!skip) {
@@ -308,8 +323,11 @@ const Post = RestModel.extend({
// need to wait to hear back from server (stuff may not be loaded)
- return Discourse.Post.updateBookmark(this.id, this.bookmarked)
- .then(result => this.set("topic.bookmarked", result.topic_bookmarked))
+ return Post.updateBookmark(this.id, this.bookmarked)
+ .then(result => {
+ this.set("topic.bookmarked", result.topic_bookmarked);
+ this.appEvents.trigger("page:bookmark-post-toggled", this);
+ })
.catch(error => {
this.toggleProperty("bookmarked");
if (bookmarkedTopic) {
@@ -319,6 +337,43 @@ const Post = RestModel.extend({
});
},
+ toggleBookmarkWithReminder() {
+ this.toggleProperty("bookmarked_with_reminder");
+ if (this.bookmarked_with_reminder) {
+ let controller = showModal("bookmark", {
+ model: {
+ postId: this.id
+ },
+ title: "post.bookmarks.create",
+ modalClass: "bookmark-with-reminder"
+ });
+ controller.setProperties({
+ onCloseWithoutSaving: () => {
+ this.toggleProperty("bookmarked_with_reminder");
+ this.appEvents.trigger("post-stream:refresh", { id: this.id });
+ },
+ afterSave: reminderAtISO => {
+ this.setProperties({
+ "topic.bookmarked": true,
+ bookmark_reminder_at: reminderAtISO
+ });
+ this.appEvents.trigger("post-stream:refresh", { id: this.id });
+ }
+ });
+ } else {
+ this.set("bookmark_reminder_at", null);
+ return Post.destroyBookmark(this.id)
+ .then(result => {
+ this.set("topic.bookmarked", result.topic_bookmarked);
+ this.appEvents.trigger("page:bookmark-post-toggled", this);
+ })
+ .catch(error => {
+ this.toggleProperty("bookmarked_with_reminder");
+ throw new Error(error);
+ });
+ }
+ },
+
updateActionsSummary(json) {
if (json && json.id === this.id) {
json = Post.munge(json);
@@ -336,11 +391,11 @@ const Post = RestModel.extend({
Post.reopenClass({
munge(json) {
if (json.actions_summary) {
- const lookup = Ember.Object.create();
+ const lookup = EmberObject.create();
// this area should be optimized, it is creating way too many objects per post
json.actions_summary = json.actions_summary.map(a => {
- a.actionType = Discourse.Site.current().postActionTypeById(a.id);
+ a.actionType = Site.current().postActionTypeById(a.id);
a.count = a.count || 0;
const actionSummary = ActionSummary.create(a);
lookup[a.actionType.name_key] = actionSummary;
@@ -355,7 +410,7 @@ Post.reopenClass({
}
if (json && json.reply_to_user) {
- json.reply_to_user = Discourse.User.create(json.reply_to_user);
+ json.reply_to_user = User.create(json.reply_to_user);
}
return json;
@@ -368,6 +423,12 @@ Post.reopenClass({
});
},
+ destroyBookmark(postId) {
+ return ajax(`/posts/${postId}/bookmark`, {
+ type: "DELETE"
+ });
+ },
+
deleteMany(post_ids, { agreeWithFirstReplyFlag = true } = {}) {
return ajax("/posts/destroy_many", {
type: "DELETE",
@@ -384,7 +445,7 @@ Post.reopenClass({
loadRevision(postId, version) {
return ajax(`/posts/${postId}/revisions/${version}.json`).then(result =>
- Ember.Object.create(result)
+ EmberObject.create(result)
);
},
@@ -402,7 +463,7 @@ Post.reopenClass({
loadQuote(postId) {
return ajax(`/posts/${postId}.json`).then(result => {
- const post = Discourse.Post.create(result);
+ const post = Post.create(result);
return Quote.build(post, post.raw, { raw: true, full: true });
});
},
diff --git a/app/assets/javascripts/discourse/models/rest.js.es6 b/app/assets/javascripts/discourse/models/rest.js.es6
index 8b731f584f..a140dad504 100644
--- a/app/assets/javascripts/discourse/models/rest.js.es6
+++ b/app/assets/javascripts/discourse/models/rest.js.es6
@@ -1,6 +1,10 @@
-const RestModel = Ember.Object.extend({
- isNew: Ember.computed.equal("__state", "new"),
- isCreated: Ember.computed.equal("__state", "created"),
+import { equal } from "@ember/object/computed";
+import EmberObject from "@ember/object";
+import { Promise } from "rsvp";
+
+const RestModel = EmberObject.extend({
+ isNew: equal("__state", "new"),
+ isCreated: equal("__state", "created"),
isSaving: false,
beforeCreate() {},
@@ -8,7 +12,7 @@ const RestModel = Ember.Object.extend({
update(props) {
if (this.isSaving) {
- return Ember.RSVP.reject();
+ return Promise.reject();
}
props = props || this.updateProperties();
@@ -36,7 +40,7 @@ const RestModel = Ember.Object.extend({
_saveNew(props) {
if (this.isSaving) {
- return Ember.RSVP.reject();
+ return Promise.reject();
}
props = props || this.createProperties();
diff --git a/app/assets/javascripts/discourse/models/result-set.js.es6 b/app/assets/javascripts/discourse/models/result-set.js.es6
index dc9f1f9ec4..56e2c164b5 100644
--- a/app/assets/javascripts/discourse/models/result-set.js.es6
+++ b/app/assets/javascripts/discourse/models/result-set.js.es6
@@ -1,4 +1,5 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { Promise } from "rsvp";
export default Ember.ArrayProxy.extend({
loading: false,
@@ -14,7 +15,7 @@ export default Ember.ArrayProxy.extend({
__type: null,
resultSetMeta: null,
- @computed("totalRows", "length")
+ @discourseComputed("totalRows", "length")
canLoadMore(totalRows, length) {
return length < totalRows;
},
@@ -34,7 +35,7 @@ export default Ember.ArrayProxy.extend({
.finally(() => this.set("loadingMore", false));
}
- return Ember.RSVP.resolve();
+ return Promise.resolve();
},
refresh() {
diff --git a/app/assets/javascripts/discourse/models/reviewable-history.js.es6 b/app/assets/javascripts/discourse/models/reviewable-history.js.es6
index 0b1b14f54e..dec1f37be5 100644
--- a/app/assets/javascripts/discourse/models/reviewable-history.js.es6
+++ b/app/assets/javascripts/discourse/models/reviewable-history.js.es6
@@ -1,3 +1,4 @@
+import { equal } from "@ember/object/computed";
import RestModel from "discourse/models/rest";
export const CREATED = 0;
@@ -5,5 +6,5 @@ export const TRANSITIONED_TO = 1;
export const EDITED = 2;
export default RestModel.extend({
- created: Ember.computed.equal("reviewable_history_type", CREATED)
+ created: equal("reviewable_history_type", CREATED)
});
diff --git a/app/assets/javascripts/discourse/models/reviewable.js.es6 b/app/assets/javascripts/discourse/models/reviewable.js.es6
index 25fc62f63d..fd6ad39603 100644
--- a/app/assets/javascripts/discourse/models/reviewable.js.es6
+++ b/app/assets/javascripts/discourse/models/reviewable.js.es6
@@ -1,7 +1,8 @@
+import discourseComputed from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
import RestModel from "discourse/models/rest";
-import computed from "ember-addons/ember-computed-decorators";
import Category from "discourse/models/category";
+import { Promise } from "rsvp";
export const PENDING = 0;
export const APPROVED = 1;
@@ -10,7 +11,7 @@ export const IGNORED = 3;
export const DELETED = 4;
export default RestModel.extend({
- @computed("type", "topic")
+ @discourseComputed("type", "topic")
humanType(type, topic) {
// Display "Queued Topic" if the post will create a topic
if (type === "ReviewableQueuedPost" && !topic) {
@@ -25,7 +26,7 @@ export default RestModel.extend({
update(updates) {
// If no changes, do nothing
if (Object.keys(updates).length === 0) {
- return Ember.RSVP.resolve();
+ return Promise.resolve();
}
let adapter = this.store.adapterFor("reviewable");
diff --git a/app/assets/javascripts/discourse/models/session.js.es6 b/app/assets/javascripts/discourse/models/session.js.es6
index f323540640..9f05080ea9 100644
--- a/app/assets/javascripts/discourse/models/session.js.es6
+++ b/app/assets/javascripts/discourse/models/session.js.es6
@@ -1,5 +1,6 @@
import RestModel from "discourse/models/rest";
import Singleton from "discourse/mixins/singleton";
+import deprecated from "discourse-common/lib/deprecated";
// A data model representing current session data. You can put transient
// data here you might want later. It is not stored or serialized anywhere.
@@ -10,4 +11,14 @@ const Session = RestModel.extend({
});
Session.reopenClass(Singleton);
+
+Object.defineProperty(Discourse, "Session", {
+ get() {
+ deprecated("Import the Session object instead of using Discourse.Session", {
+ since: "2.4.0",
+ dropFrom: "2.5.0"
+ });
+ return Session;
+ }
+});
export default Session;
diff --git a/app/assets/javascripts/discourse/models/site.js.es6 b/app/assets/javascripts/discourse/models/site.js.es6
index 1089956d6c..5f9065d3d8 100644
--- a/app/assets/javascripts/discourse/models/site.js.es6
+++ b/app/assets/javascripts/discourse/models/site.js.es6
@@ -1,12 +1,18 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { get } from "@ember/object";
+import { isEmpty } from "@ember/utils";
+import { alias, sort } from "@ember/object/computed";
+import EmberObject from "@ember/object";
import Archetype from "discourse/models/archetype";
import PostActionType from "discourse/models/post-action-type";
import Singleton from "discourse/mixins/singleton";
import RestModel from "discourse/models/rest";
+import TrustLevel from "discourse/models/trust-level";
import PreloadStore from "preload-store";
+import deprecated from "discourse-common/lib/deprecated";
const Site = RestModel.extend({
- isReadOnly: Ember.computed.alias("is_readonly"),
+ isReadOnly: alias("is_readonly"),
init() {
this._super(...arguments);
@@ -14,7 +20,7 @@ const Site = RestModel.extend({
this.topicCountDesc = ["topic_count:desc"];
},
- @computed("notification_types")
+ @discourseComputed("notification_types")
notificationLookup(notificationTypes) {
const result = [];
Object.keys(notificationTypes).forEach(
@@ -23,21 +29,21 @@ const Site = RestModel.extend({
return result;
},
- @computed("post_action_types.[]")
+ @discourseComputed("post_action_types.[]")
flagTypes() {
const postActionTypes = this.post_action_types;
if (!postActionTypes) return [];
return postActionTypes.filterBy("is_flag", true);
},
- categoriesByCount: Ember.computed.sort("categories", "topicCountDesc"),
+ categoriesByCount: sort("categories", "topicCountDesc"),
collectUserFields(fields) {
fields = fields || {};
let siteFields = this.user_fields;
- if (!Ember.isEmpty(siteFields)) {
+ if (!isEmpty(siteFields)) {
return siteFields.map(f => {
let value = fields ? fields[f.id.toString()] : null;
value = value || "—".htmlSafe();
@@ -48,7 +54,7 @@ const Site = RestModel.extend({
},
// Sort subcategories under parents
- @computed("categoriesByCount", "categories.[]")
+ @discourseComputed("categoriesByCount", "categories.[]")
sortedCategories(cats) {
const result = [],
remaining = {};
@@ -75,13 +81,13 @@ const Site = RestModel.extend({
return result;
},
- @computed
+ @discourseComputed
baseUri() {
return Discourse.baseUri;
},
// Returns it in the correct order, by setting
- @computed
+ @discourseComputed("categories.[]")
categoriesList() {
return this.siteSettings.fixed_category_positions
? this.categories
@@ -107,7 +113,7 @@ const Site = RestModel.extend({
updateCategory(newCategory) {
const categories = this.categories;
- const categoryId = Ember.get(newCategory, "id");
+ const categoryId = get(newCategory, "id");
const existingCategory = categories.findBy("id", categoryId);
// Don't update null permissions
@@ -117,11 +123,13 @@ const Site = RestModel.extend({
if (existingCategory) {
existingCategory.setProperties(newCategory);
+ return existingCategory;
} else {
// TODO insert in right order?
newCategory = this.store.createRecord("category", newCategory);
categories.pushObject(newCategory);
this.categoriesById[categoryId] = newCategory;
+ return newCategory;
}
}
});
@@ -172,14 +180,12 @@ Site.reopenClass(Singleton, {
}
if (result.trust_levels) {
- result.trustLevels = result.trust_levels.map(tl =>
- Discourse.TrustLevel.create(tl)
- );
+ result.trustLevels = result.trust_levels.map(tl => TrustLevel.create(tl));
delete result.trust_levels;
}
if (result.post_action_types) {
- result.postActionByIdLookup = Ember.Object.create();
+ result.postActionByIdLookup = EmberObject.create();
result.post_action_types = result.post_action_types.map(p => {
const actionType = PostActionType.create(p);
result.postActionByIdLookup.set("action" + p.id, actionType);
@@ -188,7 +194,7 @@ Site.reopenClass(Singleton, {
}
if (result.topic_flag_types) {
- result.topicFlagByIdLookup = Ember.Object.create();
+ result.topicFlagByIdLookup = EmberObject.create();
result.topic_flag_types = result.topic_flag_types.map(p => {
const actionType = PostActionType.create(p);
result.topicFlagByIdLookup.set("action" + p.id, actionType);
@@ -204,13 +210,25 @@ Site.reopenClass(Singleton, {
}
if (result.user_fields) {
- result.user_fields = result.user_fields.map(uf =>
- Ember.Object.create(uf)
- );
+ result.user_fields = result.user_fields.map(uf => EmberObject.create(uf));
}
return result;
}
});
+let warned = false;
+Object.defineProperty(Discourse, "Site", {
+ get() {
+ if (!warned) {
+ deprecated("Import the Site class instead of using Discourse.Site", {
+ since: "2.4.0",
+ dropFrom: "2.6.0"
+ });
+ warned = true;
+ }
+ return Site;
+ }
+});
+
export default Site;
diff --git a/app/assets/javascripts/discourse/models/static-page.js.es6 b/app/assets/javascripts/discourse/models/static-page.js.es6
index d32160c727..7ec19c23b7 100644
--- a/app/assets/javascripts/discourse/models/static-page.js.es6
+++ b/app/assets/javascripts/discourse/models/static-page.js.es6
@@ -1,9 +1,12 @@
+import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
-const StaticPage = Ember.Object.extend();
+import { Promise } from "rsvp";
+
+const StaticPage = EmberObject.extend();
StaticPage.reopenClass({
find(path) {
- return new Ember.RSVP.Promise(resolve => {
+ return new Promise(resolve => {
// Models shouldn't really be doing Ajax request, but this is a huge speed boost if we
// preload content.
const $preloaded = $('noscript[data-path="/' + path + '"]');
@@ -12,11 +15,11 @@ StaticPage.reopenClass({
text = text.match(
/((?:.|[\n\r])*)/
)[1];
- resolve(StaticPage.create({ path: path, html: text }));
+ resolve(StaticPage.create({ path, html: text }));
} else {
- ajax(path + ".html", { dataType: "html" }).then(function(result) {
- resolve(StaticPage.create({ path: path, html: result }));
- });
+ ajax(`/${path}.html`, { dataType: "html" }).then(result =>
+ resolve(StaticPage.create({ path, html: result }))
+ );
}
});
}
diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6
index 96a96f502f..2ee28b6620 100644
--- a/app/assets/javascripts/discourse/models/store.js.es6
+++ b/app/assets/javascripts/discourse/models/store.js.es6
@@ -1,7 +1,12 @@
+import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import RestModel from "discourse/models/rest";
import ResultSet from "discourse/models/result-set";
import { getRegister } from "discourse-common/lib/get-owner";
+import { underscore } from "@ember/string";
+import { set } from "@ember/object";
+import Category from "discourse/models/category";
+import { Promise } from "rsvp";
let _identityMap;
@@ -44,8 +49,9 @@ function findAndRemoveMap(type, id) {
flushMap();
-export default Ember.Object.extend({
+export default EmberObject.extend({
_plurals: {
+ category: "categories",
"post-reply": "post-replies",
"post-reply-history": "post_reply_histories",
reviewable_history: "reviewable_histories"
@@ -92,7 +98,8 @@ export default Ember.Object.extend({
if (typeof findArgs === "object") {
return this._resultSet(type, result, findArgs);
} else {
- return this._hydrate(type, result[Ember.String.underscore(type)], result);
+ const apiName = this.adapterFor(type).apiNameFor(type);
+ return this._hydrate(type, result[underscore(apiName)], result);
}
},
@@ -146,8 +153,9 @@ export default Ember.Object.extend({
},
refreshResults(resultSet, type, url) {
+ const adapter = this.adapterFor(type);
return ajax(url).then(result => {
- const typeName = Ember.String.underscore(this.pluralize(type));
+ const typeName = underscore(this.pluralize(adapter.apiNameFor(type)));
const content = result[typeName].map(obj =>
this._hydrate(type, obj, result)
);
@@ -156,8 +164,9 @@ export default Ember.Object.extend({
},
appendResults(resultSet, type, url) {
+ const adapter = this.adapterFor(type);
return ajax(url).then(result => {
- let typeName = Ember.String.underscore(this.pluralize(type));
+ const typeName = underscore(this.pluralize(adapter.apiNameFor(type)));
let pageTarget = result.meta || result;
let totalRows =
@@ -198,7 +207,7 @@ export default Ember.Object.extend({
// If the record is new, don't perform an Ajax call
if (record.get("isNew")) {
removeMap(type, record.get("id"));
- return Ember.RSVP.Promise.resolve(true);
+ return Promise.resolve(true);
}
return this.adapterFor(type)
@@ -210,7 +219,15 @@ export default Ember.Object.extend({
},
_resultSet(type, result, findArgs) {
- const typeName = Ember.String.underscore(this.pluralize(type));
+ const adapter = this.adapterFor(type);
+ const typeName = underscore(this.pluralize(adapter.apiNameFor(type)));
+
+ if (!result[typeName]) {
+ // eslint-disable-next-line no-console
+ console.error(`JSON response is missing \`${typeName}\` key`, result);
+ return;
+ }
+
const content = result[typeName].map(obj =>
this._hydrate(type, obj, result)
);
@@ -265,7 +282,7 @@ export default Ember.Object.extend({
// to category. That should either respect this or be
// removed.
if (subType === "category" && type !== "topic") {
- return Discourse.Category.findById(id);
+ return Category.findById(id);
}
if (root.meta && root.meta.types) {
@@ -312,7 +329,7 @@ export default Ember.Object.extend({
obj[subType] = hydrated;
delete obj[k];
} else {
- Ember.set(obj, subType, null);
+ set(obj, subType, null);
}
}
}
diff --git a/app/assets/javascripts/discourse/models/tag-group.js.es6 b/app/assets/javascripts/discourse/models/tag-group.js.es6
index 6d0579a1b9..18afc52fe7 100644
--- a/app/assets/javascripts/discourse/models/tag-group.js.es6
+++ b/app/assets/javascripts/discourse/models/tag-group.js.es6
@@ -1,77 +1,18 @@
-import { ajax } from "discourse/lib/ajax";
+import discourseComputed from "discourse-common/utils/decorators";
import RestModel from "discourse/models/rest";
-import computed from "ember-addons/ember-computed-decorators";
import PermissionType from "discourse/models/permission-type";
export default RestModel.extend({
- @computed("name", "tag_names", "saving")
- disableSave(name, tagNames, saving) {
- return saving || Ember.isEmpty(name) || Ember.isEmpty(tagNames);
- },
+ @discourseComputed("permissions")
+ permissionName(permissions) {
+ if (!permissions) return "public";
- @computed("id")
- disableDelete(id) {
- return !parseInt(id);
- },
-
- @computed("permissions")
- permissionName: {
- get(permissions) {
- if (!permissions) return "public";
-
- if (permissions["everyone"] === PermissionType.FULL) {
- return "public";
- } else if (permissions["everyone"] === PermissionType.READONLY) {
- return "visible";
- } else {
- return "private";
- }
- },
-
- set(value) {
- if (value === "private") {
- this.set("permissions", { staff: PermissionType.FULL });
- } else if (value === "visible") {
- this.set("permissions", {
- staff: PermissionType.FULL,
- everyone: PermissionType.READONLY
- });
- } else {
- this.set("permissions", { everyone: PermissionType.FULL });
- }
+ if (permissions["everyone"] === PermissionType.FULL) {
+ return "public";
+ } else if (permissions["everyone"] === PermissionType.READONLY) {
+ return "visible";
+ } else {
+ return "private";
}
- },
-
- save() {
- this.set("savingStatus", I18n.t("saving"));
- this.set("saving", true);
-
- const isNew = this.id === "new";
- const url = isNew ? "/tag_groups" : `/tag_groups/${this.id}`;
- const data = this.getProperties(
- "name",
- "tag_names",
- "parent_tag_name",
- "one_per_topic",
- "permissions"
- );
-
- return ajax(url, {
- data,
- type: isNew ? "POST" : "PUT"
- })
- .then(result => {
- if (result.tag_group && result.tag_group.id) {
- this.set("id", result.tag_group.id);
- }
- })
- .finally(() => {
- this.set("savingStatus", I18n.t("saved"));
- this.set("saving", false);
- });
- },
-
- destroy() {
- return ajax(`/tag_groups/${this.id}`, { type: "DELETE" });
}
});
diff --git a/app/assets/javascripts/discourse/models/tag.js.es6 b/app/assets/javascripts/discourse/models/tag.js.es6
index c9665111ac..0985bb3dfb 100644
--- a/app/assets/javascripts/discourse/models/tag.js.es6
+++ b/app/assets/javascripts/discourse/models/tag.js.es6
@@ -1,14 +1,19 @@
+import discourseComputed from "discourse-common/utils/decorators";
import RestModel from "discourse/models/rest";
-import computed from "ember-addons/ember-computed-decorators";
export default RestModel.extend({
- @computed("count", "pm_count")
+ @discourseComputed("count", "pm_count")
totalCount(count, pmCount) {
return count + pmCount;
},
- @computed("count", "pm_count")
+ @discourseComputed("count", "pm_count")
pmOnly(count, pmCount) {
return count === 0 && pmCount > 0;
+ },
+
+ @discourseComputed("id")
+ searchContext(id) {
+ return { type: "tag", id, tag: this, name: id };
}
});
diff --git a/app/assets/javascripts/discourse/models/topic-details.js.es6 b/app/assets/javascripts/discourse/models/topic-details.js.es6
index 9f486f0712..849b072bd5 100644
--- a/app/assets/javascripts/discourse/models/topic-details.js.es6
+++ b/app/assets/javascripts/discourse/models/topic-details.js.es6
@@ -1,6 +1,7 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
-import computed from "ember-addons/ember-computed-decorators";
-
+import User from "discourse/models/user";
/**
A model representing a Topic's details that aren't always present, such as a list of participants.
When showing topics in lists and such this information should not be required.
@@ -16,14 +17,14 @@ const TopicDetails = RestModel.extend({
if (details.allowed_users) {
details.allowed_users = details.allowed_users.map(function(u) {
- return Discourse.User.create(u);
+ return User.create(u);
});
}
if (details.participants) {
details.participants = details.participants.map(function(p) {
p.topic = topic;
- return Ember.Object.create(p);
+ return EmberObject.create(p);
});
}
@@ -31,7 +32,7 @@ const TopicDetails = RestModel.extend({
this.set("loaded", true);
},
- @computed("notification_level", "notifications_reason_id")
+ @discourseComputed("notification_level", "notifications_reason_id")
notificationReasonText(level, reason) {
if (typeof level !== "number") {
level = 1;
@@ -47,13 +48,13 @@ const TopicDetails = RestModel.extend({
}
if (
- Discourse.User.currentProp("mailing_list_mode") &&
+ User.currentProp("mailing_list_mode") &&
level > NotificationLevels.MUTED
) {
return I18n.t("topic.notifications.reasons.mailing_list_mode");
} else {
return I18n.t(localeString, {
- username: Discourse.User.currentProp("username_lower"),
+ username: User.currentProp("username_lower"),
basePath: Discourse.BaseUri
});
}
diff --git a/app/assets/javascripts/discourse/models/topic-list.js.es6 b/app/assets/javascripts/discourse/models/topic-list.js.es6
index e12a90e77e..489c926de3 100644
--- a/app/assets/javascripts/discourse/models/topic-list.js.es6
+++ b/app/assets/javascripts/discourse/models/topic-list.js.es6
@@ -1,7 +1,25 @@
+import { notEmpty } from "@ember/object/computed";
+import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import RestModel from "discourse/models/rest";
-import Model from "discourse/models/model";
import { getOwner } from "discourse-common/lib/get-owner";
+import { Promise } from "rsvp";
+import Category from "discourse/models/category";
+import Session from "discourse/models/session";
+import { isEmpty } from "@ember/utils";
+import User from "discourse/models/user";
+
+function extractByKey(collection, klass) {
+ const retval = {};
+ if (isEmpty(collection)) {
+ return retval;
+ }
+
+ collection.forEach(function(item) {
+ retval[item.id] = klass.create(item);
+ });
+ return retval;
+}
// Whether to show the category badge in topic lists
function displayCategoryInList(site, category) {
@@ -22,7 +40,7 @@ function displayCategoryInList(site, category) {
}
const TopicList = RestModel.extend({
- canLoadMore: Ember.computed.notEmpty("more_topics_url"),
+ canLoadMore: notEmpty("more_topics_url"),
forEachNew(topics, callback) {
const topicIds = [];
@@ -52,7 +70,7 @@ const TopicList = RestModel.extend({
loadMore() {
if (this.loadingMore) {
- return Ember.RSVP.resolve();
+ return Promise.resolve();
}
let moreUrl = this.more_topics_url;
@@ -89,13 +107,13 @@ const TopicList = RestModel.extend({
more_topics_url: result.topic_list.more_topics_url
});
- Discourse.Session.currentProp("topicList", this);
+ Session.currentProp("topicList", this);
return this.more_topics_url;
}
});
} else {
// Return a promise indicating no more results
- return Ember.RSVP.resolve();
+ return Promise.resolve();
}
},
@@ -119,7 +137,7 @@ const TopicList = RestModel.extend({
i++;
});
- if (storeInSession) Discourse.Session.currentProp("topicList", this);
+ if (storeInSession) Session.currentProp("topicList", this);
});
}
});
@@ -133,9 +151,9 @@ TopicList.reopenClass({
// Stitch together our side loaded data
- const categories = Discourse.Category.list(),
- users = Model.extractByKey(result.users, Discourse.User),
- groups = Model.extractByKey(result.primary_groups, Ember.Object);
+ const categories = Category.list(),
+ users = extractByKey(result.users, User),
+ groups = extractByKey(result.primary_groups, EmberObject);
return result.topic_list[listKey].map(t => {
t.category = categories.findBy("id", t.category_id);
diff --git a/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 b/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6
index 0b4ef2ca93..8f79703ef7 100644
--- a/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6
+++ b/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6
@@ -1,16 +1,18 @@
+import { get } from "@ember/object";
+import { isEmpty } from "@ember/utils";
import { NotificationLevels } from "discourse/lib/notification-levels";
-import {
- default as computed,
- on
-} from "ember-addons/ember-computed-decorators";
-import { defaultHomepage } from "discourse/lib/utilities";
+import discourseComputed, { on } from "discourse-common/utils/decorators";
import PreloadStore from "preload-store";
+import Category from "discourse/models/category";
+import EmberObject from "@ember/object";
+import User from "discourse/models/user";
function isNew(topic) {
return (
topic.last_read_post_number === null &&
((topic.notification_level !== 0 && !topic.notification_level) ||
- topic.notification_level >= NotificationLevels.TRACKING)
+ topic.notification_level >= NotificationLevels.TRACKING) &&
+ isUnseen(topic)
);
}
@@ -22,7 +24,23 @@ function isUnread(topic) {
);
}
-const TopicTrackingState = Discourse.Model.extend({
+function isUnseen(topic) {
+ return !topic.is_seen;
+}
+
+function hasMutedTags(topicTagIds, mutedTagIds) {
+ if (!mutedTagIds || !topicTagIds) {
+ return false;
+ }
+ return (
+ (Discourse.SiteSettings.remove_muted_tags_from_latest === "always" &&
+ topicTagIds.any(tagId => mutedTagIds.includes(tagId))) ||
+ (Discourse.SiteSettings.remove_muted_tags_from_latest === "only_muted" &&
+ topicTagIds.every(tagId => mutedTagIds.includes(tagId)))
+ );
+}
+
+const TopicTrackingState = EmberObject.extend({
messageCount: 0,
@on("init")
@@ -42,9 +60,7 @@ const TopicTrackingState = Discourse.Model.extend({
}
if (["new_topic", "latest"].includes(data.message_type)) {
- const muted_category_ids = Discourse.User.currentProp(
- "muted_category_ids"
- );
+ const muted_category_ids = User.currentProp("muted_category_ids");
if (
muted_category_ids &&
muted_category_ids.includes(data.payload.category_id)
@@ -53,12 +69,10 @@ const TopicTrackingState = Discourse.Model.extend({
}
}
- // fill parent_category_id we need it for counting new/unread
- if (data.payload && data.payload.category_id) {
- var category = Discourse.Category.findById(data.payload.category_id);
-
- if (category && category.parent_category_id) {
- data.payload.parent_category_id = category.parent_category_id;
+ if (["new_topic", "latest"].includes(data.message_type)) {
+ const mutedTagIds = User.currentProp("muted_tag_ids");
+ if (hasMutedTags(data.payload.topic_tag_ids, mutedTagIds)) {
+ return;
}
}
@@ -66,16 +80,29 @@ const TopicTrackingState = Discourse.Model.extend({
tracker.notify(data);
}
+ if (data.message_type === "dismiss_new") {
+ Object.keys(tracker.states).forEach(k => {
+ const topic = tracker.states[k];
+ if (
+ !data.payload.category_id ||
+ topic.category_id === parseInt(data.payload.category_id, 0)
+ ) {
+ tracker.states[k] = Object.assign({}, topic, {
+ is_seen: true
+ });
+ }
+ });
+ tracker.notifyPropertyChange("states");
+ tracker.incrementMessageCount();
+ }
+
if (["new_topic", "unread", "read"].includes(data.message_type)) {
tracker.notify(data);
const old = tracker.states["t" + data.topic_id];
-
- // don't add tracking state for read stuff that was not tracked in first place
- if (old || data.message_type !== "read") {
- if (!_.isEqual(old, data.payload)) {
- tracker.states["t" + data.topic_id] = data.payload;
- tracker.incrementMessageCount();
- }
+ if (!_.isEqual(old, data.payload)) {
+ tracker.states["t" + data.topic_id] = data.payload;
+ tracker.notifyPropertyChange("states");
+ tracker.incrementMessageCount();
}
}
};
@@ -134,7 +161,7 @@ const TopicTrackingState = Discourse.Model.extend({
const categoryId = data.payload && data.payload.category_id;
if (filterCategory && filterCategory.get("id") !== categoryId) {
- const category = categoryId && Discourse.Category.findById(categoryId);
+ const category = categoryId && Category.findById(categoryId);
if (
!category ||
category.get("parentCategory.id") !== filterCategory.get("id")
@@ -143,18 +170,6 @@ const TopicTrackingState = Discourse.Model.extend({
}
}
- if (filter === defaultHomepage()) {
- const suppressed_from_latest_category_ids = Discourse.Site.currentProp(
- "suppressed_from_latest_category_ids"
- );
- if (
- suppressed_from_latest_category_ids &&
- suppressed_from_latest_category_ids.includes(data.payload.category_id)
- ) {
- return;
- }
- }
-
if (
["all", "latest", "new"].includes(filter) &&
data.message_type === "new_topic"
@@ -194,9 +209,9 @@ const TopicTrackingState = Discourse.Model.extend({
if (split.length >= 4) {
filter = split[split.length - 1];
- // c/cat/subcat/l/latest
- var category = Discourse.Category.findSingleBySlug(
- split.splice(1, split.length - 3).join("/")
+ // c/cat/subcat/6/l/latest
+ var category = Category.findSingleBySlug(
+ split.splice(1, split.length - 4).join("/")
);
this.set("filterCategory", category);
} else {
@@ -207,7 +222,7 @@ const TopicTrackingState = Discourse.Model.extend({
this.set("incomingCount", 0);
},
- @computed("incomingCount")
+ @discourseComputed("incomingCount")
hasIncoming(incomingCount) {
return incomingCount && incomingCount > 0;
},
@@ -218,7 +233,7 @@ const TopicTrackingState = Discourse.Model.extend({
// If we have a cached topic list, we can update it from our tracking information.
updateTopics(topics) {
- if (Ember.isEmpty(topics)) {
+ if (isEmpty(topics)) {
return;
}
@@ -228,7 +243,11 @@ const TopicTrackingState = Discourse.Model.extend({
if (state) {
const lastRead = t.get("last_read_post_number");
- if (lastRead !== state.last_read_post_number) {
+ const isSeen = t.get("is_seen");
+ if (
+ lastRead !== state.last_read_post_number ||
+ isSeen !== state.is_seen
+ ) {
const postsCount = t.get("posts_count");
let newPosts = postsCount - state.highest_post_number,
unread = postsCount - state.last_read_post_number;
@@ -248,7 +267,8 @@ const TopicTrackingState = Discourse.Model.extend({
last_read_post_number: state.last_read_post_number,
new_posts: newPosts,
unread: unread,
- unseen: !state.last_read_post_number
+ is_seen: state.is_seen,
+ unseen: !state.last_read_post_number && isUnseen(state)
});
}
}
@@ -335,38 +355,43 @@ const TopicTrackingState = Discourse.Model.extend({
this.incrementProperty("messageCount");
},
- countNew(category_id) {
+ getSubCategoryIds(categoryId) {
+ const result = [categoryId];
+ const categories = Category.list();
+
+ for (let i = 0; i < result.length; ++i) {
+ for (let j = 0; j < categories.length; ++j) {
+ if (result[i] === categories[j].parent_category_id) {
+ result[result.length] = categories[j].id;
+ }
+ }
+ }
+
+ return new Set(result);
+ },
+
+ countNew(categoryId) {
+ const subcategoryIds = this.getSubCategoryIds(categoryId);
return _.chain(this.states)
.filter(isNew)
.filter(
topic =>
topic.archetype !== "private_message" &&
!topic.deleted &&
- (topic.category_id === category_id ||
- topic.parent_category_id === category_id ||
- !category_id)
+ (!categoryId || subcategoryIds.has(topic.category_id))
)
.value().length;
},
- resetNew() {
- Object.keys(this.states).forEach(id => {
- if (this.states[id].last_read_post_number === null) {
- delete this.states[id];
- }
- });
- },
-
- countUnread(category_id) {
+ countUnread(categoryId) {
+ const subcategoryIds = this.getSubCategoryIds(categoryId);
return _.chain(this.states)
.filter(isUnread)
.filter(
topic =>
topic.archetype !== "private_message" &&
!topic.deleted &&
- (topic.category_id === category_id ||
- topic.parent_category_id === category_id ||
- !category_id)
+ (!categoryId || subcategoryIds.has(topic.category_id))
)
.value().length;
},
@@ -392,8 +417,8 @@ const TopicTrackingState = Discourse.Model.extend({
);
}
- let categoryId = category ? Ember.get(category, "id") : null;
- let categoryName = category ? Ember.get(category, "name") : null;
+ let categoryId = category ? get(category, "id") : null;
+ let categoryName = category ? get(category, "name") : null;
if (name === "new") {
return this.countNew(categoryId);
@@ -409,15 +434,10 @@ const TopicTrackingState = Discourse.Model.extend({
loadStates(data) {
const states = this.states;
- const idMap = Discourse.Category.idMap();
// I am taking some shortcuts here to avoid 500 gets for a large list
if (data) {
data.forEach(topic => {
- var category = idMap[topic.category_id];
- if (category && category.parent_category_id) {
- topic.parent_category_id = category.parent_category_id;
- }
states["t" + topic.topic_id] = topic;
});
}
diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6
index a86a33303f..fcf1892ec8 100644
--- a/app/assets/javascripts/discourse/models/topic.js.es6
+++ b/app/assets/javascripts/discourse/models/topic.js.es6
@@ -1,3 +1,6 @@
+import { get } from "@ember/object";
+import { not, notEmpty, equal, and, or } from "@ember/object/computed";
+import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { flushMap } from "discourse/models/store";
import RestModel from "discourse/models/rest";
@@ -10,23 +13,26 @@ 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,
+import discourseComputed, {
observes,
on
-} from "ember-addons/ember-computed-decorators";
+} from "discourse-common/utils/decorators";
+import Category from "discourse/models/category";
+import Session from "discourse/models/session";
+import { Promise } from "rsvp";
+import Site from "discourse/models/site";
+import User from "discourse/models/user";
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/")}${topic.id}`;
const jsonUrl = (data.nearPost ? `${url}/${data.nearPost}` : url) + ".json";
delete data.nearPost;
delete data.__type;
delete data.store;
- return PreloadStore.getAndRemove(`topic_${topicId}`, () =>
+ return PreloadStore.getAndRemove(`topic_${topic.id}`, () =>
ajax(jsonUrl, { data })
).then(json => {
topic.updateFromJson(json);
@@ -40,18 +46,18 @@ const Topic = RestModel.extend({
message: null,
errorLoading: false,
- @computed("last_read_post_number", "highest_post_number")
+ @discourseComputed("last_read_post_number", "highest_post_number")
visited(lastReadPostNumber, highestPostNumber) {
// >= to handle case where there are deleted posts at the end of the topic
return lastReadPostNumber >= highestPostNumber;
},
- @computed("posters.firstObject")
+ @discourseComputed("posters.firstObject")
creator(poster) {
return poster && poster.user;
},
- @computed("posters.[]")
+ @discourseComputed("posters.[]")
lastPoster(posters) {
let user;
if (posters && posters.length > 0) {
@@ -63,8 +69,8 @@ const Topic = RestModel.extend({
return user || this.creator;
},
- @computed("posters.[]", "participants.[]")
- featuredUsers(posters, participants) {
+ @discourseComputed("posters.[]", "participants.[]", "allowed_user_count")
+ featuredUsers(posters, participants, allowedUserCount) {
let users = posters;
const maxUserCount = 5;
const posterCount = users.length;
@@ -90,14 +96,21 @@ const Topic = RestModel.extend({
});
}
+ if (this.isPrivateMessage && allowedUserCount > maxUserCount) {
+ users.splice(maxUserCount - 2, 1); // remove second-last avatar
+ users.push({
+ moreCount: `+${allowedUserCount - maxUserCount + 1}`
+ });
+ }
+
return users;
},
- @computed("fancy_title")
+ @discourseComputed("fancy_title")
fancyTitle(title) {
let fancyTitle = censor(
- emojiUnescape(title || ""),
- Discourse.Site.currentProp("censored_words")
+ emojiUnescape(title) || "",
+ Site.currentProp("censored_regexp")
);
if (Discourse.SiteSettings.support_mixed_text_direction) {
@@ -108,7 +121,7 @@ const Topic = RestModel.extend({
},
// returns createdAt if there's no bumped date
- @computed("bumped_at", "createdAt")
+ @discourseComputed("bumped_at", "createdAt")
bumpedAt(bumped_at, createdAt) {
if (bumped_at) {
return new Date(bumped_at);
@@ -117,7 +130,7 @@ const Topic = RestModel.extend({
}
},
- @computed("bumpedAt", "createdAt")
+ @discourseComputed("bumpedAt", "createdAt")
bumpedAtTitle(bumpedAt, createdAt) {
const firstPost = I18n.t("first_post");
const lastPost = I18n.t("last_post");
@@ -127,12 +140,12 @@ const Topic = RestModel.extend({
return `${firstPost}: ${createdAtDate}\n${lastPost}: ${bumpedAtDate}`;
},
- @computed("created_at")
+ @discourseComputed("created_at")
createdAt(created_at) {
return new Date(created_at);
},
- @computed
+ @discourseComputed
postStream() {
return this.store.createRecord("postStream", {
id: this.id,
@@ -140,7 +153,7 @@ const Topic = RestModel.extend({
});
},
- @computed("tags")
+ @discourseComputed("tags")
visibleListTags(tags) {
if (!tags || !Discourse.SiteSettings.suppress_overlapping_tags_in_list) {
return tags;
@@ -158,7 +171,7 @@ const Topic = RestModel.extend({
return newTags;
},
- @computed("related_messages")
+ @discourseComputed("related_messages")
relatedMessages(relatedMessages) {
if (relatedMessages) {
const store = this.store;
@@ -170,7 +183,7 @@ const Topic = RestModel.extend({
}
},
- @computed("suggested_topics")
+ @discourseComputed("suggested_topics")
suggestedTopics(suggestedTopics) {
if (suggestedTopics) {
const store = this.store;
@@ -182,12 +195,12 @@ const Topic = RestModel.extend({
}
},
- @computed("posts_count")
+ @discourseComputed("posts_count")
replyCount(postsCount) {
return postsCount - 1;
},
- @computed
+ @discourseComputed
details() {
return this.store.createRecord("topicDetails", {
id: this.id,
@@ -195,10 +208,10 @@ const Topic = RestModel.extend({
});
},
- invisible: Ember.computed.not("visible"),
- deleted: Ember.computed.notEmpty("deleted_at"),
+ invisible: not("visible"),
+ deleted: notEmpty("deleted_at"),
- @computed("id")
+ @discourseComputed("id")
searchContext(id) {
return { type: "topic", id };
},
@@ -206,7 +219,7 @@ const Topic = RestModel.extend({
@on("init")
@observes("category_id")
_categoryIdChanged() {
- this.set("category", Discourse.Category.findById(this.category_id));
+ this.set("category", Category.findById(this.category_id));
},
@observes("categoryName")
@@ -221,21 +234,21 @@ const Topic = RestModel.extend({
categoryClass: fmt("category.fullSlug", "category-%@"),
- @computed("tags")
+ @discourseComputed("tags")
tagClasses(tags) {
return tags && tags.map(t => `tag-${t}`).join(" ");
},
- @computed("url")
+ @discourseComputed("url")
shareUrl(url) {
- const user = Discourse.User.current();
+ const user = User.current();
const userQueryString = user ? `?u=${user.get("username_lower")}` : "";
return `${url}${userQueryString}`;
},
printUrl: fmt("url", "%@/print"),
- @computed("id", "slug")
+ @discourseComputed("id", "slug")
url(id, slug) {
slug = slug || "";
if (slug.trim().length === 0) {
@@ -253,18 +266,18 @@ const Topic = RestModel.extend({
return url;
},
- @computed("new_posts", "unread")
+ @discourseComputed("new_posts", "unread")
totalUnread(newPosts, unread) {
const count = (unread || 0) + (newPosts || 0);
return count > 0 ? count : null;
},
- @computed("last_read_post_number", "url")
+ @discourseComputed("last_read_post_number", "url")
lastReadUrl(lastReadPostNumber) {
return this.urlForPostNumber(lastReadPostNumber);
},
- @computed("last_read_post_number", "highest_post_number", "url")
+ @discourseComputed("last_read_post_number", "highest_post_number", "url")
lastUnreadUrl(lastReadPostNumber, highestPostNumber) {
if (highestPostNumber <= lastReadPostNumber) {
if (this.get("category.navigate_to_first_post_after_read")) {
@@ -277,23 +290,23 @@ const Topic = RestModel.extend({
}
},
- @computed("highest_post_number", "url")
+ @discourseComputed("highest_post_number", "url")
lastPostUrl(highestPostNumber) {
return this.urlForPostNumber(highestPostNumber);
},
- @computed("url")
+ @discourseComputed("url")
firstPostUrl() {
return this.urlForPostNumber(1);
},
- @computed("url")
+ @discourseComputed("url")
summaryUrl() {
const summaryQueryString = this.has_summary ? "?filter=summary" : "";
return `${this.urlForPostNumber(1)}${summaryQueryString}`;
},
- @computed("last_poster.username")
+ @discourseComputed("last_poster.username")
lastPosterUrl(username) {
return userPath(username);
},
@@ -301,9 +314,9 @@ 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.
- @computed("new_posts", "id")
+ @discourseComputed("new_posts", "id")
displayNewPosts(newPosts, id) {
- const highestSeen = Discourse.Session.currentProp("highestSeenByTopic")[id];
+ const highestSeen = Session.currentProp("highestSeenByTopic")[id];
if (highestSeen) {
const delta = highestSeen - this.last_read_post_number;
if (delta > 0) {
@@ -317,7 +330,7 @@ const Topic = RestModel.extend({
return newPosts;
},
- @computed("views")
+ @discourseComputed("views")
viewsHeat(v) {
if (v >= Discourse.SiteSettings.topic_views_heat_high) {
return "heatmap-high";
@@ -331,13 +344,13 @@ const Topic = RestModel.extend({
return null;
},
- @computed("archetype")
+ @discourseComputed("archetype")
archetypeObject(archetype) {
- return Discourse.Site.currentProp("archetypes").findBy("id", archetype);
+ return Site.currentProp("archetypes").findBy("id", archetype);
},
- isPrivateMessage: Ember.computed.equal("archetype", "private_message"),
- isBanner: Ember.computed.equal("archetype", "banner"),
+ isPrivateMessage: equal("archetype", "private_message"),
+ isBanner: equal("archetype", "banner"),
toggleStatus(property) {
this.toggleProperty(property);
@@ -372,12 +385,12 @@ const Topic = RestModel.extend({
toggleBookmark() {
if (this.bookmarking) {
- return Ember.RSVP.Promise.resolve();
+ return Promise.resolve();
}
this.set("bookmarking", true);
const stream = this.postStream;
- const posts = Ember.get(stream, "posts");
+ const posts = get(stream, "posts");
const firstPost =
posts && posts[0] && posts[0].get("post_number") === 1 && posts[0];
const bookmark = !this.bookmarked;
@@ -389,6 +402,9 @@ const Topic = RestModel.extend({
this.toggleProperty("bookmarked");
if (bookmark && firstPost) {
firstPost.set("bookmarked", true);
+ if (this.siteSettings.enable_bookmarks_with_reminders) {
+ firstPost.set("bookmarked_with_reminder", true);
+ }
return [firstPost.id];
}
if (!bookmark && posts) {
@@ -396,7 +412,14 @@ const Topic = RestModel.extend({
posts.forEach(post => {
if (post.get("bookmarked")) {
post.set("bookmarked", false);
- updated.push(post.get("id"));
+ updated.push(post.id);
+ }
+ if (
+ this.siteSettings.enable_bookmarks_with_reminders &&
+ post.get("bookmarked_with_reminder")
+ ) {
+ post.set("bookmarked_with_reminder", false);
+ updated.push(post.id);
}
});
return updated;
@@ -411,11 +434,13 @@ const Topic = RestModel.extend({
const unbookmarkedPosts = [];
if (!bookmark && posts) {
posts.forEach(
- post => post.get("bookmarked") && unbookmarkedPosts.push(post)
+ post =>
+ (post.get("bookmarked") || post.get("bookmarked_with_reminder")) &&
+ unbookmarkedPosts.push(post)
);
}
- return new Ember.RSVP.Promise(resolve => {
+ return new Promise(resolve => {
if (unbookmarkedPosts.length > 1) {
bootbox.confirm(
I18n.t("bookmarks.confirm_clear"),
@@ -453,16 +478,19 @@ const Topic = RestModel.extend({
// Delete this topic
destroy(deleted_by) {
- this.setProperties({
- deleted_at: new Date(),
- deleted_by: deleted_by,
- "details.can_delete": false,
- "details.can_recover": true
- });
return ajax(`/t/${this.id}`, {
data: { context: window.location.pathname },
type: "DELETE"
- });
+ })
+ .then(() => {
+ this.setProperties({
+ deleted_at: new Date(),
+ deleted_by: deleted_by,
+ "details.can_delete": false,
+ "details.can_recover": true
+ });
+ })
+ .catch(popupAjaxError);
},
// Recover this topic if deleted
@@ -481,12 +509,12 @@ const Topic = RestModel.extend({
// Update our attributes from a JSON result
updateFromJson(json) {
- this.details.updateFromJson(json.details);
-
const keys = Object.keys(json);
- keys.removeObject("details");
- keys.removeObject("post_stream");
+ if (!json.view_hidden) {
+ this.details.updateFromJson(json.details);
+ keys.removeObjects(["details", "post_stream"]);
+ }
keys.forEach(key => this.set(key, json[key]));
},
@@ -496,10 +524,7 @@ const Topic = RestModel.extend({
);
},
- isPinnedUncategorized: Ember.computed.and(
- "pinned",
- "category.isUncategorizedCategory"
- ),
+ isPinnedUncategorized: and("pinned", "category.isUncategorizedCategory"),
clearPin() {
// Clear the pin optimistically from the object
@@ -533,20 +558,21 @@ const Topic = RestModel.extend({
});
},
- @computed("excerpt")
+ @discourseComputed("excerpt")
escapedExcerpt(excerpt) {
return emojiUnescape(excerpt);
},
- hasExcerpt: Ember.computed.notEmpty("excerpt"),
+ hasExcerpt: notEmpty("excerpt"),
- @computed("excerpt")
+ @discourseComputed("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"),
+ canClearPin: and("pinned", "readLastPost"),
+ canEditTags: or("details.can_edit", "details.can_edit_tags"),
archiveMessage() {
this.set("archiving", true);
@@ -599,16 +625,29 @@ const Topic = RestModel.extend({
});
},
- convertTopic(type) {
- return ajax(`/t/${this.id}/convert-topic/${type}`, { type: "PUT" })
- .then(() => window.location.reload())
- .catch(popupAjaxError);
+ convertTopic(type, opts) {
+ let args = { type: "PUT" };
+ if (opts && opts.categoryId) {
+ args.data = { category_id: opts.categoryId };
+ }
+ return ajax(`/t/${this.id}/convert-topic/${type}`, args);
},
resetBumpDate() {
return ajax(`/t/${this.id}/reset-bump-date`, { type: "PUT" }).catch(
popupAjaxError
);
+ },
+
+ updateTags(tags) {
+ if (!tags || tags.length === 0) {
+ tags = [""];
+ }
+
+ return ajax(`/t/${this.id}/tags`, {
+ type: "PUT",
+ data: { tags: tags }
+ });
}
});
@@ -622,10 +661,10 @@ Topic.reopenClass({
createActionSummary(result) {
if (result.actions_summary) {
- const lookup = Ember.Object.create();
+ const lookup = EmberObject.create();
result.actions_summary = result.actions_summary.map(a => {
a.post = result;
- a.actionType = Discourse.Site.current().postActionTypeById(a.id);
+ a.actionType = Site.current().postActionTypeById(a.id);
const actionSummary = ActionSummary.create(a);
lookup.set(a.actionType.get("name_key"), actionSummary);
return actionSummary;
@@ -635,8 +674,6 @@ Topic.reopenClass({
},
update(topic, props) {
- props = JSON.parse(JSON.stringify(props)) || {};
-
// We support `category_id` and `categoryId` for compatibility
if (typeof props.categoryId !== "undefined") {
props.category_id = props.categoryId;
@@ -648,11 +685,11 @@ Topic.reopenClass({
delete props.category_id;
}
- if (props.tags && props.tags.length === 0) {
- props.tags = [""];
- }
-
- return ajax(topic.get("url"), { type: "PUT", data: props }).then(result => {
+ return ajax(topic.get("url"), {
+ type: "PUT",
+ data: JSON.stringify(props),
+ contentType: "application/json"
+ }).then(result => {
// The title can be cleaned up server side
props.title = result.basic_topic.title;
props.fancy_title = result.basic_topic.fancy_title;
@@ -733,8 +770,13 @@ Topic.reopenClass({
});
},
- bulkOperationByFilter(filter, operation, categoryId) {
- const data = { filter, operation };
+ bulkOperationByFilter(filter, operation, categoryId, options) {
+ let data = { filter, operation };
+
+ if (options && options.includeSubcategories) {
+ data.include_subcategories = true;
+ }
+
if (categoryId) data.category_id = categoryId;
return ajax("/topics/bulk", {
type: "PUT",
@@ -742,8 +784,11 @@ Topic.reopenClass({
});
},
- resetNew() {
- return ajax("/topics/reset-new", { type: "PUT" });
+ resetNew(category, include_subcategories) {
+ const data = category
+ ? { category_id: category.id, include_subcategories }
+ : {};
+ return ajax("/topics/reset-new", { type: "PUT", data });
},
idForSlug(slug) {
diff --git a/app/assets/javascripts/discourse/models/user-action-group.js.es6 b/app/assets/javascripts/discourse/models/user-action-group.js.es6
index b0706cd324..c432cd694c 100644
--- a/app/assets/javascripts/discourse/models/user-action-group.js.es6
+++ b/app/assets/javascripts/discourse/models/user-action-group.js.es6
@@ -1,8 +1,7 @@
-/**
- A data model representing a group of UserActions
-**/
-export default Discourse.Model.extend({
- push: function(item) {
+import EmberObject from "@ember/object";
+
+export default EmberObject.extend({
+ push(item) {
if (!this.items) {
this.items = [];
}
diff --git a/app/assets/javascripts/discourse/models/user-action-stat.js.es6 b/app/assets/javascripts/discourse/models/user-action-stat.js.es6
index fd531cde9a..0791edd34d 100644
--- a/app/assets/javascripts/discourse/models/user-action-stat.js.es6
+++ b/app/assets/javascripts/discourse/models/user-action-stat.js.es6
@@ -1,10 +1,10 @@
+import discourseComputed from "discourse-common/utils/decorators";
import RestModel from "discourse/models/rest";
import UserAction from "discourse/models/user-action";
import { i18n } from "discourse/lib/computed";
-import computed from "ember-addons/ember-computed-decorators";
export default RestModel.extend({
- @computed("action_type")
+ @discourseComputed("action_type")
isPM(actionType) {
return (
actionType === UserAction.TYPES.messages_sent ||
@@ -14,7 +14,7 @@ export default RestModel.extend({
description: i18n("action_type", "user_action_groups.%@"),
- @computed("action_type")
+ @discourseComputed("action_type")
isResponse(actionType) {
return (
actionType === UserAction.TYPES.replies ||
diff --git a/app/assets/javascripts/discourse/models/user-action.js.es6 b/app/assets/javascripts/discourse/models/user-action.js.es6
index 50600bc7fb..eea26a7ab4 100644
--- a/app/assets/javascripts/discourse/models/user-action.js.es6
+++ b/app/assets/javascripts/discourse/models/user-action.js.es6
@@ -1,9 +1,12 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import { or, equal, and } from "@ember/object/computed";
import RestModel from "discourse/models/rest";
-import { on } from "ember-addons/ember-computed-decorators";
-import computed from "ember-addons/ember-computed-decorators";
+import { on } from "discourse-common/utils/decorators";
import UserActionGroup from "discourse/models/user-action-group";
import { postUrl } from "discourse/lib/utilities";
import { userPath } from "discourse/lib/url";
+import Category from "discourse/models/category";
+import User from "discourse/models/user";
const UserActionTypes = {
likes_given: 1,
@@ -30,11 +33,11 @@ const UserAction = RestModel.extend({
_attachCategory() {
const categoryId = this.category_id;
if (categoryId) {
- this.set("category", Discourse.Category.findById(categoryId));
+ this.set("category", Category.findById(categoryId));
}
},
- @computed("action_type")
+ @discourseComputed("action_type")
descriptionKey(action) {
if (action === null || UserAction.TO_SHOW.indexOf(action) >= 0) {
if (this.isPM) {
@@ -65,61 +68,55 @@ const UserAction = RestModel.extend({
}
},
- @computed("username")
+ @discourseComputed("username")
sameUser(username) {
- return username === Discourse.User.currentProp("username");
+ return username === User.currentProp("username");
},
- @computed("target_username")
+ @discourseComputed("target_username")
targetUser(targetUsername) {
- return targetUsername === Discourse.User.currentProp("username");
+ return targetUsername === User.currentProp("username");
},
- presentName: Ember.computed.or("name", "username"),
- targetDisplayName: Ember.computed.or("target_name", "target_username"),
- actingDisplayName: Ember.computed.or("acting_name", "acting_username"),
+ presentName: or("name", "username"),
+ targetDisplayName: or("target_name", "target_username"),
+ actingDisplayName: or("acting_name", "acting_username"),
- @computed("target_username")
+ @discourseComputed("target_username")
targetUserUrl(username) {
return userPath(username);
},
- @computed("username")
+ @discourseComputed("username")
usernameLower(username) {
return username.toLowerCase();
},
- @computed("usernameLower")
+ @discourseComputed("usernameLower")
userUrl(usernameLower) {
return userPath(usernameLower);
},
- @computed()
+ @discourseComputed()
postUrl() {
return postUrl(this.slug, this.topic_id, this.post_number);
},
- @computed()
+ @discourseComputed()
replyUrl() {
return postUrl(this.slug, this.topic_id, this.reply_to_post_number);
},
- replyType: Ember.computed.equal("action_type", UserActionTypes.replies),
- postType: Ember.computed.equal("action_type", UserActionTypes.posts),
- topicType: Ember.computed.equal("action_type", UserActionTypes.topics),
- bookmarkType: Ember.computed.equal("action_type", UserActionTypes.bookmarks),
- messageSentType: Ember.computed.equal(
- "action_type",
- UserActionTypes.messages_sent
- ),
- messageReceivedType: Ember.computed.equal(
- "action_type",
- UserActionTypes.messages_received
- ),
- mentionType: Ember.computed.equal("action_type", UserActionTypes.mentions),
- isPM: Ember.computed.or("messageSentType", "messageReceivedType"),
- postReplyType: Ember.computed.or("postType", "replyType"),
- removableBookmark: Ember.computed.and("bookmarkType", "sameUser"),
+ replyType: equal("action_type", UserActionTypes.replies),
+ postType: equal("action_type", UserActionTypes.posts),
+ topicType: equal("action_type", UserActionTypes.topics),
+ bookmarkType: equal("action_type", UserActionTypes.bookmarks),
+ messageSentType: equal("action_type", UserActionTypes.messages_sent),
+ messageReceivedType: equal("action_type", UserActionTypes.messages_received),
+ mentionType: equal("action_type", UserActionTypes.mentions),
+ isPM: or("messageSentType", "messageReceivedType"),
+ postReplyType: or("postType", "replyType"),
+ removableBookmark: and("bookmarkType", "sameUser"),
addChild(action) {
let groups = this.childGroups;
@@ -150,7 +147,7 @@ const UserAction = RestModel.extend({
}
},
- @computed(
+ @discourseComputed(
"childGroups",
"childGroups.likes.items",
"childGroups.likes.items.[]",
diff --git a/app/assets/javascripts/discourse/models/user-badge.js.es6 b/app/assets/javascripts/discourse/models/user-badge.js.es6
index a8850674a4..67b3133e15 100644
--- a/app/assets/javascripts/discourse/models/user-badge.js.es6
+++ b/app/assets/javascripts/discourse/models/user-badge.js.es6
@@ -1,9 +1,13 @@
+import discourseComputed from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
import Badge from "discourse/models/badge";
-import computed from "ember-addons/ember-computed-decorators";
+import { Promise } from "rsvp";
+import Topic from "discourse/models/topic";
+import EmberObject from "@ember/object";
+import User from "discourse/models/user";
-const UserBadge = Discourse.Model.extend({
- @computed
+const UserBadge = EmberObject.extend({
+ @discourseComputed
postUrl: function() {
if (this.topic_title) {
return "/t/-/" + this.topic_id + "/" + this.post_number;
@@ -25,7 +29,7 @@ UserBadge.reopenClass({
}
var users = {};
json.users.forEach(function(userJson) {
- users[userJson.id] = Discourse.User.create(userJson);
+ users[userJson.id] = User.create(userJson);
});
// Create Topic objects.
@@ -34,7 +38,7 @@ UserBadge.reopenClass({
}
var topics = {};
json.topics.forEach(function(topicJson) {
- topics[topicJson.id] = Discourse.Topic.create(topicJson);
+ topics[topicJson.id] = Topic.create(topicJson);
});
// Create the badges.
@@ -96,7 +100,7 @@ UserBadge.reopenClass({
**/
findByUsername: function(username, options) {
if (!username) {
- return Ember.RSVP.resolve([]);
+ return Promise.resolve([]);
}
var url = "/user-badges/" + username + ".json";
if (options && options.grouped) {
diff --git a/app/assets/javascripts/discourse/models/user-draft.js.es6 b/app/assets/javascripts/discourse/models/user-draft.js.es6
index 2ab353e92c..6e31ae3b2f 100644
--- a/app/assets/javascripts/discourse/models/user-draft.js.es6
+++ b/app/assets/javascripts/discourse/models/user-draft.js.es6
@@ -1,33 +1,32 @@
+import discourseComputed from "discourse-common/utils/decorators";
import RestModel from "discourse/models/rest";
-import computed from "ember-addons/ember-computed-decorators";
import { postUrl } from "discourse/lib/utilities";
import { userPath } from "discourse/lib/url";
import User from "discourse/models/user";
-
import {
NEW_TOPIC_KEY,
NEW_PRIVATE_MESSAGE_KEY
} from "discourse/models/composer";
export default RestModel.extend({
- @computed("draft_username")
+ @discourseComputed("draft_username")
editableDraft(draftUsername) {
return draftUsername === User.currentProp("username");
},
- @computed("username_lower")
+ @discourseComputed("username_lower")
userUrl(usernameLower) {
return userPath(usernameLower);
},
- @computed("topic_id")
+ @discourseComputed("topic_id")
postUrl(topicId) {
if (!topicId) return;
return postUrl(this.slug, this.topic_id, this.post_number);
},
- @computed("draft_key")
+ @discourseComputed("draft_key")
draftType(draftKey) {
switch (draftKey) {
case NEW_TOPIC_KEY:
diff --git a/app/assets/javascripts/discourse/models/user-drafts-stream.js.es6 b/app/assets/javascripts/discourse/models/user-drafts-stream.js.es6
index c384b49569..7472cc8e67 100644
--- a/app/assets/javascripts/discourse/models/user-drafts-stream.js.es6
+++ b/app/assets/javascripts/discourse/models/user-drafts-stream.js.es6
@@ -1,10 +1,10 @@
+import discourseComputed from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
import { url } from "discourse/lib/computed";
import RestModel from "discourse/models/rest";
import UserDraft from "discourse/models/user-draft";
import { emojiUnescape } from "discourse/lib/text";
-import computed from "ember-addons/ember-computed-decorators";
-
+import { Promise } from "rsvp";
import {
NEW_TOPIC_KEY,
NEW_PRIVATE_MESSAGE_KEY
@@ -38,7 +38,7 @@ export default RestModel.extend({
return this.findItems();
},
- @computed("content.length", "loaded")
+ @discourseComputed("content.length", "loaded")
noContent(contentLength, loaded) {
return loaded && contentLength === 0;
},
@@ -55,11 +55,11 @@ export default RestModel.extend({
const lastLoadedUrl = this.lastLoadedUrl;
if (lastLoadedUrl === findUrl) {
- return Ember.RSVP.resolve();
+ return Promise.resolve();
}
if (this.loading) {
- return Ember.RSVP.resolve();
+ return Promise.resolve();
}
this.set("loading", true);
diff --git a/app/assets/javascripts/discourse/models/user-posts-stream.js.es6 b/app/assets/javascripts/discourse/models/user-posts-stream.js.es6
index 7521b82dce..5a71149fb5 100644
--- a/app/assets/javascripts/discourse/models/user-posts-stream.js.es6
+++ b/app/assets/javascripts/discourse/models/user-posts-stream.js.es6
@@ -1,9 +1,11 @@
-import { on } from "ember-addons/ember-computed-decorators";
+import { on } from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
import { url } from "discourse/lib/computed";
import UserAction from "discourse/models/user-action";
+import { Promise } from "rsvp";
+import EmberObject from "@ember/object";
-export default Discourse.Model.extend({
+export default EmberObject.extend({
loaded: false,
@on("init")
@@ -24,7 +26,7 @@ export default Discourse.Model.extend({
filterBy(opts) {
if (this.loaded && this.filter === opts.filter) {
- return Ember.RSVP.resolve();
+ return Promise.resolve();
}
this.setProperties(
@@ -43,7 +45,7 @@ export default Discourse.Model.extend({
findItems() {
if (this.loading || !this.canLoadMore) {
- return Ember.RSVP.reject();
+ return Promise.reject();
}
this.set("loading", true);
diff --git a/app/assets/javascripts/discourse/models/user-stream.js.es6 b/app/assets/javascripts/discourse/models/user-stream.js.es6
index 35e44cc661..81af6b30f8 100644
--- a/app/assets/javascripts/discourse/models/user-stream.js.es6
+++ b/app/assets/javascripts/discourse/models/user-stream.js.es6
@@ -3,10 +3,8 @@ import { url } from "discourse/lib/computed";
import RestModel from "discourse/models/rest";
import UserAction from "discourse/models/user-action";
import { emojiUnescape } from "discourse/lib/text";
-import {
- default as computed,
- on
-} from "ember-addons/ember-computed-decorators";
+import { Promise } from "rsvp";
+import discourseComputed, { on } from "discourse-common/utils/decorators";
export default RestModel.extend({
loaded: false,
@@ -16,9 +14,9 @@ export default RestModel.extend({
this.setProperties({ itemsLoaded: 0, content: [] });
},
- @computed("filter")
+ @discourseComputed("filter")
filterParam(filter) {
- if (filter === Discourse.UserAction.TYPES.replies) {
+ if (filter === UserAction.TYPES.replies) {
return [UserAction.TYPES.replies, UserAction.TYPES.quotes].join(",");
}
@@ -50,7 +48,7 @@ export default RestModel.extend({
return this.findItems();
},
- @computed("loaded", "content.[]")
+ @discourseComputed("loaded", "content.[]")
noContent(loaded, content) {
return loaded && content.length === 0;
},
@@ -92,11 +90,11 @@ export default RestModel.extend({
// Don't load the same stream twice. We're probably at the end.
const lastLoadedUrl = this.lastLoadedUrl;
if (lastLoadedUrl === findUrl) {
- return Ember.RSVP.resolve();
+ return Promise.resolve();
}
if (this.loading) {
- return Ember.RSVP.resolve();
+ return Promise.resolve();
}
this.set("loading", true);
diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index d16f31721d..6927676805 100644
--- a/app/assets/javascripts/discourse/models/user.js.es6
+++ b/app/assets/javascripts/discourse/models/user.js.es6
@@ -1,3 +1,6 @@
+import { isEmpty } from "@ember/utils";
+import { gt, equal, or } from "@ember/object/computed";
+import EmberObject, { computed } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { url } from "discourse/lib/computed";
import RestModel from "discourse/models/rest";
@@ -5,10 +8,7 @@ import UserStream from "discourse/models/user-stream";
import UserPostsStream from "discourse/models/user-posts-stream";
import Singleton from "discourse/mixins/singleton";
import { longDate } from "discourse/lib/formatter";
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Badge from "discourse/models/badge";
import UserBadge from "discourse/models/user-badge";
import UserActionStat from "discourse/models/user-action-stat";
@@ -20,47 +20,64 @@ import PreloadStore from "preload-store";
import { defaultHomepage } from "discourse/lib/utilities";
import { userPath } from "discourse/lib/url";
import Category from "discourse/models/category";
+import { Promise } from "rsvp";
+import { getProperties } from "@ember/object";
+import deprecated from "discourse-common/lib/deprecated";
+import Site from "discourse/models/site";
-export const SECOND_FACTOR_METHODS = { TOTP: 1, BACKUP_CODE: 2 };
+export const SECOND_FACTOR_METHODS = {
+ TOTP: 1,
+ BACKUP_CODE: 2,
+ SECURITY_KEY: 3
+};
const isForever = dt => moment().diff(dt, "years") < -500;
const User = RestModel.extend({
- hasPMs: Ember.computed.gt("private_messages_stats.all", 0),
- hasStartedPMs: Ember.computed.gt("private_messages_stats.mine", 0),
- hasUnreadPMs: Ember.computed.gt("private_messages_stats.unread", 0),
+ hasPMs: gt("private_messages_stats.all", 0),
+ hasStartedPMs: gt("private_messages_stats.mine", 0),
+ hasUnreadPMs: gt("private_messages_stats.unread", 0),
redirected_to_top: {
reason: null
},
- @computed("can_be_deleted", "post_count")
+ @discourseComputed("can_be_deleted", "post_count")
canBeDeleted(canBeDeleted, postCount) {
return canBeDeleted && postCount <= 5;
},
- @computed()
+ @discourseComputed()
stream() {
return UserStream.create({ user: this });
},
- @computed()
+ @discourseComputed()
postsStream() {
return UserPostsStream.create({ user: this });
},
- @computed()
+ @discourseComputed()
userDraftsStream() {
return UserDraftsStream.create({ user: this });
},
- staff: Ember.computed.or("admin", "moderator"),
+ staff: computed("admin", "moderator", {
+ get() {
+ return this.admin || this.moderator;
+ },
+
+ // prevents staff property to be overridden
+ set() {
+ return this.admin || this.moderator;
+ }
+ }),
destroySession() {
return ajax(`/session/${this.username}`, { type: "DELETE" });
},
- @computed("username_lower")
+ @discourseComputed("username_lower")
searchContext(username) {
return {
type: "user",
@@ -69,20 +86,17 @@ const User = RestModel.extend({
};
},
- @computed("username", "name")
+ @discourseComputed("username", "name")
displayName(username, name) {
- if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(name)) {
+ if (Discourse.SiteSettings.enable_names && !isEmpty(name)) {
return name;
}
return username;
},
- @computed("profile_background_upload_url")
+ @discourseComputed("profile_background_upload_url")
profileBackgroundUrl(bgUrl) {
- if (
- Ember.isEmpty(bgUrl) ||
- !Discourse.SiteSettings.allow_profile_backgrounds
- ) {
+ if (isEmpty(bgUrl) || !Discourse.SiteSettings.allow_profile_backgrounds) {
return "".htmlSafe();
}
return (
@@ -92,18 +106,18 @@ const User = RestModel.extend({
).htmlSafe();
},
- @computed()
+ @discourseComputed()
path() {
// no need to observe, requires a hard refresh to update
return userPath(this.username_lower);
},
- @computed()
+ @discourseComputed()
userApiKeys() {
const keys = this.user_api_keys;
if (keys) {
return keys.map(raw => {
- let obj = Ember.Object.create(raw);
+ let obj = EmberObject.create(raw);
obj.revoke = () => {
this.revokeApiKey(obj);
@@ -156,65 +170,65 @@ const User = RestModel.extend({
adminPath: url("id", "username_lower", "/admin/users/%@1/%@2"),
- @computed()
+ @discourseComputed()
mutedTopicsPath() {
return defaultHomepage() === "latest"
? Discourse.getURL("/?state=muted")
: Discourse.getURL("/latest?state=muted");
},
- @computed()
+ @discourseComputed()
watchingTopicsPath() {
return defaultHomepage() === "latest"
? Discourse.getURL("/?state=watching")
: Discourse.getURL("/latest?state=watching");
},
- @computed()
+ @discourseComputed()
trackingTopicsPath() {
return defaultHomepage() === "latest"
? Discourse.getURL("/?state=tracking")
: Discourse.getURL("/latest?state=tracking");
},
- @computed("username")
+ @discourseComputed("username")
username_lower(username) {
return username.toLowerCase();
},
- @computed("trust_level")
+ @discourseComputed("trust_level")
trustLevel(trustLevel) {
- return Discourse.Site.currentProp("trustLevels").findBy(
+ return Site.currentProp("trustLevels").findBy(
"id",
parseInt(trustLevel, 10)
);
},
- isBasic: Ember.computed.equal("trust_level", 0),
- isLeader: Ember.computed.equal("trust_level", 3),
- isElder: Ember.computed.equal("trust_level", 4),
- canManageTopic: Ember.computed.or("staff", "isElder"),
+ isBasic: equal("trust_level", 0),
+ isLeader: equal("trust_level", 3),
+ isElder: equal("trust_level", 4),
+ canManageTopic: or("staff", "isElder"),
- @computed("previous_visit_at")
+ @discourseComputed("previous_visit_at")
previousVisitAt(previous_visit_at) {
return new Date(previous_visit_at);
},
- @computed("suspended_till")
+ @discourseComputed("suspended_till")
suspended(suspendedTill) {
return suspendedTill && moment(suspendedTill).isAfter();
},
- @computed("suspended_till")
+ @discourseComputed("suspended_till")
suspendedForever: isForever,
- @computed("silenced_till")
+ @discourseComputed("silenced_till")
silencedForever: isForever,
- @computed("suspended_till")
+ @discourseComputed("suspended_till")
suspendedTillDate: longDate,
- @computed("silenced_till")
+ @discourseComputed("silenced_till")
silencedTillDate: longDate,
changeUsername(new_username) {
@@ -232,7 +246,7 @@ const User = RestModel.extend({
},
copy() {
- return Discourse.User.create(this.getProperties(Object.keys(this)));
+ return User.create(this.getProperties(Object.keys(this)));
},
save(fields) {
@@ -253,7 +267,8 @@ const User = RestModel.extend({
"tracked_tags",
"watched_tags",
"watching_first_post_tags",
- "date_of_birth"
+ "date_of_birth",
+ "primary_group_id"
];
const data = this.getProperties(
@@ -284,7 +299,8 @@ const User = RestModel.extend({
"homepage_id",
"hide_profile_and_presence",
"text_size",
- "title_count_mode"
+ "title_count_mode",
+ "timezone"
];
if (fields) {
@@ -336,14 +352,14 @@ const User = RestModel.extend({
})
.then(result => {
this.set("bio_excerpt", result.user.bio_excerpt);
- const userProps = Ember.getProperties(
+ const userProps = getProperties(
this.user_option,
"enable_quoting",
"enable_defer",
"external_links_in_new_tab",
"dynamic_favicon"
);
- Discourse.User.current().setProperties(userProps);
+ User.current().setProperties(userProps);
this.setProperties(updatedState);
})
.finally(() => {
@@ -366,6 +382,64 @@ const User = RestModel.extend({
});
},
+ requestSecurityKeyChallenge() {
+ return ajax("/u/create_second_factor_security_key.json", {
+ type: "POST"
+ });
+ },
+
+ registerSecurityKey(credential) {
+ return ajax("/u/register_second_factor_security_key.json", {
+ data: credential,
+ type: "POST"
+ });
+ },
+
+ createSecondFactorTotp() {
+ return ajax("/u/create_second_factor_totp.json", {
+ type: "POST"
+ });
+ },
+
+ enableSecondFactorTotp(authToken, name) {
+ return ajax("/u/enable_second_factor_totp.json", {
+ data: {
+ second_factor_token: authToken,
+ name
+ },
+ type: "POST"
+ });
+ },
+
+ disableAllSecondFactors() {
+ return ajax("/u/disable_second_factor.json", {
+ type: "PUT"
+ });
+ },
+
+ updateSecondFactor(id, name, disable, targetMethod) {
+ return ajax("/u/second_factor.json", {
+ data: {
+ second_factor_target: targetMethod,
+ name,
+ disable,
+ id
+ },
+ type: "PUT"
+ });
+ },
+
+ updateSecurityKey(id, name, disable) {
+ return ajax("/u/security_key.json", {
+ data: {
+ name,
+ disable,
+ id
+ },
+ type: "PUT"
+ });
+ },
+
toggleSecondFactor(authToken, authMethod, targetMethod, enable) {
return ajax("/u/second_factor.json", {
data: {
@@ -378,12 +452,8 @@ const User = RestModel.extend({
});
},
- generateSecondFactorCodes(authToken, authMethod) {
+ generateSecondFactorCodes() {
return ajax("/u/second_factors_backup.json", {
- data: {
- second_factor_token: authToken,
- second_factor_method: authMethod
- },
type: "PUT"
});
},
@@ -422,7 +492,7 @@ const User = RestModel.extend({
numGroupsToDisplay: 2,
- @computed("groups.[]")
+ @discourseComputed("groups.[]")
filteredGroups() {
const groups = this.groups || [];
@@ -431,21 +501,21 @@ const User = RestModel.extend({
});
},
- @computed("filteredGroups", "numGroupsToDisplay")
+ @discourseComputed("filteredGroups", "numGroupsToDisplay")
displayGroups(filteredGroups, numGroupsToDisplay) {
const groups = filteredGroups.slice(0, numGroupsToDisplay);
return groups.length === 0 ? null : groups;
},
- @computed("filteredGroups", "numGroupsToDisplay")
+ @discourseComputed("filteredGroups", "numGroupsToDisplay")
showMoreGroupsLink(filteredGroups, numGroupsToDisplay) {
return filteredGroups.length > numGroupsToDisplay;
},
// The user's stat count, excluding PMs.
- @computed("statsExcludingPms.@each.count")
+ @discourseComputed("statsExcludingPms.@each.count")
statsCountNonPM() {
- if (Ember.isEmpty(this.statsExcludingPms)) return 0;
+ if (isEmpty(this.statsExcludingPms)) return 0;
let count = 0;
this.statsExcludingPms.forEach(val => {
if (this.inAllStream(val)) {
@@ -456,9 +526,9 @@ const User = RestModel.extend({
},
// The user's stats, excluding PMs.
- @computed("stats.@each.isPM")
+ @discourseComputed("stats.@each.isPM")
statsExcludingPms() {
- if (Ember.isEmpty(this.stats)) return [];
+ if (isEmpty(this.stats)) return [];
return this.stats.rejectBy("isPM");
},
@@ -466,10 +536,18 @@ const User = RestModel.extend({
const user = this;
return PreloadStore.getAndRemove(`user_${user.get("username")}`, () => {
- return ajax(userPath(`${user.get("username")}.json`), { data: options });
+ const useCardRoute = options && options.forCard;
+
+ if (options) delete options.forCard;
+
+ const path = useCardRoute
+ ? `${user.get("username")}/card.json`
+ : `${user.get("username")}.json`;
+
+ return ajax(userPath(path), { data: options });
}).then(json => {
- if (!Ember.isEmpty(json.user.stats)) {
- json.user.stats = Discourse.User.groupStats(
+ if (!isEmpty(json.user.stats)) {
+ json.user.stats = User.groupStats(
json.user.stats.map(s => {
if (s.count) s.count = parseInt(s.count, 10);
return UserActionStat.create(s);
@@ -477,7 +555,7 @@ const User = RestModel.extend({
);
}
- if (!Ember.isEmpty(json.user.groups)) {
+ if (!isEmpty(json.user.groups)) {
const groups = [];
for (let i = 0; i < json.user.groups.length; i++) {
@@ -490,10 +568,10 @@ const User = RestModel.extend({
}
if (json.user.invited_by) {
- json.user.invited_by = Discourse.User.create(json.user.invited_by);
+ json.user.invited_by = User.create(json.user.invited_by);
}
- if (!Ember.isEmpty(json.user.featured_user_badge_ids)) {
+ if (!isEmpty(json.user.featured_user_badge_ids)) {
const userBadgesMap = {};
UserBadge.createFromJson(json).forEach(userBadge => {
userBadgesMap[userBadge.get("id")] = userBadge;
@@ -513,8 +591,8 @@ const User = RestModel.extend({
},
findStaffInfo() {
- if (!Discourse.User.currentProp("staff")) {
- return Ember.RSVP.resolve(null);
+ if (!User.currentProp("staff")) {
+ return Promise.resolve(null);
}
return ajax(userPath(`${this.username_lower}/staff-info.json`)).then(
info => {
@@ -561,17 +639,14 @@ const User = RestModel.extend({
@observes("muted_category_ids")
updateMutedCategories() {
- this.set(
- "mutedCategories",
- Discourse.Category.findByIds(this.muted_category_ids)
- );
+ this.set("mutedCategories", Category.findByIds(this.muted_category_ids));
},
@observes("tracked_category_ids")
updateTrackedCategories() {
this.set(
"trackedCategories",
- Discourse.Category.findByIds(this.tracked_category_ids)
+ Category.findByIds(this.tracked_category_ids)
);
},
@@ -579,7 +654,7 @@ const User = RestModel.extend({
updateWatchedCategories() {
this.set(
"watchedCategories",
- Discourse.Category.findByIds(this.watched_category_ids)
+ Category.findByIds(this.watched_category_ids)
);
},
@@ -587,11 +662,11 @@ const User = RestModel.extend({
updateWatchedFirstPostCategories() {
this.set(
"watchedFirstPostCategories",
- Discourse.Category.findByIds(this.watched_first_post_category_ids)
+ Category.findByIds(this.watched_first_post_category_ids)
);
},
- @computed("can_delete_account")
+ @discourseComputed("can_delete_account")
canDeleteAccount(canDeleteAccount) {
return !Discourse.SiteSettings.enable_sso && canDeleteAccount;
},
@@ -603,7 +678,7 @@ const User = RestModel.extend({
data: { context: window.location.pathname }
});
} else {
- return Ember.RSVP.reject(I18n.t("user.delete_yourself_not_allowed"));
+ return Promise.reject(I18n.t("user.delete_yourself_not_allowed"));
}
},
@@ -612,7 +687,7 @@ const User = RestModel.extend({
type: "PUT",
data: { notification_level: level, expiring_at: expiringAt }
}).then(() => {
- const currentUser = Discourse.User.current();
+ const currentUser = User.current();
if (currentUser) {
if (level === "normal" || level === "mute") {
currentUser.ignored_users.removeObject(this.username);
@@ -698,7 +773,7 @@ const User = RestModel.extend({
: this.admin || group.get("is_group_owner");
},
- @computed("groups.@each.title", "badges.[]")
+ @discourseComputed("groups.@each.title", "badges.[]")
availableTitles() {
let titles = [];
@@ -724,7 +799,7 @@ const User = RestModel.extend({
});
},
- @computed("user_option.text_size_seq", "user_option.text_size")
+ @discourseComputed("user_option.text_size_seq", "user_option.text_size")
currentTextSize(serverSeq, serverSize) {
if ($.cookie("text_size")) {
const [cookieSize, cookieSeq] = $.cookie("text_size").split("|");
@@ -747,7 +822,7 @@ const User = RestModel.extend({
}
},
- @computed("second_factor_enabled", "staff")
+ @discourseComputed("second_factor_enabled", "staff")
enforcedSecondFactor(secondFactorEnabled, staff) {
const enforce = Discourse.SiteSettings.enforce_second_factor;
return (
@@ -758,7 +833,7 @@ const User = RestModel.extend({
});
User.reopenClass(Singleton, {
- // Find a `Discourse.User` for a given username.
+ // Find a `User` for a given username.
findByUsername(username, options) {
const user = User.create({ username: username });
return user.findDetails(options);
@@ -784,6 +859,11 @@ User.reopenClass(Singleton, {
return null;
},
+ resetCurrent(user) {
+ this._super(user);
+ Discourse.currentUser = user;
+ },
+
checkUsername(username, email, for_user_id) {
return ajax(userPath("check_username"), {
data: { username, email, for_user_id }
@@ -827,11 +907,26 @@ User.reopenClass(Singleton, {
username: attrs.accountUsername,
password_confirmation: attrs.accountPasswordConfirm,
challenge: attrs.accountChallenge,
- user_fields: attrs.userFields
+ user_fields: attrs.userFields,
+ timezone: moment.tz.guess()
},
type: "POST"
});
}
});
+let warned = false;
+Object.defineProperty(Discourse, "User", {
+ get() {
+ if (!warned) {
+ deprecated("Import the User class instead of using User", {
+ since: "2.4.0",
+ dropFrom: "2.6.0"
+ });
+ warned = true;
+ }
+ return User;
+ }
+});
+
export default User;
diff --git a/app/assets/javascripts/discourse/pre-initializers/dynamic-route-builders.js.es6 b/app/assets/javascripts/discourse/pre-initializers/dynamic-route-builders.js.es6
index a8de4fc07d..9c96d01e42 100644
--- a/app/assets/javascripts/discourse/pre-initializers/dynamic-route-builders.js.es6
+++ b/app/assets/javascripts/discourse/pre-initializers/dynamic-route-builders.js.es6
@@ -2,6 +2,8 @@ import buildCategoryRoute from "discourse/routes/build-category-route";
import buildTopicRoute from "discourse/routes/build-topic-route";
import DiscoverySortableController from "discourse/controllers/discovery-sortable";
import TagsShowRoute from "discourse/routes/tags-show";
+import Site from "discourse/models/site";
+import User from "discourse/models/user";
export default {
after: "inject-discourse-objects",
@@ -18,8 +20,9 @@ export default {
app.DiscoveryCategoryNoneRoute = buildCategoryRoute("default", {
no_subcategories: true
});
+ app.DiscoveryCategoryWithIDRoute = buildCategoryRoute("default");
- const site = Discourse.Site.current();
+ const site = Site.current();
site.get("filters").forEach(filter => {
const filterCapitalized = filter.capitalize();
app[
@@ -54,8 +57,8 @@ export default {
Discourse.DiscoveryTopRoute = buildTopicRoute("top", {
actions: {
willTransition() {
- Discourse.User.currentProp("should_be_redirected_to_top", false);
- Discourse.User.currentProp("redirected_to_top.reason", null);
+ User.currentProp("should_be_redirected_to_top", false);
+ User.currentProp("redirected_to_top.reason", null);
return this._super(...arguments);
}
}
@@ -95,15 +98,26 @@ export default {
});
app["TagsShowCategoryRoute"] = TagsShowRoute.extend();
+ app["TagsShowCategoryNoneRoute"] = TagsShowRoute.extend({
+ noSubcategories: true
+ });
app["TagsShowParentCategoryRoute"] = TagsShowRoute.extend();
+ app["TagShowRoute"] = TagsShowRoute;
+
site.get("filters").forEach(function(filter) {
app["TagsShow" + filter.capitalize() + "Route"] = TagsShowRoute.extend({
navMode: filter
});
+ app["TagShow" + filter.capitalize() + "Route"] = TagsShowRoute.extend({
+ navMode: filter
+ });
app[
"TagsShowCategory" + filter.capitalize() + "Route"
] = TagsShowRoute.extend({ navMode: filter });
+ app[
+ "TagsShowNoneCategory" + filter.capitalize() + "Route"
+ ] = TagsShowRoute.extend({ navMode: filter, noSubcategories: true });
app[
"TagsShowParentCategory" + filter.capitalize() + "Route"
] = TagsShowRoute.extend({ navMode: filter });
diff --git a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6
index 7e1d8142e7..924e6f6a50 100644
--- a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6
+++ b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6
@@ -1,15 +1,15 @@
import Session from "discourse/models/session";
import KeyValueStore from "discourse/lib/key-value-store";
-import AppEvents from "discourse/lib/app-events";
import Store from "discourse/models/store";
-import DiscourseURL from "discourse/lib/url";
import DiscourseLocation from "discourse/lib/discourse-location";
+import Discourse from "discourse";
import SearchService from "discourse/services/search";
-import {
- startTracking,
- default as TopicTrackingState
+import TopicTrackingState, {
+ startTracking
} from "discourse/models/topic-tracking-state";
import ScreenTrack from "discourse/lib/screen-track";
+import Site from "discourse/models/site";
+import User from "discourse/models/user";
const ALL_TARGETS = ["controller", "component", "route", "model", "adapter"];
@@ -17,10 +17,7 @@ export default {
name: "inject-discourse-objects",
initialize(container, app) {
- const appEvents = AppEvents.create();
- app.register("app-events:main", appEvents, { instantiate: false });
- ALL_TARGETS.forEach(t => app.inject(t, "appEvents", "app-events:main"));
- DiscourseURL.appEvents = appEvents;
+ ALL_TARGETS.forEach(t => app.inject(t, "appEvents", "service:app-events"));
// backwards compatibility: remove when plugins have updated
app.register("store:main", Store);
@@ -34,8 +31,9 @@ export default {
app.register("message-bus:main", messageBus, { instantiate: false });
ALL_TARGETS.forEach(t => app.inject(t, "messageBus", "message-bus:main"));
- const currentUser = Discourse.User.current();
+ const currentUser = User.current();
app.register("current-user:main", currentUser, { instantiate: false });
+ Discourse.currentUser = currentUser;
const topicTrackingState = TopicTrackingState.create({
messageBus,
@@ -54,7 +52,7 @@ export default {
app.inject(t, "siteSettings", "site-settings:main")
);
- const site = Discourse.Site.current();
+ const site = Site.current();
app.register("site:main", site, { instantiate: false });
ALL_TARGETS.forEach(t => app.inject(t, "site", "site:main"));
diff --git a/app/assets/javascripts/discourse/pre-initializers/sniff-capabilities.js.es6 b/app/assets/javascripts/discourse/pre-initializers/sniff-capabilities.js.es6
index a4decf58b5..c8c35032ce 100644
--- a/app/assets/javascripts/discourse/pre-initializers/sniff-capabilities.js.es6
+++ b/app/assets/javascripts/discourse/pre-initializers/sniff-capabilities.js.es6
@@ -35,8 +35,19 @@ export default {
caps.canPasteImages = caps.isChrome || caps.isFirefox;
}
+ caps.isIpadOS =
+ ua.indexOf("Mac OS") !== -1 &&
+ !/iPhone|iPod/.test(navigator.userAgent) &&
+ touch;
+
caps.isIOS =
- /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+ (/iPhone|iPod/.test(navigator.userAgent) || caps.isIpadOS) &&
+ !window.MSStream;
+
+ caps.hasContactPicker =
+ "contacts" in navigator && "ContactsManager" in window;
+
+ caps.canVibrate = "vibrate" in navigator;
}
// We consider high res a device with 1280 horizontal pixels. High DPI tablets like
diff --git a/app/assets/javascripts/discourse/raw-views/list/post-count-or-badges.js.es6 b/app/assets/javascripts/discourse/raw-views/list/post-count-or-badges.js.es6
index 2f3dc5beb9..dd301f8b72 100644
--- a/app/assets/javascripts/discourse/raw-views/list/post-count-or-badges.js.es6
+++ b/app/assets/javascripts/discourse/raw-views/list/post-count-or-badges.js.es6
@@ -1,10 +1,12 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import { or, and } from "@ember/object/computed";
+import EmberObject from "@ember/object";
+import discourseComputed from "discourse-common/utils/decorators";
-export default Ember.Object.extend({
- postCountsPresent: Ember.computed.or("topic.unread", "topic.displayNewPosts"),
- showBadges: Ember.computed.and("postBadgesEnabled", "postCountsPresent"),
+export default EmberObject.extend({
+ postCountsPresent: or("topic.unread", "topic.displayNewPosts"),
+ showBadges: and("postBadgesEnabled", "postCountsPresent"),
- @computed
+ @discourseComputed
newDotText() {
return this.currentUser && this.currentUser.trust_level > 0
? ""
diff --git a/app/assets/javascripts/discourse/raw-views/list/posts-count-column.js.es6 b/app/assets/javascripts/discourse/raw-views/list/posts-count-column.js.es6
index 2171251840..12a27f3a22 100644
--- a/app/assets/javascripts/discourse/raw-views/list/posts-count-column.js.es6
+++ b/app/assets/javascripts/discourse/raw-views/list/posts-count-column.js.es6
@@ -1,10 +1,11 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import EmberObject from "@ember/object";
import { fmt } from "discourse/lib/computed";
-export default Ember.Object.extend({
+export default EmberObject.extend({
tagName: "td",
- @computed("topic.like_count", "topic.posts_count")
+ @discourseComputed("topic.like_count", "topic.posts_count")
ratio(likeCount, postCount) {
const likes = parseFloat(likeCount);
const posts = parseFloat(postCount);
@@ -16,12 +17,12 @@ export default Ember.Object.extend({
return (likes || 0) / posts;
},
- @computed("topic.replyCount", "ratioText")
+ @discourseComputed("topic.replyCount", "ratioText")
title(count, ratio) {
return I18n.messageFormat("posts_likes_MF", { count, ratio }).trim();
},
- @computed("ratio")
+ @discourseComputed("ratio")
ratioText(ratio) {
const settings = this.siteSettings;
if (ratio > settings.topic_post_like_heat_high) {
diff --git a/app/assets/javascripts/discourse/raw-views/list/visited-line.js.es6 b/app/assets/javascripts/discourse/raw-views/list/visited-line.js.es6
index 3626b17cb7..2428c81012 100644
--- a/app/assets/javascripts/discourse/raw-views/list/visited-line.js.es6
+++ b/app/assets/javascripts/discourse/raw-views/list/visited-line.js.es6
@@ -1,7 +1,8 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import EmberObject from "@ember/object";
-export default Ember.Object.extend({
- @computed
+export default EmberObject.extend({
+ @discourseComputed
isLastVisited: function() {
return this.lastVisitedTopic === this.topic;
}
diff --git a/app/assets/javascripts/discourse/raw-views/topic-list-header-column.js.es6 b/app/assets/javascripts/discourse/raw-views/topic-list-header-column.js.es6
index 5febdd4fef..dfa6e7039c 100644
--- a/app/assets/javascripts/discourse/raw-views/topic-list-header-column.js.es6
+++ b/app/assets/javascripts/discourse/raw-views/topic-list-header-column.js.es6
@@ -1,7 +1,8 @@
-import { default as computed } from "ember-addons/ember-computed-decorators";
+import EmberObject from "@ember/object";
+import discourseComputed from "discourse-common/utils/decorators";
-export default Ember.Object.extend({
- @computed
+export default EmberObject.extend({
+ @discourseComputed
localizedName() {
if (this.forceName) {
return this.forceName;
@@ -10,18 +11,18 @@ export default Ember.Object.extend({
return this.name ? I18n.t(this.name) : "";
},
- @computed
+ @discourseComputed
sortIcon() {
const asc = this.parent.ascending ? "up" : "down";
return `chevron-${asc}`;
},
- @computed
+ @discourseComputed
isSorting() {
return this.sortable && this.parent.order === this.order;
},
- @computed
+ @discourseComputed
className() {
const name = [];
diff --git a/app/assets/javascripts/discourse/raw-views/topic-status.js.es6 b/app/assets/javascripts/discourse/raw-views/topic-status.js.es6
index ec33498520..d6b2c74999 100644
--- a/app/assets/javascripts/discourse/raw-views/topic-status.js.es6
+++ b/app/assets/javascripts/discourse/raw-views/topic-status.js.es6
@@ -1,14 +1,15 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import EmberObject from "@ember/object";
-export default Ember.Object.extend({
+export default EmberObject.extend({
showDefault: null,
- @computed("defaultIcon")
+ @discourseComputed("defaultIcon")
renderDiv(defaultIcon) {
return (defaultIcon || this.statuses.length > 0) && !this.noDiv;
},
- @computed
+ @discourseComputed
statuses() {
const topic = this.topic;
const results = [];
diff --git a/app/assets/javascripts/discourse/routes/about.js.es6 b/app/assets/javascripts/discourse/routes/about.js.es6
index 019a23380d..f7d88a8421 100644
--- a/app/assets/javascripts/discourse/routes/about.js.es6
+++ b/app/assets/javascripts/discourse/routes/about.js.es6
@@ -1,5 +1,7 @@
import { ajax } from "discourse/lib/ajax";
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
model() {
return ajax("/about.json").then(result => {
let activeAdmins = [];
@@ -16,6 +18,14 @@ export default Discourse.Route.extend({
});
result.about.admins = activeAdmins;
result.about.moderators = activeModerators;
+
+ const { category_moderators: categoryModerators } = result.about;
+ if (categoryModerators && categoryModerators.length) {
+ categoryModerators.forEach((obj, index) => {
+ const category = this.site.categories.findBy("id", obj.category_id);
+ result.about.category_moderators[index].category = category;
+ });
+ }
return result.about;
});
},
diff --git a/app/assets/javascripts/discourse/routes/account-created-edit-email.js.es6 b/app/assets/javascripts/discourse/routes/account-created-edit-email.js.es6
index 0922f7fe09..0946fe05db 100644
--- a/app/assets/javascripts/discourse/routes/account-created-edit-email.js.es6
+++ b/app/assets/javascripts/discourse/routes/account-created-edit-email.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Route.extend({
+import Route from "@ember/routing/route";
+export default Route.extend({
setupController(controller) {
const accountCreated = this.controllerFor("account-created").get(
"accountCreated"
diff --git a/app/assets/javascripts/discourse/routes/account-created-index.js.es6 b/app/assets/javascripts/discourse/routes/account-created-index.js.es6
index 33aa658049..d3f46333a6 100644
--- a/app/assets/javascripts/discourse/routes/account-created-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/account-created-index.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Route.extend({
+import Route from "@ember/routing/route";
+export default Route.extend({
setupController(controller) {
controller.set(
"accountCreated",
diff --git a/app/assets/javascripts/discourse/routes/account-created-resent.js.es6 b/app/assets/javascripts/discourse/routes/account-created-resent.js.es6
index deb2af1970..047906d262 100644
--- a/app/assets/javascripts/discourse/routes/account-created-resent.js.es6
+++ b/app/assets/javascripts/discourse/routes/account-created-resent.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Route.extend({
+import Route from "@ember/routing/route";
+export default Route.extend({
setupController(controller) {
controller.set(
"email",
diff --git a/app/assets/javascripts/discourse/routes/account-created.js.es6 b/app/assets/javascripts/discourse/routes/account-created.js.es6
index ab117f5988..901f6c0ffb 100644
--- a/app/assets/javascripts/discourse/routes/account-created.js.es6
+++ b/app/assets/javascripts/discourse/routes/account-created.js.es6
@@ -1,6 +1,7 @@
+import Route from "@ember/routing/route";
import PreloadStore from "preload-store";
-export default Ember.Route.extend({
+export default Route.extend({
setupController(controller) {
controller.set("accountCreated", PreloadStore.get("accountCreated"));
}
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 654c683a45..16720a568e 100644
--- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6
+++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
@@ -1,3 +1,5 @@
+import Site from "discourse/models/site";
+
export default function() {
// Error page
this.route("exception", { path: "/exception" });
@@ -20,44 +22,55 @@ export default function() {
this.route("topicBySlugOrId", { path: "/t/:slugOrId", resetNamespace: true });
this.route("discovery", { path: "/", resetNamespace: true }, function() {
+ // legacy route
+ this.route("topParentCategory", { path: "/c/:slug/l/top" });
+
// top
this.route("top");
- this.route("topParentCategory", { path: "/c/:slug/l/top" });
- this.route("topCategoryNone", { path: "/c/:slug/none/l/top" });
- this.route("topCategory", { path: "/c/:parentSlug/:slug/l/top" });
+ this.route("topCategoryNone", {
+ path: "/c/*category_slug_path_with_id/none/l/top"
+ });
+ this.route("topCategory", { path: "/c/*category_slug_path_with_id/l/top" });
// top by periods
- Discourse.Site.currentProp("periods").forEach(period => {
+ Site.currentProp("periods").forEach(period => {
const top = "top" + period.capitalize();
- this.route(top, { path: "/top/" + period });
+
+ // legacy route
this.route(top + "ParentCategory", { path: "/c/:slug/l/top/" + period });
+
+ this.route(top, { path: "/top/" + period });
this.route(top + "CategoryNone", {
- path: "/c/:slug/none/l/top/" + period
+ path: "/c/*category_slug_path_with_id/none/l/top/" + period
});
this.route(top + "Category", {
- path: "/c/:parentSlug/:slug/l/top/" + period
+ path: "/c/*category_slug_path_with_id/l/top/" + period
});
});
- // filters
- Discourse.Site.currentProp("filters").forEach(filter => {
- this.route(filter, { path: "/" + filter });
+ // filters (e.g. bookmarks, posted, read, unread, latest)
+ Site.currentProp("filters").forEach(filter => {
+ // legacy route
this.route(filter + "ParentCategory", { path: "/c/:slug/l/" + filter });
+
+ this.route(filter, { path: "/" + filter });
this.route(filter + "CategoryNone", {
- path: "/c/:slug/none/l/" + filter
+ path: "/c/*category_slug_path_with_id/none/l/" + filter
});
this.route(filter + "Category", {
- path: "/c/:parentSlug/:slug/l/" + filter
+ path: "/c/*category_slug_path_with_id/l/" + filter
});
});
this.route("categories");
- // default filter for a category
+ // legacy routes
this.route("parentCategory", { path: "/c/:slug" });
- this.route("categoryNone", { path: "/c/:slug/none" });
- this.route("category", { path: "/c/:parentSlug/:slug" });
this.route("categoryWithID", { path: "/c/:parentSlug/:slug/:id" });
+
+ // default filter for a category
+ this.route("categoryNone", { path: "/c/*category_slug_path_with_id/none" });
+ this.route("category", { path: "/c/*category_slug_path_with_id" });
});
this.route("groups", { resetNamespace: true, path: "/g" }, function() {
@@ -156,7 +169,6 @@ export default function() {
this.route("email");
this.route("second-factor");
this.route("second-factor-backup");
- this.route("about", { path: "/about-me" });
});
this.route(
@@ -197,34 +209,51 @@ export default function() {
this.route("full-page-search", { path: "/search" });
- this.route("tags", { resetNamespace: true }, function() {
+ this.route("tag", { resetNamespace: true }, function() {
this.route("show", { path: "/:tag_id" });
- this.route("showCategory", { path: "/c/:category/:tag_id" });
- this.route("showParentCategory", {
- path: "/c/:parent_category/:category/:tag_id"
- });
- Discourse.Site.currentProp("filters").forEach(filter => {
+ Site.currentProp("filters").forEach(filter => {
this.route("show" + filter.capitalize(), {
path: "/:tag_id/l/" + filter
});
+ });
+ });
+
+ this.route("tags", { resetNamespace: true }, function() {
+ this.route("showCategory", {
+ path: "/c/*category_slug_path_with_id/:tag_id"
+ });
+ this.route("showCategoryNone", {
+ path: "/c/*category_slug_path_with_id/none/:tag_id"
+ });
+
+ Site.currentProp("filters").forEach(filter => {
this.route("showCategory" + filter.capitalize(), {
- path: "/c/:category/:tag_id/l/" + filter
+ path: "/c/*category_slug_path_with_id/:tag_id/l/" + filter
});
- this.route("showParentCategory" + filter.capitalize(), {
- path: "/c/:parent_category/:category/:tag_id/l/" + filter
+ this.route("showCategoryNone" + filter.capitalize(), {
+ path: "/c/*category_slug_path_with_id/none/:tag_id/l/" + filter
});
});
this.route("intersection", {
path: "intersection/:tag_id/*additional_tags"
});
+
+ // legacy routes
+ this.route("show", { path: "/:tag_id" });
+ Site.currentProp("filters").forEach(filter => {
+ this.route("show" + filter.capitalize(), {
+ path: "/:tag_id/l/" + filter
+ });
+ });
});
this.route(
"tagGroups",
{ path: "/tag_groups", resetNamespace: true },
function() {
- this.route("show", { path: "/:id" });
+ this.route("edit", { path: "/:id" });
+ this.route("new");
}
);
diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6
index 3ed2b9417e..e877d8ceff 100644
--- a/app/assets/javascripts/discourse/routes/application.js.es6
+++ b/app/assets/javascripts/discourse/routes/application.js.es6
@@ -1,3 +1,6 @@
+import { once } from "@ember/runloop";
+import { next } from "@ember/runloop";
+import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from "discourse/lib/ajax";
import { setting } from "discourse/lib/computed";
import logout from "discourse/lib/logout";
@@ -9,6 +12,7 @@ import { findAll } from "discourse/models/login-method";
import { getOwner } from "discourse-common/lib/get-owner";
import { userPath } from "discourse/lib/url";
import Composer from "discourse/models/composer";
+import { EventTarget } from "rsvp";
function unlessReadOnly(method, message) {
return function() {
@@ -20,7 +24,7 @@ function unlessReadOnly(method, message) {
};
}
-const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
+const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
siteTitle: setting("title"),
shortSiteDescription: setting("short_site_description"),
@@ -43,7 +47,8 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
_collectTitleTokens(tokens) {
tokens.push(this.siteTitle);
if (
- window.location.pathname === Discourse.getURL("/") &&
+ (window.location.pathname === Discourse.getURL("/") ||
+ window.location.pathname === Discourse.getURL("/login")) &&
this.shortSiteDescription !== ""
) {
tokens.push(this.shortSiteDescription);
@@ -54,7 +59,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
// Ember doesn't provider a router `willTransition` event so let's make one
willTransition() {
var router = getOwner(this).lookup("router:main");
- Ember.run.once(router, router.trigger, "willTransition");
+ once(router, router.trigger, "willTransition");
return this._super(...arguments);
},
@@ -66,21 +71,24 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
},
composePrivateMessage(user, post) {
- const recipient = user ? user.get("username") : "",
- reply = post
- ? window.location.protocol +
- "//" +
- window.location.host +
- post.get("url")
- : null;
+ const recipients = user ? user.get("username") : "";
+ const reply = post
+ ? `${window.location.protocol}//${window.location.host}${post.url}`
+ : null;
+ const title = post
+ ? I18n.t("composer.reference_topic_title", {
+ title: post.topic.title
+ })
+ : null;
// used only once, one less dependency
return this.controllerFor("composer").open({
action: Composer.PRIVATE_MESSAGE,
- usernames: recipient,
+ recipients,
archetypeId: "private_message",
- draftKey: "new_private_message",
- reply: reply
+ draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY,
+ reply,
+ title
});
},
@@ -137,8 +145,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
showUploadSelector(toolbarEvent) {
showModal("uploadSelector").setProperties({
toolbarEvent,
- imageUrl: null,
- imageLink: null
+ imageUrl: null
});
},
@@ -153,10 +160,17 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
this.render("hide-modal", { into: "modal", outlet: "modalBody" });
const route = getOwner(this).lookup("route:application");
- const name = route.controllerFor("modal").get("name");
- const controller = getOwner(this).lookup(`controller:${name}`);
- if (controller && controller.onClose) {
- controller.onClose();
+ let modalController = route.controllerFor("modal");
+ const controllerName = modalController.get("name");
+
+ if (controllerName) {
+ const controller = getOwner(this).lookup(
+ `controller:${controllerName}`
+ );
+ if (controller && controller.onClose) {
+ controller.onClose();
+ }
+ modalController.set("name", null);
}
},
@@ -207,14 +221,14 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
);
},
- createNewMessageViaParams(username, title, body) {
- this.openComposerWithMessageParams(username, title, body);
+ createNewMessageViaParams(recipients, title, body) {
+ this.openComposerWithMessageParams(recipients, title, body);
}
},
activate() {
this._super(...arguments);
- Ember.run.next(function() {
+ next(function() {
// Support for callbacks once the application has activated
ApplicationRoute.trigger("activate");
});
@@ -255,9 +269,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
const methods = findAll();
if (!this.siteSettings.enable_local_logins && methods.length === 1) {
- this.controllerFor("login").send("externalLogin", methods[0], {
- fullScreenLogin: true
- });
+ this.controllerFor("login").send("externalLogin", methods[0]);
} else {
showModal(modal);
this.controllerFor("modal").set("modalClass", modalClass);
@@ -276,5 +288,5 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
}
});
-RSVP.EventTarget.mixin(ApplicationRoute);
+EventTarget.mixin(ApplicationRoute);
export default ApplicationRoute;
diff --git a/app/assets/javascripts/discourse/routes/associate-account.js.es6 b/app/assets/javascripts/discourse/routes/associate-account.js.es6
index dfbe2e52f7..a182e85c88 100644
--- a/app/assets/javascripts/discourse/routes/associate-account.js.es6
+++ b/app/assets/javascripts/discourse/routes/associate-account.js.es6
@@ -1,13 +1,15 @@
+import { next } from "@ember/runloop";
+import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from "discourse/lib/ajax";
import showModal from "discourse/lib/show-modal";
import { popupAjaxError } from "discourse/lib/ajax-error";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
beforeModel() {
const params = this.paramsFor("associate-account");
this.replaceWith(`preferences.account`, this.currentUser).then(() =>
- Ember.run.next(() =>
- ajax(`/associate/${encodeURIComponent(params.token)}`)
+ next(() =>
+ ajax(`/associate/${encodeURIComponent(params.token)}.json`)
.then(model => showModal("associate-account-confirm", { model }))
.catch(popupAjaxError)
)
diff --git a/app/assets/javascripts/discourse/routes/badges-index.js.es6 b/app/assets/javascripts/discourse/routes/badges-index.js.es6
index 2df72ea584..c4acb15592 100644
--- a/app/assets/javascripts/discourse/routes/badges-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/badges-index.js.es6
@@ -1,7 +1,8 @@
+import DiscourseRoute from "discourse/routes/discourse";
import Badge from "discourse/models/badge";
import PreloadStore from "preload-store";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
model() {
if (PreloadStore.get("badges")) {
return PreloadStore.getAndRemove("badges").then(json =>
diff --git a/app/assets/javascripts/discourse/routes/badges-show.js.es6 b/app/assets/javascripts/discourse/routes/badges-show.js.es6
index 3507beac99..86b8f0d4af 100644
--- a/app/assets/javascripts/discourse/routes/badges-show.js.es6
+++ b/app/assets/javascripts/discourse/routes/badges-show.js.es6
@@ -1,8 +1,10 @@
+import DiscourseRoute from "discourse/routes/discourse";
import UserBadge from "discourse/models/user-badge";
import Badge from "discourse/models/badge";
import PreloadStore from "preload-store";
+import { hash } from "rsvp";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
queryParams: {
username: {
refreshModel: true
@@ -51,7 +53,7 @@ export default Discourse.Route.extend({
userBadgesAll
};
- return Ember.RSVP.hash(promises);
+ return hash(promises);
},
titleToken() {
diff --git a/app/assets/javascripts/discourse/routes/build-admin-user-posts-route.js.es6 b/app/assets/javascripts/discourse/routes/build-admin-user-posts-route.js.es6
index bc80113310..5bbd0108c6 100644
--- a/app/assets/javascripts/discourse/routes/build-admin-user-posts-route.js.es6
+++ b/app/assets/javascripts/discourse/routes/build-admin-user-posts-route.js.es6
@@ -1,7 +1,8 @@
+import DiscourseRoute from "discourse/routes/discourse";
import { emojiUnescape } from "discourse/lib/text";
export default function(filter) {
- return Discourse.Route.extend({
+ return DiscourseRoute.extend({
actions: {
didTransition() {
this.controllerFor("user").set("indexStream", true);
diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6
index b2a5a00ae5..f46ea868f7 100644
--- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6
+++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6
@@ -1,3 +1,4 @@
+import DiscourseRoute from "discourse/routes/discourse";
import {
filterQueryParams,
findTopicList
@@ -7,39 +8,65 @@ import TopicList from "discourse/models/topic-list";
import PermissionType from "discourse/models/permission-type";
import CategoryList from "discourse/models/category-list";
import Category from "discourse/models/category";
+import { Promise, all } from "rsvp";
// A helper function to create a category route with parameters
export default (filterArg, params) => {
- return Discourse.Route.extend({
+ return DiscourseRoute.extend({
queryParams,
+ serialize(modelParams) {
+ if (!modelParams.category_slug_path_with_id) {
+ if (modelParams.id === "none") {
+ const category_slug_path_with_id = [
+ modelParams.parentSlug,
+ modelParams.slug
+ ].join("/");
+ const category = Category.findBySlugPathWithID(
+ category_slug_path_with_id
+ );
+ this.replaceWith("discovery.categoryNone", {
+ category,
+ category_slug_path_with_id
+ });
+ } else {
+ modelParams.category_slug_path_with_id = [
+ modelParams.parentSlug,
+ modelParams.slug,
+ modelParams.id
+ ]
+ .filter(x => x)
+ .join("/");
+ }
+ }
+
+ return modelParams;
+ },
+
model(modelParams) {
- const category = Category.findBySlug(
- modelParams.slug,
- modelParams.parentSlug
+ modelParams = this.serialize(modelParams);
+
+ const category = Category.findBySlugPathWithID(
+ modelParams.category_slug_path_with_id
);
+
if (!category) {
- return Category.reloadBySlug(
- modelParams.slug,
- modelParams.parentSlug
- ).then(atts => {
- if (modelParams.parentSlug) {
- atts.category.parentCategory = Category.findBySlug(
- modelParams.parentSlug
- );
- }
- const record = this.store.createRecord("category", atts.category);
+ const parts = modelParams.category_slug_path_with_id.split("/");
+ if (parts.length > 0 && parts[parts.length - 1].match(/^\d+$/)) {
+ parts.pop();
+ }
+
+ return Category.reloadBySlugPath(parts.join("/")).then(result => {
+ const record = this.store.createRecord("category", result.category);
record.setupGroupsAndPermissions();
this.site.updateCategory(record);
- return {
- category: Category.findBySlug(
- modelParams.slug,
- modelParams.parentSlug
- )
- };
+ return { category: record };
});
}
- return { category };
+
+ if (category) {
+ return { category };
+ }
},
afterModel(model, transition) {
@@ -49,7 +76,7 @@ export default (filterArg, params) => {
}
this._setupNavigation(model.category);
- return Ember.RSVP.all([
+ return all([
this._createSubcategoryList(model.category),
this._retrieveTopicList(model.category, transition)
]);
@@ -63,36 +90,32 @@ export default (filterArg, params) => {
_setupNavigation(category) {
const noSubcategories = params && !!params.no_subcategories,
- filterMode = `c/${Discourse.Category.slugFor(category)}${
- noSubcategories ? "/none" : ""
- }/l/${this.filter(category)}`;
+ filterType = this.filter(category).split("/")[0];
this.controllerFor("navigation/category").setProperties({
category,
- filterMode: filterMode,
- noSubcategories: params && params.no_subcategories
+ filterType,
+ noSubcategories
});
},
_createSubcategoryList(category) {
this._categoryList = null;
- if (
- Ember.isNone(category.get("parentCategory")) &&
- category.get("show_subcategory_list")
- ) {
+
+ if (category.isParent && category.show_subcategory_list) {
return CategoryList.listForParent(this.store, category).then(
list => (this._categoryList = list)
);
}
// If we're not loading a subcategory list just resolve
- return Ember.RSVP.resolve();
+ return Promise.resolve();
},
_retrieveTopicList(category, transition) {
- const listFilter = `c/${Discourse.Category.slugFor(
- category
- )}/l/${this.filter(category)}`,
+ const listFilter = `c/${Category.slugFor(category)}/${
+ category.id
+ }/l/${this.filter(category)}`,
findOpts = filterQueryParams(transition.to.queryParams, params),
extras = { cached: this.isPoppedState(transition) };
@@ -198,8 +221,24 @@ export default (filterArg, params) => {
},
actions: {
+ error(err) {
+ const json = err.jqXHR.responseJSON;
+ if (json && json.extras && json.extras.html) {
+ this.controllerFor("discovery").set(
+ "errorHtml",
+ err.jqXHR.responseJSON.extras.html
+ );
+ } else {
+ this.replaceWith("exception");
+ }
+ },
+
setNotification(notification_level) {
this.currentModel.setNotification(notification_level);
+ },
+
+ triggerRefresh() {
+ this.refresh();
}
}
});
diff --git a/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6 b/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6
index 4ebca3108a..abae09a255 100644
--- a/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6
+++ b/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6
@@ -1,9 +1,11 @@
import UserTopicListRoute from "discourse/routes/user-topic-list";
+import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list";
+import UserAction from "discourse/models/user-action";
// A helper to build a user topic list route
export default (viewName, path, channel) => {
return UserTopicListRoute.extend({
- userActionType: Discourse.UserAction.TYPES.messages_received,
+ userActionType: UserAction.TYPES.messages_received,
titleToken() {
const key = viewName === "index" ? "inbox" : viewName;
@@ -18,10 +20,12 @@ export default (viewName, path, channel) => {
},
model() {
- return this.store.findFiltered("topicList", {
- filter:
- "topics/" + path + "/" + this.modelFor("user").get("username_lower")
- });
+ const filter =
+ "topics/" + path + "/" + this.modelFor("user").get("username_lower");
+ const lastTopicList = findOrResetCachedTopicList(this.session, filter);
+ return lastTopicList
+ ? lastTopicList
+ : this.store.findFiltered("topicList", { filter });
},
setupController() {
diff --git a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6
index 72db37455b..10a67214d9 100644
--- a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6
+++ b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6
@@ -1,9 +1,14 @@
+import DiscourseRoute from "discourse/routes/discourse";
import { queryParams } from "discourse/controllers/discovery-sortable";
import { defaultHomepage } from "discourse/lib/utilities";
+import Session from "discourse/models/session";
+import { Promise } from "rsvp";
+import Site from "discourse/models/site";
// A helper to build a topic route for a filter
function filterQueryParams(params, defaultParams) {
- const findOpts = defaultParams || {};
+ const findOpts = Object.assign({}, defaultParams || {});
+
if (params) {
Object.keys(queryParams).forEach(function(opt) {
if (params[opt]) {
@@ -16,8 +21,8 @@ function filterQueryParams(params, defaultParams) {
function findTopicList(store, tracking, filter, filterParams, extras) {
extras = extras || {};
- return new Ember.RSVP.Promise(function(resolve) {
- const session = Discourse.Session.current();
+ return new Promise(function(resolve) {
+ const session = Session.current();
if (extras.cached) {
const cachedList = session.get("topicList");
@@ -60,9 +65,13 @@ function findTopicList(store, tracking, filter, filterParams, extras) {
tracking.sync(list, list.filter);
tracking.trackIncoming(list.filter);
}
- Discourse.Session.currentProp("topicList", list);
+ Session.currentProp("topicList", list);
if (list.topic_list && list.topic_list.top_tags) {
- Discourse.Site.currentProp("top_tags", list.topic_list.top_tags);
+ if (list.filter.startsWith("c/") || list.filter.startsWith("tags/c/")) {
+ Site.currentProp("category_top_tags", list.topic_list.top_tags);
+ } else {
+ Site.currentProp("top_tags", list.topic_list.top_tags);
+ }
}
return list;
});
@@ -70,12 +79,15 @@ function findTopicList(store, tracking, filter, filterParams, extras) {
export default function(filter, extras) {
extras = extras || {};
- return Discourse.Route.extend(
+ return DiscourseRoute.extend(
{
queryParams,
beforeModel() {
- this.controllerFor("navigation/default").set("filterMode", filter);
+ this.controllerFor("navigation/default").set(
+ "filterType",
+ filter.split("/")[0]
+ );
},
model(data, transition) {
diff --git a/app/assets/javascripts/discourse/routes/discourse.js.es6 b/app/assets/javascripts/discourse/routes/discourse.js.es6
index 51bdcbbc2b..980e986cd9 100644
--- a/app/assets/javascripts/discourse/routes/discourse.js.es6
+++ b/app/assets/javascripts/discourse/routes/discourse.js.es6
@@ -1,7 +1,10 @@
+import { once } from "@ember/runloop";
import Composer from "discourse/models/composer";
import { getOwner } from "discourse-common/lib/get-owner";
+import Route from "@ember/routing/route";
+import deprecated from "discourse-common/lib/deprecated";
-const DiscourseRoute = Ember.Route.extend({
+const DiscourseRoute = Route.extend({
showFooter: false,
// Set to true to refresh a model without a transition if a query param
@@ -54,7 +57,7 @@ const DiscourseRoute = Ember.Route.extend({
},
refreshTitle() {
- Ember.run.once(this, this._refreshTitleOnce);
+ once(this, this._refreshTitleOnce);
},
clearTopicDraft() {
@@ -105,4 +108,14 @@ const DiscourseRoute = Ember.Route.extend({
}
});
+Object.defineProperty(Discourse, "Route", {
+ get() {
+ deprecated("Import the Route class instead of using Discourse.Route", {
+ since: "2.4.0",
+ dropFrom: "2.5.0"
+ });
+ return Route;
+ }
+});
+
export default DiscourseRoute;
diff --git a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6
index 49605dd6c3..451d9cc3f8 100644
--- a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6
+++ b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6
@@ -1,3 +1,6 @@
+import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
+import DiscourseRoute from "discourse/routes/discourse";
import showModal from "discourse/lib/show-modal";
import OpenComposer from "discourse/mixins/open-composer";
import CategoryList from "discourse/models/category-list";
@@ -6,8 +9,10 @@ import TopicList from "discourse/models/topic-list";
import { ajax } from "discourse/lib/ajax";
import PreloadStore from "preload-store";
import { searchPriorities } from "discourse/components/concerns/category-search-priorities";
+import { hash } from "rsvp";
+import Site from "discourse/models/site";
-const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
+const DiscoveryCategoriesRoute = DiscourseRoute.extend(OpenComposer, {
renderTemplate() {
this.render("navigation/categories", { outlet: "navigation-bar" });
this.render("discovery/categories", { outlet: "list-container" });
@@ -17,10 +22,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
let style =
!this.site.mobileView && this.siteSettings.desktop_category_page_style;
- let parentCategory = this.get("model.parentCategory");
- if (parentCategory) {
- return CategoryList.listForParent(this.store, parentCategory);
- } else if (style === "categories_and_latest_topics") {
+ if (style === "categories_and_latest_topics") {
return this._findCategoriesAndTopics("latest");
} else if (style === "categories_and_top_topics") {
return this._findCategoriesAndTopics("top");
@@ -41,16 +43,20 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
},
_findCategoriesAndTopics(filter) {
- return Ember.RSVP.hash({
+ return hash({
wrappedCategoriesList: PreloadStore.getAndRemove("categories_list"),
topicsList: PreloadStore.getAndRemove(`topic_list_${filter}`)
- }).then(hash => {
- let { wrappedCategoriesList, topicsList } = hash;
+ }).then(response => {
+ let { wrappedCategoriesList, topicsList } = response;
let categoriesList =
wrappedCategoriesList && wrappedCategoriesList.category_list;
if (categoriesList && topicsList) {
- return Ember.Object.create({
+ if (topicsList.topic_list && topicsList.topic_list.top_tags) {
+ Site.currentProp("top_tags", topicsList.topic_list.top_tags);
+ }
+
+ return EmberObject.create({
categories: CategoryList.categoriesFrom(
this.store,
wrappedCategoriesList
@@ -65,7 +71,11 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
}
// Otherwise, return the ajax result
return ajax(`/categories_and_${filter}`).then(result => {
- return Ember.Object.create({
+ if (result.topic_list && result.topic_list.top_tags) {
+ Site.currentProp("top_tags", result.topic_list.top_tags);
+ }
+
+ return EmberObject.create({
categories: CategoryList.categoriesFrom(this.store, result),
topics: TopicList.topicsFrom(this.store, result),
can_create_category: result.category_list.can_create_category,
@@ -132,9 +142,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
},
didTransition() {
- Ember.run.next(() =>
- this.controllerFor("application").set("showFooter", true)
- );
+ next(() => this.controllerFor("application").set("showFooter", true));
return true;
}
}
diff --git a/app/assets/javascripts/discourse/routes/discovery-category-with-id.js.es6 b/app/assets/javascripts/discourse/routes/discovery-category-with-id.js.es6
deleted file mode 100644
index e5ed07790e..0000000000
--- a/app/assets/javascripts/discourse/routes/discovery-category-with-id.js.es6
+++ /dev/null
@@ -1,7 +0,0 @@
-import Category from "discourse/models/category";
-
-export default Discourse.DiscoveryCategoryRoute.extend({
- model(params) {
- return { category: Category.findById(params.id) };
- }
-});
diff --git a/app/assets/javascripts/discourse/routes/discovery.js.es6 b/app/assets/javascripts/discourse/routes/discovery.js.es6
index 32926561b6..d812950666 100644
--- a/app/assets/javascripts/discourse/routes/discovery.js.es6
+++ b/app/assets/javascripts/discourse/routes/discovery.js.es6
@@ -2,16 +2,18 @@
The parent route for all discovery routes.
Handles the logic for showing the loading spinners.
**/
+import DiscourseRoute from "discourse/routes/discourse";
import OpenComposer from "discourse/mixins/open-composer";
import { scrollTop } from "discourse/mixins/scroll-top";
+import User from "discourse/models/user";
-export default Discourse.Route.extend(OpenComposer, {
+export default DiscourseRoute.extend(OpenComposer, {
redirect() {
return this.redirectIfLoginRequired();
},
beforeModel(transition) {
- const user = Discourse.User;
+ const user = User;
const url = transition.intent.url;
if (
@@ -60,12 +62,15 @@ export default Discourse.Route.extend(OpenComposer, {
},
dismissReadTopics(dismissTopics) {
- var operationType = dismissTopics ? "topics" : "posts";
- this.controllerFor("discovery/topics").send("dismissRead", operationType);
+ const operationType = dismissTopics ? "topics" : "posts";
+ this.send("dismissRead", operationType);
},
dismissRead(operationType) {
- this.controllerFor("discovery/topics").send("dismissRead", operationType);
+ const controller = this.controllerFor("discovery/topics");
+ controller.send("dismissRead", operationType, {
+ includeSubcategories: !controller.noSubcategories
+ });
}
}
});
diff --git a/app/assets/javascripts/discourse/routes/email-login.js.es6 b/app/assets/javascripts/discourse/routes/email-login.js.es6
index 617de051cd..34936369b1 100644
--- a/app/assets/javascripts/discourse/routes/email-login.js.es6
+++ b/app/assets/javascripts/discourse/routes/email-login.js.es6
@@ -1,11 +1,12 @@
+import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from "discourse/lib/ajax";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
titleToken() {
return I18n.t("login.title");
},
model(params) {
- return ajax(`/session/email-login/${params.token}`);
+ return ajax(`/session/email-login/${params.token}.json`);
}
});
diff --git a/app/assets/javascripts/discourse/routes/exception.js.es6 b/app/assets/javascripts/discourse/routes/exception.js.es6
index 1b64efb9a6..d90cd0fb76 100644
--- a/app/assets/javascripts/discourse/routes/exception.js.es6
+++ b/app/assets/javascripts/discourse/routes/exception.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
serialize() {
return "";
},
diff --git a/app/assets/javascripts/discourse/routes/forgot-password.js.es6 b/app/assets/javascripts/discourse/routes/forgot-password.js.es6
index e88fbdb082..58309565b5 100644
--- a/app/assets/javascripts/discourse/routes/forgot-password.js.es6
+++ b/app/assets/javascripts/discourse/routes/forgot-password.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
import { defaultHomepage } from "discourse/lib/utilities";
import buildStaticRoute from "discourse/routes/build-static-route";
@@ -11,7 +12,7 @@ ForgotPasswordRoute.reopen({
this.replaceWith(
loginRequired ? "login" : `discovery.${defaultHomepage()}`
).then(e => {
- Ember.run.next(() => e.send("showForgotPassword"));
+ next(() => e.send("showForgotPassword"));
});
}
});
diff --git a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 b/app/assets/javascripts/discourse/routes/full-page-search.js.es6
index 97ec4772e7..ca7906e109 100644
--- a/app/assets/javascripts/discourse/routes/full-page-search.js.es6
+++ b/app/assets/javascripts/discourse/routes/full-page-search.js.es6
@@ -1,3 +1,4 @@
+import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from "discourse/lib/ajax";
import {
translateResults,
@@ -8,7 +9,7 @@ import PreloadStore from "preload-store";
import { getTransient, setTransient } from "discourse/lib/page-tracker";
import { escapeExpression } from "discourse/lib/utilities";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
queryParams: {
q: {},
expanded: false,
diff --git a/app/assets/javascripts/discourse/routes/group-activity-index.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-index.js.es6
index 84011db59f..de7b551a80 100644
--- a/app/assets/javascripts/discourse/routes/group-activity-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-activity-index.js.es6
@@ -1,5 +1,12 @@
-export default Ember.Route.extend({
- beforeModel: function() {
- this.transitionTo("group.activity.posts");
+import Route from "@ember/routing/route";
+
+export default Route.extend({
+ beforeModel() {
+ const group = this.modelFor("group");
+ if (group.can_see_members) {
+ this.transitionTo("group.activity.posts");
+ } else {
+ this.transitionTo("group.activity.mentions");
+ }
}
});
diff --git a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6
index 62aad28945..86b35b6534 100644
--- a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6
@@ -1,5 +1,8 @@
+import { get } from "@ember/object";
+import DiscourseRoute from "discourse/routes/discourse";
+
export function buildGroupPage(type) {
- return Discourse.Route.extend({
+ return DiscourseRoute.extend({
type,
titleToken() {
@@ -7,7 +10,7 @@ export function buildGroupPage(type) {
},
model(params, transition) {
- let categoryId = Ember.get(transition.to, "queryParams.category_id");
+ let categoryId = get(transition.to, "queryParams.category_id");
return this.modelFor("group").findPosts({ type, categoryId });
},
diff --git a/app/assets/javascripts/discourse/routes/group-activity-topics.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-topics.js.es6
index a36ec0e6e5..04da3aeb67 100644
--- a/app/assets/javascripts/discourse/routes/group-activity-topics.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-activity-topics.js.es6
@@ -1,4 +1,8 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
+ showFooter: true,
+
titleToken() {
return I18n.t(`groups.topics`);
},
diff --git a/app/assets/javascripts/discourse/routes/group-index.js.es6 b/app/assets/javascripts/discourse/routes/group-index.js.es6
index 2aab4410ab..a3aed42816 100644
--- a/app/assets/javascripts/discourse/routes/group-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-index.js.es6
@@ -1,6 +1,7 @@
+import DiscourseRoute from "discourse/routes/discourse";
import showModal from "discourse/lib/show-modal";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
titleToken() {
return I18n.t("groups.members.title");
},
@@ -18,7 +19,7 @@ export default Discourse.Route.extend({
filterInput: this._params.filter
});
- controller.refreshMembers();
+ controller.findMembers(true);
},
actions: {
diff --git a/app/assets/javascripts/discourse/routes/group-manage-index.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-index.js.es6
index 590722bb0f..4e9cf5c71c 100644
--- a/app/assets/javascripts/discourse/routes/group-manage-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-manage-index.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
showFooter: true,
beforeModel() {
diff --git a/app/assets/javascripts/discourse/routes/group-manage-interaction.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-interaction.js.es6
index e8f3be3da2..e273e2d22f 100644
--- a/app/assets/javascripts/discourse/routes/group-manage-interaction.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-manage-interaction.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
showFooter: true,
titleToken() {
diff --git a/app/assets/javascripts/discourse/routes/group-manage-logs.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-logs.js.es6
index 56b092ecfe..ad49c84619 100644
--- a/app/assets/javascripts/discourse/routes/group-manage-logs.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-manage-logs.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
titleToken() {
return I18n.t("groups.manage.logs.title");
},
diff --git a/app/assets/javascripts/discourse/routes/group-manage-membership.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-membership.js.es6
index 37746f3e91..1ded3e2b07 100644
--- a/app/assets/javascripts/discourse/routes/group-manage-membership.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-manage-membership.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
showFooter: true,
titleToken() {
diff --git a/app/assets/javascripts/discourse/routes/group-manage-profile.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-profile.js.es6
index e0f133e0ce..b720151ff4 100644
--- a/app/assets/javascripts/discourse/routes/group-manage-profile.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-manage-profile.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
showFooter: true,
titleToken() {
diff --git a/app/assets/javascripts/discourse/routes/group-manage.js.es6 b/app/assets/javascripts/discourse/routes/group-manage.js.es6
index 54122bcbd4..70b5f053c7 100644
--- a/app/assets/javascripts/discourse/routes/group-manage.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-manage.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
showFooter: true,
titleToken() {
diff --git a/app/assets/javascripts/discourse/routes/group-members.js.es6 b/app/assets/javascripts/discourse/routes/group-members.js.es6
index 8ec71ae22a..872052de67 100644
--- a/app/assets/javascripts/discourse/routes/group-members.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-members.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
beforeModel: function() {
this.transitionTo("group.index");
}
diff --git a/app/assets/javascripts/discourse/routes/group-messages-index.js.es6 b/app/assets/javascripts/discourse/routes/group-messages-index.js.es6
index 68750d154a..be802450b2 100644
--- a/app/assets/javascripts/discourse/routes/group-messages-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-messages-index.js.es6
@@ -1,4 +1,5 @@
-export default Ember.Route.extend({
+import Route from "@ember/routing/route";
+export default Route.extend({
beforeModel: function() {
this.transitionTo("group.messages.inbox");
}
diff --git a/app/assets/javascripts/discourse/routes/group-messages.js.es6 b/app/assets/javascripts/discourse/routes/group-messages.js.es6
index 82f26f93e2..1a605ec168 100644
--- a/app/assets/javascripts/discourse/routes/group-messages.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-messages.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
titleToken() {
return I18n.t("groups.messages");
},
diff --git a/app/assets/javascripts/discourse/routes/group-requests.js.es6 b/app/assets/javascripts/discourse/routes/group-requests.js.es6
index c469c9895a..548529cab0 100644
--- a/app/assets/javascripts/discourse/routes/group-requests.js.es6
+++ b/app/assets/javascripts/discourse/routes/group-requests.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
titleToken() {
return I18n.t("groups.requests.title");
},
@@ -16,6 +18,6 @@ export default Discourse.Route.extend({
filterInput: this._params.filter
});
- controller.refreshRequesters(true);
+ controller.findRequesters(true);
}
});
diff --git a/app/assets/javascripts/discourse/routes/group.js.es6 b/app/assets/javascripts/discourse/routes/group.js.es6
index 9e2c7f87de..bd58f6d3a7 100644
--- a/app/assets/javascripts/discourse/routes/group.js.es6
+++ b/app/assets/javascripts/discourse/routes/group.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
titleToken() {
return [this.modelFor("group").get("name")];
},
diff --git a/app/assets/javascripts/discourse/routes/groups-index.js.es6 b/app/assets/javascripts/discourse/routes/groups-index.js.es6
index e7189d778f..a510bd93b6 100644
--- a/app/assets/javascripts/discourse/routes/groups-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/groups-index.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
titleToken() {
return I18n.t("groups.index.title");
},
diff --git a/app/assets/javascripts/discourse/routes/groups-new.js.es6 b/app/assets/javascripts/discourse/routes/groups-new.js.es6
index 8c2bbd29a7..67f76347d9 100644
--- a/app/assets/javascripts/discourse/routes/groups-new.js.es6
+++ b/app/assets/javascripts/discourse/routes/groups-new.js.es6
@@ -1,6 +1,7 @@
+import DiscourseRoute from "discourse/routes/discourse";
import Group from "discourse/models/group";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
showFooter: true,
titleToken() {
diff --git a/app/assets/javascripts/discourse/routes/invites-show.js.es6 b/app/assets/javascripts/discourse/routes/invites-show.js.es6
index 93f4bdcc40..7ec701b842 100644
--- a/app/assets/javascripts/discourse/routes/invites-show.js.es6
+++ b/app/assets/javascripts/discourse/routes/invites-show.js.es6
@@ -1,6 +1,7 @@
+import DiscourseRoute from "discourse/routes/discourse";
import PreloadStore from "preload-store";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
titleToken() {
return I18n.t("invites.accept_title");
},
diff --git a/app/assets/javascripts/discourse/routes/login.js.es6 b/app/assets/javascripts/discourse/routes/login.js.es6
index 8465ceb6bc..c41f884f30 100644
--- a/app/assets/javascripts/discourse/routes/login.js.es6
+++ b/app/assets/javascripts/discourse/routes/login.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
import buildStaticRoute from "discourse/routes/build-static-route";
import { defaultHomepage } from "discourse/lib/utilities";
@@ -7,7 +8,7 @@ LoginRoute.reopen({
beforeModel() {
if (!this.siteSettings.login_required) {
this.replaceWith(`/${defaultHomepage()}`).then(e => {
- Ember.run.next(() => e.send("showLogin"));
+ next(() => e.send("showLogin"));
});
}
}
diff --git a/app/assets/javascripts/discourse/routes/new-message.js.es6 b/app/assets/javascripts/discourse/routes/new-message.js.es6
index 42b664fde5..c85001cbbf 100644
--- a/app/assets/javascripts/discourse/routes/new-message.js.es6
+++ b/app/assets/javascripts/discourse/routes/new-message.js.es6
@@ -1,7 +1,9 @@
+import { next } from "@ember/runloop";
+import DiscourseRoute from "discourse/routes/discourse";
import User from "discourse/models/user";
import Group from "discourse/models/group";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
beforeModel(transition) {
const params = transition.to.queryParams;
@@ -14,7 +16,7 @@ export default Discourse.Route.extend({
User.findByUsername(encodeURIComponent(params.username))
.then(user => {
if (user.can_send_private_message_to_user) {
- Ember.run.next(() =>
+ next(() =>
e.send(
"createNewMessageViaParams",
user.username,
@@ -34,7 +36,7 @@ export default Discourse.Route.extend({
Group.messageable(groupName)
.then(result => {
if (result.messageable) {
- Ember.run.next(() =>
+ next(() =>
e.send(
"createNewMessageViaParams",
groupName,
diff --git a/app/assets/javascripts/discourse/routes/new-topic.js.es6 b/app/assets/javascripts/discourse/routes/new-topic.js.es6
index 266276fd06..550b4b5341 100644
--- a/app/assets/javascripts/discourse/routes/new-topic.js.es6
+++ b/app/assets/javascripts/discourse/routes/new-topic.js.es6
@@ -1,6 +1,8 @@
+import { next } from "@ember/runloop";
+import DiscourseRoute from "discourse/routes/discourse";
import Category from "discourse/models/category";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
beforeModel(transition) {
if (this.currentUser) {
let category, categoryId;
@@ -62,7 +64,7 @@ export default Discourse.Route.extend({
},
_sendTransition(event, transition, categoryId) {
- Ember.run.next(() => {
+ next(() => {
event.send(
"createNewTopicViaParams",
transition.to.queryParams.title,
diff --git a/app/assets/javascripts/discourse/routes/password-reset.js.es6 b/app/assets/javascripts/discourse/routes/password-reset.js.es6
index a0ccc31484..393b19f96a 100644
--- a/app/assets/javascripts/discourse/routes/password-reset.js.es6
+++ b/app/assets/javascripts/discourse/routes/password-reset.js.es6
@@ -1,8 +1,9 @@
+import DiscourseRoute from "discourse/routes/discourse";
import PreloadStore from "preload-store";
import { ajax } from "discourse/lib/ajax";
import { userPath } from "discourse/lib/url";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
titleToken() {
return I18n.t("login.reset_password");
},
diff --git a/app/assets/javascripts/discourse/routes/post.js.es6 b/app/assets/javascripts/discourse/routes/post.js.es6
index c63a71243c..15b8aa3d1a 100644
--- a/app/assets/javascripts/discourse/routes/post.js.es6
+++ b/app/assets/javascripts/discourse/routes/post.js.es6
@@ -1,6 +1,7 @@
+import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from "discourse/lib/ajax";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
beforeModel({ params }) {
return ajax(`/p/${params.post.id}`).then(t => {
this.transitionTo(
diff --git a/app/assets/javascripts/discourse/routes/preferences-about.js.es6 b/app/assets/javascripts/discourse/routes/preferences-about.js.es6
deleted file mode 100644
index d6e29ec781..0000000000
--- a/app/assets/javascripts/discourse/routes/preferences-about.js.es6
+++ /dev/null
@@ -1,46 +0,0 @@
-import RestrictedUserRoute from "discourse/routes/restricted-user";
-
-export default RestrictedUserRoute.extend({
- showFooter: true,
-
- model: function() {
- return this.modelFor("user");
- },
-
- renderTemplate: function() {
- this.render({ into: "user" });
- },
-
- setupController: function(controller, model) {
- controller.setProperties({ model, newBio: model.get("bio_raw") });
- },
-
- // A bit odd, but if we leave to /preferences we need to re-render that outlet
- deactivate: function() {
- this._super(...arguments);
- this.render("preferences", { into: "user", controller: "preferences" });
- },
-
- actions: {
- changeAbout: function() {
- var route = this;
- var controller = route.controllerFor("preferences/about");
-
- controller.setProperties({ saving: true });
- return controller
- .get("model")
- .save()
- .then(
- function() {
- controller.set("saving", false);
- route.transitionTo("user.index");
- },
- function() {
- // model failed to save
- controller.set("saving", false);
- bootbox.alert(I18n.t("generic_error"));
- }
- );
- }
- }
-});
diff --git a/app/assets/javascripts/discourse/routes/preferences-account.js.es6 b/app/assets/javascripts/discourse/routes/preferences-account.js.es6
index e654ff0622..f55a568997 100644
--- a/app/assets/javascripts/discourse/routes/preferences-account.js.es6
+++ b/app/assets/javascripts/discourse/routes/preferences-account.js.es6
@@ -8,7 +8,10 @@ export default RestrictedUserRoute.extend({
const user = this.modelFor("user");
if (this.siteSettings.enable_badges) {
return UserBadge.findByUsername(user.get("username")).then(userBadges => {
- user.set("badges", userBadges.map(ub => ub.badge));
+ user.set(
+ "badges",
+ userBadges.map(ub => ub.badge)
+ );
return user;
});
} else {
@@ -21,7 +24,8 @@ export default RestrictedUserRoute.extend({
controller.setProperties({
model: user,
newNameInput: user.get("name"),
- newTitleInput: user.get("title")
+ newTitleInput: user.get("title"),
+ newPrimaryGroupInput: user.get("primary_group_id")
});
},
diff --git a/app/assets/javascripts/discourse/routes/preferences-profile.js.es6 b/app/assets/javascripts/discourse/routes/preferences-profile.js.es6
index 713d79e420..02eb4df866 100644
--- a/app/assets/javascripts/discourse/routes/preferences-profile.js.es6
+++ b/app/assets/javascripts/discourse/routes/preferences-profile.js.es6
@@ -1,5 +1,13 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
+import { set } from "@ember/object";
export default RestrictedUserRoute.extend({
- showFooter: true
+ showFooter: true,
+ setupController(controller, model) {
+ if (!model.user_option.timezone) {
+ set(model, "user_option.timezone", moment.tz.guess());
+ }
+
+ controller.set("model", model);
+ }
});
diff --git a/app/assets/javascripts/discourse/routes/preferences-second-factor-backup.js.es6 b/app/assets/javascripts/discourse/routes/preferences-second-factor-backup.js.es6
deleted file mode 100644
index afe26e906a..0000000000
--- a/app/assets/javascripts/discourse/routes/preferences-second-factor-backup.js.es6
+++ /dev/null
@@ -1,21 +0,0 @@
-import RestrictedUserRoute from "discourse/routes/restricted-user";
-
-export default RestrictedUserRoute.extend({
- showFooter: true,
-
- model() {
- return this.modelFor("user");
- },
-
- renderTemplate() {
- return this.render({ into: "user" });
- },
-
- setupController(controller, model) {
- controller.setProperties({ model, newUsername: model.get("username") });
- },
-
- deactivate() {
- this.controller.setProperties({ backupCodes: null });
- }
-});
diff --git a/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 b/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6
index 703bcf32c3..cae63a6707 100644
--- a/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6
+++ b/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6
@@ -13,6 +13,25 @@ export default RestrictedUserRoute.extend({
setupController(controller, model) {
controller.setProperties({ model, newUsername: model.get("username") });
+ controller.set("loading", true);
+
+ model
+ .loadSecondFactorCodes("")
+ .then(response => {
+ if (response.error) {
+ controller.set("errorMessage", response.error);
+ } else {
+ controller.setProperties({
+ errorMessage: null,
+ loaded: !response.password_required,
+ dirty: !!response.password_required,
+ totps: response.totps,
+ security_keys: response.security_keys
+ });
+ }
+ })
+ .catch(controller.popupAjaxError)
+ .finally(() => controller.set("loading", false));
},
actions: {
@@ -26,6 +45,7 @@ export default RestrictedUserRoute.extend({
if (
transition.targetName === "preferences.second-factor" ||
!user ||
+ user.is_anonymous ||
user.second_factor_enabled ||
(settings.enforce_second_factor === "staff" && !user.staff) ||
settings.enforce_second_factor === "no"
diff --git a/app/assets/javascripts/discourse/routes/review-index.js.es6 b/app/assets/javascripts/discourse/routes/review-index.js.es6
index b6ba62ab45..3f2a8abc4a 100644
--- a/app/assets/javascripts/discourse/routes/review-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/review-index.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
model(params) {
return this.store.findAll("reviewable", params);
},
@@ -21,10 +23,33 @@ export default Discourse.Route.extend({
filterPriority: meta.priority,
reviewableTypes: meta.reviewable_types,
filterUsername: meta.username,
- filterSortOrder: meta.sort_order
+ filterFromDate: meta.from_date,
+ filterToDate: meta.to_date,
+ filterSortOrder: meta.sort_order,
+ additionalFilters: meta.additional_filters || {}
});
},
+ activate() {
+ this.messageBus.subscribe("/reviewable_claimed", data => {
+ const reviewables = this.controller.reviewables;
+ if (reviewables) {
+ const user = data.user
+ ? this.store.createRecord("user", data.user)
+ : null;
+ reviewables.forEach(reviewable => {
+ if (data.topic_id === reviewable.topic.id) {
+ reviewable.set("claimed_by", user);
+ }
+ });
+ }
+ });
+ },
+
+ deactivate() {
+ this.messageBus.unsubscribe("/reviewable_claimed");
+ },
+
actions: {
refreshRoute() {
this.refresh();
diff --git a/app/assets/javascripts/discourse/routes/review-settings.js.es6 b/app/assets/javascripts/discourse/routes/review-settings.js.es6
index 46ed6f10fa..bb5e2e5fb1 100644
--- a/app/assets/javascripts/discourse/routes/review-settings.js.es6
+++ b/app/assets/javascripts/discourse/routes/review-settings.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
model() {
return this.store.find("reviewable-settings");
},
diff --git a/app/assets/javascripts/discourse/routes/review-show.js.es6 b/app/assets/javascripts/discourse/routes/review-show.js.es6
index afe7b30a5d..2d6d9795d4 100644
--- a/app/assets/javascripts/discourse/routes/review-show.js.es6
+++ b/app/assets/javascripts/discourse/routes/review-show.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
setupController(controller, model) {
controller.set("reviewable", model);
}
diff --git a/app/assets/javascripts/discourse/routes/review-topics.js.es6 b/app/assets/javascripts/discourse/routes/review-topics.js.es6
index 7c3f058fbd..262194ed00 100644
--- a/app/assets/javascripts/discourse/routes/review-topics.js.es6
+++ b/app/assets/javascripts/discourse/routes/review-topics.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
model() {
return this.store.findAll("reviewable-topic");
},
diff --git a/app/assets/javascripts/discourse/routes/review.js.es6 b/app/assets/javascripts/discourse/routes/review.js.es6
index 6343919dfc..9c28f22c54 100644
--- a/app/assets/javascripts/discourse/routes/review.js.es6
+++ b/app/assets/javascripts/discourse/routes/review.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
titleToken() {
return I18n.t("review.title");
}
diff --git a/app/assets/javascripts/discourse/routes/signup.js.es6 b/app/assets/javascripts/discourse/routes/signup.js.es6
index a2753ded2b..9a732c67d8 100644
--- a/app/assets/javascripts/discourse/routes/signup.js.es6
+++ b/app/assets/javascripts/discourse/routes/signup.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
import buildStaticRoute from "discourse/routes/build-static-route";
const SignupRoute = buildStaticRoute("signup");
@@ -9,13 +10,13 @@ SignupRoute.reopen({
if (!this.siteSettings.login_required) {
this.replaceWith("discovery.latest").then(e => {
if (canSignUp) {
- Ember.run.next(() => e.send("showCreateAccount"));
+ next(() => e.send("showCreateAccount"));
}
});
} else {
this.replaceWith("login").then(e => {
if (canSignUp) {
- Ember.run.next(() => e.send("showCreateAccount"));
+ next(() => e.send("showCreateAccount"));
}
});
}
diff --git a/app/assets/javascripts/discourse/routes/tag-groups-edit.js.es6 b/app/assets/javascripts/discourse/routes/tag-groups-edit.js.es6
new file mode 100644
index 0000000000..a923282cce
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/tag-groups-edit.js.es6
@@ -0,0 +1,13 @@
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
+ showFooter: true,
+
+ model(params) {
+ return this.store.find("tagGroup", params.id);
+ },
+
+ afterModel(tagGroup) {
+ tagGroup.set("savingStatus", null);
+ }
+});
diff --git a/app/assets/javascripts/discourse/routes/tag-groups-new.js.es6 b/app/assets/javascripts/discourse/routes/tag-groups-new.js.es6
new file mode 100644
index 0000000000..64c460f512
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/tag-groups-new.js.es6
@@ -0,0 +1,17 @@
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
+ showFooter: true,
+
+ beforeModel() {
+ if (!this.siteSettings.tagging_enabled) {
+ this.transitionTo("tagGroups");
+ }
+ },
+
+ model() {
+ return this.store.createRecord("tagGroup", {
+ name: I18n.t("tagging.groups.new_name")
+ });
+ }
+});
diff --git a/app/assets/javascripts/discourse/routes/tag-groups-show.js.es6 b/app/assets/javascripts/discourse/routes/tag-groups-show.js.es6
deleted file mode 100644
index 165ae49d4a..0000000000
--- a/app/assets/javascripts/discourse/routes/tag-groups-show.js.es6
+++ /dev/null
@@ -1,7 +0,0 @@
-export default Discourse.Route.extend({
- showFooter: true,
-
- model(params) {
- return this.store.find("tagGroup", params.id);
- }
-});
diff --git a/app/assets/javascripts/discourse/routes/tag-groups.js.es6 b/app/assets/javascripts/discourse/routes/tag-groups.js.es6
index ccc7d79b75..1010b04c03 100644
--- a/app/assets/javascripts/discourse/routes/tag-groups.js.es6
+++ b/app/assets/javascripts/discourse/routes/tag-groups.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
showFooter: true,
model() {
diff --git a/app/assets/javascripts/discourse/routes/tags-index.js.es6 b/app/assets/javascripts/discourse/routes/tags-index.js.es6
index c9436e876d..99cd3a211d 100644
--- a/app/assets/javascripts/discourse/routes/tags-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/tags-index.js.es6
@@ -1,6 +1,7 @@
+import DiscourseRoute from "discourse/routes/discourse";
import Tag from "discourse/models/tag";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
model() {
return this.store.findAll("tag").then(result => {
if (result.extras) {
diff --git a/app/assets/javascripts/discourse/routes/tags-show.js.es6 b/app/assets/javascripts/discourse/routes/tags-show.js.es6
index a538c4f5ef..08fe0b24d2 100644
--- a/app/assets/javascripts/discourse/routes/tags-show.js.es6
+++ b/app/assets/javascripts/discourse/routes/tags-show.js.es6
@@ -1,15 +1,19 @@
+import DiscourseRoute from "discourse/routes/discourse";
import Composer from "discourse/models/composer";
import showModal from "discourse/lib/show-modal";
-import { findTopicList } from "discourse/routes/build-topic-route";
+import {
+ filterQueryParams,
+ findTopicList
+} from "discourse/routes/build-topic-route";
+import { queryParams } from "discourse/controllers/discovery-sortable";
import PermissionType from "discourse/models/permission-type";
+import Category from "discourse/models/category";
+import FilterModeMixin from "discourse/mixins/filter-mode";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend(FilterModeMixin, {
navMode: "latest",
- queryParams: {
- ascending: { refreshModel: true },
- order: { refreshModel: true }
- },
+ queryParams,
renderTemplate() {
const controller = this.controllerFor("tags.show");
@@ -20,8 +24,6 @@ export default Discourse.Route.extend({
const tag = this.store.createRecord("tag", {
id: Handlebars.Utils.escapeExpression(params.tag_id)
});
- let f = "";
-
if (params.additional_tags) {
this.set(
"additionalTags",
@@ -35,22 +37,9 @@ export default Discourse.Route.extend({
this.set("additionalTags", null);
}
- if (params.category) {
- f = "c/";
- if (params.parent_category) {
- f += `${params.parent_category}/`;
- }
- f += `${params.category}/l/`;
- }
- f += this.navMode;
- this.set("filterMode", f);
+ this.set("filterType", this.navMode.split("/")[0]);
- if (params.category) {
- this.set("categorySlug", params.category);
- }
- if (params.parent_category) {
- this.set("parentCategorySlug", params.parent_category);
- }
+ this.set("categorySlugPathWithID", params.category_slug_path_with_id);
if (tag && tag.get("id") !== "none" && this.currentUser) {
// If logged in, we should get the tag's user settings
@@ -67,48 +56,40 @@ export default Discourse.Route.extend({
afterModel(tag, transition) {
const controller = this.controllerFor("tags.show");
- controller.set("loading", true);
+ controller.setProperties({
+ loading: true,
+ showInfo: false
+ });
- const params = controller.getProperties("order", "ascending");
- params.order = transition.to.queryParams.order || params.order;
- params.ascending = transition.to.queryParams.ascending || params.ascending;
-
- const categorySlug = this.categorySlug;
- const parentCategorySlug = this.parentCategorySlug;
- const filter = this.navMode;
+ const params = filterQueryParams(transition.to.queryParams, {});
+ const category = this.categorySlugPathWithID
+ ? Category.findBySlugPathWithID(this.categorySlugPathWithID)
+ : null;
+ const topicFilter = this.navMode;
const tagId = tag ? tag.id.toLowerCase() : "none";
+ let filter;
- if (categorySlug) {
- const category = Discourse.Category.findBySlug(
- categorySlug,
- parentCategorySlug
- );
- if (parentCategorySlug) {
- params.filter = `tags/c/${parentCategorySlug}/${categorySlug}/${tagId}/l/${filter}`;
- } else {
- params.filter = `tags/c/${categorySlug}/${tagId}/l/${filter}`;
- }
- if (category) {
- category.setupGroupsAndPermissions();
- this.set("category", category);
+ if (category) {
+ category.setupGroupsAndPermissions();
+ this.set("category", category);
+ filter = `tags/c/${Category.slugFor(category)}/${category.id}`;
+
+ if (this.noSubcategories) {
+ filter += "/none";
}
+
+ filter += `/${tagId}/l/${topicFilter}`;
} else if (this.additionalTags) {
- params.filter = `tags/intersection/${tagId}/${this.get(
- "additionalTags"
- ).join("/")}`;
this.set("category", null);
+ filter = `tags/intersection/${tagId}/${this.additionalTags.join("/")}`;
} else {
- params.filter = `tags/${tagId}/l/${filter}`;
this.set("category", null);
+ filter = `tag/${tagId}/l/${topicFilter}`;
}
- return findTopicList(
- this.store,
- this.topicTrackingState,
- params.filter,
- params,
- { cached: true }
- ).then(list => {
+ return findTopicList(this.store, this.topicTrackingState, filter, params, {
+ cached: true
+ }).then(list => {
if (list.topic_list.tags && list.topic_list.tags.length === 1) {
// Update name of tag (case might be different)
tag.setProperties({
@@ -166,10 +147,17 @@ export default Discourse.Route.extend({
tag: model,
additionalTags: this.additionalTags,
category: this.category,
- filterMode: this.filterMode,
+ filterType: this.filterType,
navMode: this.navMode,
- tagNotification: this.tagNotification
+ tagNotification: this.tagNotification,
+ noSubcategories: this.noSubcategories
});
+ this.searchService.set("searchContext", model.get("searchContext"));
+ },
+
+ deactivate() {
+ this._super(...arguments);
+ this.searchService.set("searchContext", null);
},
actions: {
diff --git a/app/assets/javascripts/discourse/routes/topic-by-slug-or-id.js.es6 b/app/assets/javascripts/discourse/routes/topic-by-slug-or-id.js.es6
index dd5b8e444b..62339b6519 100644
--- a/app/assets/javascripts/discourse/routes/topic-by-slug-or-id.js.es6
+++ b/app/assets/javascripts/discourse/routes/topic-by-slug-or-id.js.es6
@@ -1,7 +1,8 @@
-import { default as Topic, ID_CONSTRAINT } from "discourse/models/topic";
+import DiscourseRoute from "discourse/routes/discourse";
+import Topic, { ID_CONSTRAINT } from "discourse/models/topic";
import DiscourseURL from "discourse/lib/url";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
model(params) {
if (params.slugOrId.match(ID_CONSTRAINT)) {
return { url: `/t/topic/${params.slugOrId}` };
diff --git a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6
index 63a0ebcfb2..dd2215520f 100644
--- a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6
+++ b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6
@@ -1,8 +1,12 @@
+import { isEmpty } from "@ember/utils";
+import { scheduleOnce } from "@ember/runloop";
+import DiscourseRoute from "discourse/routes/discourse";
import DiscourseURL from "discourse/lib/url";
import Draft from "discourse/models/draft";
+import ENV from "discourse-common/config/environment";
// This route is used for retrieving a topic based on params
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
// Avoid default model hook
model(params) {
return params;
@@ -35,6 +39,11 @@ export default Discourse.Route.extend({
// TODO we are seeing errors where closest post is null and this is exploding
// we need better handling and logging for this condition.
+ // there are no closestPost for hidden topics
+ if (topic.view_hidden) {
+ return;
+ }
+
// The post we requested might not exist. Let's find the closest post
const closestPost = postStream.closestPostForPostNumber(
params.nearPost || 1
@@ -47,10 +56,11 @@ export default Discourse.Route.extend({
enteredAt: new Date().getTime().toString()
});
+ this.appEvents.trigger("page:topic-loaded", topic);
topicController.subscribe();
// Highlight our post after the next render
- Ember.run.scheduleOnce("afterRender", () =>
+ scheduleOnce("afterRender", () =>
this.appEvents.trigger("post:highlight", closest)
);
@@ -60,7 +70,7 @@ export default Discourse.Route.extend({
}
DiscourseURL.jumpToPost(closest, opts);
- if (!Ember.isEmpty(topic.draft)) {
+ if (!isEmpty(topic.draft)) {
composerController.open({
draft: Draft.getLocal(topic.draft_key, topic.draft),
draftKey: topic.draft_key,
@@ -71,10 +81,23 @@ export default Discourse.Route.extend({
}
})
.catch(e => {
- if (!Ember.testing) {
+ if (ENV.environment !== "test") {
// eslint-disable-next-line no-console
console.log("Could not view topic", e);
}
});
+ },
+
+ actions: {
+ willTransition() {
+ this.controllerFor("topic").set(
+ "previousURL",
+ document.location.pathname
+ );
+
+ // NOTE: omitting this return can break the back button when transitioning quickly between
+ // topics and the latest page.
+ return true;
+ }
}
});
diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6
index c25d72e1e8..d4c22f9b3e 100644
--- a/app/assets/javascripts/discourse/routes/topic.js.es6
+++ b/app/assets/javascripts/discourse/routes/topic.js.es6
@@ -1,5 +1,12 @@
+import { get } from "@ember/object";
+import { isEmpty } from "@ember/utils";
+import { cancel } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import { later } from "@ember/runloop";
+import DiscourseRoute from "discourse/routes/discourse";
import DiscourseURL from "discourse/lib/url";
import { ID_CONSTRAINT } from "discourse/models/topic";
+import { EventTarget } from "rsvp";
let isTransitioning = false,
scheduledReplace = null,
@@ -9,7 +16,7 @@ const SCROLL_DELAY = 500;
import showModal from "discourse/lib/show-modal";
-const TopicRoute = Discourse.Route.extend({
+const TopicRoute = DiscourseRoute.extend({
redirect() {
return this.redirectIfLoginRequired();
},
@@ -167,9 +174,9 @@ const TopicRoute = Discourse.Route.extend({
postUrl += "/" + currentPost;
}
- Ember.run.cancel(scheduledReplace);
+ cancel(scheduledReplace);
lastScrollPos = parseInt($(document).scrollTop(), 10);
- scheduledReplace = Ember.run.later(
+ scheduledReplace = later(
this,
"_replaceUnlessScrolling",
postUrl,
@@ -185,7 +192,7 @@ const TopicRoute = Discourse.Route.extend({
willTransition() {
this._super(...arguments);
- Ember.run.cancel(scheduledReplace);
+ cancel(scheduledReplace);
isTransitioning = true;
return true;
}
@@ -200,7 +207,7 @@ const TopicRoute = Discourse.Route.extend({
return;
}
lastScrollPos = currentPos;
- scheduledReplace = Ember.run.later(
+ scheduledReplace = later(
this,
"_replaceUnlessScrolling",
url,
@@ -210,13 +217,13 @@ const TopicRoute = Discourse.Route.extend({
setupParams(topic, params) {
const postStream = topic.get("postStream");
- postStream.set("summary", Ember.get(params, "filter") === "summary");
+ postStream.set("summary", get(params, "filter") === "summary");
- const usernames = Ember.get(params, "username_filters"),
+ const usernames = get(params, "username_filters"),
userFilters = postStream.get("userFilters");
userFilters.clear();
- if (!Ember.isEmpty(usernames) && usernames !== "undefined") {
+ if (!isEmpty(usernames) && usernames !== "undefined") {
userFilters.addObjects(usernames.split(","));
}
@@ -225,9 +232,13 @@ const TopicRoute = Discourse.Route.extend({
model(params, transition) {
if (params.slug.match(ID_CONSTRAINT)) {
- return DiscourseURL.routeTo(`/t/topic/${params.slug}/${params.id}`, {
+ transition.abort();
+
+ DiscourseURL.routeTo(`/t/topic/${params.slug}/${params.id}`, {
replaceURL: true
});
+
+ return;
}
const queryParams = transition.to.queryParams;
@@ -297,11 +308,11 @@ const TopicRoute = Discourse.Route.extend({
// We reset screen tracking every time a topic is entered
this.screenTrack.start(model.get("id"), controller);
- Ember.run.scheduleOnce("afterRender", () => {
+ scheduleOnce("afterRender", () => {
this.appEvents.trigger("header:update-topic", model);
});
}
});
-RSVP.EventTarget.mixin(TopicRoute);
+EventTarget.mixin(TopicRoute);
export default TopicRoute;
diff --git a/app/assets/javascripts/discourse/routes/unknown.js.es6 b/app/assets/javascripts/discourse/routes/unknown.js.es6
index 4b2b993549..3df278df6f 100644
--- a/app/assets/javascripts/discourse/routes/unknown.js.es6
+++ b/app/assets/javascripts/discourse/routes/unknown.js.es6
@@ -1,6 +1,7 @@
+import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from "discourse/lib/ajax";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
model() {
return ajax("/404-body", { dataType: "html" });
}
diff --git a/app/assets/javascripts/discourse/routes/user-activity-drafts.js.es6 b/app/assets/javascripts/discourse/routes/user-activity-drafts.js.es6
index 2ca94b05e5..a004e6631e 100644
--- a/app/assets/javascripts/discourse/routes/user-activity-drafts.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-activity-drafts.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
model() {
let userDraftsStream = this.modelFor("user").get("userDraftsStream");
return userDraftsStream.load(this.site).then(() => userDraftsStream);
@@ -10,9 +12,16 @@ export default Discourse.Route.extend({
setupController(controller, model) {
controller.set("model", model);
+ },
+
+ activate() {
this.appEvents.on("draft:destroyed", this, this.refresh);
},
+ deactivate() {
+ this.appEvents.off("draft:destroyed", this, this.refresh);
+ },
+
actions: {
didTransition() {
this.controllerFor("user-activity")._showFooter();
diff --git a/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6 b/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6
index 8d9d1e8144..1aea1056cf 100644
--- a/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6
@@ -1,6 +1,7 @@
+import DiscourseRoute from "discourse/routes/discourse";
import ViewingActionType from "discourse/mixins/viewing-action-type";
-export default Discourse.Route.extend(ViewingActionType, {
+export default DiscourseRoute.extend(ViewingActionType, {
queryParams: {
acting_username: { refreshModel: true }
},
diff --git a/app/assets/javascripts/discourse/routes/user-activity.js.es6 b/app/assets/javascripts/discourse/routes/user-activity.js.es6
index a827e3d998..f36beaf1d5 100644
--- a/app/assets/javascripts/discourse/routes/user-activity.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-activity.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
model() {
let user = this.modelFor("user");
if (user.get("profile_hidden")) {
diff --git a/app/assets/javascripts/discourse/routes/user-badges.js.es6 b/app/assets/javascripts/discourse/routes/user-badges.js.es6
index 0efe8069fc..ffe6008da9 100644
--- a/app/assets/javascripts/discourse/routes/user-badges.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-badges.js.es6
@@ -1,7 +1,8 @@
+import DiscourseRoute from "discourse/routes/discourse";
import ViewingActionType from "discourse/mixins/viewing-action-type";
import UserBadge from "discourse/models/user-badge";
-export default Discourse.Route.extend(ViewingActionType, {
+export default DiscourseRoute.extend(ViewingActionType, {
model() {
return UserBadge.findByUsername(
this.modelFor("user").get("username_lower"),
diff --git a/app/assets/javascripts/discourse/routes/user-index.js.es6 b/app/assets/javascripts/discourse/routes/user-index.js.es6
index eef87b16eb..ca51d8fce9 100644
--- a/app/assets/javascripts/discourse/routes/user-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-index.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
beforeModel() {
const { currentUser } = this;
const viewingMe =
diff --git a/app/assets/javascripts/discourse/routes/user-invited-index.js.es6 b/app/assets/javascripts/discourse/routes/user-invited-index.js.es6
index bf7a358215..721f4fb8b2 100644
--- a/app/assets/javascripts/discourse/routes/user-invited-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-invited-index.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
beforeModel: function() {
this.replaceWith("userInvited.show", "pending");
}
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 54f367b739..3cd83f79ce 100644
--- a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6
@@ -1,7 +1,8 @@
+import DiscourseRoute from "discourse/routes/discourse";
import Invite from "discourse/models/invite";
import showModal from "discourse/lib/show-modal";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
model(params) {
Invite.findInvitedCount(this.modelFor("user")).then(result =>
this.set("invitesCount", result)
diff --git a/app/assets/javascripts/discourse/routes/user-notifications-index.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications-index.js.es6
index 1e998969a3..28f0041253 100644
--- a/app/assets/javascripts/discourse/routes/user-notifications-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-notifications-index.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
controllerName: "user-notifications",
renderTemplate() {
this.render("user/notifications-index");
diff --git a/app/assets/javascripts/discourse/routes/user-notifications.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications.js.es6
index e4e65c7f9b..3feb837746 100644
--- a/app/assets/javascripts/discourse/routes/user-notifications.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-notifications.js.es6
@@ -1,6 +1,7 @@
+import DiscourseRoute from "discourse/routes/discourse";
import ViewingActionType from "discourse/mixins/viewing-action-type";
-export default Discourse.Route.extend(ViewingActionType, {
+export default DiscourseRoute.extend(ViewingActionType, {
renderTemplate() {
this.render("user/notifications");
},
diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6
index 0d37dac923..0aece428d2 100644
--- a/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6
@@ -1,4 +1,5 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
+import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list";
export default createPMRoute("groups", "private-messages-groups").extend({
groupName: null,
@@ -16,9 +17,11 @@ export default createPMRoute("groups", "private-messages-groups").extend({
model(params) {
const username = this.modelFor("user").get("username_lower");
- return this.store.findFiltered("topicList", {
- filter: `topics/private-messages-group/${username}/${params.name}/archive`
- });
+ const filter = `topics/private-messages-group/${username}/${params.name}/archive`;
+ const lastTopicList = findOrResetCachedTopicList(this.session, filter);
+ return lastTopicList
+ ? lastTopicList
+ : this.store.findFiltered("topicList", { filter });
},
afterModel(model) {
diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6
index cea1b022f4..971cb506cb 100644
--- a/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6
@@ -1,4 +1,5 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
+import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list";
export default createPMRoute("groups", "private-messages-groups").extend({
groupName: null,
@@ -11,9 +12,11 @@ export default createPMRoute("groups", "private-messages-groups").extend({
model(params) {
const username = this.modelFor("user").get("username_lower");
- return this.store.findFiltered("topicList", {
- filter: `topics/private-messages-group/${username}/${params.name}`
- });
+ const filter = `topics/private-messages-group/${username}/${params.name}`;
+ const lastTopicList = findOrResetCachedTopicList(this.session, filter);
+ return lastTopicList
+ ? lastTopicList
+ : this.store.findFiltered("topicList", { filter });
},
afterModel(model) {
diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-tags.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-tags.js.es6
index 7007920e38..cfe2d026bb 100644
--- a/app/assets/javascripts/discourse/routes/user-private-messages-tags.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-private-messages-tags.js.es6
@@ -1,12 +1,14 @@
+import EmberObject from "@ember/object";
+import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
model() {
const username = this.modelFor("user").get("username_lower");
return ajax(`/tags/personal_messages/${username}`)
.then(result => {
- return result.tags.map(tag => Ember.Object.create(tag));
+ return result.tags.map(tag => EmberObject.create(tag));
})
.catch(popupAjaxError);
},
diff --git a/app/assets/javascripts/discourse/routes/user-private-messages.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages.js.es6
index 2596727a6a..ef6854e056 100644
--- a/app/assets/javascripts/discourse/routes/user-private-messages.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-private-messages.js.es6
@@ -1,6 +1,8 @@
+import DiscourseRoute from "discourse/routes/discourse";
import Draft from "discourse/models/draft";
+import Composer from "discourse/models/composer";
-export default Discourse.Route.extend({
+export default DiscourseRoute.extend({
renderTemplate() {
this.render("user/messages");
},
@@ -17,7 +19,7 @@ export default Discourse.Route.extend({
if (data.draft) {
composerController.open({
draft: data.draft,
- draftKey: "new_private_message",
+ draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY,
ignoreIfChanged: true,
draftSequence: data.draft_sequence
});
diff --git a/app/assets/javascripts/discourse/routes/user-summary.js.es6 b/app/assets/javascripts/discourse/routes/user-summary.js.es6
index 698d2f4edc..6827063517 100644
--- a/app/assets/javascripts/discourse/routes/user-summary.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-summary.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
showFooter: true,
model() {
diff --git a/app/assets/javascripts/discourse/routes/user-topic-list.js.es6 b/app/assets/javascripts/discourse/routes/user-topic-list.js.es6
index c82e0dceea..80f2c66734 100644
--- a/app/assets/javascripts/discourse/routes/user-topic-list.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-topic-list.js.es6
@@ -1,6 +1,7 @@
+import DiscourseRoute from "discourse/routes/discourse";
import ViewingActionType from "discourse/mixins/viewing-action-type";
-export default Discourse.Route.extend(ViewingActionType, {
+export default DiscourseRoute.extend(ViewingActionType, {
renderTemplate() {
this.render("user-topics-list");
},
diff --git a/app/assets/javascripts/discourse/routes/user.js.es6 b/app/assets/javascripts/discourse/routes/user.js.es6
index 4eaaa4c910..85248c9038 100644
--- a/app/assets/javascripts/discourse/routes/user.js.es6
+++ b/app/assets/javascripts/discourse/routes/user.js.es6
@@ -1,4 +1,7 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+import User from "discourse/models/user";
+
+export default DiscourseRoute.extend({
titleToken() {
const username = this.modelFor("user").username;
if (username) {
@@ -39,7 +42,7 @@ export default Discourse.Route.extend({
return this.currentUser;
}
- return Discourse.User.create({
+ return User.create({
username: encodeURIComponent(params.username)
});
},
diff --git a/app/assets/javascripts/discourse/routes/users.js.es6 b/app/assets/javascripts/discourse/routes/users.js.es6
index 009ee9e88d..b500a985b5 100644
--- a/app/assets/javascripts/discourse/routes/users.js.es6
+++ b/app/assets/javascripts/discourse/routes/users.js.es6
@@ -1,4 +1,6 @@
-export default Discourse.Route.extend({
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
queryParams: {
period: { refreshModel: true },
order: { refreshModel: true },
diff --git a/app/assets/javascripts/discourse/services/app-events.js.es6 b/app/assets/javascripts/discourse/services/app-events.js.es6
new file mode 100644
index 0000000000..fa73e9d463
--- /dev/null
+++ b/app/assets/javascripts/discourse/services/app-events.js.es6
@@ -0,0 +1,49 @@
+import deprecated from "discourse-common/lib/deprecated";
+import Service from "@ember/service";
+
+export default Service.extend(Ember.Evented, {
+ _events: {},
+
+ on() {
+ if (arguments.length === 2) {
+ let [name, fn] = arguments;
+ let target = {};
+ this._events[name] = this._events[name] || [];
+ this._events[name].push({ target, fn });
+
+ this._super(name, target, fn);
+ } else if (arguments.length === 3) {
+ let [name, target, fn] = arguments;
+ this._events[name] = this._events[name] || [];
+ this._events[name].push({ target, fn });
+
+ this._super(...arguments);
+ }
+ return this;
+ },
+
+ off() {
+ let name = arguments[0];
+ let fn = arguments[2];
+
+ if (this._events[name]) {
+ if (arguments.length === 1) {
+ deprecated(
+ "Removing all event listeners at once is deprecated, please remove each listener individually."
+ );
+
+ this._events[name].forEach(ref => {
+ this._super(name, ref.target, ref.fn);
+ });
+ delete this._events[name];
+ } else if (arguments.length === 3) {
+ this._super(...arguments);
+
+ this._events[name] = this._events[name].filter(e => e.fn !== fn);
+ if (this._events[name].length === 0) delete this._events[name];
+ }
+ }
+
+ return this;
+ }
+});
diff --git a/app/assets/javascripts/discourse/services/emoji-store.js.es6 b/app/assets/javascripts/discourse/services/emoji-store.js.es6
new file mode 100644
index 0000000000..19fbd662fc
--- /dev/null
+++ b/app/assets/javascripts/discourse/services/emoji-store.js.es6
@@ -0,0 +1,49 @@
+import KeyValueStore from "discourse/lib/key-value-store";
+import Service from "@ember/service";
+
+const EMOJI_USAGE = "emojiUsage";
+const EMOJI_SELECTED_DIVERSITY = "emojiSelectedDiversity";
+const TRACKED_EMOJIS = 15;
+const STORE_NAMESPACE = "discourse_emojis_";
+
+export default Service.extend({
+ init() {
+ this._super(...arguments);
+
+ this.store = new KeyValueStore(STORE_NAMESPACE);
+
+ if (!this.store.getObject(EMOJI_USAGE)) {
+ this.favorites = [];
+ }
+ },
+
+ get diversity() {
+ return this.store.getObject(EMOJI_SELECTED_DIVERSITY) || 1;
+ },
+
+ set diversity(value) {
+ this.store.setObject({ key: EMOJI_SELECTED_DIVERSITY, value: value || 1 });
+ },
+
+ get favorites() {
+ return this.store.getObject(EMOJI_USAGE) || [];
+ },
+
+ set favorites(value) {
+ this.store.setObject({ key: EMOJI_USAGE, value: value || [] });
+ },
+
+ track(code) {
+ const normalizedCode = code.replace(/(^:)|(:$)/g, "");
+ const recent = this.favorites.filter(r => r !== normalizedCode);
+ recent.unshift(normalizedCode);
+ recent.length = Math.min(recent.length, TRACKED_EMOJIS);
+ this.favorites = recent;
+ },
+
+ reset() {
+ const store = new KeyValueStore(STORE_NAMESPACE);
+ store.setObject({ key: EMOJI_USAGE, value: [] });
+ store.setObject({ key: EMOJI_SELECTED_DIVERSITY, value: 1 });
+ }
+});
diff --git a/app/assets/javascripts/discourse/services/logs-notice.js.es6 b/app/assets/javascripts/discourse/services/logs-notice.js.es6
index f70e021992..7292e4465f 100644
--- a/app/assets/javascripts/discourse/services/logs-notice.js.es6
+++ b/app/assets/javascripts/discourse/services/logs-notice.js.es6
@@ -1,13 +1,14 @@
-import {
- default as computed,
+import { isEmpty } from "@ember/utils";
+import EmberObject from "@ember/object";
+import discourseComputed, {
on,
observes
-} from "ember-addons/ember-computed-decorators";
+} from "discourse-common/utils/decorators";
import { autoUpdatingRelativeAge } from "discourse/lib/formatter";
const LOGS_NOTICE_KEY = "logs-notice-text";
-const LogsNotice = Ember.Object.extend({
+const LogsNotice = EmberObject.extend({
text: "",
@on("init")
@@ -45,24 +46,24 @@ const LogsNotice = Ember.Object.extend({
});
},
- @computed("text")
+ @discourseComputed("text")
isEmpty(text) {
- return Ember.isEmpty(text);
+ return isEmpty(text);
},
- @computed("text")
+ @discourseComputed("text")
message(text) {
return new Handlebars.SafeString(text);
},
- @computed("currentUser")
+ @discourseComputed("currentUser")
isAdmin(currentUser) {
return currentUser && currentUser.admin;
},
- @computed("isEmpty", "isAdmin")
- hidden(isEmpty, isAdmin) {
- return !isAdmin || isEmpty;
+ @discourseComputed("isEmpty", "isAdmin")
+ hidden(thisIsEmpty, isAdmin) {
+ return !isAdmin || thisIsEmpty;
},
@observes("text")
@@ -70,7 +71,7 @@ const LogsNotice = Ember.Object.extend({
this.keyValueStore.setItem(LOGS_NOTICE_KEY, this.text);
},
- @computed(
+ @discourseComputed(
"siteSettings.alert_admins_if_errors_per_hour",
"siteSettings.alert_admins_if_errors_per_minute"
)
diff --git a/app/assets/javascripts/discourse/services/search.js.es6 b/app/assets/javascripts/discourse/services/search.js.es6
index 2a47789da4..d7a8edcd10 100644
--- a/app/assets/javascripts/discourse/services/search.js.es6
+++ b/app/assets/javascripts/discourse/services/search.js.es6
@@ -1,9 +1,8 @@
-import {
- default as computed,
- observes
-} from "ember-addons/ember-computed-decorators";
+import { get } from "@ember/object";
+import EmberObject from "@ember/object";
+import discourseComputed, { observes } from "discourse-common/utils/decorators";
-export default Ember.Object.extend({
+export default EmberObject.extend({
searchContextEnabled: false, // checkbox to scope search
searchContext: null,
term: null,
@@ -14,11 +13,11 @@ export default Ember.Object.extend({
this.set("highlightTerm", this.term);
},
- @computed("searchContext")
+ @discourseComputed("searchContext")
contextType: {
get(searchContext) {
if (searchContext) {
- return Ember.get(searchContext, "type");
+ return get(searchContext, "type");
}
},
set(value, searchContext) {
diff --git a/app/assets/javascripts/discourse/services/theme-settings.js.es6 b/app/assets/javascripts/discourse/services/theme-settings.js.es6
index 5e1e8a8155..032024b56b 100644
--- a/app/assets/javascripts/discourse/services/theme-settings.js.es6
+++ b/app/assets/javascripts/discourse/services/theme-settings.js.es6
@@ -1,4 +1,7 @@
-export default Ember.Service.extend({
+import { get } from "@ember/object";
+import Service from "@ember/service";
+
+export default Service.extend({
settings: null,
init() {
@@ -12,7 +15,7 @@ export default Ember.Service.extend({
getSetting(themeId, settingsKey) {
if (this._settings[themeId]) {
- return this._settings[themeId][settingsKey];
+ return get(this._settings[themeId], settingsKey);
}
return null;
},
diff --git a/app/assets/javascripts/discourse/templates/about.hbs b/app/assets/javascripts/discourse/templates/about.hbs
index e0b3ca0c17..6e75a04a6d 100644
--- a/app/assets/javascripts/discourse/templates/about.hbs
+++ b/app/assets/javascripts/discourse/templates/about.hbs
@@ -28,9 +28,7 @@
{{d-icon "users"}} {{i18n 'about.our_admins'}}
- {{#each model.admins as |a|}}
- {{user-info user=a}}
- {{/each}}
+ {{about-page-users users=model.admins}}
@@ -45,9 +43,7 @@
{{d-icon "users"}} {{i18n 'about.our_moderators'}}
- {{#each model.moderators as |m|}}
- {{user-info user=m}}
- {{/each}}
+ {{about-page-users users=model.moderators}}
@@ -57,46 +53,59 @@
connectorTagName='section'
args=(hash model=model)}}
+ {{#if model.category_moderators.length}}
+ {{#each model.category_moderators as |cm|}}
+
+ {{category-link cm.category}}{{i18n "about.moderators"}}
+
+ {{about-page-users users=cm.moderators}}
+
+
+
+ {{/each}}
+ {{/if}}
- {{d-icon "bar-chart"}} {{i18n 'about.stats'}}
+ {{d-icon "far-chart-bar"}} {{i18n 'about.stats'}}
-
-
- {{i18n 'about.stat.last_7_days'}}
- {{i18n 'about.stat.last_30_days'}}
- {{i18n 'about.stat.all_time'}}
-
-
- {{i18n 'about.topic_count'}}
- {{number model.stats.topics_7_days}}
- {{number model.stats.topics_30_days}}
- {{number model.stats.topic_count}}
-
-
- {{i18n 'about.post_count'}}
- {{number model.stats.posts_7_days}}
- {{number model.stats.posts_30_days}}
- {{number model.stats.post_count}}
-
-
- {{i18n 'about.user_count'}}
- {{number model.stats.users_7_days}}
- {{number model.stats.users_30_days}}
- {{number model.stats.user_count}}
-
-
- {{i18n 'about.active_user_count'}}
- {{number model.stats.active_users_7_days}}
- {{number model.stats.active_users_30_days}}
- —
-
-
- {{i18n 'about.like_count'}}
- {{number model.stats.likes_7_days}}
- {{number model.stats.likes_30_days}}
- {{number model.stats.like_count}}
-
+
+
+
+ {{i18n 'about.stat.last_7_days'}}
+ {{i18n 'about.stat.last_30_days'}}
+ {{i18n 'about.stat.all_time'}}
+
+
+ {{i18n 'about.topic_count'}}
+ {{number model.stats.topics_7_days}}
+ {{number model.stats.topics_30_days}}
+ {{number model.stats.topic_count}}
+
+
+ {{i18n 'about.post_count'}}
+ {{number model.stats.posts_7_days}}
+ {{number model.stats.posts_30_days}}
+ {{number model.stats.post_count}}
+
+
+ {{i18n 'about.user_count'}}
+ {{number model.stats.users_7_days}}
+ {{number model.stats.users_30_days}}
+ {{number model.stats.user_count}}
+
+
+ {{i18n 'about.active_user_count'}}
+ {{number model.stats.active_users_7_days}}
+ {{number model.stats.active_users_30_days}}
+ —
+
+
+ {{i18n 'about.like_count'}}
+ {{number model.stats.likes_7_days}}
+ {{number model.stats.likes_30_days}}
+ {{number model.stats.like_count}}
+
+
diff --git a/app/assets/javascripts/discourse/templates/application.hbs b/app/assets/javascripts/discourse/templates/application.hbs
index ad010cbbf6..437754ce7b 100644
--- a/app/assets/javascripts/discourse/templates/application.hbs
+++ b/app/assets/javascripts/discourse/templates/application.hbs
@@ -7,7 +7,7 @@
toggleAnonymous=(route-action "toggleAnonymous")
logout=(route-action "logout")}}
-{{plugin-outlet name="below-site-header" args=(hash currentPath=currentPath)}}
+{{plugin-outlet name="below-site-header" args=(hash currentPath=router._router.currentPath)}}
{{plugin-outlet name="above-main-container"}}
@@ -16,9 +16,10 @@
{{custom-html name="top"}}
{{/if}}
{{notification-consent-banner}}
+ {{pwa-install-banner}}
{{global-notice}}
{{create-topics-notice}}
- {{plugin-outlet name="top-notices" args=(hash currentPath=currentPath)}}
+ {{plugin-outlet name="top-notices" args=(hash currentPath=router._router.currentPath)}}
{{outlet}}
{{outlet "user-card"}}
diff --git a/app/assets/javascripts/discourse/templates/badge-selector-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/badge-selector-autocomplete.hbr
similarity index 100%
rename from app/assets/javascripts/discourse/templates/badge-selector-autocomplete.raw.hbs
rename to app/assets/javascripts/discourse/templates/badge-selector-autocomplete.hbr
diff --git a/app/assets/javascripts/discourse/templates/badges/show.hbs b/app/assets/javascripts/discourse/templates/badges/show.hbs
index 933bc48de8..9eeb3d7fa2 100644
--- a/app/assets/javascripts/discourse/templates/badges/show.hbs
+++ b/app/assets/javascripts/discourse/templates/badges/show.hbs
@@ -13,7 +13,7 @@
{{i18n 'badges.allow_title'}}
{{d-button
- class="btn btn-default pad-left no-text"
+ class="btn-default pad-left"
action=(action "toggleSetUserTitle")
icon="pencil-alt"}}
diff --git a/app/assets/javascripts/discourse/templates/category-selector-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/category-selector-autocomplete.hbr
similarity index 100%
rename from app/assets/javascripts/discourse/templates/category-selector-autocomplete.raw.hbs
rename to app/assets/javascripts/discourse/templates/category-selector-autocomplete.hbr
diff --git a/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/category-tag-autocomplete.hbr
similarity index 100%
rename from app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs
rename to app/assets/javascripts/discourse/templates/category-tag-autocomplete.hbr
diff --git a/app/assets/javascripts/discourse/templates/components/about-page-users.hbs b/app/assets/javascripts/discourse/templates/components/about-page-users.hbs
new file mode 100644
index 0000000000..4c946af466
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/about-page-users.hbs
@@ -0,0 +1,22 @@
+{{#each usersTemplates as |userTemplate|}}
+
+
+
+
+
{{userTemplate.title}}
+
+
+{{/each}}
diff --git a/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs b/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs
index e980fc5062..f19538efea 100644
--- a/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs
+++ b/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs
@@ -1,10 +1,14 @@
-
- {{d-icon "far-image"}} {{uploadButtonText}}
-
+
+ {{d-icon "far-image"}}
+ {{#if uploading}}
+ {{i18n 'uploading'}} {{uploadProgress}}%
+ {{else}}
+ {{i18n 'upload'}}
+ {{/if}}
+
+
-{{#if uploading}}
- {{i18n 'upload_selector.uploading'}} {{uploadProgress}}%
-{{/if}}
+
{{#if imageIsNotASquare}}
{{i18n 'user.change_avatar.image_is_not_a_square'}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/backup-codes.hbs b/app/assets/javascripts/discourse/templates/components/backup-codes.hbs
index c8ddb725be..bd591ee6c3 100644
--- a/app/assets/javascripts/discourse/templates/components/backup-codes.hbs
+++ b/app/assets/javascripts/discourse/templates/components/backup-codes.hbs
@@ -1,5 +1,5 @@
-
+
{{d-button
action=(action "copyToClipboard")
diff --git a/app/assets/javascripts/discourse/templates/components/backup-uploader.hbs b/app/assets/javascripts/discourse/templates/components/backup-uploader.hbs
index affa124962..255d2f01d5 100644
--- a/app/assets/javascripts/discourse/templates/components/backup-uploader.hbs
+++ b/app/assets/javascripts/discourse/templates/components/backup-uploader.hbs
@@ -1,4 +1,4 @@
{{d-icon "upload"}} {{uploadButtonText}}
-
+
diff --git a/app/assets/javascripts/discourse/templates/components/badge-title.hbs b/app/assets/javascripts/discourse/templates/components/badge-title.hbs
index 23b1f4e22d..abb66e44aa 100644
--- a/app/assets/javascripts/discourse/templates/components/badge-title.hbs
+++ b/app/assets/javascripts/discourse/templates/components/badge-title.hbs
@@ -12,13 +12,19 @@
{{combo-box
value=selectedUserBadgeId
nameProperty="badge.name"
- content=selectableUserBadges}}
+ content=selectableUserBadges
+ onChange=(action (mut selectedUserBadgeId))
+ }}
- {{savingStatus}}
+ {{d-button
+ class="btn-primary"
+ action=(action "save")
+ disabled=saving
+ label=(if saving "saving" "save")}}
{{#if saved}}{{i18n 'saved'}}{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs b/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs
index d1d105121e..b047846db5 100644
--- a/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs
+++ b/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs
@@ -1,11 +1,9 @@
{{#conditional-loading-spinner condition=loading}}
{{#if hasIncoming}}
{{/if}}
@@ -18,7 +16,10 @@
canBulkSelect=canBulkSelect
selected=selected
skipHeader=skipHeader
- tagsForUser=tagsForUser}}
+ tagsForUser=tagsForUser
+ onScroll=onScroll
+ scrollOnLoad=scrollOnLoad}}
+
{{else}}
{{#unless loadingMore}}
diff --git a/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs b/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs
index 9f41a5d160..b42063fb9b 100644
--- a/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs
+++ b/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs
@@ -1,22 +1,20 @@
-{{category-drop
- category=firstCategory
- categories=parentCategoriesSorted
- countSubcategories=true}}
-
-{{#if childCategories}}
- {{category-drop
- category=secondCategory
- parentCategory=firstCategory
- categories=childCategories
- subCategory=true
- noSubcategories=noSubcategories}}
-{{/if}}
+{{#each categoryBreadcrumbs as |breadcrumb|}}
+ {{#if breadcrumb.hasOptions}}
+ {{category-drop
+ category=breadcrumb.category
+ categories=breadcrumb.options
+ options=(hash
+ parentCategory=breadcrumb.parentCategory
+ subCategory=breadcrumb.isSubcategory
+ noSubcategories=breadcrumb.noSubcategories
+ autoFilterable=true
+ )
+ }}
+ {{/if}}
+{{/each}}
{{#if siteSettings.tagging_enabled}}
- {{tag-drop
- firstCategory=firstCategory
- secondCategory=secondCategory
- tagId=tagId}}
+ {{tag-drop currentCategory=category tagId=tagId}}
{{/if}}
{{plugin-outlet name="bread-crumbs-right" connectorTagName="li" tagName=""}}
diff --git a/app/assets/javascripts/discourse/templates/components/cancel-link.hbs b/app/assets/javascripts/discourse/templates/components/cancel-link.hbs
index 8c3a3cb4c7..4455e8b8d4 100644
--- a/app/assets/javascripts/discourse/templates/components/cancel-link.hbs
+++ b/app/assets/javascripts/discourse/templates/components/cancel-link.hbs
@@ -1,3 +1,3 @@
-{{#link-to route args}}
+{{#link-to route args class="cancel"}}
{{i18n 'cancel'}}
{{/link-to}}
diff --git a/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs b/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs
index 7a793714c0..6df50c4eef 100644
--- a/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs
+++ b/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs
@@ -11,10 +11,10 @@
{{/if}}
+ {{category-title-before category=c}}
{{#if c.read_restricted}}
{{d-icon 'lock'}}
{{/if}}
- {{category-title-before category=c}}
{{c.name}}
diff --git a/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs b/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs
index 8c51088578..888eefce97 100644
--- a/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs
+++ b/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs
@@ -1,5 +1,6 @@
{{#each categories as |c|}}
-
+
{{#if c.uploaded_logo.url}}
@@ -12,19 +13,40 @@
{{{text-overflow class="overflow" text=c.description_excerpt}}}
-
- {{#if c.subcategories}}
+ {{#if c.isGrandParent}}
+ {{#each c.subcategories as |subcategory|}}
+
+
+ {{category-title-link tagName="h4" category=subcategory}}
+ {{#if subcategory.subcategories}}
+
+ {{#each subcategory.subcategories as |subsubcategory|}}
+ {{#unless subsubcategory.isMuted}}
+
+ {{category-title-before category=subsubcategory}}
+ {{category-link subsubcategory hideParent="true"}}
+
+ {{/unless}}
+ {{/each}}
+
+ {{/if}}
+
+
+ {{/each}}
+ {{else if c.subcategories}}
diff --git a/app/assets/javascripts/discourse/templates/components/categories-only.hbs b/app/assets/javascripts/discourse/templates/components/categories-only.hbs
index 4f93c8a316..4f153b4e7f 100644
--- a/app/assets/javascripts/discourse/templates/components/categories-only.hbs
+++ b/app/assets/javascripts/discourse/templates/components/categories-only.hbs
@@ -11,25 +11,62 @@
{{#each categories as |c|}}
-
+
- {{category-title-link category=c}}
+ {{category-title-link category=c}}
+ {{#if c.description_excerpt}}
{{{dir-span c.description_excerpt}}}
-
- {{#if c.subcategories}}
+ {{/if}}
+ {{#if c.isGrandParent}}
+
+
+ {{#each c.subcategories as |subcategory|}}
+
+
+ {{category-title-link tagName="h4" category=subcategory}}
+ {{#if subcategory.description_excerpt}}
+
+ {{{dir-span subcategory.description_excerpt}}}
+
+ {{/if}}
+ {{#if subcategory.subcategories}}
+
+ {{#each subcategory.subcategories as |subsubcategory|}}
+ {{#unless subsubcategory.isMuted}}
+
+ {{category-title-before category=subsubcategory}}
+ {{category-link subsubcategory hideParent="true"}}
+
+ {{/unless}}
+ {{/each}}
+
+ {{else}}
+ {{#if subcategory.description_excerpt}}
+
+ {{{dir-span subcategory.description_excerpt}}}
+
+ {{/if}}
+ {{/if}}
+
+
+ {{/each}}
+
+
+ {{else if c.subcategories}}
- {{#each c.subcategories as |s|}}
-
- {{category-title-before category=s}}
- {{category-link s hideParent="true"}}
- {{category-unread category=s}}
-
+ {{#each c.subcategories as |subcategory|}}
+ {{#unless subcategory.isMuted}}
+
+ {{category-title-before category=subcategory}}
+ {{category-link subcategory hideParent="true"}}
+ {{category-unread category=subcategory}}
+
+ {{/unless}}
{{/each}}
{{/if}}
-
{{{c.stat}}}
diff --git a/app/assets/javascripts/discourse/templates/components/category-title-link.hbs b/app/assets/javascripts/discourse/templates/components/category-title-link.hbs
index 542a900080..f973a28f1f 100644
--- a/app/assets/javascripts/discourse/templates/components/category-title-link.hbs
+++ b/app/assets/javascripts/discourse/templates/components/category-title-link.hbs
@@ -1,10 +1,9 @@
-{{category-title-before category=category}}
+ {{category-title-before category=category}}
{{#if category.read_restricted}}
{{d-icon 'lock'}}
{{/if}}
-
{{dir-span category.name}}
{{#if category.uploaded_logo.url}}
diff --git a/app/assets/javascripts/discourse/templates/components/choose-topic.hbs b/app/assets/javascripts/discourse/templates/components/choose-topic.hbs
index 8ddf70fe57..da9ca0a3de 100644
--- a/app/assets/javascripts/discourse/templates/components/choose-topic.hbs
+++ b/app/assets/javascripts/discourse/templates/components/choose-topic.hbs
@@ -1,4 +1,4 @@
-{{i18n 'choose_topic.title.search'}}
+{{i18n labelText}}
{{text-field value=topicTitle placeholderKey="choose_topic.title.placeholder" id="choose-topic-title"}}
@@ -11,16 +11,14 @@
{{#each topics as |t|}}
-
-
- {{t.title}}
-
-
- {{#if t.category.parentCategory}}
- {{bound-category-link t.category.parentCategory}}
- {{/if}}
- {{bound-category-link t.category}}
-
+
+ {{raw "topic-status" topic=t}}
+
+ {{t.title}}
+
+
+ {{bound-category-link t.category recursive=true hideParent=true}}
+
{{/each}}
diff --git a/app/assets/javascripts/discourse/templates/components/color-input.hbs b/app/assets/javascripts/discourse/templates/components/color-input.hbs
index 1b9f36ecb3..3b8d76fbb8 100644
--- a/app/assets/javascripts/discourse/templates/components/color-input.hbs
+++ b/app/assets/javascripts/discourse/templates/components/color-input.hbs
@@ -1 +1 @@
-{{text-field class="hex-input" value=hexValue maxlength="6"}}
+{{text-field class="hex-input" value=hexValue maxlength="6"}}
diff --git a/app/assets/javascripts/discourse/templates/components/composer-action-title.hbs b/app/assets/javascripts/discourse/templates/components/composer-action-title.hbs
index 8f010be7ce..3f26b6e3c5 100644
--- a/app/assets/javascripts/discourse/templates/components/composer-action-title.hbs
+++ b/app/assets/javascripts/discourse/templates/components/composer-action-title.hbs
@@ -3,12 +3,13 @@
{{else}}
{{composer-actions
composerModel=model
- options=model.replyOptions
+ replyOptions=model.replyOptions
canWhisper=canWhisper
openComposer=openComposer
closeComposer=closeComposer
action=model.action
- tabindex=tabindex}}
+ tabindex=tabindex
+ }}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/composer-title.hbs b/app/assets/javascripts/discourse/templates/components/composer-title.hbs
index 76f63dac1f..a8483595de 100644
--- a/app/assets/javascripts/discourse/templates/components/composer-title.hbs
+++ b/app/assets/javascripts/discourse/templates/components/composer-title.hbs
@@ -3,7 +3,7 @@
id="reply-title"
maxLength=titleMaxLength
placeholderKey=composer.titlePlaceholder
- disabled=composer.loading
+ disabled=disabled
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 9a9b1cfcf8..bf89487741 100644
--- a/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs
+++ b/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs
@@ -1,6 +1,6 @@
{{#if showSelector}}
{{user-selector topicId=topicId
- onChangeCallback=(action "triggerResize")
+ onChangeCallback=(action "triggerResize")
id="private-message-users"
includeMessageableGroups='true'
placeholderKey="composer.users_placeholder"
@@ -10,8 +10,10 @@
allowEmails='true'
autocomplete="discourse"}}
{{else}}
-
- {{limitedUsernames}}
- {{hiddenUsersCount}}
-
+
+
+ {{limitedUsernames}}
+ {{hiddenUsersCount}}
+
+
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/count-i18n.hbs b/app/assets/javascripts/discourse/templates/components/count-i18n.hbs
new file mode 100644
index 0000000000..f7a5927a07
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/count-i18n.hbs
@@ -0,0 +1 @@
+{{i18nCount}}
diff --git a/app/assets/javascripts/discourse/templates/components/create-topic-button.hbs b/app/assets/javascripts/discourse/templates/components/create-topic-button.hbs
index e99673f9cb..3a0e3ebfa9 100644
--- a/app/assets/javascripts/discourse/templates/components/create-topic-button.hbs
+++ b/app/assets/javascripts/discourse/templates/components/create-topic-button.hbs
@@ -1,7 +1,7 @@
{{#if canCreateTopic}}
{{d-button
+ class="btn-default"
id="create-topic"
- class="btn btn-default"
action=action
icon="plus"
disabled=disabled
diff --git a/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs b/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs
index ad3120827f..51fc598783 100644
--- a/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs
+++ b/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs
@@ -1,6 +1,6 @@
{{d-icon "upload"}} {{uploadButtonText}}
-
+
{{#if uploading}}
{{i18n 'upload_selector.uploading'}} {{uploadProgress}}%
diff --git a/app/assets/javascripts/discourse/templates/components/d-editor-modal.hbs b/app/assets/javascripts/discourse/templates/components/d-editor-modal.hbs
deleted file mode 100644
index 9c1426e41e..0000000000
--- a/app/assets/javascripts/discourse/templates/components/d-editor-modal.hbs
+++ /dev/null
@@ -1,8 +0,0 @@
-{{#unless hidden}}
- {{yield}}
-
-
- {{d-button class="btn-primary" label="composer.modal_ok" action=(action "ok")}}
- {{d-button class="btn-danger" label="composer.modal_cancel" action=(action "cancel")}}
-
-{{/unless}}
diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs
index 544168fe05..f868131b63 100644
--- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs
+++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs
@@ -1,13 +1,3 @@
-
-
-
- {{#d-editor-modal class="insert-link" hidden=insertLinkHidden okAction=(action "insertLink")}}
-
{{i18n "composer.link_dialog_title"}}
- {{text-field value=linkUrl placeholderKey="composer.link_url_placeholder" class="link-url"}}
- {{text-field value=linkText placeholderKey="composer.link_optional_text" class="link-text"}}
- {{/d-editor-modal}}
-
-
@@ -15,12 +5,15 @@
{{#each group.buttons as |b|}}
{{#if b.popupMenu}}
{{toolbar-popup-menu-options
- onSelect=onPopupMenuAction
+ content=popupMenuOptions
+ onChange=onPopupMenuAction
onExpand=(action b.action b)
- title=b.title
- headerIcon=b.icon
class=b.className
- content=popupMenuOptions}}
+ options=(hash
+ popupTitle=b.title
+ icon=b.icon
+ )
+ }}
{{else}}
{{d-button
action=b.action
@@ -47,14 +40,14 @@
class="d-editor-input"
placeholder=placeholderTranslated
disabled=disabled
- change=change}}
+ input=change}}
{{popup-input-tip validation=validation}}
{{plugin-outlet name="after-d-editor" tagName="" args=outletArgs}}
{{{preview}}}
- {{plugin-outlet name="editor-preview" classNames="d-editor-plugin"}}
+ {{plugin-outlet name="editor-preview" classNames="d-editor-plugin" args=outletArgs}}
diff --git a/app/assets/javascripts/discourse/templates/components/d-modal-cancel.hbs b/app/assets/javascripts/discourse/templates/components/d-modal-cancel.hbs
index e4fc5950d9..f61b3ed7c7 100644
--- a/app/assets/javascripts/discourse/templates/components/d-modal-cancel.hbs
+++ b/app/assets/javascripts/discourse/templates/components/d-modal-cancel.hbs
@@ -1 +1 @@
-
{{i18n 'cancel'}}
+
{{i18n 'cancel'}}
diff --git a/app/assets/javascripts/discourse/templates/components/d-modal.hbs b/app/assets/javascripts/discourse/templates/components/d-modal.hbs
index 4e7cd0dca2..6c6106bbad 100644
--- a/app/assets/javascripts/discourse/templates/components/d-modal.hbs
+++ b/app/assets/javascripts/discourse/templates/components/d-modal.hbs
@@ -3,11 +3,7 @@
diff --git a/app/assets/javascripts/discourse/templates/components/group-member.hbs b/app/assets/javascripts/discourse/templates/components/group-member.hbs
index ddc6456b83..dcdfc050d6 100644
--- a/app/assets/javascripts/discourse/templates/components/group-member.hbs
+++ b/app/assets/javascripts/discourse/templates/components/group-member.hbs
@@ -1 +1,9 @@
-{{avatar member imageSize="small"}} {{member.username}} {{#unless automatic}}{{d-icon "times"}} {{/unless}}
+
+ {{avatar member imageSize="small"}}
+
+{{member.username}}
+{{#unless automatic}}
+
+ {{d-icon "times"}}
+
+{{/unless}}
diff --git a/app/assets/javascripts/discourse/templates/components/group-members-input.hbs b/app/assets/javascripts/discourse/templates/components/group-members-input.hbs
deleted file mode 100644
index 73c5820a85..0000000000
--- a/app/assets/javascripts/discourse/templates/components/group-members-input.hbs
+++ /dev/null
@@ -1,30 +0,0 @@
-{{i18n 'groups.members.title'}} ({{model.user_count}})
-
-{{#if model.members}}
-
-
- {{#each model.members as |member|}}
- {{group-member member=member automatic=model.automatic removeAction=(action "removeMember")}}
- {{/each}}
-
-{{/if}}
-
-{{#unless model.automatic}}
-
- {{user-selector usernames=model.usernames
- placeholderKey="groups.selector_placeholder"
- id="member-selector"}}
-
- {{#if addButton}}
- {{d-button action=(action "addMembers")
- class="add"
- icon="plus"
- disabled=disableAddButton
- label="groups.manage.add_members"}}
- {{/if}}
-
-{{/unless}}
diff --git a/app/assets/javascripts/discourse/templates/components/group-navigation.hbs b/app/assets/javascripts/discourse/templates/components/group-navigation.hbs
index 29ad6b7f1e..eb9c9140cb 100644
--- a/app/assets/javascripts/discourse/templates/components/group-navigation.hbs
+++ b/app/assets/javascripts/discourse/templates/components/group-navigation.hbs
@@ -6,7 +6,10 @@
{{/link-to}}
{{else}}
- {{group-dropdown content=group.extras.visible_group_names value=group.name}}
+ {{group-dropdown
+ groups=group.extras.visible_group_names
+ value=group.name
+ }}
{{/if}}
{{#each tabs as |tab|}}
@@ -18,4 +21,5 @@
{{/link-to}}
{{/each}}
+ {{plugin-outlet name="group-reports-nav-item" args=(hash group=group)}}
{{/mobile-nav}}
diff --git a/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs b/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs
index 5ed87a5c02..fcc29b71b2 100644
--- a/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs
+++ b/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs
@@ -3,12 +3,34 @@
{{i18n 'admin.groups.manage.interaction.visibility'}}
{{i18n 'admin.groups.manage.interaction.visibility_levels.title'}}
+ {{combo-box
+ name="alias"
+ valueProperty="value"
+ value=model.visibility_level
+ content=visibilityLevelOptions
+ castInteger=true
+ class="groups-form-visibility-level"
+ onChange=(action (mut model.visibility_level))
+ }}
+
+
+ {{i18n 'admin.groups.manage.interaction.visibility_levels.description'}}
+
+
+
+
+
{{i18n 'admin.groups.manage.interaction.members_visibility_levels.title'}}
+
{{combo-box name="alias"
- valueAttribute="value"
- value=model.visibility_level
+ valueProperty="value"
+ value=model.members_visibility_level
content=visibilityLevelOptions
castInteger=true
- class="groups-form-visibility-level"}}
+ class="groups-form-members-visibility-level"}}
+
+
+ {{i18n 'admin.groups.manage.interaction.members_visibility_levels.description'}}
+
{{/if}}
@@ -16,21 +38,37 @@
{{i18n 'groups.manage.interaction.posting'}}
{{i18n 'groups.alias_levels.mentionable'}}
- {{combo-box name="alias"
- valueAttribute="value"
- value=model.mentionable_level
- content=aliasLevelOptions
- class="groups-form-mentionable-level"}}
+ {{combo-box
+ name="alias"
+ valueProperty="value"
+ value=model.mentionable_level
+ content=aliasLevelOptions
+ class="groups-form-mentionable-level"
+ onChange=(action (mut model.mentionable_level))
+ }}
{{i18n 'groups.alias_levels.messageable'}}
- {{combo-box name="alias"
- valueAttribute="value"
- value=model.messageable_level
- content=aliasLevelOptions
- class="groups-form-messageable-level"}}
+ {{combo-box
+ name="alias"
+ valueProperty="value"
+ value=model.messageable_level
+ content=aliasLevelOptions
+ class="groups-form-messageable-level"
+ onChange=(action (mut model.messageable_level))
+ }}
+
+
+
+
+ {{input type="checkbox"
+ checked=model.publish_read_state
+ class="groups-form-publish-read-state"}}
+
+ {{i18n 'admin.groups.manage.interaction.publish_read_state'}}
+
{{#if showEmailSettings}}
@@ -52,7 +90,12 @@
{{i18n 'groups.notification_level'}}
- {{notifications-button i18nPrefix='groups.notifications'
- value=model.default_notification_level
- class="groups-form-default-notification-level"}}
+ {{notifications-button
+ value=model.default_notification_level
+ class="groups-form-default-notification-level"
+ options=(hash
+ none="select_kit.default_header_text"
+ i18nPrefix="groups.notifications"
+ )
+ }}
diff --git a/app/assets/javascripts/discourse/templates/components/groups-form-membership-fields.hbs b/app/assets/javascripts/discourse/templates/components/groups-form-membership-fields.hbs
index f2c6ca7593..3b1a41304a 100644
--- a/app/assets/javascripts/discourse/templates/components/groups-form-membership-fields.hbs
+++ b/app/assets/javascripts/discourse/templates/components/groups-form-membership-fields.hbs
@@ -6,9 +6,17 @@
{{i18n 'admin.groups.manage.membership.automatic_membership_email_domains'}}
- {{list-setting name="automatic_membership"
- settingValue=model.emailDomains
- class="group-form-automatic-membership-automatic"}}
+ {{list-setting
+ name="automatic_membership"
+ class="group-form-automatic-membership-automatic"
+ value=emailDomains
+ choices=emailDomains
+ settingName="name"
+ nameProperty=null
+ valueProperty=null
+ onChange=(action "onChangeEmailDomainsSetting")
+ options=(hash allowAny=true)
+ }}
{{input type="checkbox"
@@ -17,7 +25,23 @@
{{i18n 'admin.groups.manage.membership.automatic_membership_retroactive'}}
+
+ {{plugin-outlet name="groups-form-membership-below-automatic"
+ args=(hash model=model)}}
+
+
+ {{i18n "admin.groups.manage.membership.effects"}}
+ {{i18n 'admin.groups.manage.membership.trust_levels_title'}}
+
+ {{combo-box
+ name="grant_trust_level"
+ valueProperty="value"
+ value=groupTrustLevel
+ content=trustLevelOptions
+ class="groups-form-grant-trust-level"
+ onChange=(action (mut model.grant_trust_level))
+ }}
{{input type="checkbox"
checked=model.primary_group
@@ -25,20 +49,7 @@
{{i18n 'admin.groups.manage.membership.primary_group'}}
-
- {{plugin-outlet name="groups-form-membership-below-automatic"
- args=(hash model=model)}}
-
-
- {{i18n "admin.groups.manage.membership.trust_level"}}
- {{i18n 'admin.groups.manage.membership.trust_levels_title'}}
-
- {{combo-box name="grant_trust_level"
- valueAttribute="value"
- value=model.grant_trust_level
- content=trustLevelOptions
- class="groups-form-grant-trust-level"}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs
index 4c9b410290..bf75d0929a 100644
--- a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs
+++ b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs
@@ -5,19 +5,25 @@
@@ -24,13 +27,13 @@
{{#each reviewable.editable_fields as |f|}}
- {{component
- (concat "reviewable-field-" f.type)
+ {{component (concat "reviewable-field-" f.type)
tagName=''
value=(editable-value reviewable f.id)
tagCategoryId=reviewable.category.id
valueChanged=(action "valueChanged" f.id)
- categoryChanged=(action "categoryChanged")}}
+ categoryChanged=(action "categoryChanged")
+ }}
{{/each}}
@@ -44,7 +47,7 @@
{{#if claimEnabled}}
{{{claimHelp}}}
- {{reviewable-claimed-topic topicId=reviewable.topic.id claimedBy=reviewable.claimed_by}}
+ {{reviewable-claimed-topic topicId=topicId claimedBy=reviewable.claimed_by}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/reviewable-queued-post.hbs b/app/assets/javascripts/discourse/templates/components/reviewable-queued-post.hbs
index fb54fe9763..30aff53c6a 100644
--- a/app/assets/javascripts/discourse/templates/components/reviewable-queued-post.hbs
+++ b/app/assets/javascripts/discourse/templates/components/reviewable-queued-post.hbs
@@ -5,6 +5,11 @@
{{category-badge reviewable.category}}
{{reviewable-tags tags=reviewable.payload.tags tagName=''}}
+ {{#if reviewable.payload.via_email}}
+
+ {{d-icon "far-envelope" title="post.via_email"}}
+
+ {{/if}}
{{/reviewable-topic-link}}
diff --git a/app/assets/javascripts/discourse/templates/components/reviewable-score.js.es6 b/app/assets/javascripts/discourse/templates/components/reviewable-score.js.es6
index abceda03ca..ee36959c61 100644
--- a/app/assets/javascripts/discourse/templates/components/reviewable-score.js.es6
+++ b/app/assets/javascripts/discourse/templates/components/reviewable-score.js.es6
@@ -1,11 +1,13 @@
-import computed from "ember-addons/ember-computed-decorators";
+import discourseComputed from "discourse-common/utils/decorators";
+import { gt } from "@ember/object/computed";
+import Component from "@ember/component";
-export default Ember.Component.extend({
+export default Component.extend({
tagName: "",
- showStatus: Ember.computed.gt("rs.status", 0),
+ showStatus: gt("rs.status", 0),
- @computed("rs.score_type.title", "reviewable.target_created_by")
+ @discourseComputed("rs.score_type.title", "reviewable.target_created_by")
title(title, targetCreatedBy) {
if (title && targetCreatedBy) {
return title.replace("{{username}}", targetCreatedBy.username);
diff --git a/app/assets/javascripts/discourse/templates/components/reviewable-user.hbs b/app/assets/javascripts/discourse/templates/components/reviewable-user.hbs
index 96977b4b53..e3f4ea1efd 100644
--- a/app/assets/javascripts/discourse/templates/components/reviewable-user.hbs
+++ b/app/assets/javascripts/discourse/templates/components/reviewable-user.hbs
@@ -12,15 +12,28 @@
{{/if}}
-
+
{{reviewable-field classes='reviewable-user-details name'
name=(i18n 'review.user.name')
value=reviewable.payload.name}}
-
+
{{reviewable-field classes='reviewable-user-details email'
name=(i18n 'review.user.email')
value=reviewable.payload.email}}
-
+
+ {{reviewable-field classes='reviewable-user-details bio'
+ name=(i18n 'review.user.bio')
+ value=reviewable.payload.bio}}
+
+ {{#if reviewable.payload.website}}
+
+
{{i18n 'review.user.website'}}
+
+
+ {{/if}}
+
{{#each userFields as |f|}}
{{reviewable-field classes='reviewable-user-details user-field'
name=f.name
diff --git a/app/assets/javascripts/discourse/templates/components/score-value.hbs b/app/assets/javascripts/discourse/templates/components/score-value.hbs
new file mode 100644
index 0000000000..e78eeb32b2
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/score-value.hbs
@@ -0,0 +1,11 @@
+{{#if value}}
+
+ {{float value}}
+ {{#if label}}
+
+ {{i18n (concat "review.explain." label ".name")}}
+
+ {{/if}}
+
+ +
+{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs b/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs
index 371a1c3849..36e1b5a4cb 100644
--- a/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs
+++ b/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs
@@ -1,19 +1,25 @@
-
+{{plugin-outlet name="advanced-search-options-above" args=(hash searchedTerms=searchedTerms) tagName=""}}
+
+
{{i18n "search.advanced.posted_by.label"}}
{{user-selector
- excludeCurrentUser=false
- usernames=searchedTerms.username
- class="user-selector"
- single="true"
- canReceiveUpdates="true"}}
+ excludeCurrentUser=false
+ usernames=searchedTerms.username
+ single=true
+ canReceiveUpdates=true
+ class="user-selector"
+ }}
{{i18n "search.advanced.in_category.label"}}
- {{search-advanced-category-chooser value=searchedTerms.category}}
+ {{search-advanced-category-chooser
+ value=searchedTerms.category.id
+ onChange=(action "onChangeCategory")
+ }}
@@ -36,7 +42,7 @@
-->
{{#if siteSettings.tagging_enabled}}
-
+
{{i18n "search.advanced.with_tags.label"}}
@@ -45,7 +51,9 @@
allowCreate=false
filterPlaceholder=null
everyTag=true
- unlimitedTagCount=true}}
+ unlimitedTagCount=true
+ onChange=(action (mut searchedTerms.tags))
+ }}
{{ input type="checkbox" class="all-tags" checked=searchedTerms.special.all_tags}} {{i18n "search.advanced.filters.all_tags"}}
@@ -54,7 +62,7 @@
{{/if}}
-
+
{{i18n "search.advanced.filters.label"}}
@@ -62,27 +70,51 @@
{{input type="checkbox" class="in-title" checked=searchedTerms.special.in.title}} {{i18n "search.advanced.filters.title"}}
{{input type="checkbox" class="in-likes" checked=searchedTerms.special.in.likes}} {{i18n "search.advanced.filters.likes"}}
- {{input type="checkbox" class="in-private" checked=searchedTerms.special.in.private}} {{i18n "search.advanced.filters.private"}}
+ {{input type="checkbox" class="in-private" checked=searchedTerms.special.in.personal}} {{i18n "search.advanced.filters.private"}}
{{input type="checkbox" class="in-seen" checked=searchedTerms.special.in.seen}} {{i18n "search.advanced.filters.seen"}}
{{/if}}
- {{combo-box id="in" valueAttribute="value" content=inOptions value=searchedTerms.in none="user.locale.any"}}
+ {{combo-box
+ id="in"
+ valueProperty="value"
+ content=inOptions
+ value=searchedTerms.in
+ none="user.locale.any"
+ onChange=(action (mut searchedTerms.in))
+ }}
{{i18n "search.advanced.statuses.label"}}
- {{combo-box id="status" valueAttribute="value" content=statusOptions value=searchedTerms.status none="user.locale.any"}}
+ {{combo-box
+ id="status"
+ valueProperty="value"
+ content=statusOptions
+ value=searchedTerms.status
+ none="user.locale.any"
+ onChange=(action (mut searchedTerms.status))
+ }}
-
+
{{i18n "search.advanced.post.time.label"}}
- {{combo-box id="postTime" valueAttribute="value" content=postTimeOptions value=searchedTerms.time.when}}
- {{date-picker value=searchedTerms.time.days id="search-post-date"}}
+ {{combo-box
+ id="postTime"
+ valueProperty="value"
+ content=postTimeOptions
+ value=searchedTerms.time.when
+ onChange=(action "onChangeWhenTime")
+ }}
+ {{date-input
+ date=searchedTerms.time.days
+ onChange=(action "onChangeWhenDate")
+ id="search-post-date"
+ }}
@@ -92,3 +124,5 @@
+
+{{plugin-outlet name="advanced-search-options-below" args=(hash searchedTerms=searchedTerms) tagName=""}}
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 f2ccf505df..4c5c717be3 100644
--- a/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs
+++ b/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs
@@ -5,12 +5,9 @@
{{/if}}
{{secondFactorDescription}}
{{yield}}
- {{#if backupEnabled}}
+ {{#if showToggleMethodLink}}
- {{discourse-linked-text
- class="toggle-second-factor-method"
- action=(action "toggleSecondFactorMethod")
- text=linkText}}
+ {{ i18n linkText }}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/second-factor-input.hbs b/app/assets/javascripts/discourse/templates/components/second-factor-input.hbs
index a17e0deb7b..8a0d24d145 100644
--- a/app/assets/javascripts/discourse/templates/components/second-factor-input.hbs
+++ b/app/assets/javascripts/discourse/templates/components/second-factor-input.hbs
@@ -6,4 +6,5 @@
id=inputId
autocorrect="off"
autocapitalize="off"
- autofocus="autofocus"}}
+ autofocus="autofocus"
+ placeholder=placeholder}}
diff --git a/app/assets/javascripts/discourse/templates/components/security-key-form.hbs b/app/assets/javascripts/discourse/templates/components/security-key-form.hbs
new file mode 100644
index 0000000000..e5bc247ad3
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/security-key-form.hbs
@@ -0,0 +1,14 @@
+
+ {{d-button
+ action=action
+ icon="key"
+ id="security-key-authenticate-button"
+ label="login.security_key_authenticate"
+ type="button"
+ class='btn btn-large btn-primary'}}
+
+ {{#if otherMethodAllowed}}
+ {{ i18n 'login.security_key_alternative' }}
+ {{/if}}
+
+
diff --git a/app/assets/javascripts/discourse/templates/components/share-popup.hbs b/app/assets/javascripts/discourse/templates/components/share-popup.hbs
index 55f287720b..6f0428b1aa 100644
--- a/app/assets/javascripts/discourse/templates/components/share-popup.hbs
+++ b/app/assets/javascripts/discourse/templates/components/share-popup.hbs
@@ -35,8 +35,7 @@
{{/if}}
-
- {{d-icon "times"}}
-
+
+ {{d-button action="close" class="btn btn-flat close" icon="times" aria-label="share.close" title="share.close"}}
diff --git a/app/assets/javascripts/discourse/templates/components/shared-draft-controls.hbs b/app/assets/javascripts/discourse/templates/components/shared-draft-controls.hbs
index feeed4be43..23b2d45ae2 100644
--- a/app/assets/javascripts/discourse/templates/components/shared-draft-controls.hbs
+++ b/app/assets/javascripts/discourse/templates/components/shared-draft-controls.hbs
@@ -8,7 +8,8 @@
{{i18n "shared_drafts.destination_category"}}
{{category-chooser
value=topic.destination_category_id
- onChooseCategory=(action "updateDestinationCategory")}}
+ onChange=(action "updateDestinationCategory")
+ }}
@@ -17,7 +18,7 @@
action=(action "publish")
label="shared_drafts.publish"
class="btn-primary publish-shared-draft"
- icon="clipboard"}}
+ icon="far-clipboard"}}
{{/if}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/signup-cta.hbs b/app/assets/javascripts/discourse/templates/components/signup-cta.hbs
index 1498e7b005..f6587b4043 100644
--- a/app/assets/javascripts/discourse/templates/components/signup-cta.hbs
+++ b/app/assets/javascripts/discourse/templates/components/signup-cta.hbs
@@ -10,7 +10,7 @@
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/suggested-topics.hbs b/app/assets/javascripts/discourse/templates/components/suggested-topics.hbs
index e70cfec46a..ffd142b1be 100644
--- a/app/assets/javascripts/discourse/templates/components/suggested-topics.hbs
+++ b/app/assets/javascripts/discourse/templates/components/suggested-topics.hbs
@@ -1,4 +1,7 @@
-{{{suggestedTitle}}}
+
+ {{i18n suggestedTitleLabel}}
+
+
{{#if topic.isPrivateMessage}}
{{basic-topic-list
diff --git a/app/assets/javascripts/discourse/templates/components/tag-groups-form.hbs b/app/assets/javascripts/discourse/templates/components/tag-groups-form.hbs
new file mode 100644
index 0000000000..e0cd0eb1d9
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/tag-groups-form.hbs
@@ -0,0 +1,89 @@
+
+
{{text-field value=buffered.name}}
+
+
+
+
+
+ {{i18n 'tagging.groups.parent_tag_label'}}
+ {{tag-chooser
+ tags=buffered.parent_tag_name
+ everyTag=true
+ maximum=1
+ allowCreate=true
+ excludeSynonyms=true
+ filterPlaceholder="tagging.groups.parent_tag_placeholder"}}
+ {{i18n 'tagging.groups.parent_tag_description'}}
+
+
+
+
+ {{input type="checkbox" checked=buffered.one_per_topic name="onepertopic"}}
+ {{i18n 'tagging.groups.one_per_topic_label'}}
+
+
+
+
+
+ {{radio-button
+ class="tag-permissions-choice"
+ name="tag-permissions-choice"
+ value="public"
+ id="public-permission"
+ selection=buffered.permissionName
+ onChange=(action "setPermissions")}}
+
+
+ {{i18n 'tagging.groups.everyone_can_use'}}
+
+
+
+ {{radio-button
+ class="tag-permissions-choice"
+ name="tag-permissions-choice"
+ value="visible"
+ id="visible-permission"
+ selection=buffered.permissionName
+ onChange=(action "setPermissions")}}
+
+
+ {{i18n 'tagging.groups.usable_only_by_staff'}}
+
+
+
+ {{radio-button
+ class="tag-permissions-choice"
+ name="tag-permissions-choice"
+ value="private"
+ id="private-permission"
+ selection=buffered.permissionName
+ onChange=(action "setPermissions")}}
+
+
+ {{i18n 'tagging.groups.visible_only_to_staff'}}
+
+
+
+
+ {{d-button
+ class="btn-default"
+ action=(action "save")
+ disabled=savingDisabled
+ label="tagging.groups.save"}}
+
+ {{d-button
+ class="btn-danger"
+ action=(action "destroy")
+ disabled=buffered.isNew
+ icon="far-trash-alt"
+ label="tagging.groups.delete"}}
+
diff --git a/app/assets/javascripts/discourse/templates/components/tag-info.hbs b/app/assets/javascripts/discourse/templates/components/tag-info.hbs
new file mode 100644
index 0000000000..3b616fec81
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/tag-info.hbs
@@ -0,0 +1,74 @@
+
+ {{#if tagInfo}}
+
+ {{discourse-tag tagInfo.name tagName="div" size="large"}}
+ {{#if canAdminTag}}
+ {{d-button class="btn-default" action=(action "renameTag") icon="pencil-alt" label="tagging.rename_tag" id="rename-tag"}}
+ {{d-button class="btn-default" action=(action "toggleEditControls") icon="cog" label="tagging.edit_synonyms" id="edit-synonyms"}}
+ {{#if deleteAction}}
+ {{d-button class="btn-danger delete-tag" action=(action "deleteTag") icon="far-trash-alt" label="tagging.delete_tag" id="delete-tag"}}
+ {{/if}}
+ {{/if}}
+
+
+ {{#if tagInfo.tag_group_names}}
+ {{tagGroupsInfo}}
+ {{/if}}
+ {{#if tagInfo.categories}}
+ {{categoriesInfo}}
+
+ {{#each tagInfo.categories as |category|}}
+ {{category-link category}}
+ {{/each}}
+ {{/if}}
+ {{#if nothingToShow}}
+ {{#if tagInfo.category_restricted}}
+ {{i18n "tagging.category_restricted"}}
+ {{else}}
+ {{i18n "tagging.default_info"}}
+ {{/if}}
+ {{/if}}
+
+ {{#if tagInfo.synonyms}}
+
+
{{i18n "tagging.synonyms"}}
+
{{{i18n "tagging.synonyms_description" base_tag_name=tagInfo.name}}}
+
+ {{#each tagInfo.synonyms as |tag|}}
+
+ {{/each}}
+
+
+ {{/if}}
+ {{#if editSynonymsMode}}
+
+ {{i18n 'tagging.add_synonyms_label'}}
+ {{tag-chooser
+ id="add-synonyms"
+ tags=newSynonyms
+ everyTag=true
+ excludeSynonyms=true
+ excludeHasSynonyms=true
+ unlimitedTagCount=true}}
+
+ {{d-button
+ class="btn-default"
+ action=(action "addSynonyms")
+ disabled=addSynonymsDisabled
+ label="tagging.add_synonyms"}}
+ {{/if}}
+ {{/if}}
+ {{#if loading}}
+ {{i18n 'loading'}}
+ {{/if}}
+
diff --git a/app/assets/javascripts/discourse/templates/components/tag-list.hbs b/app/assets/javascripts/discourse/templates/components/tag-list.hbs
index 24adf8aa0e..b7ce5970dc 100644
--- a/app/assets/javascripts/discourse/templates/components/tag-list.hbs
+++ b/app/assets/javascripts/discourse/templates/components/tag-list.hbs
@@ -9,8 +9,7 @@
{{/if}}
{{#each sortedTags as |tag|}}
- {{discourse-tag tag.id isPrivateMessage=isPrivateMessage tagsForUser=tagsForUser}} {{#if tag.pmOnly}}{{d-icon "far-envelope"}}{{/if}}{{#if tag.totalCount}} x {{tag.totalCount}} {{/if}}
+ {{discourse-tag tag.id isPrivateMessage=isPrivateMessage pmOnly=tag.pmOnly 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/time-input.hbs b/app/assets/javascripts/discourse/templates/components/time-input.hbs
new file mode 100644
index 0000000000..51ada91bf6
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/time-input.hbs
@@ -0,0 +1,40 @@
+
+ {{#if nativePicker}}
+ {{input
+ class="field time"
+ type="time"
+ value=(concat _hours ":" _minutes)
+ change=(action "onChangeTime")
+ }}
+ {{else}}
+ {{input
+ class="field hours"
+ type="number"
+ title="Hours"
+ minlength=2
+ maxlength=2
+ max="23"
+ min="0"
+ placeholder="00"
+ value=_hours
+ input=(action "onInput" (hash prop="hours"))
+ focus-in=(action "onFocusIn")
+ }}
+
+
:
+
+ {{input
+ class="field minutes"
+ title="Minutes"
+ type="number"
+ minlength=2
+ maxlength=2
+ max="59"
+ min="0"
+ placeholder="00"
+ value=_minutes
+ input=(action "onInput" (hash prop="minutes"))
+ focus-in=(action "onFocusIn")
+ }}
+ {{/if}}
+
diff --git a/app/assets/javascripts/discourse/templates/components/topic-category.hbs b/app/assets/javascripts/discourse/templates/components/topic-category.hbs
index 4b2db9cb1d..35cea87190 100644
--- a/app/assets/javascripts/discourse/templates/components/topic-category.hbs
+++ b/app/assets/javascripts/discourse/templates/components/topic-category.hbs
@@ -1,8 +1,5 @@
{{#unless topic.isPrivateMessage}}
- {{#if topic.category.parentCategory}}
- {{bound-category-link topic.category.parentCategory}}
- {{/if}}
- {{bound-category-link topic.category hideParent=true}}
+ {{bound-category-link topic.category recursive=true hideParent=true}}
{{/unless}}