- {{#if sendingEmail}}
- {{i18n 'admin.email.sending_test'}}
- {{else}}
-
- {{text-field value=email placeholderKey="admin.email.test_email_address"}}
-
- {{#if sentEmail}}
-
{{i18n 'admin.email.sent_test'}}
+
+ {{#if showSendEmailForm}}
+
+ {{#if sendingEmail}}
+ {{i18n 'admin.email.sending_test'}}
+ {{else}}
+
+ {{text-field value=email placeholderKey="admin.email.test_email_address"}}
+
+ {{#if sentEmail}}
+ {{i18n 'admin.email.sent_test'}}
+ {{/if}}
{{/if}}
+
+ {{/if}}
+
+
+ {{#if showHtml}}
+ {{#if htmlEmpty}}
+
{{i18n 'admin.email.no_result'}}
+ {{else}}
+
+ {{/if}}
+ {{else}}
+
{{{model.text_content}}}
{{/if}}
-
- {{/if}}
-
-
- {{#if showHtml}}
- {{#if htmlEmpty}}
-
{{i18n 'admin.email.no_result'}}
- {{else}}
-
- {{/if}}
- {{else}}
-
{{{model.text_content}}}
- {{/if}}
-
-{{/conditional-loading-spinner}}
+{{/conditional-loading-spinner}}
\ No newline at end of file
diff --git a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs
index d072bfec45..bba9a724ca 100644
--- a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs
+++ b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs
@@ -39,70 +39,66 @@
{{#staff-actions}}
-{{#load-more selector=".staff-logs tr" action=(action "loadMore")}}
- {{#if showTable}}
-
-
-
- | {{i18n 'admin.logs.staff_actions.staff_user'}} |
- {{i18n 'admin.logs.action'}} |
- {{i18n 'admin.logs.staff_actions.subject'}} |
- {{i18n 'admin.logs.staff_actions.when'}} |
- {{i18n 'admin.logs.staff_actions.details'}} |
- {{i18n 'admin.logs.staff_actions.context'}} |
-
-
-
- {{#each model as |item|}}
-
-
-
- {{#if item.acting_user}}
- {{#link-to 'adminUser' item.acting_user}}{{avatar item.acting_user imageSize="tiny"}}{{/link-to}}
- {{item.acting_user.username}}
- {{else}}
-
- {{d-icon "far-trash-alt"}}
-
+ {{#load-more selector=".staff-logs tr" action=(action "loadMore")}}
+ {{#if model.content}}
+
- {{else}}
- {{i18n 'search.no_results'}}
- {{/if}}
-
- {{conditional-loading-spinner condition=loading}}
-{{/load-more}}
+ {{#if item.useModalForDetails}}
+ {{d-icon "info-circle"}} {{i18n 'admin.logs.staff_actions.show'}}
+ {{/if}}
+ |
+ {{item.context}} |
+
+ {{/each}}
+
+
+ {{else if model.loadingMore}}
+ {{conditional-loading-spinner condition=model.loadingMore}}
+ {{else}}
+ {{i18n 'search.no_results'}}
+ {{/if}}
+ {{/load-more}}
{{/staff-actions}}
diff --git a/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs b/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs
index 153dbc9e8f..a41bfa93a9 100644
--- a/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs
+++ b/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs
@@ -44,7 +44,7 @@
{{#if local}}
-
+
{{i18n 'admin.customize.theme.import_file_tip'}}
{{/if}}
diff --git a/app/assets/javascripts/admin/templates/modal/admin-watched-word-test.hbs b/app/assets/javascripts/admin/templates/modal/admin-watched-word-test.hbs
new file mode 100644
index 0000000000..a46f4ddb7b
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/modal/admin-watched-word-test.hbs
@@ -0,0 +1,16 @@
+{{#d-modal-body rawTitle=(i18n "admin.watched_words.test.modal_title" action=model.name) class="watched-words-test-modal"}}
+
{{i18n "admin.watched_words.test.description"}}
+ {{textarea name="test_value" value=value autofocus="autofocus"}}
+ {{#if matches}}
+
+ {{i18n "admin.watched_words.test.found_matches"}}
+
+ {{#each matches as |match|}}
+ - {{match}}
+ {{/each}}
+
+
+ {{else}}
+
{{i18n "admin.watched_words.test.no_matches"}}
+ {{/if}}
+{{/d-modal-body}}
diff --git a/app/assets/javascripts/admin/templates/watched-words-action.hbs b/app/assets/javascripts/admin/templates/watched-words-action.hbs
index ad04b20c5b..68222ccedb 100644
--- a/app/assets/javascripts/admin/templates/watched-words-action.hbs
+++ b/app/assets/javascripts/admin/templates/watched-words-action.hbs
@@ -3,14 +3,24 @@
{{actionDescription}}
-{{watched-word-form
- actionKey=actionNameKey
- action=(action "recordAdded")
- filteredContent=filteredContent
- regularExpressions=adminWatchedWords.regularExpressions}}
+ {{watched-word-form
+ actionKey=actionNameKey
+ action=(action "recordAdded")
+ filteredContent=filteredContent
+ regularExpressions=adminWatchedWords.regularExpressions}}
-{{watched-word-uploader uploading=uploading actionKey=actionNameKey done=(action "uploadComplete")}}
+
+
+ {{d-button
+ class="btn-default download-link"
+ href=downloadLink
+ icon="download"
+ label="admin.watched_words.download"}}
+
+ {{watched-word-uploader uploading=uploading actionKey=actionNameKey done=(action "uploadComplete")}}
+
+
+
+
+ {{d-button
+ label="admin.watched_words.test.button_label"
+ icon="far-eye"
+ action=(action "test")}}
+ {{d-button
+ class="btn-danger clear-all"
+ label="admin.watched_words.clear_all"
+ icon="trash-alt"
+ action=(action "clearAll")}}
+
diff --git a/app/assets/javascripts/admin/templates/web-hooks.hbs b/app/assets/javascripts/admin/templates/web-hooks.hbs
index b48c0e37c3..389f7d4815 100644
--- a/app/assets/javascripts/admin/templates/web-hooks.hbs
+++ b/app/assets/javascripts/admin/templates/web-hooks.hbs
@@ -25,7 +25,7 @@
{{webHook.description}} |
{{#link-to 'adminWebHooks.show' webHook tagName='button' classNames='btn btn-default no-text'}}{{d-icon 'far-edit'}}{{/link-to}}
- {{d-button class="destroy btn-danger" action=(action "destroy") actionParam=webHook icon="remove"}}
+ {{d-button class="destroy btn-danger" action=(action "destroy") actionParam=webHook icon="times"}}
|
{{/each}}
diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6
index d5aecd76d4..726b49cfde 100644
--- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6
+++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6
@@ -35,7 +35,8 @@ const REPLACEMENTS = {
"notification.topic_reminder": "far-clock",
"notification.watching_first_post": "far-dot-circle",
"notification.group_message_summary": "users",
- "notification.post_approved": "check"
+ "notification.post_approved": "check",
+ "notification.membership_request_accepted": "user-plus"
};
// TODO: use lib/svg_sprite/fa4-renames.json here
@@ -579,13 +580,9 @@ function warnIfMissing(id) {
}
function warnIfDeprecated(oldId, newId) {
- if (
- typeof Discourse !== "undefined" &&
- Discourse.Environment === "development" &&
- !Ember.testing
- ) {
- deprecated(`Icon "${oldId}" is now "${newId}".`);
- }
+ deprecated(
+ `Please replace all occurrences of "${oldId}"" with "${newId}". FontAwesome 4.7 icon names are now deprecated and will be removed in the next release.`
+ );
}
function handleIconId(icon) {
diff --git a/app/assets/javascripts/discourse/components/avatar-uploader.js.es6 b/app/assets/javascripts/discourse/components/avatar-uploader.js.es6
index 5fe9aecec6..7349ce5a6b 100644
--- a/app/assets/javascripts/discourse/components/avatar-uploader.js.es6
+++ b/app/assets/javascripts/discourse/components/avatar-uploader.js.es6
@@ -6,13 +6,6 @@ export default Ember.Component.extend(UploadMixin, {
tagName: "span",
imageIsNotASquare: false,
- @computed("uploading")
- uploadButtonText(uploading) {
- return uploading
- ? I18n.t("uploading")
- : I18n.t("user.change_avatar.upload_picture");
- },
-
validateUploadedFilesOptions() {
return { imagesOnly: true };
},
diff --git a/app/assets/javascripts/discourse/components/backup-codes.js.es6 b/app/assets/javascripts/discourse/components/backup-codes.js.es6
index 08d33416f4..51be5edccd 100644
--- a/app/assets/javascripts/discourse/components/backup-codes.js.es6
+++ b/app/assets/javascripts/discourse/components/backup-codes.js.es6
@@ -25,9 +25,9 @@ export default Ember.Component.extend({
didRender() {
this._super(...arguments);
- const $backupCodes = this.$("#backupCodes");
- if ($backupCodes.length) {
- $backupCodes.height($backupCodes[0].scrollHeight);
+ const backupCodes = this.element.querySelector("#backupCodes");
+ if (backupCodes) {
+ backupCodes.style.height = backupCodes.scrollHeight;
}
},
@@ -49,8 +49,8 @@ export default Ember.Component.extend({
},
_selectAllBackupCodes() {
- const $textArea = this.$("#backupCodes");
- $textArea[0].focus();
- $textArea[0].setSelectionRange(0, this.formattedBackupCodes.length);
+ const textArea = this.element.querySelector("#backupCodes");
+ textArea.focus();
+ textArea.setSelectionRange(0, this.formattedBackupCodes.length);
}
});
diff --git a/app/assets/javascripts/discourse/components/backup-uploader.js.es6 b/app/assets/javascripts/discourse/components/backup-uploader.js.es6
index 939777f0a7..66b2665095 100644
--- a/app/assets/javascripts/discourse/components/backup-uploader.js.es6
+++ b/app/assets/javascripts/discourse/components/backup-uploader.js.es6
@@ -35,7 +35,7 @@ export default Ember.Component.extend(UploadMixin, {
},
_init: function() {
- const $upload = this.$();
+ const $upload = $(this.element);
$upload.on("fileuploadadd", (e, data) => {
ajax("/admin/backups/upload_url", {
diff --git a/app/assets/javascripts/discourse/components/basic-topic-list.js.es6 b/app/assets/javascripts/discourse/components/basic-topic-list.js.es6
index e08bf9d589..e8af6acc94 100644
--- a/app/assets/javascripts/discourse/components/basic-topic-list.js.es6
+++ b/app/assets/javascripts/discourse/components/basic-topic-list.js.es6
@@ -33,6 +33,51 @@ export default Ember.Component.extend({
}
},
+ didInsertElement() {
+ this._super(...arguments);
+
+ this.topics.forEach(topic => {
+ const includeUnreadIndicator =
+ typeof topic.unread_by_group_member !== "undefined";
+
+ if (includeUnreadIndicator) {
+ const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`;
+ this.messageBus.subscribe(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);
+
+ this.topics.forEach(topic => {
+ const includeUnreadIndicator =
+ typeof topic.unread_by_group_member !== "undefined";
+
+ if (includeUnreadIndicator) {
+ const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`;
+ this.messageBus.unsubscribe(unreadIndicatorChannel);
+ }
+ });
+ },
+
+ @computed("topics")
+ showUnreadIndicator(topics) {
+ return topics.some(
+ topic => typeof topic.unread_by_group_member !== "undefined"
+ );
+ },
+
click(e) {
// Mobile basic-topic-list doesn't use the `topic-list-item` view so
// the event for the topic entrance is never wired up.
diff --git a/app/assets/javascripts/discourse/components/category-panel-base.js.es6 b/app/assets/javascripts/discourse/components/category-panel-base.js.es6
deleted file mode 100644
index e38c2b7310..0000000000
--- a/app/assets/javascripts/discourse/components/category-panel-base.js.es6
+++ /dev/null
@@ -1,14 +0,0 @@
-const CategoryPanelBase = Ember.Component.extend({
- classNameBindings: [":modal-tab", "activeTab::invisible"]
-});
-
-export default CategoryPanelBase;
-
-export function buildCategoryPanel(tab, extras) {
- return CategoryPanelBase.extend(
- {
- activeTab: Ember.computed.equal("selectedTab", tab)
- },
- extras || {}
- );
-}
diff --git a/app/assets/javascripts/discourse/components/composer-body.js.es6 b/app/assets/javascripts/discourse/components/composer-body.js.es6
index bd7a4488eb..1d89f93f1a 100644
--- a/app/assets/javascripts/discourse/components/composer-body.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-body.js.es6
@@ -93,9 +93,9 @@ export default Ember.Component.extend(KeyEnterEscape, {
},
setupComposerResizeEvents() {
- const $composer = this.$();
- const $grippie = this.$(".grippie");
- const $document = Ember.$(document);
+ const $composer = $(this.element);
+ const $grippie = $(this.element.querySelector(".grippie"));
+ const $document = $(document);
let origComposerSize = 0;
let lastMousePos = 0;
@@ -105,7 +105,7 @@ export default Ember.Component.extend(KeyEnterEscape, {
const currentMousePos = mouseYPos(event);
let size = origComposerSize + (lastMousePos - currentMousePos);
- const winHeight = Ember.$(window).height();
+ const winHeight = $(window).height();
size = Math.min(size, winHeight - headerHeight());
size = Math.max(size, MIN_COMPOSER_SIZE);
this.movePanels(size);
@@ -145,11 +145,11 @@ export default Ember.Component.extend(KeyEnterEscape, {
};
triggerOpen();
- afterTransition(this.$(), () => {
+ afterTransition($(this.element), () => {
resize();
triggerOpen();
});
- positioningWorkaround(this.$());
+ positioningWorkaround($(this.element));
},
willDestroyElement() {
diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6
index 616a0228c8..9061181974 100644
--- a/app/assets/javascripts/discourse/components/composer-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6
@@ -112,7 +112,7 @@ export default Ember.Component.extend({
@observes("focusTarget")
setFocus() {
if (this.focusTarget === "editor") {
- this.$("textarea").putCursorAtEnd();
+ $(this.element.querySelector("textarea")).putCursorAtEnd();
}
},
@@ -155,21 +155,29 @@ export default Ember.Component.extend({
};
},
+ userSearchTerm(term) {
+ const topicId = this.get("topic.id");
+ // maybe this is a brand new topic, so grab category from composer
+ const categoryId =
+ this.get("topic.category_id") || this.get("composer.categoryId");
+
+ return userSearch({
+ term,
+ topicId,
+ categoryId,
+ includeMentionableGroups: true
+ });
+ },
+
@on("didInsertElement")
_composerEditorInit() {
- const topicId = this.get("topic.id");
- const $input = this.$(".d-editor-input");
- const $preview = this.$(".d-editor-preview-wrapper");
+ const $input = $(this.element.querySelector(".d-editor-input"));
+ const $preview = $(this.element.querySelector(".d-editor-preview-wrapper"));
if (this.siteSettings.enable_mentions) {
$input.autocomplete({
template: findRawTemplate("user-selector-autocomplete"),
- dataSource: term =>
- userSearch({
- term,
- topicId,
- includeMentionableGroups: true
- }),
+ dataSource: term => this.userSearchTerm.call(this, term),
key: "@",
transformComplete: v => v.username || v.name,
afterComplete() {
@@ -206,7 +214,7 @@ export default Ember.Component.extend({
!this.get("composer.canEditTitle") &&
(!this.capabilities.isIOS || safariHacksDisabled())
) {
- this.$(".d-editor-input").putCursorAtEnd();
+ $(this.element.querySelector(".d-editor-input")).putCursorAtEnd();
}
this._bindUploadTarget();
@@ -237,7 +245,7 @@ export default Ember.Component.extend({
reason = I18n.t("composer.error.post_missing");
} else if (missingReplyCharacters > 0) {
reason = I18n.t("composer.error.post_length", { min: minimumPostLength });
- const tl = Discourse.User.currentProp("trust_level");
+ const tl = this.get("currentUser.trust_level");
if (tl === 0 || tl === 1) {
reason +=
"
" +
@@ -345,12 +353,13 @@ export default Ember.Component.extend({
},
_teardownInputPreviewSync() {
- [this.$(".d-editor-input"), this.$(".d-editor-preview-wrapper")].forEach(
- $element => {
- $element.off("mouseenter touchstart");
- $element.off("scroll");
- }
- );
+ [
+ $(this.element.querySelector(".d-editor-input")),
+ $(this.element.querySelector(".d-editor-preview-wrapper"))
+ ].forEach($element => {
+ $element.off("mouseenter touchstart");
+ $element.off("scroll");
+ });
REBUILD_SCROLL_MAP_EVENTS.forEach(event => {
this.appEvents.off(event, this, this._resetShouldBuildScrollMap);
@@ -647,14 +656,11 @@ export default Ember.Component.extend({
this._unbindUploadTarget(); // in case it's still bound, let's clean it up first
this._pasted = false;
- const $element = this.$();
- const csrf = this.session.get("csrfToken");
+ const $element = $(this.element);
$element.fileupload({
url: Discourse.getURL(
- `/uploads.json?client_id=${
- this.messageBus.clientId
- }&authenticity_token=${encodeURIComponent(csrf)}`
+ `/uploads.json?client_id=${this.messageBus.clientId}`
),
dataType: "json",
pasteZone: $element
@@ -867,7 +873,8 @@ export default Ember.Component.extend({
// wraps previewed upload markdown in a codeblock in its own class to keep a track
// of indexes later on to replace the correct upload placeholder in the composer
if ($preview.find(".codeblock-image").length === 0) {
- this.$(".d-editor-preview *")
+ $(this.element)
+ .find(".d-editor-preview *")
.contents()
.each(function() {
if (this.nodeType !== 3) return; // TEXT_NODE
@@ -890,7 +897,7 @@ export default Ember.Component.extend({
this._validUploads = 0;
$("#reply-control .mobile-file-upload").off("click.uploader");
this.messageBus.unsubscribe("/uploads/composer");
- const $uploadTarget = this.$();
+ const $uploadTarget = $(this.element);
try {
$uploadTarget.fileupload("destroy");
} catch (e) {
@@ -925,7 +932,7 @@ export default Ember.Component.extend({
},
showPreview() {
- const $preview = this.$(".d-editor-preview-wrapper");
+ const $preview = $(this.element.querySelector(".d-editor-preview-wrapper"));
this._placeImageScaleButtons($preview);
this.send("togglePreview");
},
@@ -1071,7 +1078,7 @@ export default Ember.Component.extend({
if (this._enableAdvancedEditorPreviewSync()) {
this._syncScroll(
this._syncEditorAndPreviewScroll,
- this.$(".d-editor-input"),
+ $(this.element.querySelector(".d-editor-input")),
$preview
);
}
diff --git a/app/assets/javascripts/discourse/components/composer-message.js.es6 b/app/assets/javascripts/discourse/components/composer-message.js.es6
index dc8d387a1e..4cdda3fc49 100644
--- a/app/assets/javascripts/discourse/components/composer-message.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-message.js.es6
@@ -11,7 +11,7 @@ export default Ember.Component.extend({
didInsertElement() {
this._super(...arguments);
- this.$().show();
+ this.element.style.display = "block";
},
actions: {
diff --git a/app/assets/javascripts/discourse/components/composer-title.js.es6 b/app/assets/javascripts/discourse/components/composer-title.js.es6
index 345e2ca9b4..5302ab85f4 100644
--- a/app/assets/javascripts/discourse/components/composer-title.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-title.js.es6
@@ -14,9 +14,9 @@ export default Ember.Component.extend({
didInsertElement() {
this._super(...arguments);
if (this.focusTarget === "title") {
- const $input = this.$("input");
+ const $input = $(this.element.querySelector("input"));
- afterTransition(this.$().closest("#reply-control"), () => {
+ afterTransition($(this.element).closest("#reply-control"), () => {
$input.putCursorAtEnd();
});
}
@@ -133,14 +133,14 @@ export default Ember.Component.extend({
.finally(() => {
this.set("composer.loading", false);
Ember.run.schedule("afterRender", () => {
- this.$("input").putCursorAtEnd();
+ $(this.element.querySelector("input")).putCursorAtEnd();
});
});
} else {
this._updatePost(loadOnebox);
this.set("composer.loading", false);
Ember.run.schedule("afterRender", () => {
- this.$("input").putCursorAtEnd();
+ $(this.element.querySelector("input")).putCursorAtEnd();
});
}
}
diff --git a/app/assets/javascripts/discourse/components/composer-user-selector.js.es6 b/app/assets/javascripts/discourse/components/composer-user-selector.js.es6
index 77c7537ece..04a1ba7dc5 100644
--- a/app/assets/javascripts/discourse/components/composer-user-selector.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-user-selector.js.es6
@@ -12,14 +12,14 @@ export default Ember.Component.extend({
this._super(...arguments);
if (this.focusTarget === "usernames") {
- this.$("input").putCursorAtEnd();
+ $(this.element.querySelector("input")).putCursorAtEnd();
}
},
@observes("usernames")
_checkWidth() {
let width = 0;
- const $acWrap = this.$().find(".ac-wrap");
+ const $acWrap = $(this.element).find(".ac-wrap");
const limit = $acWrap.width();
this.set("defaultUsernameCount", 0);
@@ -76,7 +76,7 @@ export default Ember.Component.extend({
this.set("showSelector", true);
Ember.run.schedule("afterRender", () => {
- this.$()
+ $(this.element)
.find("input")
.focus();
});
@@ -84,7 +84,7 @@ export default Ember.Component.extend({
triggerResize() {
this.appEvents.trigger("composer:resize");
- const $this = this.$().find(".ac-wrap");
+ const $this = $(this.element).find(".ac-wrap");
if ($this.height() >= 150) $this.scrollTop($this.height());
}
}
diff --git a/app/assets/javascripts/discourse/components/create-account.js.es6 b/app/assets/javascripts/discourse/components/create-account.js.es6
index 74f08fbb9b..9d91b58b35 100644
--- a/app/assets/javascripts/discourse/components/create-account.js.es6
+++ b/app/assets/javascripts/discourse/components/create-account.js.es6
@@ -8,7 +8,7 @@ export default Ember.Component.extend({
this.set("email", $.cookie("email"));
}
- this.$().on("keydown.discourse-create-account", e => {
+ $(this.element).on("keydown.discourse-create-account", e => {
if (!this.disabled && e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
@@ -17,7 +17,7 @@ export default Ember.Component.extend({
}
});
- this.$().on("click.dropdown-user-field-label", "[for]", event => {
+ $(this.element).on("click.dropdown-user-field-label", "[for]", event => {
const $element = $(event.target);
const $target = $(`#${$element.attr("for")}`);
@@ -31,7 +31,7 @@ export default Ember.Component.extend({
willDestroyElement() {
this._super(...arguments);
- this.$().off("keydown.discourse-create-account");
- this.$().off("click.dropdown-user-field-label");
+ $(this.element).off("keydown.discourse-create-account");
+ $(this.element).off("click.dropdown-user-field-label");
}
});
diff --git a/app/assets/javascripts/discourse/components/csv-uploader.js.es6 b/app/assets/javascripts/discourse/components/csv-uploader.js.es6
index f080dcf755..a37e10f3cc 100644
--- a/app/assets/javascripts/discourse/components/csv-uploader.js.es6
+++ b/app/assets/javascripts/discourse/components/csv-uploader.js.es6
@@ -32,7 +32,7 @@ export default Ember.Component.extend(UploadMixin, {
},
_init: function() {
- const $upload = this.$();
+ const $upload = $(this.element);
$upload.on("fileuploadadd", (e, data) => {
bootbox.confirm(
diff --git a/app/assets/javascripts/discourse/components/d-editor-modal.js.es6 b/app/assets/javascripts/discourse/components/d-editor-modal.js.es6
index 6456bdc7d5..4085b8cc8f 100644
--- a/app/assets/javascripts/discourse/components/d-editor-modal.js.es6
+++ b/app/assets/javascripts/discourse/components/d-editor-modal.js.es6
@@ -7,8 +7,8 @@ export default Ember.Component.extend({
_hiddenChanged() {
if (!this.hidden) {
Ember.run.scheduleOnce("afterRender", () => {
- const $modal = this.$();
- const $parent = this.$().closest(".d-editor");
+ const $modal = $(this.element);
+ const $parent = $(this.element).closest(".d-editor");
const w = $parent.width();
const h = $parent.height();
const dir = $("html").css("direction") === "rtl" ? "right" : "left";
@@ -27,7 +27,7 @@ export default Ember.Component.extend({
@on("didInsertElement")
_listenKeys() {
- this.$().on("keydown.d-modal", key => {
+ $(this.element).on("keydown.d-modal", key => {
if (this.hidden) {
return;
}
@@ -45,7 +45,7 @@ export default Ember.Component.extend({
@on("willDestroyElement")
_stopListening() {
- this.$().off("keydown.d-modal");
+ $(this.element).off("keydown.d-modal");
},
actions: {
diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6
index ccafc0bd04..8cd3c541ba 100644
--- a/app/assets/javascripts/discourse/components/d-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/d-editor.js.es6
@@ -220,6 +220,7 @@ export default Ember.Component.extend({
_mouseTrap: null,
showLink: true,
emojiPickerIsActive: false,
+ emojiStore: Ember.inject.service("emoji-store"),
@computed("placeholder")
placeholderTranslated(placeholder) {
@@ -231,7 +232,7 @@ export default Ember.Component.extend({
this.set("ready", true);
if (this.autofocus) {
- this.$("textarea").focus();
+ this.element.querySelector("textarea").focus();
}
},
@@ -244,13 +245,13 @@ export default Ember.Component.extend({
didInsertElement() {
this._super(...arguments);
- const $editorInput = this.$(".d-editor-input");
+ const $editorInput = $(this.element.querySelector(".d-editor-input"));
this._applyEmojiAutocomplete($editorInput);
this._applyCategoryHashtagAutocomplete($editorInput);
Ember.run.scheduleOnce("afterRender", this, this._readyNow);
- const mouseTrap = Mousetrap(this.$(".d-editor-input")[0]);
+ const mouseTrap = Mousetrap(this.element.querySelector(".d-editor-input"));
const shortcuts = this.get("toolbar.shortcuts");
Object.keys(shortcuts).forEach(sc => {
@@ -262,28 +263,31 @@ export default Ember.Component.extend({
});
// disable clicking on links in the preview
- this.$(".d-editor-preview").on("click.preview", e => {
- if (wantsNewWindow(e)) {
- return;
+ $(this.element.querySelector(".d-editor-preview")).on(
+ "click.preview",
+ e => {
+ if (wantsNewWindow(e)) {
+ return;
+ }
+ const $target = $(e.target);
+ if ($target.is("a.mention")) {
+ this.appEvents.trigger(
+ "click.discourse-preview-user-card-mention",
+ $target
+ );
+ }
+ if ($target.is("a.mention-group")) {
+ this.appEvents.trigger(
+ "click.discourse-preview-group-card-mention-group",
+ $target
+ );
+ }
+ if ($target.is("a")) {
+ e.preventDefault();
+ return false;
+ }
}
- const $target = $(e.target);
- if ($target.is("a.mention")) {
- this.appEvents.trigger(
- "click.discourse-preview-user-card-mention",
- $target
- );
- }
- if ($target.is("a.mention-group")) {
- this.appEvents.trigger(
- "click.discourse-preview-group-card-mention-group",
- $target
- );
- }
- if ($target.is("a")) {
- e.preventDefault();
- return false;
- }
- });
+ );
if (this.composerEvents) {
this.appEvents.on("composer:insert-block", this, "_insertBlock");
@@ -313,7 +317,7 @@ export default Ember.Component.extend({
Object.keys(this.get("toolbar.shortcuts")).forEach(sc =>
mouseTrap.unbind(sc)
);
- this.$(".d-editor-preview").off("click.preview");
+ $(this.element.querySelector(".d-editor-preview")).off("click.preview");
},
@computed
@@ -348,7 +352,7 @@ export default Ember.Component.extend({
if (this._state !== "inDOM") {
return;
}
- const $preview = this.$(".d-editor-preview");
+ const $preview = $(this.element.querySelector(".d-editor-preview"));
if ($preview.length === 0) return;
if (this.previewUpdated) {
@@ -375,7 +379,7 @@ export default Ember.Component.extend({
_applyCategoryHashtagAutocomplete() {
const siteSettings = this.siteSettings;
- this.$(".d-editor-input").autocomplete({
+ $(this.element.querySelector(".d-editor-input")).autocomplete({
template: findRawTemplate("category-tag-autocomplete"),
key: "#",
afterComplete: () => this._focusTextArea(),
@@ -419,6 +423,7 @@ export default Ember.Component.extend({
transformComplete: v => {
if (v.code) {
+ this.emojiStore.track(v.code);
return `${v.code}:`;
} else {
$editorInput.autocomplete({ cancel: true });
@@ -455,7 +460,17 @@ export default Ember.Component.extend({
}
if (term === "") {
- return resolve(["slight_smile", "smile", "wink", "sunny", "blush"]);
+ if (this.emojiStore.favorites.length) {
+ return resolve(this.emojiStore.favorites.slice(0, 5));
+ } else {
+ return resolve([
+ "slight_smile",
+ "smile",
+ "wink",
+ "sunny",
+ "blush"
+ ]);
+ }
}
if (translations[full]) {
@@ -500,7 +515,7 @@ export default Ember.Component.extend({
return;
}
- const textarea = this.$("textarea.d-editor-input")[0];
+ const textarea = this.element.querySelector("textarea.d-editor-input");
const value = textarea.value;
let start = textarea.selectionStart;
let end = textarea.selectionEnd;
@@ -533,8 +548,8 @@ export default Ember.Component.extend({
_selectText(from, length) {
Ember.run.scheduleOnce("afterRender", () => {
- const $textarea = this.$("textarea.d-editor-input");
- const textarea = $textarea[0];
+ const textarea = this.element.querySelector("textarea.d-editor-input");
+ const $textarea = $(textarea);
const oldScrollPos = $textarea.scrollTop();
if (!this.capabilities.isIOS || safariHacksDisabled()) {
$textarea.focus();
@@ -687,7 +702,7 @@ export default Ember.Component.extend({
return;
}
- const textarea = this.$("textarea.d-editor-input")[0];
+ const textarea = this.element.querySelector("textarea.d-editor-input");
// Determine post-replace selection.
const newSelection = determinePostReplaceSelection({
@@ -737,7 +752,7 @@ export default Ember.Component.extend({
}
const value = pre + text + post;
- const $textarea = this.$("textarea.d-editor-input");
+ const $textarea = $(this.element.querySelector("textarea.d-editor-input"));
this.set("value", value);
@@ -749,7 +764,7 @@ export default Ember.Component.extend({
},
_addText(sel, text, options) {
- const $textarea = this.$("textarea.d-editor-input");
+ const $textarea = $(this.element.querySelector("textarea.d-editor-input"));
if (options && options.ensureSpace) {
if ((sel.pre + "").length > 0) {
@@ -870,8 +885,11 @@ export default Ember.Component.extend({
// ensures textarea scroll position is correct
_focusTextArea() {
- const $textarea = this.$("textarea.d-editor-input");
- Ember.run.scheduleOnce("afterRender", () => $textarea.blur().focus());
+ const textarea = this.element.querySelector("textarea.d-editor-input");
+ Ember.run.scheduleOnce("afterRender", () => {
+ textarea.blur();
+ textarea.focus();
+ });
},
actions: {
diff --git a/app/assets/javascripts/discourse/components/d-modal-body.js.es6 b/app/assets/javascripts/discourse/components/d-modal-body.js.es6
index f9f46e6167..8e8f01b0bc 100644
--- a/app/assets/javascripts/discourse/components/d-modal-body.js.es6
+++ b/app/assets/javascripts/discourse/components/d-modal-body.js.es6
@@ -7,7 +7,7 @@ export default Ember.Component.extend({
this._super(...arguments);
$("#modal-alert").hide();
- let fixedParent = this.$().closest(".d-modal.fixed-modal");
+ let fixedParent = $(this.element).closest(".d-modal.fixed-modal");
if (fixedParent.length) {
this.set("fixed", true);
fixedParent.modal("show");
@@ -26,8 +26,12 @@ export default Ember.Component.extend({
},
_afterFirstRender() {
- if (!this.site.mobileView && this.autoFocus !== "false") {
- this.$("input:first").focus();
+ if (
+ !this.site.mobileView &&
+ this.autoFocus !== "false" &&
+ this.element.querySelector("input")
+ ) {
+ this.element.querySelector("input").focus();
}
const maxHeight = this.maxHeight;
@@ -35,7 +39,7 @@ export default Ember.Component.extend({
const maxHeightFloat = parseFloat(maxHeight) / 100.0;
if (maxHeightFloat > 0) {
const viewPortHeight = $(window).height();
- this.$().css(
+ $(this.element).css(
"max-height",
Math.floor(maxHeightFloat * viewPortHeight) + "px"
);
diff --git a/app/assets/javascripts/discourse/components/d-modal.js.es6 b/app/assets/javascripts/discourse/components/d-modal.js.es6
index cdf5fff878..f870780789 100644
--- a/app/assets/javascripts/discourse/components/d-modal.js.es6
+++ b/app/assets/javascripts/discourse/components/d-modal.js.es6
@@ -66,7 +66,7 @@ export default Ember.Component.extend({
}
if (data.fixed) {
- this.$().removeClass("hidden");
+ this.element.classList.remove("hidden");
}
if (data.title) {
diff --git a/app/assets/javascripts/discourse/components/d-navigation.js.es6 b/app/assets/javascripts/discourse/components/d-navigation.js.es6
index dc1eee1c3c..26870bcd02 100644
--- a/app/assets/javascripts/discourse/components/d-navigation.js.es6
+++ b/app/assets/javascripts/discourse/components/d-navigation.js.es6
@@ -1,6 +1,9 @@
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
+ router: Ember.inject.service(),
+ persistedQueryParams: null,
+
tagName: "",
@computed("category")
@@ -27,9 +30,25 @@ export default Ember.Component.extend({
if (filterMode.indexOf("top/") === 0) {
filterMode = filterMode.replace("top/", "");
}
+
+ let params;
+ const currentRouteQueryParams = this.get("router.currentRoute.queryParams");
+ if (this.persistedQueryParams && currentRouteQueryParams) {
+ const currentKeys = Object.keys(currentRouteQueryParams);
+ const discoveryKeys = Object.keys(this.persistedQueryParams);
+ const supportedKeys = currentKeys.filter(
+ i => discoveryKeys.indexOf(i) > 0
+ );
+ params = supportedKeys.reduce((object, key) => {
+ object[key] = currentRouteQueryParams[key];
+ return object;
+ }, {});
+ }
+
return Discourse.NavItem.buildList(category, {
filterMode,
- noSubcategories
+ noSubcategories,
+ persistedQueryParams: params
});
}
});
diff --git a/app/assets/javascripts/discourse/components/date-input.js.es6 b/app/assets/javascripts/discourse/components/date-input.js.es6
new file mode 100644
index 0000000000..d29962c02c
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/date-input.js.es6
@@ -0,0 +1,101 @@
+/* global Pikaday:true */
+import loadScript from "discourse/lib/load-script";
+import {
+ default as computed,
+ on
+} from "ember-addons/ember-computed-decorators";
+
+export default Ember.Component.extend({
+ classNames: ["d-date-input"],
+ date: null,
+ _picker: null,
+
+ @computed("site.mobileView")
+ inputType(mobileView) {
+ return mobileView ? "date" : "text";
+ },
+
+ @on("didInsertElement")
+ _loadDatePicker() {
+ const container = this.element.querySelector(`#${this.containerId}`);
+
+ if (this.site.mobileView) {
+ this._loadNativePicker(container);
+ } else {
+ this._loadPikadayPicker(container);
+ }
+ },
+
+ didUpdateAttrs() {
+ this._super(...arguments);
+
+ if (this._picker) {
+ this._picker.setDate(this.date, true);
+ }
+ },
+
+ _loadPikadayPicker(container) {
+ loadScript("/javascripts/pikaday.js").then(() => {
+ Ember.run.next(() => {
+ const default_opts = {
+ field: this.element.querySelector(".date-picker"),
+ container: container || this.element,
+ bound: container === null,
+ format: "LL",
+ firstDay: 1,
+ i18n: {
+ previousMonth: I18n.t("dates.previous_month"),
+ nextMonth: I18n.t("dates.next_month"),
+ months: moment.months(),
+ weekdays: moment.weekdays(),
+ weekdaysShort: moment.weekdaysShort()
+ },
+ onSelect: date => this._handleSelection(date)
+ };
+
+ this._picker = new Pikaday(Object.assign(default_opts, this._opts()));
+ this._picker.setDate(this.date, true);
+ });
+ });
+ },
+
+ _loadNativePicker(container) {
+ const wrapper = container || this.element;
+ const picker = wrapper.querySelector("input.date-picker");
+ picker.onchange = () => this._handleSelection(picker.value);
+ picker.hide = () => {
+ /* do nothing for native */
+ };
+ picker.destroy = () => {
+ /* do nothing for native */
+ };
+ this._picker = picker;
+ },
+
+ _handleSelection(value) {
+ if (!this.element || this.isDestroying || this.isDestroyed) return;
+
+ this._picker && this._picker.hide();
+
+ if (this.onChange) {
+ this.onChange(moment(value).toDate());
+ }
+ },
+
+ @on("willDestroyElement")
+ _destroy() {
+ if (this._picker) {
+ this._picker.destroy();
+ }
+ this._picker = null;
+ },
+
+ @computed()
+ placeholder() {
+ return I18n.t("dates.placeholder");
+ },
+
+ _opts() {
+ return null;
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/date-picker.js.es6 b/app/assets/javascripts/discourse/components/date-picker.js.es6
index a6a0886937..2c7d073169 100644
--- a/app/assets/javascripts/discourse/components/date-picker.js.es6
+++ b/app/assets/javascripts/discourse/components/date-picker.js.es6
@@ -41,7 +41,7 @@ export default Ember.Component.extend({
nextMonth: I18n.t("dates.next_month"),
months: moment.months(),
weekdays: moment.weekdays(),
- weekdaysShort: moment.weekdaysShort()
+ weekdaysShort: moment.weekdaysMin()
},
onSelect: date => this._handleSelection(date)
};
diff --git a/app/assets/javascripts/discourse/components/date-time-input-range.js.es6 b/app/assets/javascripts/discourse/components/date-time-input-range.js.es6
new file mode 100644
index 0000000000..3754be93cd
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/date-time-input-range.js.es6
@@ -0,0 +1,51 @@
+export default Ember.Component.extend({
+ classNames: ["d-date-time-input-range"],
+
+ from: null,
+ to: null,
+ onChangeTo: null,
+ onChangeFrom: null,
+ currentPanel: "from",
+ showFromTime: true,
+ showToTime: true,
+ error: null,
+
+ fromPanelActive: Ember.computed.equal("currentPanel", "from"),
+ toPanelActive: Ember.computed.equal("currentPanel", "to"),
+
+ _valid(state) {
+ if (state.to < state.from) {
+ return I18n.t("date_time_picker.errors.to_before_from");
+ }
+
+ return true;
+ },
+
+ actions: {
+ _onChange(options, value) {
+ if (this.onChange) {
+ const state = {
+ from: this.from,
+ to: this.to
+ };
+
+ const diff = {};
+ diff[options.prop] = value;
+
+ const newState = Object.assign(state, diff);
+
+ const validation = this._valid(newState);
+ if (validation === true) {
+ this.set("error", null);
+ this.onChange(newState);
+ } else {
+ this.set("error", validation);
+ }
+ }
+ },
+
+ onChangePanel(panel) {
+ this.set("currentPanel", panel);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/date-time-input.js.es6 b/app/assets/javascripts/discourse/components/date-time-input.js.es6
new file mode 100644
index 0000000000..ce173e3d42
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/date-time-input.js.es6
@@ -0,0 +1,35 @@
+export default Ember.Component.extend({
+ classNames: ["d-date-time-input"],
+ date: null,
+ showTime: true,
+
+ _hours: Ember.computed("date", function() {
+ return this.date ? this.date.getHours() : null;
+ }),
+
+ _minutes: Ember.computed("date", function() {
+ return this.date ? this.date.getMinutes() : null;
+ }),
+
+ actions: {
+ onChangeTime(time) {
+ if (this.onChange) {
+ const year = this.date.getFullYear();
+ const month = this.date.getMonth();
+ const day = this.date.getDate();
+ this.onChange(new Date(year, month, day, time.hours, time.minutes));
+ }
+ },
+
+ onChangeDate(date) {
+ if (this.onChange) {
+ const year = date.getFullYear();
+ const month = date.getMonth();
+ const day = date.getDate();
+ this.onChange(
+ new Date(year, month, day, this._hours || 0, this._minutes || 0)
+ );
+ }
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/components/discourse-topic.js.es6 b/app/assets/javascripts/discourse/components/discourse-topic.js.es6
index dc84102a30..f94788b470 100644
--- a/app/assets/javascripts/discourse/components/discourse-topic.js.es6
+++ b/app/assets/javascripts/discourse/components/discourse-topic.js.es6
@@ -102,7 +102,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) {
@@ -120,7 +120,10 @@ 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();
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..10a73fcd5e 100644
--- a/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6
+++ b/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6
@@ -22,6 +22,11 @@ const DiscoveryTopicsListComponent = Ember.Component.extend(
}
},
+ @observes("topicTrackingState.states")
+ _updateTopics() {
+ this.topicTrackingState.updateTopics(this.model.topics);
+ },
+
@observes("incomingCount")
_updateTitle() {
Discourse.updateContextCount(this.incomingCount);
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..be41b0a09c 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,33 @@
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
import PermissionType from "discourse/models/permission-type";
+import { on } from "ember-addons/ember-computed-decorators";
export default buildCategoryPanel("security", {
editingPermissions: false,
selectedGroup: null,
selectedPermission: null,
+ showPendingGroupChangesAlert: false,
+ interactedWithDropdowns: false,
+
+ @on("init")
+ _registerValidator() {
+ this.registerValidator(() => {
+ if (
+ !this.showPendingGroupChangesAlert &&
+ this.interactedWithDropdowns &&
+ this.activeTab
+ ) {
+ this.set("showPendingGroupChangesAlert", true);
+ return true;
+ }
+ });
+ },
actions: {
+ onDropdownChange() {
+ this.set("interactedWithDropdowns", true);
+ },
+
editPermissions() {
if (!this.get("category.is_special")) {
this.set("editingPermissions", true);
@@ -25,6 +46,10 @@ export default buildCategoryPanel("security", {
"selectedGroup",
this.get("category.availableGroups.firstObject")
);
+ this.setProperties({
+ showPendingGroupChangesAlert: false,
+ interactedWithDropdowns: false
+ });
},
removePermission(permission) {
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..5f2f6b4912 100644
--- a/app/assets/javascripts/discourse/components/edit-category-tab.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-tab.js.es6
@@ -27,7 +27,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-topic-template.js.es6 b/app/assets/javascripts/discourse/components/edit-category-topic-template.js.es6
index ab643885a4..b1d5c79d81 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
@@ -4,7 +4,7 @@ export default buildCategoryPanel("topic-template", {
_activeTabChanged: function() {
if (this.activeTab) {
Ember.run.scheduleOnce("afterRender", () =>
- this.$(".d-editor-input").focus()
+ this.element.querySelector(".d-editor-input").focus()
);
}
}.observes("activeTab")
diff --git a/app/assets/javascripts/discourse/components/emoji-picker.js.es6 b/app/assets/javascripts/discourse/components/emoji-picker.js.es6
index 0a972e7227..dfe6287c67 100644
--- a/app/assets/javascripts/discourse/components/emoji-picker.js.es6
+++ b/app/assets/javascripts/discourse/components/emoji-picker.js.es6
@@ -1,7 +1,7 @@
import { on, observes } from "ember-addons/ember-computed-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,
@@ -10,21 +10,14 @@ import {
import { safariHacksDisabled } from "discourse/lib/utilities";
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({
automaticPositioning: true,
+ emojiStore: Ember.inject.service("emoji-store"),
close() {
this._unbindEvents();
@@ -46,11 +39,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 +78,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 +97,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 +206,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 +290,7 @@ export default Ember.Component.extend({
},
_bindResizing() {
- this.$(window).on("resize", () => {
+ $(window).on("resize", () => {
run.throttle(this, this._positionPicker, 16);
});
@@ -326,7 +304,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;
@@ -468,7 +446,7 @@ export default Ember.Component.extend({
_isReplyControlExpanded() {
const verticalSpace =
- this.$(window).height() -
+ $(window).height() -
$(".d-header").height() -
$("#reply-control").height();
@@ -480,7 +458,7 @@ export default Ember.Component.extend({
return;
}
- let windowWidth = this.$(window).width();
+ let windowWidth = $(window).width();
const desktopModalePositioning = options => {
let attributes = {
@@ -608,12 +586,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/expanding-text-area.js.es6 b/app/assets/javascripts/discourse/components/expanding-text-area.js.es6
index 7af160b4fe..064764713f 100644
--- a/app/assets/javascripts/discourse/components/expanding-text-area.js.es6
+++ b/app/assets/javascripts/discourse/components/expanding-text-area.js.es6
@@ -5,7 +5,7 @@ export default Ember.TextArea.extend({
@on("didInsertElement")
_startWatching() {
Ember.run.scheduleOnce("afterRender", () => {
- this.$().focus();
+ $(this.element).focus();
autosize(this.element);
});
},
@@ -19,6 +19,6 @@ export default Ember.TextArea.extend({
@on("willDestroyElement")
_disableAutosize() {
- autosize.destroy(this.$());
+ autosize.destroy($(this.element));
}
});
diff --git a/app/assets/javascripts/discourse/components/flag-selection.js.es6 b/app/assets/javascripts/discourse/components/flag-selection.js.es6
index ca02fd03bd..8499713ce2 100644
--- a/app/assets/javascripts/discourse/components/flag-selection.js.es6
+++ b/app/assets/javascripts/discourse/components/flag-selection.js.es6
@@ -3,14 +3,14 @@ import { observes } from "ember-addons/ember-computed-decorators";
// Mostly hacks because `flag.hbs` didn't use `radio-button`
export default Ember.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")
diff --git a/app/assets/javascripts/discourse/components/footer-nav.js.es6 b/app/assets/javascripts/discourse/components/footer-nav.js.es6
index 8e1a4e8a2f..2e932686c9 100644
--- a/app/assets/javascripts/discourse/components/footer-nav.js.es6
+++ b/app/assets/javascripts/discourse/components/footer-nav.js.es6
@@ -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/generated-invite-link.js.es6 b/app/assets/javascripts/discourse/components/generated-invite-link.js.es6
index 35179719ec..74426a45b6 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,7 @@
export default Ember.Component.extend({
didInsertElement() {
this._super(...arguments);
- this.$("input")
+ $(this.element.querySelector("input"))
.select()
.focus();
}
diff --git a/app/assets/javascripts/discourse/components/group-selector.js.es6 b/app/assets/javascripts/discourse/components/group-selector.js.es6
index 9447f63875..c70c51fbc7 100644
--- a/app/assets/javascripts/discourse/components/group-selector.js.es6
+++ b/app/assets/javascripts/discourse/components/group-selector.js.es6
@@ -22,7 +22,7 @@ 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
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..6cf923d39d 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
@@ -19,12 +19,12 @@ export default Ember.Component.extend({
@computed("model.visibility_level", "model.public_admission")
disableMembershipRequestSetting(visibility_level, publicAdmission) {
visibility_level = parseInt(visibility_level);
- return visibility_level !== 0 || publicAdmission;
+ return ![0, 1].includes(visibility_level) || publicAdmission;
},
@computed("model.visibility_level", "model.allow_membership_requests")
disablePublicSetting(visibility_level, allowMembershipRequests) {
visibility_level = parseInt(visibility_level);
- return visibility_level !== 0 || allowMembershipRequests;
+ return ![0, 1].includes(visibility_level) || allowMembershipRequests;
}
});
diff --git a/app/assets/javascripts/discourse/components/highlight-text.js.es6 b/app/assets/javascripts/discourse/components/highlight-text.js.es6
index 73c2b045bc..6e8be431dc 100644
--- a/app/assets/javascripts/discourse/components/highlight-text.js.es6
+++ b/app/assets/javascripts/discourse/components/highlight-text.js.es6
@@ -5,7 +5,7 @@ export default Ember.Component.extend({
_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/ignored-user-list.js.es6 b/app/assets/javascripts/discourse/components/ignored-user-list.js.es6
index 9c951dfeed..ac81cc505d 100644
--- a/app/assets/javascripts/discourse/components/ignored-user-list.js.es6
+++ b/app/assets/javascripts/discourse/components/ignored-user-list.js.es6
@@ -20,6 +20,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..df9ce3e2b2 100644
--- a/app/assets/javascripts/discourse/components/image-uploader.js.es6
+++ b/app/assets/javascripts/discourse/components/image-uploader.js.es6
@@ -76,11 +76,13 @@ export default Ember.Component.extend(UploadMixin, {
},
_openLightbox() {
- Ember.run.next(() => this.$("a.lightbox").magnificPopup("open"));
+ Ember.run.next(() =>
+ $(this.element.querySelector("a.lightbox")).magnificPopup("open")
+ );
},
_applyLightbox() {
- if (this.imageUrl) Ember.run.next(() => lightbox(this.$()));
+ if (this.imageUrl) Ember.run.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..23b8fbe688 100644
--- a/app/assets/javascripts/discourse/components/images-uploader.js.es6
+++ b/app/assets/javascripts/discourse/components/images-uploader.js.es6
@@ -7,9 +7,7 @@ export default Ember.Component.extend(UploadMixin, {
@computed("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/link-to-input.js.es6 b/app/assets/javascripts/discourse/components/link-to-input.js.es6
index 34eaedc7d4..0afdbd304b 100644
--- a/app/assets/javascripts/discourse/components/link-to-input.js.es6
+++ b/app/assets/javascripts/discourse/components/link-to-input.js.es6
@@ -5,7 +5,7 @@ export default Ember.Component.extend({
this.onClick();
Ember.run.schedule("afterRender", () => {
- this.$()
+ $(this.element)
.find("input")
.focus();
});
diff --git a/app/assets/javascripts/discourse/components/mobile-nav.js.es6 b/app/assets/javascripts/discourse/components/mobile-nav.js.es6
index f63183b217..49392fc336 100644
--- a/app/assets/javascripts/discourse/components/mobile-nav.js.es6
+++ b/app/assets/javascripts/discourse/components/mobile-nav.js.es6
@@ -4,7 +4,7 @@ export default Ember.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);
@@ -24,13 +24,15 @@ export default Ember.Component.extend({
},
_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();
},
@@ -43,9 +45,12 @@ export default Ember.Component.extend({
$(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/navigation-bar.js.es6 b/app/assets/javascripts/discourse/components/navigation-bar.js.es6
index de4ba53a1d..4be9c2c055 100644
--- a/app/assets/javascripts/discourse/components/navigation-bar.js.es6
+++ b/app/assets/javascripts/discourse/components/navigation-bar.js.es6
@@ -73,8 +73,8 @@ export default Ember.Component.extend({
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(() => {
if (!this.element || this.isDestroying || this.isDestroyed) {
diff --git a/app/assets/javascripts/discourse/components/navigation-item.js.es6 b/app/assets/javascripts/discourse/components/navigation-item.js.es6
index 5ef8f99d15..10290334a7 100644
--- a/app/assets/javascripts/discourse/components/navigation-item.js.es6
+++ b/app/assets/javascripts/discourse/components/navigation-item.js.es6
@@ -26,15 +26,28 @@ export default Ember.Component.extend(
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("category.id");
if (categoryId) {
- href += `?category_id=${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("&")}`;
+ }
+
if (
!this.active &&
this.currentUser &&
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..5f4fda605c 100644
--- a/app/assets/javascripts/discourse/components/popup-input-tip.js.es6
+++ b/app/assets/javascripts/discourse/components/popup-input-tip.js.es6
@@ -29,7 +29,7 @@ export default Ember.Component.extend(
@observes("lastShownAt")
bounce() {
if (this.lastShownAt) {
- var $elem = this.$();
+ var $elem = $(this.element);
if (!this.animateAttribute) {
this.animateAttribute =
$elem.css("left") === "auto" ? "right" : "left";
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..458fa3cebd
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/pwa-install-banner.js.es6
@@ -0,0 +1,67 @@
+import {
+ default as computed,
+ on
+} from "ember-addons/ember-computed-decorators";
+
+const USER_DISMISSED_PROMPT_KEY = "dismissed-pwa-install-banner";
+
+export default Ember.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 = Ember.run.bind(
+ this,
+ this._handleInstallPromptEvent
+ );
+ window.addEventListener("beforeinstallprompt", this._promptEventHandler);
+ },
+
+ @on("willDestroyElement")
+ _unregisterListener() {
+ window.removeEventListener("beforeinstallprompt", this._promptEventHandler);
+ },
+
+ @computed
+ 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);
+ }
+ },
+
+ @computed("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..0bde5b4ffa 100644
--- a/app/assets/javascripts/discourse/components/quote-button.js.es6
+++ b/app/assets/javascripts/discourse/components/quote-button.js.es6
@@ -83,7 +83,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;
diff --git a/app/assets/javascripts/discourse/components/radio-button.js.es6 b/app/assets/javascripts/discourse/components/radio-button.js.es6
index 8cdf74ecb9..4a7929d058 100644
--- a/app/assets/javascripts/discourse/components/radio-button.js.es6
+++ b/app/assets/javascripts/discourse/components/radio-button.js.es6
@@ -12,7 +12,7 @@ export default Ember.Component.extend({
],
click() {
- const value = this.$().val();
+ const value = $(this.element).val();
if (this.selection === value) {
this.set("selection", undefined);
}
diff --git a/app/assets/javascripts/discourse/components/related-messages.js.es6 b/app/assets/javascripts/discourse/components/related-messages.js.es6
index 807359d924..e7d65a4591 100644
--- a/app/assets/javascripts/discourse/components/related-messages.js.es6
+++ b/app/assets/javascripts/discourse/components/related-messages.js.es6
@@ -5,6 +5,30 @@ export default Ember.Component.extend({
elementId: "related-messages",
classNames: ["suggested-topics"],
+ @computed("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);
+ }
+ },
+
+ @computed
+ searchLink() {
+ return Discourse.getURL(
+ `/search?expanded=true&q=%40${this.targetUser.username}%20in%3Apersonal-direct`
+ );
+ },
+
@computed("topic")
relatedTitle(topic) {
const href = this.currentUser && this.currentUser.pmPath(topic);
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..1255c5bddc
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/reviewable-queued-post.js.es6
@@ -0,0 +1,9 @@
+import showModal from "discourse/lib/show-modal";
+
+export default Ember.Component.extend({
+ actions: {
+ showRawEmail() {
+ showModal("raw-email").set("rawEmail", this.reviewable.payload.raw_email);
+ }
+ }
+});
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..89a48126f3 100644
--- a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
+++ b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
@@ -40,7 +40,8 @@ export default MountWidget.extend({
"gaps",
"selectedQuery",
"selectedPostsCount",
- "searchService"
+ "searchService",
+ "showReadIndicator"
);
},
@@ -89,7 +90,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,
@@ -289,6 +292,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();
}
@@ -314,12 +323,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 +340,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..50da93e6c2 100644
--- a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6
+++ b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6
@@ -19,7 +19,7 @@ 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_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;
@@ -93,7 +93,7 @@ export default Ember.Component.extend({
in: {
title: false,
likes: false,
- private: false,
+ personal: false,
seen: false
},
all_tags: false
@@ -140,8 +140,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(
@@ -512,9 +512,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")
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..349918e22e 100644
--- a/app/assets/javascripts/discourse/components/search-text-field.js.es6
+++ b/app/assets/javascripts/discourse/components/search-text-field.js.es6
@@ -13,7 +13,7 @@ export default TextField.extend({
@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/share-panel.js.es6 b/app/assets/javascripts/discourse/components/share-panel.js.es6
index 872c1771bf..136e6a9ccc 100644
--- a/app/assets/javascripts/discourse/components/share-panel.js.es6
+++ b/app/assets/javascripts/discourse/components/share-panel.js.es6
@@ -41,8 +41,10 @@ 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", () => {
if (!this.capabilities.touch) {
diff --git a/app/assets/javascripts/discourse/components/share-popup.js.es6 b/app/assets/javascripts/discourse/components/share-popup.js.es6
index eb878763b7..a396b61cfb 100644
--- a/app/assets/javascripts/discourse/components/share-popup.js.es6
+++ b/app/assets/javascripts/discourse/components/share-popup.js.es6
@@ -54,7 +54,7 @@ export default Ember.Component.extend({
_showUrl($target, url) {
const $currentTargetOffset = $target.offset();
- const $this = this.$();
+ const $this = $(this.element);
if (Ember.isEmpty(url)) {
return;
diff --git a/app/assets/javascripts/discourse/components/site-header.js.es6 b/app/assets/javascripts/discourse/components/site-header.js.es6
index 98e8dbc6ed..99c41d5350 100644
--- a/app/assets/javascripts/discourse/components/site-header.js.es6
+++ b/app/assets/javascripts/discourse/components/site-header.js.es6
@@ -9,10 +9,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,
@@ -112,8 +108,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 +116,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 +194,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 +204,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,10 +222,6 @@ 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);
window.cancelAnimationFrame(this._scheduledMovingAnimation);
},
diff --git a/app/assets/javascripts/discourse/components/text-overflow.js.es6 b/app/assets/javascripts/discourse/components/text-overflow.js.es6
index 402b3a9cf1..a63b6fed5d 100644
--- a/app/assets/javascripts/discourse/components/text-overflow.js.es6
+++ b/app/assets/javascripts/discourse/components/text-overflow.js.es6
@@ -2,7 +2,7 @@ export default Ember.Component.extend({
didInsertElement() {
this._super(...arguments);
Ember.run.next(null, () => {
- const $this = this.$();
+ const $this = $(this.element);
if ($this) {
$this.find("hr").remove();
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..bdeb1b3a72
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/time-input.js.es6
@@ -0,0 +1,73 @@
+import { isNumeric } from "discourse/lib/utilities";
+
+export default Ember.Component.extend({
+ classNames: ["d-time-input"],
+ hours: null,
+ minutes: null,
+ _hours: Ember.computed.oneWay("hours"),
+ _minutes: Ember.computed.oneWay("minutes"),
+ isSafari: Ember.computed.oneWay("capabilities.isSafari"),
+ isMobile: Ember.computed.oneWay("site.mobileView"),
+ nativePicker: Ember.computed.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);
+ }
+
+ Ember.run.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/topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/topic-list-item.js.es6
index cc0c07d801..1116d97e61 100644
--- a/app/assets/javascripts/discourse/components/topic-list-item.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-list-item.js.es6
@@ -35,6 +35,47 @@ export const ListItemDefaults = {
attributeBindings: ["data-topic-id"],
"data-topic-id": Ember.computed.alias("topic.id"),
+ 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);
+ }
+ },
+
+ @computed("topic.id")
+ unreadIndicatorChannel(topicId) {
+ return `/private-messages/unread-indicator/${topicId}`;
+ },
+
+ @computed("topic.unread_by_group_member")
+ unreadClass(unreadByGroupMember) {
+ return unreadByGroupMember ? "" : "read";
+ },
+
+ @computed("topic.unread_by_group_member")
+ includeUnreadIndicator(unreadByGroupMember) {
+ return typeof unreadByGroupMember !== "undefined";
+ },
+
@computed
newDotText() {
return this.currentUser && this.currentUser.trust_level > 0
@@ -153,7 +194,7 @@ export const ListItemDefaults = {
navigateToTopic,
highlight(opts = { isLastViewedTopic: false }) {
- const $topic = this.$();
+ const $topic = $(this.element);
$topic
.addClass("highlighted")
.attr("data-islastviewedtopic", opts.isLastViewedTopic);
diff --git a/app/assets/javascripts/discourse/components/topic-progress.js.es6 b/app/assets/javascripts/discourse/components/topic-progress.js.es6
index 599de61ab5..f387de7bee 100644
--- a/app/assets/javascripts/discourse/components/topic-progress.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-progress.js.es6
@@ -126,7 +126,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,7 +151,7 @@ export default Ember.Component.extend({
},
_dock() {
- const $wrapper = this.$();
+ const $wrapper = $(this.element);
if (!$wrapper || $wrapper.length === 0) return;
const $html = $("html");
diff --git a/app/assets/javascripts/discourse/components/topic-status.js.es6 b/app/assets/javascripts/discourse/components/topic-status.js.es6
index a8dbe464a0..d063847e83 100644
--- a/app/assets/javascripts/discourse/components/topic-status.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-status.js.es6
@@ -29,7 +29,7 @@ export default Ember.Component.extend(
@computed("disableActions")
canAct(disableActions) {
- return Discourse.User.current() && !disableActions;
+ return this.currentUser && !disableActions;
},
buildBuffer(buffer) {
diff --git a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 b/app/assets/javascripts/discourse/components/topic-timeline.js.es6
index 1b0d92ca51..12e9aefea6 100644
--- a/app/assets/javascripts/discourse/components/topic-timeline.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-timeline.js.es6
@@ -52,8 +52,8 @@ export default MountWidget.extend(Docking, {
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 timeline = this.element.querySelector(".timeline-container");
+ const timelineHeight = (timeline && timeline.offsetHeight) || 400;
const footerHeight = $(".timeline-footer-controls").outerHeight(true) || 0;
const prev = this.dockAt;
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..981cbb125d 100644
--- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6
+++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6
@@ -8,6 +8,7 @@ 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";
export default Ember.Component.extend(
CardContentsBase,
@@ -65,11 +66,7 @@ export default Ember.Component.extend(
@computed("user.name")
nameFirst(name) {
- return (
- !this.siteSettings.prioritize_username_in_ux &&
- name &&
- name.trim().length > 0
- );
+ return prioritizeNameInUx(name, this.siteSettings);
},
@computed("username")
@@ -137,8 +134,8 @@ export default Ember.Component.extend(
return;
}
- const $this = this.$();
- if (!$this) {
+ const thisElem = this.element;
+ if (!thisElem) {
return;
}
@@ -146,7 +143,7 @@ export default Ember.Component.extend(
const bg = Ember.isEmpty(url)
? ""
: `url(${Discourse.getURLWithCDN(url)})`;
- $this.css("background-image", bg);
+ thisElem.style.backgroundImage = bg;
},
_showCallback(username, $target) {
@@ -187,6 +184,14 @@ export default Ember.Component.extend(
this._close();
},
+ composePM(user, post) {
+ this._close();
+
+ Ember.getOwner(this)
+ .lookup("router:main")
+ .send("composePrivateMessage", user, post);
+ },
+
cancelFilter() {
const postStream = this.postStream;
postStream.cancelFilter();
diff --git a/app/assets/javascripts/discourse/components/user-stream.js.es6 b/app/assets/javascripts/discourse/components/user-stream.js.es6
index ca24119a52..c29b80b09f 100644
--- a/app/assets/javascripts/discourse/components/user-stream.js.es6
+++ b/app/assets/javascripts/discourse/components/user-stream.js.es6
@@ -30,8 +30,12 @@ export default Ember.Component.extend(LoadMore, {
$(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"),
@@ -40,10 +44,10 @@ export default Ember.Component.extend(LoadMore, {
_destroyed: 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");
+ $(this.element).off("click.discourse-redirect", ".excerpt a");
}.on("willDestroyElement"),
actions: {
diff --git a/app/assets/javascripts/discourse/controllers/application.js.es6 b/app/assets/javascripts/discourse/controllers/application.js.es6
index ce23c502fa..7cd2898bf1 100644
--- a/app/assets/javascripts/discourse/controllers/application.js.es6
+++ b/app/assets/javascripts/discourse/controllers/application.js.es6
@@ -4,6 +4,7 @@ import { isAppWebview, isiOSPWA } from "discourse/lib/utilities";
export default Ember.Controller.extend({
showTop: true,
showFooter: false,
+ router: Ember.inject.service(),
@computed
canSignUp() {
@@ -16,7 +17,7 @@ export default Ember.Controller.extend({
@computed
loginRequired() {
- return Discourse.SiteSettings.login_required && !Discourse.User.current();
+ return Discourse.SiteSettings.login_required && !this.currentUser;
},
@computed
diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6
index d907a0f947..b96051e906 100644
--- a/app/assets/javascripts/discourse/controllers/composer.js.es6
+++ b/app/assets/javascripts/discourse/controllers/composer.js.es6
@@ -81,7 +81,7 @@ export function addPopupMenuOptionsCallback(callback) {
export default Ember.Controller.extend({
topicController: Ember.inject.controller("topic"),
- application: Ember.inject.controller(),
+ router: Ember.inject.service(),
replyAsNewTopicDraft: Ember.computed.equal(
"model.draftKey",
@@ -199,11 +199,7 @@ export default Ember.Controller.extend({
);
},
- @computed
- isStaffUser() {
- const currentUser = this.currentUser;
- return currentUser && currentUser.get("staff");
- },
+ isStaffUser: Ember.computed.reads("currentUser.staff"),
canUnlistTopic: Ember.computed.and("model.creatingTopic", "isStaffUser"),
@@ -296,7 +292,7 @@ export default Ember.Controller.extend({
@computed("model.creatingPrivateMessage", "model.targetUsernames")
showWarning(creatingPrivateMessage, usernames) {
- if (!Discourse.User.currentProp("staff")) {
+ if (!this.get("currentUser.staff")) {
return false;
}
@@ -734,7 +730,7 @@ export default Ember.Controller.extend({
});
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");
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..58ccfbcf88
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/convert-to-public-topic.js.es6
@@ -0,0 +1,26 @@
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+
+export default Ember.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/discovery-sortable.js.es6 b/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6
index b1fc3b3509..4ce4284c55 100644
--- a/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6
+++ b/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6
@@ -1,3 +1,5 @@
+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 },
@@ -31,6 +33,12 @@ export const addDiscoveryQueryParam = function(p, opts) {
cOpts[p] = Ember.computed.alias(`discoveryTopics.${p}`);
cOpts["queryParams"] = Object.keys(queryParams);
Controller.reopen(cOpts);
+
+ if (opts && opts.persisted) {
+ DiscourseNavigation.reopen({
+ persistedQueryParams: queryParams
+ });
+ }
};
export default Controller;
diff --git a/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6
index dedcef9811..2d8cc9f9ed 100644
--- a/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6
+++ b/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6
@@ -14,10 +14,7 @@ export default DiscoveryController.extend({
// this makes sure the composer isn't scoping to a specific category
category: null,
- @computed
- canEdit() {
- return Discourse.User.currentProp("staff");
- },
+ canEdit: Ember.computed.reads("currentUser.staff"),
@computed("model.categories.[].featuredTopics.length")
latestTopicOnly() {
diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6
index 0e18b632b5..381292baaa 100644
--- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6
+++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6
@@ -16,7 +16,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
@on("init")
_initPanels() {
- this.set("panels", []);
+ this.setProperties({
+ panels: [],
+ validators: []
+ });
},
onShow() {
@@ -75,7 +78,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
},
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",
diff --git a/app/assets/javascripts/discourse/controllers/exception.js.es6 b/app/assets/javascripts/discourse/controllers/exception.js.es6
index 1f4c454da4..6b341b155a 100644
--- a/app/assets/javascripts/discourse/controllers/exception.js.es6
+++ b/app/assets/javascripts/discourse/controllers/exception.js.es6
@@ -17,7 +17,7 @@ const ButtonBackBright = {
classes: "btn-primary",
action: "tryLoading",
key: "errors.buttons.again",
- icon: "refresh"
+ icon: "sync"
},
ButtonLoadPage = {
classes: "btn-primary",
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..c7175a47fd 100644
--- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6
+++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6
@@ -161,9 +161,9 @@ export default Ember.Controller.extend({
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)
);
},
diff --git a/app/assets/javascripts/discourse/controllers/group-activity.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity.js.es6
index 86356eeaac..26fa94835a 100644
--- a/app/assets/javascripts/discourse/controllers/group-activity.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-activity.js.es6
@@ -1,4 +1,4 @@
export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+ router: Ember.inject.service(),
queryParams: ["category_id"]
});
diff --git a/app/assets/javascripts/discourse/controllers/group-index.js.es6 b/app/assets/javascripts/discourse/controllers/group-index.js.es6
index 0018563b0d..690997aa54 100644
--- a/app/assets/javascripts/discourse/controllers/group-index.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-index.js.es6
@@ -29,7 +29,7 @@ export default Ember.Controller.extend({
this.set("loading", true);
const model = this.model;
- if (model) {
+ if (model && model.can_see_members) {
model.findMembers(this.memberParams).finally(() => {
this.set(
"application.showFooter",
diff --git a/app/assets/javascripts/discourse/controllers/group-manage.js.es6 b/app/assets/javascripts/discourse/controllers/group-manage.js.es6
index 93e17ca0fe..795627c3cb 100644
--- a/app/assets/javascripts/discourse/controllers/group-manage.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-manage.js.es6
@@ -1,7 +1,7 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+ router: Ember.inject.service(),
@computed("model.automatic")
tabs(automatic) {
diff --git a/app/assets/javascripts/discourse/controllers/group-messages.js.es6 b/app/assets/javascripts/discourse/controllers/group-messages.js.es6
index d4180e8b6b..cda126a2e2 100644
--- a/app/assets/javascripts/discourse/controllers/group-messages.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group-messages.js.es6
@@ -1,3 +1,3 @@
export default Ember.Controller.extend({
- application: Ember.inject.controller()
+ router: Ember.inject.service()
});
diff --git a/app/assets/javascripts/discourse/controllers/history.js.es6 b/app/assets/javascripts/discourse/controllers/history.js.es6
index df2833e187..53627c157f 100644
--- a/app/assets/javascripts/discourse/controllers/history.js.es6
+++ b/app/assets/javascripts/discourse/controllers/history.js.es6
@@ -7,7 +7,7 @@ import { sanitizeAsync } from "discourse/lib/text";
import { iconHTML } from "discourse-common/lib/icon-library";
function customTagArray(fieldName) {
- return function() {
+ return Ember.computed(fieldName, function() {
var val = this.get(fieldName);
if (!val) {
return val;
@@ -16,7 +16,7 @@ function customTagArray(fieldName) {
val = [val];
}
return val;
- }.property(fieldName);
+ });
}
// This controller handles displaying of history
diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6
index d4180e8b6b..cda126a2e2 100644
--- a/app/assets/javascripts/discourse/controllers/preferences.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6
@@ -1,3 +1,3 @@
export default Ember.Controller.extend({
- application: Ember.inject.controller()
+ router: Ember.inject.service()
});
diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
index 952788273f..f5c1f6f5ad 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
@@ -176,7 +176,9 @@ export default Ember.Controller.extend(
label: I18n.t("cancel"),
class: "d-modal-cancel",
link: true,
- callback: () => this.set("deleting", false)
+ callback: () => {
+ this.set("deleting", false);
+ }
},
{
label:
@@ -231,7 +233,18 @@ export default Ember.Controller.extend(
type: "POST",
data: token ? { token_id: token.id } : {}
}
- );
+ )
+ .then(() => {
+ if (!token) {
+ const redirect = this.siteSettings.logout_redirect;
+ if (Ember.isEmpty(redirect)) {
+ window.location.pathname = Discourse.getURL("/");
+ } else {
+ window.location.href = redirect;
+ }
+ }
+ })
+ .catch(popupAjaxError);
},
showToken(token) {
diff --git a/app/assets/javascripts/discourse/controllers/preferences/users.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/users.js.es6
index 16c7630429..9f213a3090 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/users.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/users.js.es6
@@ -1,7 +1,5 @@
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"),
@@ -15,34 +13,6 @@ export default Ember.Controller.extend(PreferencesTabController, {
},
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
diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6
index 1e86063b0d..7a7996e627 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic.js.es6
@@ -238,6 +238,10 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
this.set("buffered.category_id", selection.value);
},
+ topicTagsChanged({ target }) {
+ this.set("buffered.tags", target.value);
+ },
+
deletePending(pending) {
return ajax(`/review/${pending.id}`, { type: "DELETE" })
.then(() => {
@@ -722,11 +726,6 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
this._jumpToPostId(postId);
},
- hideMultiSelect() {
- this.set("multiSelect", false);
- this._forceRefreshPostStream();
- },
-
toggleMultiSelect() {
this.toggleProperty("multiSelect");
this._forceRefreshPostStream();
@@ -1073,11 +1072,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() {
@@ -1347,6 +1352,17 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
})
.then(() => refresh({ id: data.id, refreshLikes: true }));
break;
+ case "read":
+ postStream
+ .triggerChangedPost(data.id, data.updated_at, {
+ preserveCooked: true
+ })
+ .then(() =>
+ refresh({
+ id: data.id,
+ refreshReaders: topic.show_read_indicator
+ })
+ );
case "revised":
case "rebaked": {
postStream
diff --git a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity.js.es6
index e214e58826..d8e6f29dca 100644
--- a/app/assets/javascripts/discourse/controllers/user-activity.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-activity.js.es6
@@ -2,6 +2,7 @@ import { exportUserArchive } from "discourse/lib/export-csv";
export default Ember.Controller.extend({
application: Ember.inject.controller(),
+ router: Ember.inject.service(),
user: Ember.inject.controller(),
userActionType: null,
diff --git a/app/assets/javascripts/discourse/controllers/user-card.js.es6 b/app/assets/javascripts/discourse/controllers/user-card.js.es6
index cf84b13232..0f26ea5e59 100644
--- a/app/assets/javascripts/discourse/controllers/user-card.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-card.js.es6
@@ -6,7 +6,7 @@ import {
export default Ember.Controller.extend({
topic: Ember.inject.controller(),
- application: Ember.inject.controller(),
+ router: Ember.inject.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..23bbd45295 100644
--- a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6
@@ -42,15 +42,9 @@ export default Ember.Controller.extend({
);
},
- @computed
- canInviteToForum() {
- return Discourse.User.currentProp("can_invite_to_forum");
- },
+ canInviteToForum: Ember.computed.reads("currentUser.can_invite_to_forum"),
- @computed
- canBulkInvite() {
- return Discourse.User.currentProp("admin");
- },
+ canBulkInvite: Ember.computed.reads("currentUser.admin"),
showSearch: Ember.computed.gte("totalInvites", 10),
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..c5e1bd9452 100644
--- a/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6
@@ -2,26 +2,24 @@ import computed from "ember-addons/ember-computed-decorators";
import Topic from "discourse/models/topic";
export default Ember.Controller.extend({
- application: Ember.inject.controller(),
+ router: Ember.inject.service(),
userTopicsList: Ember.inject.controller("user-topics-list"),
user: Ember.inject.controller(),
pmView: false,
viewingSelf: Ember.computed.alias("user.viewingSelf"),
isGroup: Ember.computed.equal("pmView", "groups"),
- currentPath: Ember.computed.alias("application.currentPath"),
+ currentPath: Ember.computed.alias("router._router.currentPath"),
selected: Ember.computed.alias("userTopicsList.selected"),
bulkSelectEnabled: Ember.computed.alias("userTopicsList.bulkSelectEnabled"),
showToggleBulkSelect: true,
pmTaggingEnabled: Ember.computed.alias("site.can_tag_pms"),
tagId: null,
- @computed("user.viewingSelf")
- showNewPM(viewingSelf) {
- return (
- viewingSelf && Discourse.User.currentProp("can_send_private_messages")
- );
- },
+ showNewPM: Ember.computed.and(
+ "user.viewingSelf",
+ "currentUser.can_send_private_messages"
+ ),
@computed("selected.[]", "bulkSelectEnabled")
hasSelection(selected, bulkSelectEnabled) {
diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6
index 0051d0377e..e59b0edfdc 100644
--- a/app/assets/javascripts/discourse/controllers/user.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user.js.es6
@@ -2,12 +2,13 @@ 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";
export default Ember.Controller.extend(CanCheckEmails, {
indexStream: false,
- application: Ember.inject.controller(),
+ router: Ember.inject.service(),
userNotifications: Ember.inject.controller("user-notifications"),
- currentPath: Ember.computed.alias("application.currentPath"),
+ currentPath: Ember.computed.alias("router._router.currentPath"),
adminTools: optionalService(),
@computed("model.username")
@@ -87,11 +88,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
@computed("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")
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..0751b6f0e2 100644
--- a/app/assets/javascripts/discourse/helpers/icon-or-image.js.es6
+++ b/app/assets/javascripts/discourse/helpers/icon-or-image.js.es6
@@ -6,7 +6,7 @@ export default htmlHelper(function({ icon, image }) {
return `

`;
}
- if (Ember.isEmpty(icon) || icon.indexOf("fa-") < 0) {
+ if (Ember.isEmpty(icon)) {
return "";
}
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/subscribe-user-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6
index 44f4025408..b71f37aaca 100644
--- a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6
+++ b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6
@@ -131,12 +131,7 @@ export default {
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..ca83d7a639 100644
--- a/app/assets/javascripts/discourse/initializers/title-notifications.js.es6
+++ b/app/assets/javascripts/discourse/initializers/title-notifications.js.es6
@@ -6,8 +6,7 @@ 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")
@@ -15,6 +14,12 @@ export default {
},
_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 e2f28ce32e..1cec5148e7 100644
--- a/app/assets/javascripts/discourse/lib/ajax.js.es6
+++ b/app/assets/javascripts/discourse/lib/ajax.js.es6
@@ -40,6 +40,12 @@ function handleRedirect(data) {
}
}
+export function updateCsrfToken() {
+ return ajax("/session/csrf").then(result => {
+ Discourse.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
@@ -140,7 +146,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);
@@ -157,10 +163,7 @@ export function ajax() {
!Discourse.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);
+ ajaxObj = updateCsrfToken().then(() => {
performAjax(resolve, reject);
});
});
diff --git a/app/assets/javascripts/discourse/lib/computed.js.es6 b/app/assets/javascripts/discourse/lib/computed.js.es6
index 2683d944fe..e6a440eb83 100644
--- a/app/assets/javascripts/discourse/lib/computed.js.es6
+++ b/app/assets/javascripts/discourse/lib/computed.js.es6
@@ -51,10 +51,9 @@ export function propertyLessThan(p1, p2) {
**/
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);
}
/**
@@ -68,10 +67,9 @@ export function i18n(...args) {
**/
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);
}
/**
@@ -85,10 +83,9 @@ export function fmt(...args) {
**/
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);
}
/**
@@ -102,7 +99,7 @@ export function url(...args) {
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/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
index 5e0a3996ea..664a1cbdc6 100644
--- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
+++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
@@ -441,7 +441,7 @@ export default {
// Discard selection if it is not in viewport, so users can combine
// keyboard shortcuts with mouse scrolling.
- if ($selected.length !== 0) {
+ if ($selected.length !== 0 && !fast) {
const offset = minimumOffset();
const beginScreen = $(window).scrollTop() - offset;
const endScreen = beginScreen + window.innerHeight + offset;
diff --git a/app/assets/javascripts/discourse/lib/page-tracker.js.es6 b/app/assets/javascripts/discourse/lib/page-tracker.js.es6
index e6390e08ea..d7577ed5ae 100644
--- a/app/assets/javascripts/discourse/lib/page-tracker.js.es6
+++ b/app/assets/javascripts/discourse/lib/page-tracker.js.es6
@@ -37,7 +37,7 @@ export function startPageTracking(router, appEvents) {
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 19c2c5fc0d..950fdcd166 100644
--- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6
+++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6
@@ -45,7 +45,7 @@ import { addCategorySortCriteria } from "discourse/components/edit-category-sett
import { queryRegistry } from "discourse/widgets/widget";
// 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.32";
class PluginApi {
constructor(version, container) {
@@ -529,7 +529,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) {
@@ -734,7 +734,8 @@ class PluginApi {
* name: "link-to-bugs-category",
* displayName: "bugs"
* href: "/c/bugs",
- * customFilter: (category, args) => { category && category.get('name') !== 'bug' }
+ * customFilter: (category, args, router) => { category && category.name !== 'bug' }
+ * customHref: (category, args, router) => { if (category && category.name) === 'not-a-bug') "/a-feature"; }
* })
*/
addNavigationBarItem(item) {
@@ -745,6 +746,22 @@ 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);
+ };
+ }
+
addNavItem(item);
}
}
@@ -962,7 +979,7 @@ function decorate(klass, evt, cb, id) {
const mixin = {};
mixin["_decorate_" + _decorateId++] = function($elem) {
- $elem = $elem || this.$();
+ $elem = $elem || $(this.element);
if ($elem) {
cb($elem);
}
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 8c132c830b..b6e5c5c5e9 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
@@ -68,7 +68,7 @@ export function getTopicFooterButtons() {
.filter(x => x)
);
- const computedFunc = Ember.computed({
+ return Ember.computed(...dependentKeys, {
get() {
const _isFunction = descriptor =>
descriptor && typeof descriptor === "function";
@@ -122,8 +122,6 @@ export function getTopicFooterButtons() {
.reverse();
}
});
-
- return computedFunc.property.apply(computedFunc, dependentKeys);
}
export function clearTopicFooterButtons() {
diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
index b063a8094a..1c234632e7 100644
--- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
+++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
@@ -41,6 +41,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 +63,6 @@ function calcHeight() {
}
let workaroundActive = false;
-let composingTopic = false;
export function isWorkaroundActive() {
return workaroundActive;
@@ -124,11 +128,24 @@ function positioningWorkaround($fixedElement) {
if (fixedElement.style.top === "0px") {
if (this !== document.activeElement) {
evt.preventDefault();
+
+ // 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";
+ }, 50);
}
return;
}
+ // don't trigger keyboard on disabled element (happens when a category is required)
+ if (this.disabled) {
+ return;
+ }
+
originalScrollTop = $(window).scrollTop();
// take care of body
@@ -148,9 +165,7 @@ function positioningWorkaround($fixedElement) {
fixedElement.style.top = "0px";
- composingTopic = $("#reply-control .category-chooser").length > 0;
-
- const height = calcHeight(composingTopic);
+ const height = calcHeight();
fixedElement.style.height = height + "px";
$(fixedElement).addClass("no-transition");
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/text.js.es6 b/app/assets/javascripts/discourse/lib/text.js.es6
index ee51cb53d1..cb02036ced 100644
--- a/app/assets/javascripts/discourse/lib/text.js.es6
+++ b/app/assets/javascripts/discourse/lib/text.js.es6
@@ -13,7 +13,7 @@ function getOpts(opts) {
{
getURL: Discourse.getURLWithCDN,
currentUser: Discourse.__container__.lookup("current-user:main"),
- censoredWords: site.censored_words,
+ censoredRegexp: site.censored_regexp,
siteSettings,
formatUsername
},
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/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6
index 7a1872b4fe..5f882d8a93 100644
--- a/app/assets/javascripts/discourse/lib/transform-post.js.es6
+++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6
@@ -71,7 +71,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]));
diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6
index bba1db7ebb..66b82829ec 100644
--- a/app/assets/javascripts/discourse/lib/url.js.es6
+++ b/app/assets/javascripts/discourse/lib/url.js.es6
@@ -23,7 +23,8 @@ const SERVER_SIDE_ONLY = [
/\.rss$/,
/\.json$/,
/^\/admin\/upgrade$/,
- /^\/logs($|\/)/
+ /^\/logs($|\/)/,
+ /^\/admin\/logs\/watched_words\/action\/[^\/]+\/download$/
];
export function rewritePath(path) {
@@ -405,11 +406,9 @@ const DiscourseURL = Ember.Object.extend({
@property router
**/
- router: function() {
+ get router() {
return Discourse.__container__.lookup("router:main");
- }
- .property()
- .volatile(),
+ },
// Get a controller. Note that currently it uses `__container__` which is not
// advised but there is no other way to access the router.
diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6
index 753460f1fa..1cb70232e9 100644
--- a/app/assets/javascripts/discourse/lib/user-search.js.es6
+++ b/app/assets/javascripts/discourse/lib/user-search.js.es6
@@ -4,7 +4,7 @@ import { userPath } from "discourse/lib/url";
import { emailValid } from "discourse/lib/utilities";
var cache = {},
- cacheTopicId,
+ cacheKey,
cacheTime,
currentTerm,
oldSearch;
@@ -12,6 +12,7 @@ var cache = {},
function performSearch(
term,
topicId,
+ categoryId,
includeGroups,
includeMentionableGroups,
includeMessageableGroups,
@@ -28,7 +29,7 @@ function performSearch(
// 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) {
+ if (term === "" && !topicId && !categoryId) {
return [];
}
@@ -37,6 +38,7 @@ function performSearch(
data: {
term: term,
topic_id: topicId,
+ category_id: categoryId,
include_groups: includeGroups,
include_mentionable_groups: includeMentionableGroups,
include_messageable_groups: includeMessageableGroups,
@@ -140,6 +142,7 @@ export default function userSearch(options) {
includeMessageableGroups = options.includeMessageableGroups,
allowedUsers = options.allowedUsers,
topicId = options.topicId,
+ categoryId = options.categoryId,
groupMembersOf = options.groupMembersOf;
if (oldSearch) {
@@ -150,11 +153,13 @@ export default function userSearch(options) {
currentTerm = term;
return new Ember.RSVP.Promise(function(resolve) {
- if (new Date() - cacheTime > 30000 || cacheTopicId !== topicId) {
+ 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 +173,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..3f2be55872 100644
--- a/app/assets/javascripts/discourse/lib/utilities.js.es6
+++ b/app/assets/javascripts/discourse/lib/utilities.js.es6
@@ -588,7 +588,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 =
diff --git a/app/assets/javascripts/discourse/mixins/buffered-content.js.es6 b/app/assets/javascripts/discourse/mixins/buffered-content.js.es6
index aefee9990e..36f6de6071 100644
--- a/app/assets/javascripts/discourse/mixins/buffered-content.js.es6
+++ b/app/assets/javascripts/discourse/mixins/buffered-content.js.es6
@@ -1,11 +1,11 @@
/* global BufferedProxy: true */
export function bufferedProperty(property) {
const mixin = {
- buffered: function() {
+ buffered: Ember.computed(property, function() {
return Ember.ObjectProxy.extend(BufferedProxy).create({
content: this.get(property)
});
- }.property(property),
+ }),
rollbackBuffer: function() {
this.buffered.discardBufferedChanges();
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..212278f2e0 100644
--- a/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6
+++ b/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6
@@ -73,7 +73,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}`;
@@ -164,7 +164,7 @@ 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;
@@ -227,7 +227,7 @@ export default Ember.Mixin.create({
position.top = avatarOverflowSize;
}
- this.$().css(position);
+ $(this.element).css(position);
}
}
@@ -236,23 +236,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());
+ Ember.run.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/open-composer.js.es6 b/app/assets/javascripts/discourse/mixins/open-composer.js.es6
index e3576cda08..7d925ab652 100644
--- a/app/assets/javascripts/discourse/mixins/open-composer.js.es6
+++ b/app/assets/javascripts/discourse/mixins/open-composer.js.es6
@@ -3,8 +3,17 @@ import Composer from "discourse/models/composer";
export default Ember.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,
draftSequence: controller.get("model.draft_sequence") || 0
diff --git a/app/assets/javascripts/discourse/mixins/pan-events.js.es6 b/app/assets/javascripts/discourse/mixins/pan-events.js.es6
index d64f714917..1608d72fea 100644
--- a/app/assets/javascripts/discourse/mixins/pan-events.js.es6
+++ b/app/assets/javascripts/discourse/mixins/pan-events.js.es6
@@ -13,12 +13,12 @@ export default Ember.Mixin.create({
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/upload.js.es6 b/app/assets/javascripts/discourse/mixins/upload.js.es6
index 394b6a73f3..d3f83ff419 100644
--- a/app/assets/javascripts/discourse/mixins/upload.js.es6
+++ b/app/assets/javascripts/discourse/mixins/upload.js.es6
@@ -23,8 +23,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
);
},
@@ -36,7 +34,7 @@ export default Ember.Mixin.create({
},
_initialize: function() {
- const $upload = this.$();
+ const $upload = $(this.element);
const reset = () =>
this.setProperties({ uploading: false, uploadProgress: 0 });
const maxFiles = this.getWithDefault(
@@ -108,7 +106,7 @@ export default Ember.Mixin.create({
_destroy: function() {
this.messageBus && this.messageBus.unsubscribe("/uploads/" + this.type);
- const $upload = this.$();
+ const $upload = $(this.element);
try {
$upload.fileupload("destroy");
} catch (e) {
diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6
index 7e4f38a585..903d61b521 100644
--- a/app/assets/javascripts/discourse/models/composer.js.es6
+++ b/app/assets/javascripts/discourse/models/composer.js.es6
@@ -56,7 +56,8 @@ const CLOSED = "closed",
categoryId: "topic.category.id",
tags: "topic.tags",
featuredLink: "topic.featured_link"
- };
+ },
+ FAST_REPLY_LENGTH_THRESHOLD = 10000;
export const SAVE_LABELS = {
[EDIT]: "composer.save_edit",
@@ -69,11 +70,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({
@@ -308,12 +309,6 @@ const Composer = RestModel.extend({
return options;
},
- @computed
- isStaffUser() {
- const currentUser = Discourse.User.current();
- return currentUser && currentUser.staff;
- },
-
@computed(
"loading",
"canEditTitle",
@@ -451,10 +446,63 @@ const Composer = RestModel.extend({
@computed("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")
@@ -709,6 +757,11 @@ const Composer = RestModel.extend({
const topicProps = this.getProperties(
Object.keys(_edit_topic_serializer)
);
+ // frontend should have featuredLink but backend needs featured_link
+ if (topicProps.featuredLink) {
+ topicProps.featured_link = topicProps.featuredLink;
+ delete topicProps.featuredLink;
+ }
const topic = this.topic;
diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6
index c25f0ce8d5..383c46861b 100644
--- a/app/assets/javascripts/discourse/models/group.js.es6
+++ b/app/assets/javascripts/discourse/models/group.js.es6
@@ -139,7 +139,7 @@ const Group = RestModel.extend({
@computed("visibility_level")
isPrivate(visibilityLevel) {
- return visibilityLevel !== 0;
+ return ![0, 1].includes(visibilityLevel);
},
@observes("isPrivate", "canEveryoneMention")
@@ -149,19 +149,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 +171,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) {
diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6
index a9166633f4..83e9a18a9d 100644
--- a/app/assets/javascripts/discourse/models/login-method.js.es6
+++ b/app/assets/javascripts/discourse/models/login-method.js.es6
@@ -1,4 +1,5 @@
import computed from "ember-addons/ember-computed-decorators";
+import { updateCsrfToken } from "discourse/lib/ajax";
const LoginMethod = Ember.Object.extend({
@computed
@@ -23,15 +24,21 @@ const LoginMethod = Ember.Object.extend({
if (customLogin) {
customLogin();
} else {
- let authUrl = this.custom_url || Discourse.getURL(`/auth/${name}`);
+ if (this.custom_url) {
+ window.location = this.custom_url;
+ return;
+ }
+ let authUrl = Discourse.getURL(`/auth/${name}`);
if (reconnect) {
authUrl += "?reconnect=true";
}
if (reconnect || fullScreenLogin || this.full_screen_login) {
- document.cookie = "fsl=true";
- window.location = authUrl;
+ LoginMethod.buildPostForm(authUrl).then(form => {
+ document.cookie = "fsl=true";
+ form.submit();
+ });
} else {
this.set("authenticate", name);
const left = this.lastX - 400;
@@ -44,31 +51,51 @@ const LoginMethod = Ember.Object.extend({
authUrl += authUrl.includes("?") ? "&" : "?";
authUrl += "display=popup";
}
+ LoginMethod.buildPostForm(authUrl).then(form => {
+ const windowState = window.open(
+ authUrl,
+ "auth_popup",
+ `menubar=no,status=no,height=${height},width=${width},left=${left},top=${top}`
+ );
- const windowState = window.open(
- authUrl,
- "_blank",
- "menubar=no,status=no,height=" +
- height +
- ",width=" +
- width +
- ",left=" +
- left +
- ",top=" +
- top
- );
+ form.target = "auth_popup";
+ form.submit();
- const timer = setInterval(() => {
- if (!windowState || windowState.closed) {
- clearInterval(timer);
- this.set("authenticate", null);
- }
- }, 1000);
+ const timer = setInterval(() => {
+ // If the process is aborted, reset state in this window
+ if (!windowState || windowState.closed) {
+ clearInterval(timer);
+ this.set("authenticate", null);
+ }
+ }, 1000);
+ });
}
}
}
});
+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", Discourse.Session.currentProp("csrfToken"));
+ form.appendChild(input);
+
+ document.body.appendChild(form);
+
+ return form;
+ });
+ }
+});
+
let methods;
export function findAll() {
diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6
index 64a705de89..8ae207a37c 100644
--- a/app/assets/javascripts/discourse/models/nav-item.js.es6
+++ b/app/assets/javascripts/discourse/models/nav-item.js.es6
@@ -101,8 +101,20 @@ const NavItem = Discourse.Model.extend({
});
const ExtraNavItem = NavItem.extend({
- @computed("href")
- href: href => href,
+ href: computed("href", {
+ get() {
+ if (this._href) {
+ return this._href;
+ }
+
+ return this.href;
+ },
+
+ set(key, value) {
+ return (this._href = value);
+ }
+ }),
+
customFilter: null
});
@@ -136,6 +148,9 @@ NavItem.reopenClass({
if (opts.category) {
args.category = opts.category;
}
+ if (opts.persistedQueryParams) {
+ args.persistedQueryParams = opts.persistedQueryParams;
+ }
if (opts.noSubcategories) {
args.noSubcategories = true;
}
@@ -175,6 +190,11 @@ NavItem.reopenClass({
return item.customFilter.call(this, category, args);
});
+ extraItems.forEach(item => {
+ if (!item.customHref) return;
+ item.set("href", item.customHref.call(this, category, args));
+ });
+
return items.concat(extraItems);
}
});
diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6
index a426fc2c3b..51e536965e 100644
--- a/app/assets/javascripts/discourse/models/post-stream.js.es6
+++ b/app/assets/javascripts/discourse/models/post-stream.js.es6
@@ -609,9 +609,14 @@ export default RestModel.extend({
this.set("loadingLastPost", true);
return this.findPostsByIds([postId])
.then(posts => {
- const ignoredUsers = this.get("currentUser.ignored_users");
+ const ignoredUsers =
+ Discourse.User.current() &&
+ Discourse.User.current().get("ignored_users");
posts.forEach(p => {
- if (ignoredUsers && ignoredUsers.includes(p.username)) return;
+ if (ignoredUsers && ignoredUsers.includes(p.username)) {
+ this.stream.removeObject(postId);
+ return;
+ }
this.appendPost(p);
});
})
diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6
index 2e3681c5c6..9a9fbe8e63 100644
--- a/app/assets/javascripts/discourse/models/post.js.es6
+++ b/app/assets/javascripts/discourse/models/post.js.es6
@@ -11,11 +11,17 @@ import { userPath } from "discourse/lib/url";
import Composer from "discourse/models/composer";
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: Ember.computed({
+ get() {
+ return Discourse.SiteSettings;
+ },
+
+ // prevents model created from json to overridde this property
+ set() {
+ return Discourse.SiteSettings;
+ }
+ }),
@computed("url")
shareUrl(url) {
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..5c726069d3 100644
--- a/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6
+++ b/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6
@@ -69,13 +69,10 @@ const TopicTrackingState = Discourse.Model.extend({
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();
}
}
};
diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6
index 4d3056b3bf..d44afe930e 100644
--- a/app/assets/javascripts/discourse/models/topic.js.es6
+++ b/app/assets/javascripts/discourse/models/topic.js.es6
@@ -97,7 +97,7 @@ const Topic = RestModel.extend({
fancyTitle(title) {
let fancyTitle = censor(
emojiUnescape(title || ""),
- Discourse.Site.currentProp("censored_words")
+ Discourse.Site.currentProp("censored_regexp")
);
if (Discourse.SiteSettings.support_mixed_text_direction) {
@@ -599,10 +599,12 @@ 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() {
diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index a448101597..84ac5efed4 100644
--- a/app/assets/javascripts/discourse/models/user.js.es6
+++ b/app/assets/javascripts/discourse/models/user.js.es6
@@ -54,7 +54,16 @@ const User = RestModel.extend({
return UserDraftsStream.create({ user: this });
},
- staff: Ember.computed.or("admin", "moderator"),
+ staff: Ember.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" });
@@ -746,7 +755,12 @@ const User = RestModel.extend({
return _.uniq(titles)
.sort()
- .map(Ember.Handlebars.Utils.escapeExpression);
+ .map(title => {
+ return {
+ name: Ember.Handlebars.Utils.escapeExpression(title),
+ id: title
+ };
+ });
},
@computed("user_option.text_size_seq", "user_option.text_size")
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..0a41fbab9a 100644
--- a/app/assets/javascripts/discourse/pre-initializers/sniff-capabilities.js.es6
+++ b/app/assets/javascripts/discourse/pre-initializers/sniff-capabilities.js.es6
@@ -37,6 +37,8 @@ export default {
caps.isIOS =
/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+
+ caps.isIpadOS = ua.indexOf("Mac OS") !== -1 && touch;
}
// We consider high res a device with 1280 horizontal pixels. High DPI tablets like
diff --git a/app/assets/javascripts/discourse/routes/about.js.es6 b/app/assets/javascripts/discourse/routes/about.js.es6
index 019a23380d..3b19492a6b 100644
--- a/app/assets/javascripts/discourse/routes/about.js.es6
+++ b/app/assets/javascripts/discourse/routes/about.js.es6
@@ -16,6 +16,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/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6
index 4dbc79fded..f9cdcab186 100644
--- a/app/assets/javascripts/discourse/routes/application.js.es6
+++ b/app/assets/javascripts/discourse/routes/application.js.es6
@@ -156,10 +156,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);
}
},
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..4ad23d5f64 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,10 @@
export default Ember.Route.extend({
- beforeModel: function() {
- this.transitionTo("group.activity.posts");
+ 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/services/emoji-store.js.es6 b/app/assets/javascripts/discourse/services/emoji-store.js.es6
new file mode 100644
index 0000000000..7c7a5d082a
--- /dev/null
+++ b/app/assets/javascripts/discourse/services/emoji-store.js.es6
@@ -0,0 +1,48 @@
+import KeyValueStore from "discourse/lib/key-value-store";
+
+const EMOJI_USAGE = "emojiUsage";
+const EMOJI_SELECTED_DIVERSITY = "emojiSelectedDiversity";
+const TRACKED_EMOJIS = 15;
+const STORE_NAMESPACE = "discourse_emojis_";
+
+export default Ember.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/theme-settings.js.es6 b/app/assets/javascripts/discourse/services/theme-settings.js.es6
index 5e1e8a8155..1ff2300f7f 100644
--- a/app/assets/javascripts/discourse/services/theme-settings.js.es6
+++ b/app/assets/javascripts/discourse/services/theme-settings.js.es6
@@ -12,7 +12,7 @@ export default Ember.Service.extend({
getSetting(themeId, settingsKey) {
if (this._settings[themeId]) {
- return this._settings[themeId][settingsKey];
+ return Ember.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..5abd436bca 100644
--- a/app/assets/javascripts/discourse/templates/about.hbs
+++ b/app/assets/javascripts/discourse/templates/about.hbs
@@ -57,8 +57,21 @@
connectorTagName='section'
args=(hash model=model)}}
+ {{#if model.category_moderators.length}}
+ {{#each model.category_moderators as |cm|}}
+
+ {{category-link cm.category}}{{i18n "about.moderators"}}
+
+ {{#each cm.moderators as |m|}}
+ {{user-info user=m}}
+ {{/each}}
+
+
+
+ {{/each}}
+ {{/if}}
- {{d-icon "bar-chart"}} {{i18n 'about.stats'}}
+ {{d-icon "far-chart-bar"}} {{i18n 'about.stats'}}
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/components/avatar-uploader.hbs b/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs
index e980fc5062..69c627e7ba 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 @@
-